1use super::common::{pf, pfo};
2use super::*;
3use crate::command_runtime::execute_trade_read;
4use crate::msg_header;
5use bytes::Bytes;
6use futu_command_spec::TradeQueryEnvironment;
7use futu_domain_trade_account::{
8 SimFundsDynamicFacts, SimFundsFacts, SimFundsStaticFacts, SimPositionIdentityProfitFacts,
9 SimPositionMarketFacts, SimQueryResponseStatusDecision, SimQueryResponseStatusFacts,
10 decide_sim_query_response_status_like_cpp, project_sim_funds_like_cpp,
11 project_sim_position_currency_like_cpp, project_sim_position_identity_profit_like_cpp,
12 project_sim_position_sec_market_like_cpp, sim_account_header_market_like_cpp,
13 sim_account_query_market_from_cache_like_cpp,
14};
15
16pub async fn query_funds_sim(
17 backend: &BackendConn,
18 acc_id: u64,
19 trd_cache: &TrdCache,
20) -> Result<()> {
21 use crate::proto_internal::sim_user_asset_interface;
22 use prost::Message;
23
24 let market = sim_header_market_for_account(trd_cache, acc_id);
31 let req = sim_user_asset_interface::CashInfoReq {
32 msg_header: Some(msg_header::build_sim(
33 acc_id,
34 Some(vec![]),
35 Some(market),
36 None,
37 )),
38 };
39
40 let operation = TradeQueryOperation::Funds;
41 let cmd = sim_trade_query_cmd(operation)?;
42 let resp = execute_trade_read(
43 backend,
44 operation,
45 TradeQueryEnvironment::Sim,
46 None,
47 Bytes::from(req.encode_to_vec()),
48 )
49 .await?;
50
51 let parsed: sim_user_asset_interface::CashInfoRsp = Message::decode(resp.body.as_ref())?;
52 ensure_sim_query_response_status(
53 "sim fund",
54 cmd,
55 parsed.result,
56 parsed.err_msg.as_deref(),
57 parsed.msg_header.as_ref(),
58 acc_id,
59 )?;
60
61 let cash_info = parsed.cash_info.as_ref().ok_or_else(|| {
62 futu_core::error::FutuError::Codec(
63 "sim fund response missing required cash_info".to_string(),
64 )
65 })?;
66 let facts = sim_funds_facts(cash_info);
67 let projected = project_sim_funds_like_cpp(&facts)
68 .map_err(|err| futu_core::error::FutuError::Codec(err.to_string()))?;
69 trd_cache.update_funds(acc_id, cached_sim_funds(projected));
70 Ok(())
71}
72
73fn sim_funds_facts(
74 cash_info: &crate::proto_internal::sim_odr_sys_cmn::CashInfo,
75) -> SimFundsFacts<'_> {
76 SimFundsFacts {
77 static_info: cash_info
78 .static_info
79 .as_ref()
80 .map(|info| SimFundsStaticFacts {
81 balance: info.balance.as_deref(),
82 hold: info.hold.as_deref(),
83 }),
84 dynamic_info: cash_info
85 .dynamic_info
86 .as_ref()
87 .map(|info| SimFundsDynamicFacts {
88 max_power_long: info.max_power_long.as_deref(),
89 total_asset: info.total_asset.as_deref(),
90 mv: info.mv.as_deref(),
91 debit_recover: info.debit_recover.as_deref(),
92 drawable: info.drawable.as_deref(),
93 short_mv: info.short_mv.as_deref(),
94 long_mv: info.long_mv.as_deref(),
95 margin_call: info.margin_call.as_deref(),
96 unrealized_profit: info.unrealized_profit.as_deref(),
97 realized_profit: info.realized_profit.as_deref(),
98 loan_max: info.loan_max.as_deref(),
99 margin_call_recover: info.margin_call_recover.as_deref(),
100 risk_level: info.risk_level,
101 risk_status: info.risk_status,
102 margin_call_balance: info.margin_call_balance.as_deref(),
103 margin_call_balance_ratio: info.margin_call_balance_ratio.as_deref(),
104 absolute_safe_mcb_ratio: info.absolute_safe_mcb_ratio.as_deref(),
105 max_power_short: info.max_power_short.as_deref(),
106 }),
107 }
108}
109
110fn cached_sim_funds(projected: futu_domain_trade_account::ProjectedSimFunds) -> CachedFunds {
111 CachedFunds {
112 power: projected.max_buy_power,
113 total_assets: projected.net_asset,
114 cash: projected.total_cash,
115 market_val: projected.market_value,
116 frozen_cash: projected.frozen_fund,
117 debt_cash: projected.debit_recover,
118 avl_withdrawal_cash: projected.drawable,
119 currency: None,
121 available_funds: None,
122 unrealized_pl: Some(projected.unrealized_profit),
123 realized_pl: Some(projected.realized_profit),
124 risk_level: Some(projected.risk_level),
125 initial_margin: None,
126 maintenance_margin: Some(projected.maintenance_margin),
127 max_power_short: Some(projected.short_power),
128 net_cash_power: None,
129 long_mv: Some(projected.long_mv),
130 short_mv: Some(projected.short_mv),
131 pending_asset: None,
132 max_withdrawal: None,
134 risk_status: Some(projected.risk_status),
135 margin_call_margin: None,
138 securities_assets: None,
139 fund_assets: None,
140 bond_assets: None,
141 crypto_mv: None,
142 exposure_level: None,
143 exposure_limit: None,
144 used_limit: None,
145 remaining_limit: None,
146 is_pdt: None,
148 pdt_seq: None,
149 beginning_dtbp: None,
150 remaining_dtbp: None,
151 dt_call_amount: None,
152 dt_status: None,
153 cash_info_list: vec![],
154 market_info_list: vec![],
155 }
156}
157
158fn sim_header_market_for_account(trd_cache: &TrdCache, acc_id: u64) -> u32 {
166 let trd_market = trd_cache
167 .accounts
168 .get(&acc_id)
169 .map(|entry| {
170 let acc = entry.value();
171 sim_account_query_market_from_cache_like_cpp(
172 acc.trd_market,
173 &acc.trd_market_auth_list,
174 0,
175 )
176 })
177 .unwrap_or(0);
178 sim_account_header_market_like_cpp(trd_market)
179}
180
181pub async fn query_positions_sim(
182 backend: &BackendConn,
183 acc_id: u64,
184 trd_market: i32,
185 trd_cache: &TrdCache,
186) -> Result<()> {
187 use crate::proto_internal::sim_user_asset_interface;
188 use prost::Message;
189
190 let market = sim_account_header_market_like_cpp(trd_market);
195 let req = sim_user_asset_interface::PstnInfoReq {
196 msg_header: Some(msg_header::build_sim(
197 acc_id,
198 Some(vec![]),
199 Some(market),
200 None,
201 )),
202 };
203
204 let operation = TradeQueryOperation::Positions;
205 let cmd = sim_trade_query_cmd(operation)?;
206 let resp = execute_trade_read(
207 backend,
208 operation,
209 TradeQueryEnvironment::Sim,
210 None,
211 Bytes::from(req.encode_to_vec()),
212 )
213 .await?;
214
215 let parsed: sim_user_asset_interface::PstnInfoRsp = Message::decode(resp.body.as_ref())?;
216 ensure_sim_query_response_status(
217 "sim position",
218 cmd,
219 parsed.result,
220 parsed.err_msg.as_deref(),
221 parsed.msg_header.as_ref(),
222 acc_id,
223 )?;
224
225 let positions: Vec<CachedPosition> = parsed
226 .pstn_infos
227 .iter()
228 .map(|p| {
229 let backend_position_id = p.pstn_id.as_deref().ok_or_else(|| {
230 futu_core::error::FutuError::Codec(
231 "sim position response missing required pstn_id".to_string(),
232 )
233 })?;
234 let raw_market = p.market;
235 let trd_market = raw_market.and_then(|m| i32::try_from(m).ok());
236 let identity_profit =
237 project_sim_position_identity_profit_like_cpp(SimPositionIdentityProfitFacts {
238 backend_position_id,
239 raw_market,
240 backend_profit_ratio: p.profit_ratio.as_deref(),
241 backend_unrealized_pl: pfo(&p.unrealized_profit),
242 backend_realized_pl: pfo(&p.realized_profit),
243 });
244 Ok(CachedPosition {
245 position_id: identity_profit.position_id,
246 business_position_id: None,
247 position_acc_id: None,
248 sub_account_id: None,
249 position_side: p.pstn_type.unwrap_or(0),
250 code: p.symbol.as_ref().cloned().unwrap_or_default(),
251 name: p.stock_name.as_ref().cloned().unwrap_or_default(),
252 qty: pf(&p.qty),
253 can_sell_qty: pf(&p.qty_avbl),
254 price: pf(&p.cur_price),
255 cost_price: pf(&p.cost_price),
256 val: pf(&p.mv),
257 pl_val: pf(&p.profit),
258 pl_ratio: identity_profit.pl_ratio,
259 sec_market: project_sim_position_sec_market_like_cpp(SimPositionMarketFacts {
260 raw_market,
261 code: p.symbol.as_deref(),
262 exchange: None,
263 }),
264 td_pl_val: pfo(&p.today_profit),
265 td_trd_val: pfo(&p.today_turnover),
266 td_buy_val: pfo(&p.today_buy_turnover),
267 td_buy_qty: pfo(&p.today_buy_qty),
268 td_sell_val: pfo(&p.today_sell_turnover),
269 td_sell_qty: pfo(&p.today_sell_qty),
270 unrealized_pl: identity_profit.unrealized_pl,
271 realized_pl: identity_profit.realized_pl,
272 currency: project_sim_position_currency_like_cpp(raw_market),
273 trd_market,
274 diluted_cost_price: pfo(&p.cost_price),
275 average_cost_price: pfo(&p.buy_avg_price),
276 average_pl_ratio: identity_profit.average_pl_ratio,
277 combo_id: None,
278 business_combo_id: None,
279 strategy_type: None,
280 position_type: None,
281 acc_id: None,
282 jp_acc_type: None,
283 })
285 })
286 .collect::<Result<Vec<_>>>()?;
287
288 tracing::debug!(count = positions.len(), "sim positions cached");
289 trd_cache.update_positions(acc_id, positions);
292
293 Ok(())
294}
295
296fn sim_trade_query_cmd(operation: TradeQueryOperation) -> Result<u16> {
297 trade_query_command(operation)
298 .sim
299 .map(|route| route.cmd_id)
300 .ok_or_else(|| {
301 futu_core::error::FutuError::Codec(format!(
302 "{operation:?} has no registered simulated-account backend route"
303 ))
304 })
305}
306
307fn ensure_sim_query_response_status(
308 kind: &str,
309 cmd: u16,
310 result: Option<i32>,
311 err_msg: Option<&str>,
312 msg_header: Option<&crate::proto_internal::sim_odr_sys_cmn::MsgHeader>,
313 acc_id: u64,
314) -> Result<()> {
315 let decision = decide_sim_query_response_status_like_cpp(SimQueryResponseStatusFacts {
316 result,
317 has_msg_header: msg_header.is_some(),
318 backend_account_id: msg_header.and_then(|header| header.account_id),
319 local_account_id: acc_id,
320 err_msg,
321 });
322
323 match decision {
324 SimQueryResponseStatusDecision::Accepted => Ok(()),
325 SimQueryResponseStatusDecision::BusinessError {
326 result,
327 err_msg: backend_msg,
328 } => {
329 let msg = decision
330 .error_message(kind, cmd)
331 .unwrap_or_else(|| format!("{kind} query returned unrenderable business error"));
332 tracing::warn!(
333 result,
334 err = backend_msg.unwrap_or("unknown"),
335 kind,
336 cmd,
337 "sim query returned business error"
338 );
339 Err(futu_core::error::FutuError::ServerError {
340 ret_type: result,
341 msg,
342 })
343 }
344 _ => Err(futu_core::error::FutuError::Codec(
345 decision
346 .error_message(kind, cmd)
347 .unwrap_or_else(|| format!("{kind} query status rejected for cmd {cmd}")),
348 )),
349 }
350}
351
352#[cfg(test)]
353mod tests;