futucli/cmd/trade_ext/
cash_flow.rs1use anyhow::{Result, bail};
2use chrono::Datelike;
3use prost::Message as _;
4
5use crate::cmd::account::{parse_trd_env, parse_trd_market};
6use crate::common::connect_gateway;
7use crate::output::OutputFormat;
8
9pub async fn run_acc_cash_flow(
11 gateway: &str,
12 acc_id: u64,
13 clearing_date: &str,
14 env: &str,
15 market: &str,
16 direction: Option<i32>,
17 _format: OutputFormat,
18) -> Result<()> {
19 let entries =
20 fetch_acc_cash_flow_single(gateway, acc_id, clearing_date, env, market, direction).await?;
21 println!(
22 "account {} clearing={} entries: {}",
23 acc_id,
24 clearing_date,
25 entries.len()
26 );
27 for f in &entries {
28 println!(
29 " date={:?} type={:?} amount={:?} remark={:?}",
30 f.clearing_date, f.cash_flow_type, f.cash_flow_amount, f.cash_flow_remark
31 );
32 }
33 Ok(())
34}
35
36pub struct AccCashFlowRangeCommand<'a> {
42 pub gateway: &'a str,
43 pub acc_id: u64,
44 pub date_from: chrono::NaiveDate,
45 pub date_to: chrono::NaiveDate,
46 pub env: &'a str,
47 pub market: &'a str,
48 pub direction: Option<i32>,
49}
50
51pub async fn run_acc_cash_flow_range(input: AccCashFlowRangeCommand<'_>) -> Result<()> {
52 let date_from = input.date_from;
53 let date_to = input.date_to;
54 if date_to < date_from {
55 bail!("--date-range: 结束日期 {date_to} 不能早于开始日期 {date_from}");
56 }
57 let span_days = (date_to - date_from).num_days();
58 if span_days > 31 {
59 bail!(
60 "--date-range: 跨度 {} 天超过 31 天上限(防 bot 脚本误调用被服务端限流)",
61 span_days
62 );
63 }
64 let mut day = date_from;
65 let mut total_entries = 0usize;
66 let mut failed_days = Vec::new();
67 let mut success_days = 0usize; while day <= date_to {
69 if matches!(day.weekday(), chrono::Weekday::Sat | chrono::Weekday::Sun) {
71 if !acc_cash_flow_advance_day(&mut day, date_to)? {
72 break;
73 }
74 continue;
75 }
76 let date_str = day.format("%Y-%m-%d").to_string();
77 match fetch_acc_cash_flow_single(
78 input.gateway,
79 input.acc_id,
80 &date_str,
81 input.env,
82 input.market,
83 input.direction,
84 )
85 .await
86 {
87 Ok(entries) => {
88 success_days += 1;
92 if !entries.is_empty() {
93 println!(
94 "== {} ({}): entries: {}",
95 date_str,
96 day.weekday(),
97 entries.len()
98 );
99 for f in &entries {
100 println!(
101 " type={:?} amount={:?} remark={:?}",
102 f.cash_flow_type, f.cash_flow_amount, f.cash_flow_remark
103 );
104 }
105 total_entries += entries.len();
106 }
107 }
108 Err(e) => {
109 eprintln!("⚠️ {date_str}: {e}");
110 failed_days.push(date_str);
111 }
113 }
114 if !acc_cash_flow_advance_day(&mut day, date_to)? {
115 break;
116 }
117 }
118 println!(
119 "---\n{date_from}..{date_to} attempted: {} success_days: {success_days} failed days: {} total entries: {total_entries}",
120 success_days + failed_days.len(),
121 failed_days.len()
122 );
123 if !failed_days.is_empty() {
124 eprintln!("failed days: {failed_days:?}");
125 }
126 if success_days == 0 && !failed_days.is_empty() {
132 anyhow::bail!(
133 "acc-cash-flow --date-range {date_from}..{date_to} 所有 {} 天全部失败 \
134 (no successful daily fetch). 失败 days: {:?}. 检查 acc_id / env / market / \
135 daemon 连接, 或单日重试 (`futucli acc-cash-flow --date <day>`).",
136 failed_days.len(),
137 failed_days
138 );
139 }
140 Ok(())
141}
142
143async fn fetch_acc_cash_flow_single(
145 gateway: &str,
146 acc_id: u64,
147 clearing_date: &str,
148 env: &str,
149 market: &str,
150 direction: Option<i32>,
151) -> Result<Vec<futu_proto::trd_flow_summary::FlowSummaryInfo>> {
152 let env_p = parse_trd_env(env)?;
153 let market_p = parse_trd_market(market)?;
154 let proto_header = futu_proto::trd_common::TrdHeader {
155 trd_env: env_p as i32,
156 acc_id,
157 trd_market: market_p as i32,
158 jp_acc_type: None,
159 };
160 let req = futu_proto::trd_flow_summary::Request {
161 c2s: futu_proto::trd_flow_summary::C2s {
162 header: proto_header,
163 clearing_date: clearing_date.to_string(),
164 cash_flow_direction: direction,
165 start_create_date: None,
166 end_create_date: None,
167 },
168 };
169 let (client, _rx) = connect_gateway(gateway, "futucli-cash-flow").await?;
170 let body = req.encode_to_vec();
171 let frame = client
172 .request(futu_core::proto_id::TRD_FLOW_SUMMARY, body)
173 .await?;
174 let resp = futu_proto::trd_flow_summary::Response::decode(frame.body.as_ref())
175 .map_err(|e| anyhow::anyhow!("decode: {e}"))?;
176 if resp.ret_type != 0 {
177 bail!(
178 "flow_summary ret_type={} msg={:?}",
179 resp.ret_type,
180 resp.ret_msg
181 );
182 }
183 let s = resp.s2c.ok_or_else(|| anyhow::anyhow!("missing s2c"))?;
184 Ok(s.flow_summary_info_list)
185}
186
187pub(crate) fn acc_cash_flow_advance_day(
188 day: &mut chrono::NaiveDate,
189 date_to: chrono::NaiveDate,
190) -> Result<bool> {
191 if *day >= date_to {
192 return Ok(false);
193 }
194 let next = day
195 .succ_opt()
196 .ok_or_else(|| anyhow::anyhow!("acc-cash-flow date range cannot advance beyond {day}"))?;
197 *day = next;
198 Ok(true)
199}