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