1use anyhow::{Result, anyhow, bail};
6use prost::Message;
7use serde::Serialize;
8use tabled::Tabled;
9
10use crate::common::{connect_gateway, parse_symbol};
11use crate::output::OutputFormat;
12
13pub struct SetPriceReminderCommand<'a> {
14 pub gateway: &'a str,
15 pub symbol: &'a str,
16 pub op: i32,
17 pub key: Option<i64>,
18 pub reminder_type: Option<i32>,
19 pub freq: Option<i32>,
20 pub value: Option<f64>,
21 pub note: Option<&'a str>,
22 pub reminder_session_list: &'a [i32],
23}
24
25pub async fn run_set_price_reminder(input: SetPriceReminderCommand<'_>) -> Result<()> {
26 let sec = parse_symbol(input.symbol)?;
27 let (client, _rx) = connect_gateway(input.gateway, "futucli-set-price-reminder").await?;
28 let req = futu_proto::qot_set_price_reminder::Request {
29 c2s: futu_proto::qot_set_price_reminder::C2s {
30 security: futu_proto::qot_common::Security {
31 market: sec.market as i32,
32 code: sec.code,
33 },
34 op: input.op,
35 key: input.key,
36 r#type: input.reminder_type,
37 freq: input.freq,
38 value: input.value,
39 note: input.note.map(String::from),
40 reminder_session_list: input.reminder_session_list.to_vec(),
43 header: None, },
45 };
46 let body = req.encode_to_vec();
47 let frame = client
48 .request(futu_core::proto_id::QOT_SET_PRICE_REMINDER, body)
49 .await?;
50 let resp = futu_proto::qot_set_price_reminder::Response::decode(frame.body.as_ref())
51 .map_err(|e| anyhow!("decode: {e}"))?;
52 if resp.ret_type != 0 {
53 bail!(
54 "set_price_reminder ret_type={} msg={:?}",
55 resp.ret_type,
56 resp.ret_msg
57 );
58 }
59 let key_out = resp.s2c.map(|s| s.key);
60 println!("✅ set_price_reminder ok: op={} key={key_out:?}", input.op);
61 Ok(())
62}
63
64pub(super) fn parse_price_reminder_market(raw: &str) -> Result<i32> {
65 if let Ok(value) = raw.parse::<i32>() {
66 return match value {
67 1 | 6 | 11 | 21 | 22 => Ok(value),
68 _ => bail!(
69 "unknown price reminder market {value}: valid = 1(HK), 6(HK_FUTURE), 11(US), 21(SH/CN), 22(SZ)"
70 ),
71 };
72 }
73
74 match raw.trim().to_ascii_uppercase().as_str() {
75 "HK" => Ok(1),
76 "HK_FUTURE" | "HKFUTURE" => Ok(6),
77 "US" => Ok(11),
78 "CN" | "SH" => Ok(21),
79 "SZ" => Ok(22),
80 other => bail!(
81 "unknown price reminder market {other:?}: valid = HK/HK_FUTURE/US/SH/SZ/CN or integer 1/6/11/21/22"
82 ),
83 }
84}
85
86#[derive(Tabled)]
87struct ReminderRow {
88 #[tabled(rename = "Key")]
89 key: i64,
90 #[tabled(rename = "Type")]
91 r#type: i32,
92 #[tabled(rename = "Value")]
93 value: String,
94 #[tabled(rename = "Freq")]
95 freq: i32,
96 #[tabled(rename = "Enable")]
97 enable: bool,
98 #[tabled(rename = "Note")]
99 note: String,
100}
101
102#[derive(Serialize)]
103struct ReminderJson {
104 symbol: String,
105 name: Option<String>,
106 key: i64,
107 reminder_type: i32,
108 value: f64,
109 freq: i32,
110 is_enable: bool,
111 note: String,
112}
113
114pub async fn run_get_price_reminder(
115 gateway: &str,
116 symbol: Option<&str>,
117 market: Option<&str>,
118 format: OutputFormat,
119) -> Result<()> {
120 let (security, market_code) = match symbol {
121 Some(s) => {
122 let sec = parse_symbol(s)?;
123 (
124 Some(futu_proto::qot_common::Security {
125 market: sec.market as i32,
126 code: sec.code,
127 }),
128 None,
129 )
130 }
131 None => (None, market.map(parse_price_reminder_market).transpose()?),
132 };
133 if security.is_none() && market_code.is_none() {
134 bail!("need either --symbol or --market");
135 }
136 let (client, _rx) = connect_gateway(gateway, "futucli-price-reminder").await?;
137 let req = futu_proto::qot_get_price_reminder::Request {
138 c2s: futu_proto::qot_get_price_reminder::C2s {
139 security,
140 market: market_code,
141 header: None,
142 },
143 };
144 let body = req.encode_to_vec();
145 let frame = client
146 .request(futu_core::proto_id::QOT_GET_PRICE_REMINDER, body)
147 .await?;
148 let resp = futu_proto::qot_get_price_reminder::Response::decode(frame.body.as_ref())
149 .map_err(|e| anyhow!("decode: {e}"))?;
150 if resp.ret_type != 0 {
151 bail!(
152 "price_reminder ret_type={} msg={:?}",
153 resp.ret_type,
154 resp.ret_msg
155 );
156 }
157 let s = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
158 let mut rows = Vec::new();
159 let mut jsons = Vec::new();
160 for pr in &s.price_reminder_list {
161 let sym = format!("{}.{}", pr.security.market, pr.security.code);
162 for r in &pr.item_list {
163 rows.push(ReminderRow {
164 key: r.key,
165 r#type: r.r#type,
166 value: format!("{:.3}", r.value),
167 freq: r.freq,
168 enable: r.is_enable,
169 note: r.note.clone(),
170 });
171 jsons.push(ReminderJson {
172 symbol: sym.clone(),
173 name: pr.name.clone(),
174 key: r.key,
175 reminder_type: r.r#type,
176 value: r.value,
177 freq: r.freq,
178 is_enable: r.is_enable,
179 note: r.note.clone(),
180 });
181 }
182 }
183 format.print_rows(&rows, &jsons)?;
184 Ok(())
185}
186
187#[derive(Tabled)]
188struct OptionExpiryRow {
189 #[tabled(rename = "Strike Time")]
190 strike_time: String,
191 #[tabled(rename = "Distance (days)")]
192 distance: i32,
193 #[tabled(rename = "Cycle")]
194 cycle: String,
195}
196
197#[derive(Serialize)]
198struct OptionExpiryJson {
199 strike_time: Option<String>,
200 distance_days: i32,
201 cycle: Option<i32>,
202}
203
204pub async fn run_option_expiration_date(
205 gateway: &str,
206 owner: &str,
207 index_type: Option<i32>,
208 format: OutputFormat,
209) -> Result<()> {
210 let sec = parse_symbol(owner)?;
211 let (client, _rx) = connect_gateway(gateway, "futucli-option-expiry").await?;
212 let req = futu_proto::qot_get_option_expiration_date::Request {
213 c2s: futu_proto::qot_get_option_expiration_date::C2s {
214 owner: futu_proto::qot_common::Security {
215 market: sec.market as i32,
216 code: sec.code,
217 },
218 index_option_type: index_type,
219 header: None, },
221 };
222 let body = req.encode_to_vec();
223 let frame = client
224 .request(futu_core::proto_id::QOT_GET_OPTION_EXPIRATION_DATE, body)
225 .await?;
226 let resp = futu_proto::qot_get_option_expiration_date::Response::decode(frame.body.as_ref())
227 .map_err(|e| anyhow!("decode: {e}"))?;
228 if resp.ret_type != 0 {
229 bail!(
230 "option_expiration_date ret_type={} msg={:?}",
231 resp.ret_type,
232 resp.ret_msg
233 );
234 }
235 let s = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
236 let mut rows = Vec::new();
237 let mut jsons = Vec::new();
238 for d in &s.date_list {
239 rows.push(OptionExpiryRow {
240 strike_time: d.strike_time.clone().unwrap_or_default(),
241 distance: d.option_expiry_date_distance,
242 cycle: d.cycle.map(|c| c.to_string()).unwrap_or_else(|| "-".into()),
243 });
244 jsons.push(OptionExpiryJson {
245 strike_time: d.strike_time.clone(),
246 distance_days: d.option_expiry_date_distance,
247 cycle: d.cycle,
248 });
249 }
250 format.print_rows(&rows, &jsons)?;
251 Ok(())
252}