Skip to main content

futucli/cmd/trade_ext/
history.rs

1use anyhow::{Result, bail};
2use serde::Serialize;
3use tabled::{Table, Tabled};
4
5use crate::cmd::account::{parse_trd_env, parse_trd_market};
6use crate::common::connect_gateway;
7use crate::output::OutputFormat;
8use futu_trd::misc::{
9    HistoryFilterConditions, get_history_order_fill_list, get_history_order_list,
10};
11use futu_trd::types::TrdHeader;
12
13#[derive(Tabled)]
14struct HistOrderRow {
15    #[tabled(rename = "ID")]
16    order_id: String,
17    #[tabled(rename = "Code")]
18    code: String,
19    #[tabled(rename = "Name")]
20    name: String,
21    #[tabled(rename = "Side")]
22    side: String,
23    #[tabled(rename = "Type")]
24    order_type: String,
25    #[tabled(rename = "Status")]
26    status: String,
27    #[tabled(rename = "Qty")]
28    qty: String,
29    #[tabled(rename = "Price")]
30    price: String,
31    #[tabled(rename = "Fill")]
32    fill: String,
33    #[tabled(rename = "Time")]
34    time: String,
35}
36
37#[derive(Serialize)]
38struct HistOrderJson {
39    order_id: u64,
40    order_id_ex: String,
41    code: String,
42    name: String,
43    trd_side: i32,
44    order_type: i32,
45    order_status: i32,
46    qty: f64,
47    price: f64,
48    fill_qty: f64,
49    fill_avg_price: f64,
50    create_time: String,
51    update_time: String,
52}
53
54pub struct HistoryOrdersCommand<'a> {
55    pub gateway: &'a str,
56    pub env: &'a str,
57    pub acc_id: u64,
58    pub market: &'a str,
59    pub codes: Vec<String>,
60    pub begin: Option<String>,
61    pub end: Option<String>,
62    pub output: OutputFormat,
63}
64
65// Ref: C++ APIServer_Trd_GetHistoryOrderList.cpp / GetHistoryOrderFillList:
66// both paths require beginTime and endTime before forwarding to backend.
67pub(crate) fn validate_history_time_range(
68    command: &str,
69    market: &str,
70    begin: Option<&str>,
71    end: Option<&str>,
72) -> Result<()> {
73    if begin.is_none() || end.is_none() {
74        bail!(
75            "{command}: --begin 和 --end 必须同时提供,格式为 YYYY-MM-DD HH:MM:SS \
76             (market={market}). OpenD history query 要求 begin/end 同时存在。"
77        );
78    }
79    Ok(())
80}
81
82pub async fn run_history_orders(input: HistoryOrdersCommand<'_>) -> Result<()> {
83    let env_p = parse_trd_env(input.env)?;
84    let market_p = parse_trd_market(input.market)?;
85    validate_history_time_range(
86        "history-orders",
87        input.market,
88        input.begin.as_deref(),
89        input.end.as_deref(),
90    )?;
91    let header = TrdHeader {
92        trd_env: env_p,
93        acc_id: input.acc_id,
94        trd_market: market_p,
95        jp_acc_type: None,
96    };
97    let filter = HistoryFilterConditions {
98        code_list: input.codes,
99        id_list: vec![],
100        begin_time: input.begin,
101        end_time: input.end,
102        filter_market: Some(market_p as i32),
103    };
104    let (client, _push_rx) = connect_gateway(input.gateway, "futucli-trade-ext").await?;
105    let list = get_history_order_list(&client, &header, &filter).await?;
106
107    if matches!(input.output, OutputFormat::Json) {
108        let json: Vec<HistOrderJson> = list
109            .iter()
110            .map(|o| HistOrderJson {
111                order_id: o.order_id,
112                order_id_ex: o.order_id_ex.clone(),
113                code: o.code.clone(),
114                name: o.name.clone(),
115                trd_side: o.trd_side,
116                order_type: o.order_type,
117                order_status: o.order_status,
118                qty: o.qty,
119                price: o.price,
120                fill_qty: o.fill_qty,
121                fill_avg_price: o.fill_avg_price,
122                create_time: o.create_time.clone(),
123                update_time: o.update_time.clone(),
124            })
125            .collect();
126        println!("{}", serde_json::to_string_pretty(&json)?);
127        return Ok(());
128    }
129
130    let rows: Vec<HistOrderRow> = list
131        .iter()
132        .map(|o| HistOrderRow {
133            order_id: o.order_id.to_string(),
134            code: o.code.clone(),
135            name: o.name.clone(),
136            side: format!("{}", o.trd_side),
137            order_type: format!("{}", o.order_type),
138            status: format!("{}", o.order_status),
139            qty: format!("{:.0}", o.qty),
140            price: format!("{:.3}", o.price),
141            fill: format!("{:.0}", o.fill_qty),
142            time: o.create_time.clone(),
143        })
144        .collect();
145    println!("history orders: {} record(s) from gateway", rows.len());
146    println!("{}", Table::new(rows));
147    Ok(())
148}
149
150#[derive(Tabled)]
151struct HistDealRow {
152    #[tabled(rename = "Fill ID")]
153    fill_id: String,
154    #[tabled(rename = "Order ID")]
155    order_id: String,
156    #[tabled(rename = "Code")]
157    code: String,
158    #[tabled(rename = "Name")]
159    name: String,
160    #[tabled(rename = "Side")]
161    side: String,
162    #[tabled(rename = "Qty")]
163    qty: String,
164    #[tabled(rename = "Price")]
165    price: String,
166    #[tabled(rename = "Time")]
167    time: String,
168}
169
170pub struct HistoryDealsCommand<'a> {
171    pub gateway: &'a str,
172    pub env: &'a str,
173    pub acc_id: u64,
174    pub market: &'a str,
175    pub codes: Vec<String>,
176    pub begin: Option<String>,
177    pub end: Option<String>,
178    pub output: OutputFormat,
179}
180
181pub async fn run_history_deals(input: HistoryDealsCommand<'_>) -> Result<()> {
182    let env_p = parse_trd_env(input.env)?;
183    let market_p = parse_trd_market(input.market)?;
184    validate_history_time_range(
185        "history-deals",
186        input.market,
187        input.begin.as_deref(),
188        input.end.as_deref(),
189    )?;
190    let header = TrdHeader {
191        trd_env: env_p,
192        acc_id: input.acc_id,
193        trd_market: market_p,
194        jp_acc_type: None,
195    };
196    let filter = HistoryFilterConditions {
197        code_list: input.codes,
198        id_list: vec![],
199        begin_time: input.begin,
200        end_time: input.end,
201        filter_market: Some(market_p as i32),
202    };
203    let (client, _push_rx) = connect_gateway(input.gateway, "futucli-trade-ext").await?;
204    let list = get_history_order_fill_list(&client, &header, &filter).await?;
205
206    if matches!(input.output, OutputFormat::Json) {
207        let json: Vec<_> = list
208            .iter()
209            .map(|f| {
210                serde_json::json!({
211                    "fill_id": f.fill_id,
212                    "fill_id_ex": f.fill_id_ex,
213                    "order_id": f.order_id,
214                    "code": f.code,
215                    "name": f.name,
216                    "trd_side": f.trd_side,
217                    "qty": f.qty,
218                    "price": f.price,
219                    "create_time": f.create_time,
220                })
221            })
222            .collect();
223        println!("{}", serde_json::to_string_pretty(&json)?);
224        return Ok(());
225    }
226    let rows: Vec<HistDealRow> = list
227        .iter()
228        .map(|f| HistDealRow {
229            fill_id: f.fill_id.to_string(),
230            order_id: f.order_id.to_string(),
231            code: f.code.clone(),
232            name: f.name.clone(),
233            side: format!("{}", f.trd_side),
234            qty: format!("{:.0}", f.qty),
235            price: format!("{:.3}", f.price),
236            time: f.create_time.clone(),
237        })
238        .collect();
239    println!("history deals: {} record(s)", rows.len());
240    println!("{}", Table::new(rows));
241    Ok(())
242}