1use std::future::Future;
2use std::time::Duration;
3
4use futu_domain_trade_write::{
5 ModifyOrderEffectiveFields,
6 effective_modify_order_fields_like_cpp as effective_fields_from_domain,
7 modify_order_uses_futures_price_precision_like_cpp,
8};
9use futu_trd::query::Order;
10use futu_trd::types::{ModifyOrderOp, TrdMarket};
11use tokio::time::{Instant, sleep_until, timeout_at};
12const MODIFY_RECONCILE_ATTEMPTS: usize = 3;
13const MODIFY_RECONCILE_DELAY: Duration = Duration::from_millis(350);
14const MODIFY_RECONCILE_DEADLINE: Duration = Duration::from_millis(2_500);
15
16#[derive(Debug, Clone)]
17pub(super) struct ModifyOrderReconcileExpectation {
18 input_order_id: u64,
19 order_id_ex: String,
20 returned_order_id: u64,
21 fields: ModifyOrderEffectiveFields,
22 qty_scale: i64,
23 price_scale: i64,
24}
25
26impl ModifyOrderReconcileExpectation {
27 pub(super) fn new(
28 input_order_id: u64,
29 order_id_ex: impl Into<String>,
30 returned_order_id: u64,
31 market: TrdMarket,
32 qty: Option<f64>,
33 price: Option<f64>,
34 ) -> Self {
35 let uses_futures_price_precision =
36 modify_order_uses_futures_price_precision_like_cpp(Some(market as i32), 0);
37 let fields = effective_modify_order_fields(market, qty, price);
38 Self {
39 input_order_id,
40 order_id_ex: order_id_ex.into(),
41 returned_order_id,
42 fields,
43 qty_scale: if market == TrdMarket::Prediction {
44 100
45 } else {
46 1
47 },
48 price_scale: if uses_futures_price_precision {
49 1_000_000_000
50 } else {
51 10_000
52 },
53 }
54 }
55
56 fn matches(&self, order: &Order) -> bool {
57 (self.input_order_id != 0 && order.order_id == self.input_order_id)
58 || (self.returned_order_id != 0 && order.order_id == self.returned_order_id)
59 || (!self.order_id_ex.is_empty() && order.order_id_ex == self.order_id_ex)
60 }
61}
62
63#[derive(Debug, Clone, PartialEq, Eq)]
64pub(super) enum ModifyOrderObservation {
65 Applied,
66 NotApplied { detail: String },
67 OrderNotVisible,
68}
69
70#[derive(Debug, Clone, PartialEq, Eq)]
71pub(super) enum ModifyOrderReconcileOutcome {
72 Applied,
73 NotApplied { detail: String },
74 OrderNotVisible,
75 RefreshFailed,
76}
77
78impl ModifyOrderReconcileOutcome {
79 pub(super) fn label(&self) -> &'static str {
80 match self {
81 Self::Applied => "applied",
82 Self::NotApplied { .. } => "not_applied",
83 Self::OrderNotVisible => "order_not_visible",
84 Self::RefreshFailed => "refresh_failed",
85 }
86 }
87
88 pub(super) fn detail(&self) -> Option<&str> {
89 match self {
90 Self::NotApplied { detail } => Some(detail),
91 _ => None,
92 }
93 }
94
95 pub(super) fn failure_message(&self) -> Option<String> {
96 if self == &Self::Applied {
97 return None;
98 }
99 let detail = self
100 .detail()
101 .map(|detail| format!(" detail={detail}"))
102 .unwrap_or_default();
103 Some(format!(
104 "reconcile_required outcome={}{}",
105 self.label(),
106 detail
107 ))
108 }
109}
110
111#[derive(Debug, Clone, Copy)]
112pub(super) struct ModifyOrderReconcilePolicy {
113 attempts: usize,
114 delay: Duration,
115 deadline: Duration,
116}
117
118impl ModifyOrderReconcilePolicy {
119 pub(super) fn cli_default() -> Self {
120 Self {
121 attempts: MODIFY_RECONCILE_ATTEMPTS,
122 delay: MODIFY_RECONCILE_DELAY,
123 deadline: MODIFY_RECONCILE_DEADLINE,
124 }
125 }
126
127 #[cfg(test)]
128 pub(super) fn for_tests(attempts: usize) -> Self {
129 Self {
130 attempts,
131 delay: Duration::ZERO,
132 deadline: Duration::from_secs(1),
133 }
134 }
135}
136
137pub(super) fn effective_modify_order_fields(
143 market: TrdMarket,
144 qty: Option<f64>,
145 price: Option<f64>,
146) -> ModifyOrderEffectiveFields {
147 effective_fields_from_domain(
148 qty,
149 price,
150 market == TrdMarket::Prediction,
151 modify_order_uses_futures_price_precision_like_cpp(Some(market as i32), 0),
152 )
153}
154
155fn same_scaled_value(actual: f64, expected: f64, scale: i64) -> bool {
156 (actual * scale as f64).round() as i64 == (expected * scale as f64).round() as i64
157}
158
159pub(super) fn observe_modify_order(
160 orders: &[Order],
161 expected: &ModifyOrderReconcileExpectation,
162) -> ModifyOrderObservation {
163 let Some(order) = orders.iter().find(|order| expected.matches(order)) else {
164 return ModifyOrderObservation::OrderNotVisible;
165 };
166
167 let qty_matches = expected
168 .fields
169 .qty
170 .is_none_or(|qty| same_scaled_value(order.qty, qty, expected.qty_scale));
171 let price_matches = expected
172 .fields
173 .price
174 .is_none_or(|price| same_scaled_value(order.price, price, expected.price_scale));
175 if qty_matches && price_matches {
176 return ModifyOrderObservation::Applied;
177 }
178
179 let mut differences = Vec::new();
180 if !qty_matches {
181 differences.push(format!(
182 "qty expected={:?} actual={}",
183 expected.fields.qty, order.qty
184 ));
185 }
186 if !price_matches {
187 differences.push(format!(
188 "price expected={:?} actual={}",
189 expected.fields.price, order.price
190 ));
191 }
192 if !order.last_err_msg.is_empty() {
195 differences.push(format!("last_err_msg={:?}", order.last_err_msg));
196 }
197 ModifyOrderObservation::NotApplied {
198 detail: differences.join("; "),
199 }
200}
201
202pub(super) fn should_reconcile_modify_order(op: ModifyOrderOp) -> bool {
203 op == ModifyOrderOp::Normal
204}
205
206pub(super) async fn reconcile_modify_order_with<Refresh, RefreshFuture>(
207 policy: ModifyOrderReconcilePolicy,
208 expected: &ModifyOrderReconcileExpectation,
209 mut refresh: Refresh,
210) -> ModifyOrderReconcileOutcome
211where
212 Refresh: FnMut(Duration) -> RefreshFuture,
213 RefreshFuture: Future<Output = Result<Vec<Order>, String>>,
214{
215 let deadline = Instant::now() + policy.deadline;
216 let mut last_successful_observation = None;
217
218 for _ in 0..policy.attempts {
219 let wake_at = Instant::now() + policy.delay;
220 if timeout_at(deadline, sleep_until(wake_at)).await.is_err() {
221 break;
222 }
223 let remaining = deadline.saturating_duration_since(Instant::now());
224 if remaining.is_zero() {
225 break;
226 }
227 let result = refresh(remaining).await;
231 let Ok(orders) = result else {
232 continue;
233 };
234 let observation = observe_modify_order(&orders, expected);
235 if observation == ModifyOrderObservation::Applied {
236 return ModifyOrderReconcileOutcome::Applied;
237 }
238 last_successful_observation = Some(observation);
239 }
240
241 match last_successful_observation {
242 Some(ModifyOrderObservation::NotApplied { detail }) => {
243 ModifyOrderReconcileOutcome::NotApplied { detail }
244 }
245 Some(ModifyOrderObservation::OrderNotVisible) => {
246 ModifyOrderReconcileOutcome::OrderNotVisible
247 }
248 Some(ModifyOrderObservation::Applied) => ModifyOrderReconcileOutcome::Applied,
249 None => ModifyOrderReconcileOutcome::RefreshFailed,
250 }
251}