futu_cache/qot_cache/
rt.rs1use std::sync::Arc;
2
3use futu_core::qot_stock_key::QotSecurityKey;
4use tokio::sync::watch;
5
6use super::{CachedTimeShare, QotCache};
7
8#[derive(Debug, Clone, Copy, Eq, PartialEq)]
9pub enum RtAverageMode {
10 PriceCount,
11 TurnoverVolume,
12}
13
14#[derive(Debug, Clone)]
15pub enum RtPushApplyOutcome {
16 Updated(CachedTimeShare),
17 NotUpdate,
18 BlankInMiddle,
19 NotFound,
20}
21
22#[derive(Debug, Clone, Copy, Eq, PartialEq)]
23pub struct RtPullGeneration {
24 pub subscription_generation: u64,
25 pub backend_generation: u64,
26}
27
28pub struct RtPullFlightGuard {
29 cache: Arc<QotCache>,
30 cache_key: String,
31 completion: watch::Sender<bool>,
32}
33
34impl Drop for RtPullFlightGuard {
35 fn drop(&mut self) {
36 self.cache.rt_pull_in_flight.remove(&self.cache_key);
37 self.completion.send_replace(true);
38 }
39}
40
41impl QotCache {
42 pub fn make_rt_key(sec_key: &str, session: i32) -> String {
43 format!("{sec_key}:s{session}")
44 }
45
46 pub fn make_rt_key_broker(key: &QotSecurityKey, session: i32) -> String {
47 format!("{}:s{}", key.cache_key(), session)
48 }
49
50 pub fn begin_rt_pull(self: &Arc<Self>, key: &QotSecurityKey) -> Option<RtPullFlightGuard> {
51 let cache_key = key.cache_key();
52 match self.rt_pull_in_flight.entry(cache_key.clone()) {
53 dashmap::mapref::entry::Entry::Occupied(_) => None,
54 dashmap::mapref::entry::Entry::Vacant(entry) => {
55 let (completion, _) = watch::channel(false);
56 entry.insert(completion.clone());
57 Some(RtPullFlightGuard {
58 cache: Arc::clone(self),
59 cache_key,
60 completion,
61 })
62 }
63 }
64 }
65
66 pub async fn wait_for_rt_pull(&self, key: &QotSecurityKey) {
67 let completion = self
68 .rt_pull_in_flight
69 .get(&key.cache_key())
70 .map(|entry| entry.value().clone());
71 if let Some(completion) = completion {
72 let mut receiver = completion.subscribe();
73 if !*receiver.borrow() {
74 let _ = receiver.changed().await;
75 }
76 }
77 }
78
79 pub fn update_rt_data_broker(
80 &self,
81 key: &QotSecurityKey,
82 session: i32,
83 rt_data: Vec<CachedTimeShare>,
84 ) {
85 let _publish = self.rt_data_publish_lock.write();
86 let cache_key = Self::make_rt_key_broker(key, session);
87 self.rt_data.insert(cache_key, rt_data);
88 }
89
90 pub fn publish_rt_pull_generation(
91 &self,
92 key: &QotSecurityKey,
93 generation: RtPullGeneration,
94 buckets: Vec<(i32, Vec<CachedTimeShare>)>,
95 ) -> bool {
96 let _publish = self.rt_data_publish_lock.write();
97 let generation_key = key.cache_key();
98 if self
99 .rt_pull_generations
100 .get(&generation_key)
101 .is_some_and(|current| current.backend_generation > generation.backend_generation)
102 {
103 return false;
104 }
105 for session in [0, 2, 3, 5] {
106 self.rt_data.remove(&Self::make_rt_key_broker(key, session));
107 }
108 for (session, points) in buckets {
109 if !points.is_empty() {
110 self.rt_data
111 .insert(Self::make_rt_key_broker(key, session), points);
112 }
113 }
114 self.rt_pull_generations.insert(generation_key, generation);
115 true
116 }
117
118 pub fn get_rt_data_broker(
119 &self,
120 key: &QotSecurityKey,
121 session: i32,
122 ) -> Option<Vec<CachedTimeShare>> {
123 let _publish = self.rt_data_publish_lock.read();
124 let cache_key = Self::make_rt_key_broker(key, session);
125 self.rt_data.get(&cache_key).map(|value| value.clone())
126 }
127
128 pub fn apply_rt_push_point_broker(
129 &self,
130 key: &QotSecurityKey,
131 session: i32,
132 mut incoming: CachedTimeShare,
133 average_mode: RtAverageMode,
134 ) -> RtPushApplyOutcome {
135 let _publish = self.rt_data_publish_lock.write();
136 if incoming.is_blank {
137 return RtPushApplyOutcome::NotUpdate;
138 }
139 let cache_key = Self::make_rt_key_broker(key, session);
140 let Some(mut bucket) = self.rt_data.get_mut(&cache_key) else {
141 return RtPushApplyOutcome::NotFound;
142 };
143 let Some(index) = bucket
144 .iter()
145 .position(|point| point.timestamp == incoming.timestamp)
146 else {
147 return RtPushApplyOutcome::NotFound;
148 };
149 if bucket[..index].iter().any(|point| point.is_blank) {
150 return RtPushApplyOutcome::BlankInMiddle;
151 }
152 if rt_push_content_eq(&bucket[index], &incoming) {
153 return RtPushApplyOutcome::NotUpdate;
154 }
155 if bucket.get(index + 1).is_some_and(|point| !point.is_blank) {
156 return RtPushApplyOutcome::NotUpdate;
157 }
158
159 incoming.last_close_price = bucket[index].last_close_price;
160 bucket[index] = incoming;
161 recompute_rt_average(&mut bucket, average_mode);
162 RtPushApplyOutcome::Updated(bucket[index].clone())
163 }
164}
165
166fn rt_push_content_eq(left: &CachedTimeShare, right: &CachedTimeShare) -> bool {
167 left.time == right.time
168 && left.minute == right.minute
169 && left.is_blank == right.is_blank
170 && left.price == right.price
171 && left.volume == right.volume
172 && left.hp_volume == right.hp_volume
173 && left.turnover == right.turnover
174 && left.timestamp == right.timestamp
175}
176
177fn recompute_rt_average(points: &mut [CachedTimeShare], mode: RtAverageMode) {
178 let Some(start) = points.iter().position(|point| !point.is_blank) else {
179 return;
180 };
181 let mut total_price = 0.0;
182 let mut price_count = 0_u64;
183 let mut total_turnover = 0.0;
184 let mut total_volume = 0.0;
185 let mut last_average = None;
186 for point in points.iter_mut().skip(start) {
187 if point.is_blank {
188 break;
189 }
190 point.avg_price = match mode {
191 RtAverageMode::PriceCount => {
192 total_price += point.price;
193 price_count += 1;
194 total_price / price_count as f64
195 }
196 RtAverageMode::TurnoverVolume => {
197 total_turnover += point.turnover;
198 total_volume += point.hp_volume;
199 if total_volume > 0.0 {
200 total_turnover / total_volume
201 } else {
202 last_average.unwrap_or(point.price)
203 }
204 }
205 };
206 last_average = Some(point.avg_price);
207 }
208}