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    let (header, secs) = build_margin_ratio_request(env, acc_id, market, symbols)?;
53    let (client, _push_rx) = connect_gateway(gateway, "futucli-margin-ratio").await?;
54    let list = get_margin_ratio(&client, &header, &secs).await?;
55
56    let rows: Vec<MarginRatioRow> = list
57        .iter()
58        .map(|r| MarginRatioRow {
59            code: r.code.clone(),
60            long: if r.is_long_permit { "yes" } else { "no" },
61            short: if r.is_short_permit { "yes" } else { "no" },
62            im_long: format!("{:.2}", r.im_long_ratio),
63            im_short: format!("{:.2}", r.im_short_ratio),
64            short_pool: format!("{:.0}", r.short_pool_remain),
65            short_fee: format!("{:.3}", r.short_fee_rate),
66        })
67        .collect();
68    let jsons: Vec<MarginRatioJson> = list
69        .iter()
70        .map(|r| MarginRatioJson {
71            code: r.code.clone(),
72            is_long_permit: r.is_long_permit,
73            is_short_permit: r.is_short_permit,
74            im_long_ratio: r.im_long_ratio,
75            im_short_ratio: r.im_short_ratio,
76            short_pool_remain: r.short_pool_remain,
77            short_fee_rate: r.short_fee_rate,
78        })
79        .collect();
80    format.print_rows(&rows, &jsons)?;
81    Ok(())
82}
83
84pub(super) fn build_margin_ratio_request(
85    env: &str,
86    acc_id: u64,
87    market: &str,
88    symbols: &[String],
89) -> Result<(TrdHeader, Vec<(i32, String)>)> {
90    if symbols.is_empty() {
91        bail!("no symbols");
92    }
93    let env_p = parse_trd_env(env)?;
94    let market_p = parse_trd_market_for_write(market)?;
95    let header = TrdHeader {
96        trd_env: env_p,
97        acc_id,
98        trd_market: market_p,
99        jp_acc_type: None,
100    };
101    // symbols 里每个 MARKET.CODE → (qot_market_int, code)
102    let secs: Vec<(i32, String)> = symbols
103        .iter()
104        .map(|s| {
105            let sec = crate::common::parse_symbol(s)?;
106            // parse_symbol 返回 futu_qot::types::Security,含 market + code
107            Ok::<_, anyhow::Error>((sec.market as i32, sec.code))
108        })
109        .collect::<Result<Vec<_>>>()?;
110    Ok((header, secs))
111}
112
113#[derive(Tabled)]
114struct OrderFeeRow {
115    #[tabled(rename = "OrderID")]
116    order_id_ex: String,
117    #[tabled(rename = "Total Fee")]
118    total: String,
119    #[tabled(rename = "Items")]
120    items: String,
121}
122
123#[derive(Serialize)]
124struct OrderFeeItemJson {
125    title: String,
126    value: f64,
127}
128
129#[derive(Serialize)]
130struct OrderFeeJson {
131    order_id_ex: String,
132    fee_amount: f64,
133    fee_list: Vec<OrderFeeItemJson>,
134}
135
136/// v1.4.31:订单费用明细(对齐 py-futu-api `order_fee_query`)
137///
138/// 下单 / 撤单前估算费用。按扩展订单号列表查,每单返总额 + 明细拆分
139/// (佣金、平台费、印花税、交易所费等)。`order_ids` 是 `orderIDEx`(服务端
140/// 唯一 id,用 `futucli order` / `history-orders` 的 `order_id_ex` 列)。
141pub async fn run_order_fee(
142    gateway: &str,
143    env: &str,
144    acc_id: u64,
145    market: &str,
146    order_ids: &[String],
147    format: OutputFormat,
148) -> Result<()> {
149    if order_ids.is_empty() {
150        bail!("need at least one --order-id");
151    }
152    let env_p = parse_trd_env(env)?;
153    let market_p = parse_trd_market_for_write(market)?;
154    let header = TrdHeader {
155        trd_env: env_p,
156        acc_id,
157        trd_market: market_p,
158        jp_acc_type: None,
159    };
160    let (client, _push_rx) = connect_gateway(gateway, "futucli-order-fee").await?;
161    let list = get_order_fee(&client, &header, order_ids).await?;
162
163    let rows: Vec<OrderFeeRow> = list
164        .iter()
165        .map(|f| OrderFeeRow {
166            order_id_ex: f.order_id_ex.clone(),
167            total: format!("{:.2}", f.fee_amount),
168            items: f
169                .fee_list
170                .iter()
171                .map(|i| format!("{}={:.2}", i.title, i.value))
172                .collect::<Vec<_>>()
173                .join(" | "),
174        })
175        .collect();
176    let jsons: Vec<OrderFeeJson> = list
177        .iter()
178        .map(|f| OrderFeeJson {
179            order_id_ex: f.order_id_ex.clone(),
180            fee_amount: f.fee_amount,
181            fee_list: f
182                .fee_list
183                .iter()
184                .map(|i| OrderFeeItemJson {
185                    title: i.title.clone(),
186                    value: i.value,
187                })
188                .collect(),
189        })
190        .collect();
191    format.print_rows(&rows, &jsons)?;
192    Ok(())
193}