futu_backend/
overnight_whitelist.rs1use std::collections::HashSet;
29use std::io::Read;
30use std::sync::Arc;
31use std::sync::atomic::{AtomicU64, Ordering};
32
33use bytes::Bytes;
34use flate2::read::GzDecoder;
35use futu_command_spec::BackendExtensionOperation;
36use futu_core::error::{FutuError, Result};
37use futu_domain_trade_write::{
38 OvernightWhitelistCacheAction, OvernightWhitelistResponseFacts,
39 plan_overnight_whitelist_response_like_cpp,
40};
41use prost::Message;
42
43use crate::conn::BackendConn;
44use crate::proto_internal::securities_switch::{
45 QryNightWhitelistClientReq, QryNightWhitelistClientRsp,
46};
47
48pub use futu_command_spec::CMD_TRD_OVERNIGHT_WHITELIST as CMD_TRD_OVERNIGHT_WHITE_LIST;
50
51pub use futu_domain_trade_write::DEFAULT_OVERNIGHT_WHITELIST_REFRESH_INTERVAL_SECS as DEFAULT_UPDATE_INTERVAL_SECS;
54pub use futu_domain_trade_write::is_valid_overnight_whitelist_broker_like_cpp as is_valid_overnight_whitelist_broker;
55
56#[derive(Debug, Clone)]
57struct OvernightWhitelistEntry {
58 hash: Option<String>,
59 stock_ids: Arc<HashSet<u64>>,
60}
61
62#[derive(Debug, Clone, Default)]
64pub struct OvernightWhitelistCache {
65 inner: Arc<dashmap::DashMap<u32, OvernightWhitelistEntry>>,
66 generation: Arc<AtomicU64>,
67}
68
69#[derive(Debug, Clone, PartialEq, Eq)]
71pub struct OvernightWhitelistSnapshot {
72 pub response_broker_id: Option<u32>,
73 pub hash: Option<String>,
74 pub stock_ids: Option<Vec<u64>>,
75 pub update_interval_secs: Option<u64>,
76}
77
78impl OvernightWhitelistCache {
79 pub fn new() -> Self {
80 Self::default()
81 }
82
83 pub fn is_stock_in_whitelist(&self, broker_id: u32, stock_id: u64) -> bool {
84 self.inner
85 .get(&broker_id)
86 .is_some_and(|entry| entry.stock_ids.contains(&stock_id))
87 }
88
89 #[must_use]
91 pub fn generation(&self) -> u64 {
92 self.generation.load(Ordering::Acquire)
93 }
94
95 fn broker_hash(&self, broker_id: u32) -> Option<String> {
96 self.inner
97 .get(&broker_id)
98 .and_then(|entry| entry.hash.clone())
99 }
100
101 fn set_whitelist(
102 &self,
103 broker_id: u32,
104 hash: Option<String>,
105 stock_ids: impl IntoIterator<Item = u64>,
106 ) {
107 let stock_ids = stock_ids.into_iter().collect::<HashSet<_>>();
108 self.inner.insert(
109 broker_id,
110 OvernightWhitelistEntry {
111 hash,
112 stock_ids: Arc::new(stock_ids),
113 },
114 );
115 self.generation.fetch_add(1, Ordering::AcqRel);
116 }
117}
118
119pub fn build_overnight_whitelist_request(
120 cache: &OvernightWhitelistCache,
121 broker_id: u32,
122) -> Vec<u8> {
123 QryNightWhitelistClientReq {
124 hash: cache.broker_hash(broker_id),
125 }
126 .encode_to_vec()
127}
128
129pub fn parse_overnight_whitelist_rsp(body: &[u8]) -> Result<OvernightWhitelistSnapshot> {
130 let rsp = QryNightWhitelistClientRsp::decode(body)
131 .map_err(|e| FutuError::Codec(format!("CMD20874 decode: {e}")))?;
132 let stock_ids = match rsp.stock_id_list.as_deref() {
133 Some(raw) => Some(unpack_stock_id_list_gzip(raw)?),
134 None => None,
135 };
136 Ok(OvernightWhitelistSnapshot {
137 response_broker_id: rsp.broker_id,
138 hash: rsp.hash,
139 stock_ids,
140 update_interval_secs: rsp.update_interval,
141 })
142}
143
144pub async fn refresh_overnight_whitelist(
145 backend: &BackendConn,
146 cache: &OvernightWhitelistCache,
147 broker_id: u32,
148) -> Result<u64> {
149 if !is_valid_overnight_whitelist_broker(broker_id) {
150 tracing::debug!(
151 broker_id,
152 "CMD20874 overnight whitelist skipped for broker not in C++ valid set"
153 );
154 return Ok(DEFAULT_UPDATE_INTERVAL_SECS);
155 }
156
157 let body = build_overnight_whitelist_request(cache, broker_id);
158 tracing::debug!(
159 broker_id,
160 old_hash_present = cache.broker_hash(broker_id).is_some(),
161 "sending CMD20874 QryNightWhitelistClientReq"
162 );
163 let resp = crate::command_runtime::execute_backend_extension(
164 backend,
165 BackendExtensionOperation::OvernightWhitelist,
166 Bytes::from(body),
167 )
168 .await?;
169 let snapshot = parse_overnight_whitelist_rsp(resp.body.as_ref())?;
170 let plan = plan_overnight_whitelist_response_like_cpp(OvernightWhitelistResponseFacts {
171 request_broker_id: broker_id,
172 response_broker_id: snapshot.response_broker_id,
173 hash: snapshot.hash,
174 stock_ids: snapshot.stock_ids,
175 update_interval_secs: snapshot.update_interval_secs,
176 });
177
178 match plan.cache_action {
179 OvernightWhitelistCacheAction::Replace {
180 broker_id,
181 hash,
182 stock_ids,
183 } => {
184 let count = stock_ids.len();
185 cache.set_whitelist(broker_id, Some(hash), stock_ids);
186 tracing::info!(
187 broker_id,
188 stock_id_count = count,
189 update_interval_secs = plan.next_refresh_interval_secs,
190 "CMD20874 overnight whitelist refreshed"
191 );
192 }
193 OvernightWhitelistCacheAction::KeepExisting => {
194 tracing::debug!(
195 broker_id,
196 update_interval_secs = plan.next_refresh_interval_secs,
197 "CMD20874 overnight whitelist response did not contain a non-empty hash and stock list; keeping existing cache"
198 );
199 }
200 }
201
202 Ok(plan.next_refresh_interval_secs)
203}
204
205pub fn unpack_stock_id_list_gzip(raw: &[u8]) -> Result<Vec<u64>> {
206 let mut decoder = GzDecoder::new(raw);
207 let mut decoded = Vec::new();
208 decoder
209 .read_to_end(&mut decoded)
210 .map_err(|e| FutuError::Codec(format!("CMD20874 gzip decode: {e}")))?;
211
212 let (chunks, remainder) = decoded.as_chunks::<8>();
213 if !remainder.is_empty() {
214 return Err(FutuError::Codec(format!(
215 "CMD20874 stock_id_list length {} is not a multiple of u64",
216 decoded.len()
217 )));
218 }
219
220 Ok(chunks
221 .iter()
222 .map(|bytes| u64::from_le_bytes(*bytes))
223 .collect())
224}
225
226#[cfg(test)]
227mod tests;