Skip to main content

futucli/cmd/
account.rs

1//! `futucli account` / `position` / `order` / `deal` — 只读账户查询
2//!
3//! 本模块只承载账户/持仓/订单/成交查询;下单、改单、撤单等写命令在
4//! `trade_ext.rs`,并由 CLI 层的 `--confirm` / env / scope guard 控制误操作风险。
5
6use anyhow::{Result, bail};
7use serde::Serialize;
8use tabled::Tabled;
9
10use crate::common::connect_gateway;
11use crate::output::OutputFormat;
12use futu_trd::{
13    currency, read_plan,
14    types::{TrdEnv, TrdHeader, TrdMarket},
15};
16
17mod list;
18#[cfg(test)]
19mod tests;
20
21#[cfg(test)]
22use list::{
23    AccJson, account_matches_sdk_filter, app_visible_card_num_resolution,
24    parse_account_market_filter, parse_account_security_firm_filter,
25};
26pub use list::{list_accounts, resolve_account_locator};
27
28// ========== 共享:参数解析 ==========
29
30/// v1.4.102 codex 27 F7 (P1) fix: write path 专用 parser, 显式拒 fund market.
31///
32/// `place-order` / `modify-order` / `cancel-order` / `cancel-all-order` 等
33/// CLI 写命令应用此 fn (不直接用 `parse_trd_market`). fund markets
34/// 仅 view-only read endpoints 支持. read 命令 (`positions` / `funds` /
35/// `cash-log` 等) 仍用 `parse_trd_market`.
36pub fn parse_trd_market_for_write(s: &str) -> Result<TrdMarket> {
37    let m = parse_trd_market(s)?;
38    if let Some(label) = futu_trd::market::view_only_fund_market_label(m as i32) {
39        bail!(
40            "trd market {label} 仅支持 view-only read commands \
41             (positions/funds/cash-log/history-orders/history-fills); \
42             write commands (place-order/modify-order/cancel-order/cancel-all-order) \
43             用对应主市场, daemon 自动按持仓 broker 路由. v1.4.102 audit 27 F7 fix"
44        )
45    }
46    Ok(m)
47}
48
49pub fn parse_trd_market(s: &str) -> Result<TrdMarket> {
50    // v1.4.93/v1.4.111: 对齐 `Trd_Common.proto::TrdMarket` 官方全集。
51    // `Trd_Common.proto::TrdMarket` + MCP schema. 也接 int 值 (per Trd_Common.proto).
52    // 5 国 (SG/AU/JP/MY/CA) + Futures=5 在 v1.4.86-90 只 4 variants 时挂.
53    //
54    // v1.4.102/v1.4.111 fund-market handoff (per pitfall #54 schema-runtime-parser sync):
55    // fund markets 是 view-only 融资融券 / 基金账户.
56    let trimmed = s.trim();
57    let upper = trimmed.to_ascii_uppercase();
58    let m = match upper.as_str() {
59        "HK" | "1" => TrdMarket::HK,
60        "US" | "2" => TrdMarket::US,
61        "CN" | "3" => TrdMarket::CN,
62        "HKCC" | "4" => TrdMarket::HKCC,
63        "FUTURES" | "5" => TrdMarket::Futures,
64        "SG" | "6" => TrdMarket::SG,
65        "CRYPTO" | "7" => TrdMarket::Crypto,
66        "AU" | "8" => TrdMarket::AU,
67        "FUTURES_SIMULATE_HK" | "FUTURESSIMULATEHK" | "10" => TrdMarket::FuturesSimulateHK,
68        "FUTURES_SIMULATE_US" | "FUTURESSIMULATEUS" | "11" => TrdMarket::FuturesSimulateUS,
69        "FUTURES_SIMULATE_SG" | "FUTURESSIMULATESG" | "12" => TrdMarket::FuturesSimulateSG,
70        "FUTURES_SIMULATE_JP" | "FUTURESSIMULATEJP" | "13" => TrdMarket::FuturesSimulateJP,
71        "JP" | "15" => TrdMarket::JP,
72        "MY" | "111" => TrdMarket::MY,
73        "CA" | "112" => TrdMarket::CA,
74        "HKFUND" | "HK_FUND" | "113" => TrdMarket::HKFund,
75        "USFUND" | "US_FUND" | "123" => TrdMarket::USFund,
76        "SGFUND" | "SG_FUND" | "124" => TrdMarket::SGFund,
77        "MYFUND" | "MY_FUND" | "125" => TrdMarket::MYFund,
78        "JPFUND" | "JP_FUND" | "126" => TrdMarket::JPFund,
79        other => bail!(
80            "unknown trd market {other:?} \
81             (HK|US|CN|HKCC|FUTURES|SG|CRYPTO|AU|FUTURES_SIMULATE_HK|\
82             FUTURES_SIMULATE_US|FUTURES_SIMULATE_SG|FUTURES_SIMULATE_JP|JP|MY|CA|\
83             HKFUND|USFUND|SGFUND|MYFUND|JPFUND or official TrdMarket int)"
84        ),
85    };
86    Ok(m)
87}
88
89pub fn parse_trd_env(s: &str) -> Result<TrdEnv> {
90    let e = match s.trim().to_ascii_lowercase().as_str() {
91        "simulate" | "sim" => TrdEnv::Simulate,
92        "real" => TrdEnv::Real,
93        other => bail!("unknown trd env {other:?} (real|simulate)"),
94    };
95    Ok(e)
96}
97
98fn build_header(env: TrdEnv, acc_id: u64, market: TrdMarket) -> TrdHeader {
99    TrdHeader {
100        trd_env: env,
101        acc_id,
102        trd_market: market,
103        jp_acc_type: None,
104    }
105}
106
107fn format_pl_ratio_percent(ratio_value: f64) -> String {
108    // Gateway/JSON keep C++ APIServer numeric `Position.plRatio` unchanged.
109    // CLI shows the user-facing percent form: `0.6078` -> `+60.78%`.
110    let percent = ratio_value * 100.0;
111    if percent > 0.0 {
112        format!("+{percent:.2}%")
113    } else {
114        format!("{percent:.2}%")
115    }
116}
117
118// ========== account funds ==========
119
120#[derive(Tabled)]
121struct FundsRow {
122    #[tabled(rename = "Metric")]
123    name: &'static str,
124    #[tabled(rename = "Value")]
125    value: String,
126}
127
128#[derive(Serialize)]
129struct FundsJson {
130    power: f64,
131    total_assets: f64,
132    cash: f64,
133    market_val: f64,
134    frozen_cash: f64,
135    debt_cash: f64,
136    avl_withdrawal_cash: f64,
137    #[serde(skip_serializing_if = "Option::is_none")]
138    crypto_mv: Option<f64>,
139    #[serde(skip_serializing_if = "Option::is_none")]
140    exposure_level: Option<i32>,
141    #[serde(skip_serializing_if = "Option::is_none")]
142    exposure_limit: Option<f64>,
143    #[serde(skip_serializing_if = "Option::is_none")]
144    used_limit: Option<f64>,
145    #[serde(skip_serializing_if = "Option::is_none")]
146    remaining_limit: Option<f64>,
147    /// v1.4.96 BUG #012 hotfix (external reviewer double-tester report 2026-04-26):
148    /// 账户主币种 (HKD / USD / CNH / 等), 之前 CLI 漏打印, 与 REST `/api/funds` +
149    /// MCP `futu_get_funds` 3-surface 不一致.
150    #[serde(skip_serializing_if = "Option::is_none")]
151    currency: Option<&'static str>,
152    /// v1.4.103 (external reviewer 反馈 P1): 综合账户多币种 cash 细分.
153    /// `[{currency: "USD", cash: 208532.79, available_balance: ..., net_cash_power: ...}, ...]`
154    #[serde(skip_serializing_if = "Vec::is_empty")]
155    cash_info_list: Vec<CashInfoJson>,
156    /// v1.4.103 (external reviewer 反馈 P1): 综合账户多市场 assets 细分.
157    /// `[{market: "US", assets: 8151509.8}, ...]`
158    #[serde(skip_serializing_if = "Vec::is_empty")]
159    market_info_list: Vec<MarketInfoJson>,
160    /// 用户显式传 currency, 但 backend 按账户基准币种返回时的提示。
161    #[serde(skip_serializing_if = "Option::is_none")]
162    currency_warning: Option<String>,
163}
164
165/// v1.4.103 (external reviewer P1): 单币种 cash detail (for cash_info_list).
166#[derive(Serialize)]
167struct CashInfoJson {
168    currency: &'static str,
169    cash: f64,
170    available_balance: f64,
171    net_cash_power: f64,
172}
173
174/// v1.4.103 (external reviewer P1): 单市场 assets detail (for market_info_list).
175#[derive(Serialize)]
176struct MarketInfoJson {
177    market: &'static str,
178    assets: f64,
179}
180
181/// v1.4.103: trd_market enum int → 字符串.
182fn trd_market_int_to_str(m: Option<i32>) -> &'static str {
183    m.and_then(futu_trd::market::trd_market_label)
184        .unwrap_or("?")
185}
186
187pub async fn funds(
188    gateway: &str,
189    env: &str,
190    acc_id: u64,
191    market: Option<&str>,
192    currency: Option<&str>,
193    format: OutputFormat,
194) -> Result<()> {
195    // v1.4.106 ergonomics: --market 改 optional. 不传时 trd_market 设
196    // `TrdMarket::Unknown=0`, daemon `GetFundsHandler` 按 `acc_id` cache 推断
197    // (lookup `acc.trd_market` 作 currency derive 兜底; 不依赖 header.trd_market
198    // 做主路由 filter, 见 `crates/futu-gateway-trd/src/handlers/trd/query.rs:88+`).
199    // 普通账户 (HK-only/US-only) 主市场由 cache.acc_entry.trd_market 决定;
200    // 综合账户 (uniCardNum 非空) acc_id 路径已经 cross-market view, market
201    // 参数对结果无影响.
202    let trd_market = match market {
203        Some(m) => parse_trd_market(m)?,
204        None => TrdMarket::Unknown,
205    };
206    let header = build_header(parse_trd_env(env)?, acc_id, trd_market);
207    let (client, _push_rx) = connect_gateway(gateway, "futucli-funds").await?;
208
209    // v1.4.103 (external reviewer P1): parse currency if provided, pass to backend.
210    let currency_int: Option<i32> = match currency {
211        Some(s) => Some(currency::parse_currency_label(s)?),
212        None => None,
213    };
214
215    let f = futu_trd::account::get_funds_with_currency(&client, &header, currency_int).await?;
216
217    // FX sanity check lives in the shared trade-read domain; CLI only decides
218    // whether to print it on stderr and include it in JSON output.
219    let currency_warning = read_plan::funds_currency_mismatch_warning(currency_int, f.currency);
220    if let Some(ref warn) = currency_warning {
221        eprintln!("⚠️  {warn}");
222    }
223
224    // v1.4.96 BUG #012: 显示账户主币种 (与 REST/MCP 对齐)
225    let currency = currency::known_currency_label(f.currency);
226    // v1.4.106 codex 1612 Candidate A: `Cash` label 太泛, 用户误把
227    // top-level summary cash 当作"所有 cash_info_list 跨币种相加".
228    // 改 `CashSummary(<cur>)` 让 user 知道这是 backend 直传的单一币种
229    // summary, 不等于 cash_info_list.sum() (跨币种不能无汇率相加). backend
230    // 未下 top-level currency 时不伪造 `?` 标签,避免让用户误以为整张表
231    // 已有明确币种。
232    let cash_summary_label: String = currency
233        .map(|cur| format!("CashSummary({cur})"))
234        .unwrap_or_else(|| "CashSummary".to_string());
235    let mut rows = vec![
236        FundsRow {
237            name: "Power",
238            value: format!("{:.2}", f.power),
239        },
240        FundsRow {
241            name: "TotalAssets",
242            value: format!("{:.2}", f.total_assets),
243        },
244        FundsRow {
245            name: Box::leak(cash_summary_label.into_boxed_str()),
246            value: format!("{:.2}", f.cash),
247        },
248        FundsRow {
249            name: "MarketVal",
250            value: format!("{:.2}", f.market_val),
251        },
252        FundsRow {
253            name: "FrozenCash",
254            value: format!("{:.2}", f.frozen_cash),
255        },
256        FundsRow {
257            name: "DebtCash",
258            value: format!("{:.2}", f.debt_cash),
259        },
260        FundsRow {
261            name: "AvlWithdrawalCash",
262            value: format!("{:.2}", f.avl_withdrawal_cash),
263        },
264    ];
265    // v1.4.96 BUG #012: 加 Currency 列 (-) 当 backend 未返时
266    rows.push(FundsRow {
267        name: "Currency",
268        value: currency
269            .map(|s| s.to_string())
270            .unwrap_or_else(|| "-".into()),
271    });
272    if let Some(value) = f.crypto_mv {
273        rows.push(FundsRow {
274            name: "CryptoMv",
275            value: format!("{value:.2}"),
276        });
277    }
278    if let Some(value) = f.exposure_level {
279        rows.push(FundsRow {
280            name: "ExposureLevel",
281            value: value.to_string(),
282        });
283    }
284    if let Some(value) = f.exposure_limit {
285        rows.push(FundsRow {
286            name: "ExposureLimit",
287            value: format!("{value:.2}"),
288        });
289    }
290    if let Some(value) = f.used_limit {
291        rows.push(FundsRow {
292            name: "UsedLimit",
293            value: format!("{value:.2}"),
294        });
295    }
296    if let Some(value) = f.remaining_limit {
297        rows.push(FundsRow {
298            name: "RemainingLimit",
299            value: format!("{value:.2}"),
300        });
301    }
302
303    // v1.4.103 (external reviewer 反馈 P1): 综合账户多币种 / 多市场细分.
304    // 当 cash_info_list / market_info_list 非空 (综合账户) 时, 展开 sub-rows
305    // 让用户看到细分数据 — 之前只显示 7 字段 top-level, 综合账户用户根本不知
306    // 道 USD market 下面有 208K USD cash / 1.05M USD assets.
307    if !f.cash_info_list.is_empty() {
308        rows.push(FundsRow {
309            name: "── CashByCurrency ──",
310            value: String::new(),
311        });
312        for ci in &f.cash_info_list {
313            let cur_str = currency::known_currency_label(ci.currency).unwrap_or("?");
314            rows.push(FundsRow {
315                name: Box::leak(format!("  {} cash", cur_str).into_boxed_str()),
316                value: format!("{:.2}", ci.cash.unwrap_or(0.0)),
317            });
318            let ncp = ci.net_cash_power.unwrap_or(0.0);
319            if ncp.abs() > 0.001 {
320                rows.push(FundsRow {
321                    name: Box::leak(format!("  {} netCashPower", cur_str).into_boxed_str()),
322                    value: format!("{:.2}", ncp),
323                });
324            }
325        }
326    }
327    if !f.market_info_list.is_empty() {
328        rows.push(FundsRow {
329            name: "── AssetsByMarket ──",
330            value: String::new(),
331        });
332        for mi in &f.market_info_list {
333            // 只显示非零 assets (省得过长)
334            let assets = mi.assets.unwrap_or(0.0);
335            if assets.abs() < 0.001 {
336                continue;
337            }
338            let mkt_str = trd_market_int_to_str(mi.trd_market);
339            rows.push(FundsRow {
340                name: Box::leak(format!("  {} assets", mkt_str).into_boxed_str()),
341                value: format!("{:.2}", assets),
342            });
343        }
344    }
345
346    // v1.4.103 (external reviewer P1): JSON output 也含细分 list (与表格视图一致).
347    let cash_info_jsons: Vec<CashInfoJson> = f
348        .cash_info_list
349        .iter()
350        .map(|ci| CashInfoJson {
351            currency: currency::known_currency_label(ci.currency).unwrap_or("UNKNOWN"),
352            cash: ci.cash.unwrap_or(0.0),
353            available_balance: ci.available_balance.unwrap_or(0.0),
354            net_cash_power: ci.net_cash_power.unwrap_or(0.0),
355        })
356        .collect();
357    let market_info_jsons: Vec<MarketInfoJson> = f
358        .market_info_list
359        .iter()
360        .map(|mi| MarketInfoJson {
361            market: trd_market_int_to_str(mi.trd_market),
362            assets: mi.assets.unwrap_or(0.0),
363        })
364        .collect();
365    let jsons = vec![FundsJson {
366        power: f.power,
367        total_assets: f.total_assets,
368        cash: f.cash,
369        market_val: f.market_val,
370        frozen_cash: f.frozen_cash,
371        debt_cash: f.debt_cash,
372        avl_withdrawal_cash: f.avl_withdrawal_cash,
373        crypto_mv: f.crypto_mv,
374        exposure_level: f.exposure_level,
375        exposure_limit: f.exposure_limit,
376        used_limit: f.used_limit,
377        remaining_limit: f.remaining_limit,
378        currency,
379        cash_info_list: cash_info_jsons,
380        market_info_list: market_info_jsons,
381        currency_warning,
382    }];
383
384    format.print_rows(&rows, &jsons)?;
385    Ok(())
386}
387
388// ========== position list ==========
389
390#[derive(Tabled)]
391struct PosRow {
392    #[tabled(rename = "Code")]
393    code: String,
394    #[tabled(rename = "Name")]
395    name: String,
396    #[tabled(rename = "Qty")]
397    qty: String,
398    #[tabled(rename = "Sellable")]
399    sellable: String,
400    #[tabled(rename = "Cost")]
401    cost: String,
402    #[tabled(rename = "Price")]
403    price: String,
404    #[tabled(rename = "Val")]
405    val: String,
406    #[tabled(rename = "PL")]
407    pl: String,
408    #[tabled(rename = "PL%")]
409    pl_pct: String,
410}
411
412#[derive(Serialize)]
413struct PosJson {
414    position_id: u64,
415    position_side: i32,
416    code: String,
417    name: String,
418    qty: f64,
419    can_sell_qty: f64,
420    price: f64,
421    cost_price: f64,
422    val: f64,
423    pl_val: f64,
424    pl_ratio: f64,
425}
426
427pub async fn positions(
428    gateway: &str,
429    env: &str,
430    acc_id: u64,
431    market: &str,
432    currency_arg: Option<&str>,
433    option_strategy_view: bool,
434    format: OutputFormat,
435) -> Result<()> {
436    let header = build_header(parse_trd_env(env)?, acc_id, parse_trd_market(market)?);
437    let (client, _push_rx) = connect_gateway(gateway, "futucli-position").await?;
438    let currency_int = match currency_arg {
439        Some(s) => Some(currency::parse_currency_label(s)?),
440        None => None,
441    };
442    let list = futu_trd::account::get_position_list_with_options(
443        &client,
444        &header,
445        futu_trd::account::PositionListOptions {
446            filter_market: Some(header.trd_market as i32),
447            currency: currency_int,
448            option_strategy_view: option_strategy_view.then_some(true),
449        },
450    )
451    .await?;
452
453    let rows: Vec<PosRow> = list
454        .iter()
455        .map(|p| PosRow {
456            code: p.code.clone(),
457            name: p.name.clone(),
458            qty: format!("{:.0}", p.qty),
459            sellable: format!("{:.0}", p.can_sell_qty),
460            cost: format!("{:.3}", p.cost_price),
461            price: format!("{:.3}", p.price),
462            val: format!("{:.2}", p.val),
463            pl: format!("{:.2}", p.pl_val),
464            pl_pct: format_pl_ratio_percent(p.pl_ratio),
465        })
466        .collect();
467
468    let jsons: Vec<PosJson> = list
469        .iter()
470        .map(|p| PosJson {
471            position_id: p.position_id,
472            position_side: p.position_side,
473            code: p.code.clone(),
474            name: p.name.clone(),
475            qty: p.qty,
476            can_sell_qty: p.can_sell_qty,
477            price: p.price,
478            cost_price: p.cost_price,
479            val: p.val,
480            pl_val: p.pl_val,
481            pl_ratio: p.pl_ratio,
482        })
483        .collect();
484
485    format.print_rows(&rows, &jsons)?;
486    Ok(())
487}
488
489// ========== order list ==========
490
491#[derive(Tabled)]
492struct OrderRow {
493    #[tabled(rename = "OrderID")]
494    order_id: String,
495    #[tabled(rename = "Code")]
496    code: String,
497    #[tabled(rename = "Side")]
498    side: String,
499    #[tabled(rename = "Type")]
500    order_type: i32,
501    #[tabled(rename = "Status")]
502    status: i32,
503    #[tabled(rename = "Qty")]
504    qty: String,
505    #[tabled(rename = "Price")]
506    price: String,
507    #[tabled(rename = "FillQty")]
508    fill_qty: String,
509    #[tabled(rename = "FillAvg")]
510    fill_avg: String,
511    #[tabled(rename = "Updated")]
512    update_time: String,
513}
514
515#[derive(Serialize)]
516struct OrderJson {
517    order_id: u64,
518    order_id_ex: String,
519    trd_side: i32,
520    order_type: i32,
521    order_status: i32,
522    code: String,
523    name: String,
524    qty: f64,
525    price: f64,
526    create_time: String,
527    update_time: String,
528    fill_qty: f64,
529    fill_avg_price: f64,
530    last_err_msg: String,
531}
532
533fn trd_side_label(d: i32) -> &'static str {
534    match d {
535        1 => "BUY",
536        2 => "SELL",
537        3 => "SELL_SHORT",
538        4 => "BUY_BACK",
539        _ => "?",
540    }
541}
542
543pub async fn orders(
544    gateway: &str,
545    env: &str,
546    acc_id: u64,
547    market: &str,
548    format: OutputFormat,
549) -> Result<()> {
550    let header = build_header(
551        parse_trd_env(env)?,
552        acc_id,
553        parse_trd_market_for_write(market)?,
554    );
555    let (client, _push_rx) = connect_gateway(gateway, "futucli-order").await?;
556    let list = futu_trd::query::get_order_list(&client, &header).await?;
557
558    let rows: Vec<OrderRow> = list
559        .iter()
560        .map(|o| OrderRow {
561            order_id: o.order_id.to_string(),
562            code: o.code.clone(),
563            side: trd_side_label(o.trd_side).to_string(),
564            order_type: o.order_type,
565            status: o.order_status,
566            qty: format!("{:.0}", o.qty),
567            price: format!("{:.3}", o.price),
568            fill_qty: format!("{:.0}", o.fill_qty),
569            fill_avg: format!("{:.3}", o.fill_avg_price),
570            update_time: o.update_time.clone(),
571        })
572        .collect();
573
574    let jsons: Vec<OrderJson> = list
575        .iter()
576        .map(|o| OrderJson {
577            order_id: o.order_id,
578            order_id_ex: o.order_id_ex.clone(),
579            trd_side: o.trd_side,
580            order_type: o.order_type,
581            order_status: o.order_status,
582            code: o.code.clone(),
583            name: o.name.clone(),
584            qty: o.qty,
585            price: o.price,
586            create_time: o.create_time.clone(),
587            update_time: o.update_time.clone(),
588            fill_qty: o.fill_qty,
589            fill_avg_price: o.fill_avg_price,
590            last_err_msg: o.last_err_msg.clone(),
591        })
592        .collect();
593
594    format.print_rows(&rows, &jsons)?;
595    Ok(())
596}
597
598// ========== deal (fill) list ==========
599
600#[derive(Tabled)]
601struct DealRow {
602    #[tabled(rename = "FillID")]
603    fill_id: String,
604    #[tabled(rename = "OrderID")]
605    order_id: String,
606    #[tabled(rename = "Code")]
607    code: String,
608    #[tabled(rename = "Side")]
609    side: String,
610    #[tabled(rename = "Qty")]
611    qty: String,
612    #[tabled(rename = "Price")]
613    price: String,
614    #[tabled(rename = "Time")]
615    time: String,
616}
617
618#[derive(Serialize)]
619struct DealJson {
620    fill_id: u64,
621    fill_id_ex: String,
622    order_id: u64,
623    trd_side: i32,
624    code: String,
625    name: String,
626    qty: f64,
627    price: f64,
628    create_time: String,
629}
630
631pub async fn deals(
632    gateway: &str,
633    env: &str,
634    acc_id: u64,
635    market: &str,
636    format: OutputFormat,
637) -> Result<()> {
638    let header = build_header(
639        parse_trd_env(env)?,
640        acc_id,
641        parse_trd_market_for_write(market)?,
642    );
643    let (client, _push_rx) = connect_gateway(gateway, "futucli-deal").await?;
644    let list = futu_trd::query::get_order_fill_list(&client, &header).await?;
645
646    let rows: Vec<DealRow> = list
647        .iter()
648        .map(|f| DealRow {
649            fill_id: f.fill_id.to_string(),
650            order_id: f.order_id.to_string(),
651            code: f.code.clone(),
652            side: trd_side_label(f.trd_side).to_string(),
653            qty: format!("{:.0}", f.qty),
654            price: format!("{:.3}", f.price),
655            time: f.create_time.clone(),
656        })
657        .collect();
658
659    let jsons: Vec<DealJson> = list
660        .iter()
661        .map(|f| DealJson {
662            fill_id: f.fill_id,
663            fill_id_ex: f.fill_id_ex.clone(),
664            order_id: f.order_id,
665            trd_side: f.trd_side,
666            code: f.code.clone(),
667            name: f.name.clone(),
668            qty: f.qty,
669            price: f.price,
670            create_time: f.create_time.clone(),
671        })
672        .collect();
673
674    format.print_rows(&rows, &jsons)?;
675    Ok(())
676}