1use anyhow::{Result, bail};
4use chrono::NaiveDate;
5use serde::Serialize;
6use tabled::Tabled;
7
8use crate::common::{connect_gateway, parse_symbol};
9use crate::output::OutputFormat;
10use futu_qot::types::{KLType, RehabType};
11
12pub fn parse_kl_type(s: &str) -> Result<KLType> {
13 let t = match s.trim().to_ascii_lowercase().as_str() {
14 "day" => KLType::Day,
15 "week" => KLType::Week,
16 "month" => KLType::Month,
17 "quarter" => KLType::Quarter,
18 "year" => KLType::Year,
19 "1min" => KLType::Min1,
20 "3min" => KLType::Min3,
21 "5min" => KLType::Min5,
22 "15min" => KLType::Min15,
23 "30min" => KLType::Min30,
24 "60min" => KLType::Min60,
25 other => bail!(
26 "unknown kline type {other:?} (day|week|month|quarter|year|1min|3min|5min|15min|30min|60min)"
27 ),
28 };
29 Ok(t)
30}
31
32#[derive(Tabled)]
33struct KLineRow {
34 #[tabled(rename = "Time")]
35 time: String,
36 #[tabled(rename = "Open")]
37 open: String,
38 #[tabled(rename = "High")]
39 high: String,
40 #[tabled(rename = "Low")]
41 low: String,
42 #[tabled(rename = "Close")]
43 close: String,
44 #[tabled(rename = "Change%")]
45 change_pct: String,
46 #[tabled(rename = "Volume")]
47 volume: String,
48 #[tabled(rename = "Turnover")]
49 turnover: String,
50}
51
52#[derive(Serialize)]
53struct KLineJson {
54 time: String,
55 timestamp: f64,
56 open: f64,
57 high: f64,
58 low: f64,
59 close: f64,
60 last_close: f64,
61 change_rate: f64,
62 volume: i64,
63 turnover: f64,
64 turnover_rate: f64,
65 pe: f64,
66}
67
68pub async fn run_with_format(
69 gateway: &str,
70 symbol: &str,
71 kl_type_str: &str,
72 count: Option<i32>,
73 begin: Option<&str>,
74 end: Option<&str>,
75 format: OutputFormat,
76) -> Result<()> {
77 let sec = parse_symbol(symbol)?;
78 let kl_type = parse_kl_type(kl_type_str)?;
79
80 let today_utc = default_end_date_today_utc();
91 let end_date = match end {
92 Some(s) => NaiveDate::parse_from_str(s, "%Y-%m-%d")?,
93 None => today_utc,
94 };
95 let n = count.unwrap_or(100);
96 let lookback_days = estimate_lookback_days(kl_type, n);
97 let begin_date = match begin {
98 Some(s) => NaiveDate::parse_from_str(s, "%Y-%m-%d")?,
99 None => end_date
100 .checked_sub_days(chrono::Days::new(lookback_days as u64))
101 .unwrap_or(end_date),
102 };
103
104 let (client, _push_rx) = connect_gateway(gateway, "futucli-kline").await?;
105 let result = futu_qot::history_kl::get_history_kl(
115 &client,
116 &sec,
117 RehabType::None,
118 kl_type,
119 &begin_date.format("%Y-%m-%d").to_string(),
120 &end_date.format("%Y-%m-%d").to_string(),
121 None, )
123 .await?;
124
125 let mut sorted_kl_list = result.kl_list.clone();
127 sorted_kl_list.sort_by(|a, b| b.time.cmp(&a.time));
128 sorted_kl_list.truncate(n as usize);
129 sorted_kl_list.sort_by(|a, b| a.time.cmp(&b.time));
131
132 let mut rows = Vec::new();
133 let mut jsons = Vec::new();
134 for k in &sorted_kl_list {
135 let sign = if k.change_rate >= 0.0 { "+" } else { "" };
136 rows.push(KLineRow {
137 time: k.time.clone(),
138 open: format!("{:.3}", k.open_price),
139 high: format!("{:.3}", k.high_price),
140 low: format!("{:.3}", k.low_price),
141 close: format!("{:.3}", k.close_price),
142 change_pct: format!("{sign}{:.2}%", k.change_rate),
143 volume: k.volume.to_string(),
144 turnover: format!("{:.0}", k.turnover),
145 });
146 jsons.push(KLineJson {
147 time: k.time.clone(),
148 timestamp: k.timestamp,
149 open: k.open_price,
150 high: k.high_price,
151 low: k.low_price,
152 close: k.close_price,
153 last_close: k.last_close_price,
154 change_rate: k.change_rate,
155 volume: k.volume,
156 turnover: k.turnover,
157 turnover_rate: k.turnover_rate,
158 pe: k.pe,
159 });
160 }
161
162 format.print_rows(&rows, &jsons)?;
163 Ok(())
164}
165
166fn default_end_date_today_utc() -> NaiveDate {
175 chrono::Utc::now().date_naive()
176}
177
178fn estimate_lookback_days(kl_type: KLType, count: i32) -> i32 {
191 let n = count.max(1);
192 match kl_type {
193 KLType::Day => ((n as f32 * 7.0 / 5.0).ceil() as i32) + 3,
195 KLType::Week => n * 7 + 7,
196 KLType::Month => n * 31 + 7,
197 KLType::Quarter => n * 92 + 30,
198 KLType::Year => n * 366 + 30,
199 KLType::Min1 => (n as f32 / 240.0).ceil() as i32 + 1,
201 KLType::Min3 => (n as f32 / 80.0).ceil() as i32 + 1,
202 KLType::Min5 => (n as f32 / 48.0).ceil() as i32 + 1,
203 KLType::Min15 => (n as f32 / 16.0).ceil() as i32 + 1,
204 KLType::Min30 => (n as f32 / 8.0).ceil() as i32 + 2,
205 KLType::Min60 => (n as f32 / 4.0).ceil() as i32 + 3,
206 _ => 365,
207 }
208}
209
210#[cfg(test)]
211mod tests_v1_4_96_bug_011_kline_default_end;