Skip to main content

futucli/cmd/trade_ext/
margin_fee.rs

1use anyhow::{Result, bail};
2use serde::Serialize;
3use tabled::Tabled;
4
5use crate::cmd::account::{parse_trd_env, parse_trd_market_for_write};
6use crate::common::connect_gateway;
7use crate::output::OutputFormat;
8use futu_trd::misc::{get_margin_ratio, get_order_fee};
9use futu_trd::types::TrdHeader;
10
11#[derive(Tabled)]
12struct MarginRatioRow {
13    #[tabled(rename = "Code")]
14    code: String,
15    #[tabled(rename = "Long?")]
16    long: &'static str,
17    #[tabled(rename = "Short?")]
18    short: &'static str,
19    #[tabled(rename = "IM Long%")]
20    im_long: String,
21    #[tabled(rename = "IM Short%")]
22    im_short: String,
23    #[tabled(rename = "Short Pool")]
24    short_pool: String,
25    #[tabled(rename = "Short Fee%")]
26    short_fee: String,
27}
28
29#[derive(Serialize)]
30struct MarginRatioJson {
31    code: String,
32    is_long_permit: bool,
33    is_short_permit: bool,
34    im_long_ratio: f64,
35    im_short_ratio: f64,
36    short_pool_remain: f64,
37    short_fee_rate: f64,
38}
39
40/// v1.4.31:融资融券比率(对齐 py-futu-api `get_margin_ratio`)
41///
42/// 按证券代码列表查每只的融资 / 融券保证金率、融券池余量、融券费率。
43/// `symbols` 是 MARKET.CODE 列表(用 `,` 分隔),如 `HK.00700,US.AAPL`。
44pub async fn run_margin_ratio(
45    gateway: &str,
46    env: &str,
47    acc_id: u64,
48    market: &str,
49    symbols: &[String],
50    format: OutputFormat,
51) -> Result<()> {
52    if symbols.is_empty() {
53        bail!("no symbols");
54    }
55    let env_p = parse_trd_env(env)?;
56    let market_p = parse_trd_market_for_write(market)?;
57    let header = TrdHeader {
58        trd_env: env_p,
59        acc_id,
60        trd_market: market_p,
61        jp_acc_type: None,
62    };
63    // symbols 里每个 MARKET.CODE → (qot_market_int, code)
64    let secs: Vec<(i32, String)> = symbols
65        .iter()
66        .map(|s| {
67            let sec = crate::common::parse_symbol(s)?;
68            // parse_symbol 返回 futu_qot::types::Security,含 market + code
69            Ok::<_, anyhow::Error>((sec.market as i32, sec.code))
70        })
71        .collect::<Result<Vec<_>>>()?;
72    let (client, _push_rx) = connect_gateway(gateway, "futucli-margin-ratio").await?;
73    let list = get_margin_ratio(&client, &header, &secs).await?;
74
75    // 服务端返回的 ratio 字段已经是百分比数值(30.0 = 30%),不要再乘 100
76    let rows: Vec<MarginRatioRow> = list
77        .iter()
78        .map(|r| MarginRatioRow {
79            code: r.code.clone(),
80            long: if r.is_long_permit { "yes" } else { "no" },
81            short: if r.is_short_permit { "yes" } else { "no" },
82            im_long: format!("{:.2}", r.im_long_ratio),
83            im_short: format!("{:.2}", r.im_short_ratio),
84            short_pool: format!("{:.0}", r.short_pool_remain),
85            short_fee: format!("{:.3}", r.short_fee_rate),
86        })
87        .collect();
88    let jsons: Vec<MarginRatioJson> = list
89        .iter()
90        .map(|r| MarginRatioJson {
91            code: r.code.clone(),
92            is_long_permit: r.is_long_permit,
93            is_short_permit: r.is_short_permit,
94            im_long_ratio: r.im_long_ratio,
95            im_short_ratio: r.im_short_ratio,
96            short_pool_remain: r.short_pool_remain,
97            short_fee_rate: r.short_fee_rate,
98        })
99        .collect();
100    format.print_rows(&rows, &jsons)?;
101    Ok(())
102}
103
104#[derive(Tabled)]
105struct OrderFeeRow {
106    #[tabled(rename = "OrderID")]
107    order_id_ex: String,
108    #[tabled(rename = "Total Fee")]
109    total: String,
110    #[tabled(rename = "Items")]
111    items: String,
112}
113
114#[derive(Serialize)]
115struct OrderFeeItemJson {
116    title: String,
117    value: f64,
118}
119
120#[derive(Serialize)]
121struct OrderFeeJson {
122    order_id_ex: String,
123    fee_amount: f64,
124    fee_list: Vec<OrderFeeItemJson>,
125}
126
127/// v1.4.31:订单费用明细(对齐 py-futu-api `order_fee_query`)
128///
129/// 下单 / 撤单前估算费用。按扩展订单号列表查,每单返总额 + 明细拆分
130/// (佣金、平台费、印花税、交易所费等)。`order_ids` 是 `orderIDEx`(服务端
131/// 唯一 id,用 `futucli order` / `history-orders` 的 `order_id_ex` 列)。
132pub async fn run_order_fee(
133    gateway: &str,
134    env: &str,
135    acc_id: u64,
136    market: &str,
137    order_ids: &[String],
138    format: OutputFormat,
139) -> Result<()> {
140    if order_ids.is_empty() {
141        bail!("need at least one --order-id");
142    }
143    let env_p = parse_trd_env(env)?;
144    let market_p = parse_trd_market_for_write(market)?;
145    let header = TrdHeader {
146        trd_env: env_p,
147        acc_id,
148        trd_market: market_p,
149        jp_acc_type: None,
150    };
151    let (client, _push_rx) = connect_gateway(gateway, "futucli-order-fee").await?;
152    let list = get_order_fee(&client, &header, order_ids).await?;
153
154    let rows: Vec<OrderFeeRow> = list
155        .iter()
156        .map(|f| OrderFeeRow {
157            order_id_ex: f.order_id_ex.clone(),
158            total: format!("{:.2}", f.fee_amount),
159            items: f
160                .fee_list
161                .iter()
162                .map(|i| format!("{}={:.2}", i.title, i.value))
163                .collect::<Vec<_>>()
164                .join(" | "),
165        })
166        .collect();
167    let jsons: Vec<OrderFeeJson> = list
168        .iter()
169        .map(|f| OrderFeeJson {
170            order_id_ex: f.order_id_ex.clone(),
171            fee_amount: f.fee_amount,
172            fee_list: f
173                .fee_list
174                .iter()
175                .map(|i| OrderFeeItemJson {
176                    title: i.title.clone(),
177                    value: i.value,
178                })
179                .collect(),
180        })
181        .collect();
182    format.print_rows(&rows, &jsons)?;
183    Ok(())
184}