futu_backend/trade_query/
sim.rs1use super::common::{pf, pfo};
2use super::*;
3use crate::msg_header;
4
5pub async fn query_funds_sim(
6 backend: &BackendConn,
7 acc_id: u64,
8 trd_cache: &TrdCache,
9) -> Result<()> {
10 use crate::proto_internal::sim_user_asset_interface;
11 use prost::Message;
12
13 let market = sim_header_market_for_account(trd_cache, acc_id);
20 let req = sim_user_asset_interface::CashInfoReq {
21 msg_header: Some(msg_header::build_sim(
22 acc_id,
23 Some(vec![]),
24 Some(market),
25 None,
26 )),
27 };
28
29 let cmd = trade_query_command(TradeQueryOperation::Funds).sim_cmd;
30 let resp = backend.request(cmd, req.encode_to_vec()).await?;
31
32 let parsed: sim_user_asset_interface::CashInfoRsp = Message::decode(resp.body.as_ref())?;
33 sim_response_status_like_cpp(
34 "sim fund",
35 cmd,
36 parsed.result,
37 parsed.err_msg.as_deref(),
38 parsed.msg_header.as_ref(),
39 acc_id,
40 )?;
41
42 if let Some(cash_info) = &parsed.cash_info {
43 let static_info = &cash_info.static_info;
44 let dynamic_info = &cash_info.dynamic_info;
45
46 let frozen = static_info.as_ref().map(|s| pf(&s.hold)).unwrap_or(0.0);
47
48 if let Some(d) = dynamic_info {
49 trd_cache.update_funds(
50 acc_id,
51 CachedFunds {
52 power: pf(&d.max_power_long),
53 total_assets: pf(&d.total_asset),
54 cash: static_info.as_ref().map(|s| pf(&s.balance)).unwrap_or(0.0),
55 market_val: pf(&d.mv),
56 frozen_cash: frozen,
57 debt_cash: pf(&d.debit_recover),
58 avl_withdrawal_cash: pf(&d.drawable),
59 currency: cash_info.currency.map(|c| c as i32),
60 available_funds: None,
61 unrealized_pl: None,
62 realized_pl: None,
63 risk_level: d.risk_level.map(|r| r as i32),
64 initial_margin: None,
65 maintenance_margin: None,
66 max_power_short: pfo(&d.max_power_short),
67 net_cash_power: None,
68 long_mv: pfo(&d.long_mv),
69 short_mv: pfo(&d.short_mv),
70 pending_asset: None,
71 max_withdrawal: pfo(&d.cash_drawable),
72 risk_status: d.risk_status.map(|r| r as i32),
73 margin_call_margin: pfo(&d.margin_call_recover),
74 securities_assets: None,
75 fund_assets: None,
76 bond_assets: None,
77 crypto_mv: None,
78 exposure_level: None,
79 exposure_limit: None,
80 used_limit: None,
81 remaining_limit: None,
82 is_pdt: None,
84 pdt_seq: None,
85 beginning_dtbp: None,
86 remaining_dtbp: None,
87 dt_call_amount: None,
88 dt_status: None,
89 cash_info_list: vec![],
90 market_info_list: vec![],
91 },
92 );
93 }
94 }
95 Ok(())
96}
97
98pub(super) fn trd_market_to_sim_header_market(trd_market: i32) -> u32 {
106 u32::try_from(trd_market).unwrap_or(0)
107}
108
109fn sim_header_market_for_account(trd_cache: &TrdCache, acc_id: u64) -> u32 {
110 let trd_market = trd_cache
111 .accounts
112 .get(&acc_id)
113 .and_then(|entry| {
114 let acc = entry.value();
115 acc.trd_market
116 .or_else(|| acc.trd_market_auth_list.first().copied())
117 })
118 .unwrap_or(0);
119 trd_market_to_sim_header_market(trd_market)
120}
121
122fn sim_trd_market_to_sec_market(trd_market: u32) -> Option<i32> {
127 match trd_market {
128 1 | 9 | 10 | 113 => Some(1), 2 | 7 | 11 | 100 | 123 => Some(2), 3 | 4 => Some(31), 6 | 12 | 124 => Some(41), 8 => Some(61), 13 | 15 | 126 => Some(51), 111 | 125 => Some(71), 112 => Some(81), _ => None,
137 }
138}
139
140fn sim_trd_market_to_currency(trd_market: u32) -> Option<i32> {
141 match trd_market {
142 1 | 9 | 10 | 113 => Some(1), 2 | 7 | 11 | 100 | 123 => Some(2), 3 | 4 => Some(3), 13 | 15 | 126 => Some(4), 6 | 12 | 124 => Some(5), 8 => Some(6), 112 => Some(7), 111 | 125 => Some(8), _ => None,
151 }
152}
153
154pub async fn query_positions_sim(
155 backend: &BackendConn,
156 acc_id: u64,
157 trd_market: i32,
158 trd_cache: &TrdCache,
159) -> Result<()> {
160 use crate::proto_internal::sim_user_asset_interface;
161 use prost::Message;
162
163 let market = trd_market_to_sim_header_market(trd_market);
168 let req = sim_user_asset_interface::PstnInfoReq {
169 msg_header: Some(msg_header::build_sim(
170 acc_id,
171 Some(vec![]),
172 Some(market),
173 None,
174 )),
175 };
176
177 let cmd = trade_query_command(TradeQueryOperation::Positions).sim_cmd;
178 let resp = backend.request(cmd, req.encode_to_vec()).await?;
179
180 let parsed: sim_user_asset_interface::PstnInfoRsp = Message::decode(resp.body.as_ref())?;
181 sim_response_status_like_cpp(
182 "sim position",
183 cmd,
184 parsed.result,
185 parsed.err_msg.as_deref(),
186 parsed.msg_header.as_ref(),
187 acc_id,
188 )?;
189
190 let positions: Vec<CachedPosition> = parsed
191 .pstn_infos
192 .iter()
193 .map(|p| {
194 let trd_market = p.market.and_then(|m| i32::try_from(m).ok());
195 CachedPosition {
196 position_id: p.pstn_id.as_ref().and_then(|s| s.parse().ok()).unwrap_or(0),
197 position_side: p.pstn_type.unwrap_or(0),
198 code: p.symbol.as_ref().cloned().unwrap_or_default(),
199 name: p.stock_name.as_ref().cloned().unwrap_or_default(),
200 qty: pf(&p.qty),
201 can_sell_qty: pf(&p.qty_avbl),
202 price: pf(&p.cur_price),
203 cost_price: pf(&p.cost_price),
204 val: pf(&p.mv),
205 pl_val: pf(&p.profit),
206 pl_ratio: pfo(&p.profit_ratio),
207 sec_market: p.market.and_then(sim_trd_market_to_sec_market),
208 td_pl_val: pfo(&p.today_profit),
209 td_trd_val: pfo(&p.today_turnover),
210 td_buy_val: pfo(&p.today_buy_turnover),
211 td_buy_qty: pfo(&p.today_buy_qty),
212 td_sell_val: pfo(&p.today_sell_turnover),
213 td_sell_qty: pfo(&p.today_sell_qty),
214 unrealized_pl: None,
215 realized_pl: None,
216 currency: p.market.and_then(sim_trd_market_to_currency),
217 trd_market,
218 diluted_cost_price: pfo(&p.cost_price),
219 average_cost_price: pfo(&p.buy_avg_price),
220 average_pl_ratio: None,
221 expiry_date_distance: None,
223 }
224 })
225 .collect();
226
227 tracing::debug!(acc_id, count = positions.len(), "sim positions cached");
228 trd_cache.update_positions(acc_id, positions);
231
232 Ok(())
233}
234
235fn sim_response_status_like_cpp(
236 kind: &str,
237 cmd: u16,
238 result: Option<i32>,
239 err_msg: Option<&str>,
240 msg_header: Option<&crate::proto_internal::sim_odr_sys_cmn::MsgHeader>,
241 acc_id: u64,
242) -> Result<()> {
243 let Some(result_code) = result else {
248 return Err(futu_core::error::FutuError::Codec(format!(
249 "{kind} CMD {cmd} missing result"
250 )));
251 };
252 if result_code != 0 {
253 let err_msg = err_msg.unwrap_or("unknown");
254 tracing::warn!(
255 acc_id,
256 result = result_code,
257 err = err_msg,
258 kind,
259 cmd,
260 "sim query returned business error"
261 );
262 return Err(futu_core::error::FutuError::ServerError {
263 ret_type: result_code,
264 msg: format!("{kind} CMD {cmd}: {err_msg}"),
265 });
266 }
267 let header = msg_header.ok_or_else(|| {
268 futu_core::error::FutuError::Codec(format!("{kind} CMD {cmd} missing msg_header"))
269 })?;
270 let backend_acc_id = header.account_id.unwrap_or(0);
271 if backend_acc_id != acc_id {
272 return Err(futu_core::error::FutuError::Codec(format!(
273 "{kind} CMD {cmd} account mismatch: server={backend_acc_id} local={acc_id}"
274 )));
275 }
276 Ok(())
277}
278
279#[cfg(test)]
280mod tests;