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 business_position_id: None,
198 position_acc_id: None,
199 sub_account_id: None,
200 position_side: p.pstn_type.unwrap_or(0),
201 code: p.symbol.as_ref().cloned().unwrap_or_default(),
202 name: p.stock_name.as_ref().cloned().unwrap_or_default(),
203 qty: pf(&p.qty),
204 can_sell_qty: pf(&p.qty_avbl),
205 price: pf(&p.cur_price),
206 cost_price: pf(&p.cost_price),
207 val: pf(&p.mv),
208 pl_val: pf(&p.profit),
209 pl_ratio: pfo(&p.profit_ratio),
210 sec_market: p.market.and_then(sim_trd_market_to_sec_market),
211 td_pl_val: pfo(&p.today_profit),
212 td_trd_val: pfo(&p.today_turnover),
213 td_buy_val: pfo(&p.today_buy_turnover),
214 td_buy_qty: pfo(&p.today_buy_qty),
215 td_sell_val: pfo(&p.today_sell_turnover),
216 td_sell_qty: pfo(&p.today_sell_qty),
217 unrealized_pl: None,
218 realized_pl: None,
219 currency: p.market.and_then(sim_trd_market_to_currency),
220 trd_market,
221 diluted_cost_price: pfo(&p.cost_price),
222 average_cost_price: pfo(&p.buy_avg_price),
223 average_pl_ratio: None,
224 combo_id: None,
225 business_combo_id: None,
226 strategy_type: None,
227 position_type: None,
228 acc_id: None,
229 jp_acc_type: None,
230 expiry_date_distance: None,
232 }
233 })
234 .collect();
235
236 tracing::debug!(acc_id, count = positions.len(), "sim positions cached");
237 trd_cache.update_positions(acc_id, positions);
240
241 Ok(())
242}
243
244fn sim_response_status_like_cpp(
245 kind: &str,
246 cmd: u16,
247 result: Option<i32>,
248 err_msg: Option<&str>,
249 msg_header: Option<&crate::proto_internal::sim_odr_sys_cmn::MsgHeader>,
250 acc_id: u64,
251) -> Result<()> {
252 let Some(result_code) = result else {
257 return Err(futu_core::error::FutuError::Codec(format!(
258 "{kind} CMD {cmd} missing result"
259 )));
260 };
261 if result_code != 0 {
262 let err_msg = err_msg.unwrap_or("unknown");
263 tracing::warn!(
264 acc_id,
265 result = result_code,
266 err = err_msg,
267 kind,
268 cmd,
269 "sim query returned business error"
270 );
271 return Err(futu_core::error::FutuError::ServerError {
272 ret_type: result_code,
273 msg: format!("{kind} CMD {cmd}: {err_msg}"),
274 });
275 }
276 let header = msg_header.ok_or_else(|| {
277 futu_core::error::FutuError::Codec(format!("{kind} CMD {cmd} missing msg_header"))
278 })?;
279 let backend_acc_id = header.account_id.ok_or_else(|| {
280 futu_core::error::FutuError::Codec(format!(
281 "{kind} CMD {cmd} msg_header missing account_id"
282 ))
283 })?;
284 if backend_acc_id != acc_id {
285 return Err(futu_core::error::FutuError::Codec(format!(
286 "{kind} CMD {cmd} account mismatch: server={backend_acc_id} local={acc_id}"
287 )));
288 }
289 Ok(())
290}
291
292#[cfg(test)]
293mod tests;