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
64#[derive(Tabled)]
65struct ReminderRow {
66 #[tabled(rename = "Key")]
67 key: i64,
68 #[tabled(rename = "Type")]
69 r#type: i32,
70 #[tabled(rename = "Value")]
71 value: String,
72 #[tabled(rename = "Freq")]
73 freq: i32,
74 #[tabled(rename = "Enable")]
75 enable: bool,
76 #[tabled(rename = "Note")]
77 note: String,
78}
79
80#[derive(Serialize)]
81struct ReminderJson {
82 symbol: String,
83 name: Option<String>,
84 key: i64,
85 reminder_type: i32,
86 value: f64,
87 freq: i32,
88 is_enable: bool,
89 note: String,
90}
91
92pub async fn run_get_price_reminder(
93 gateway: &str,
94 symbol: Option<&str>,
95 market: Option<i32>,
96 format: OutputFormat,
97) -> Result<()> {
98 let (security, market_code) = match symbol {
99 Some(s) => {
100 let sec = parse_symbol(s)?;
101 (
102 Some(futu_proto::qot_common::Security {
103 market: sec.market as i32,
104 code: sec.code,
105 }),
106 None,
107 )
108 }
109 None => (None, market),
110 };
111 if security.is_none() && market_code.is_none() {
112 bail!("need either --symbol or --market");
113 }
114 let (client, _rx) = connect_gateway(gateway, "futucli-price-reminder").await?;
115 let req = futu_proto::qot_get_price_reminder::Request {
116 c2s: futu_proto::qot_get_price_reminder::C2s {
117 security,
118 market: market_code,
119 header: None,
120 },
121 };
122 let body = req.encode_to_vec();
123 let frame = client
124 .request(futu_core::proto_id::QOT_GET_PRICE_REMINDER, body)
125 .await?;
126 let resp = futu_proto::qot_get_price_reminder::Response::decode(frame.body.as_ref())
127 .map_err(|e| anyhow!("decode: {e}"))?;
128 if resp.ret_type != 0 {
129 bail!(
130 "price_reminder ret_type={} msg={:?}",
131 resp.ret_type,
132 resp.ret_msg
133 );
134 }
135 let s = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
136 let mut rows = Vec::new();
137 let mut jsons = Vec::new();
138 for pr in &s.price_reminder_list {
139 let sym = format!("{}.{}", pr.security.market, pr.security.code);
140 for r in &pr.item_list {
141 rows.push(ReminderRow {
142 key: r.key,
143 r#type: r.r#type,
144 value: format!("{:.3}", r.value),
145 freq: r.freq,
146 enable: r.is_enable,
147 note: r.note.clone(),
148 });
149 jsons.push(ReminderJson {
150 symbol: sym.clone(),
151 name: pr.name.clone(),
152 key: r.key,
153 reminder_type: r.r#type,
154 value: r.value,
155 freq: r.freq,
156 is_enable: r.is_enable,
157 note: r.note.clone(),
158 });
159 }
160 }
161 format.print_rows(&rows, &jsons)?;
162 Ok(())
163}
164
165#[derive(Tabled)]
166struct OptionExpiryRow {
167 #[tabled(rename = "Strike Time")]
168 strike_time: String,
169 #[tabled(rename = "Distance (days)")]
170 distance: i32,
171 #[tabled(rename = "Cycle")]
172 cycle: String,
173}
174
175#[derive(Serialize)]
176struct OptionExpiryJson {
177 strike_time: Option<String>,
178 distance_days: i32,
179 cycle: Option<i32>,
180}
181
182pub async fn run_option_expiration_date(
183 gateway: &str,
184 owner: &str,
185 index_type: Option<i32>,
186 format: OutputFormat,
187) -> Result<()> {
188 let sec = parse_symbol(owner)?;
189 let (client, _rx) = connect_gateway(gateway, "futucli-option-expiry").await?;
190 let req = futu_proto::qot_get_option_expiration_date::Request {
191 c2s: futu_proto::qot_get_option_expiration_date::C2s {
192 owner: futu_proto::qot_common::Security {
193 market: sec.market as i32,
194 code: sec.code,
195 },
196 index_option_type: index_type,
197 header: None, },
199 };
200 let body = req.encode_to_vec();
201 let frame = client
202 .request(futu_core::proto_id::QOT_GET_OPTION_EXPIRATION_DATE, body)
203 .await?;
204 let resp = futu_proto::qot_get_option_expiration_date::Response::decode(frame.body.as_ref())
205 .map_err(|e| anyhow!("decode: {e}"))?;
206 if resp.ret_type != 0 {
207 bail!(
208 "option_expiration_date ret_type={} msg={:?}",
209 resp.ret_type,
210 resp.ret_msg
211 );
212 }
213 let s = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
214 let mut rows = Vec::new();
215 let mut jsons = Vec::new();
216 for d in &s.date_list {
217 rows.push(OptionExpiryRow {
218 strike_time: d.strike_time.clone().unwrap_or_default(),
219 distance: d.option_expiry_date_distance,
220 cycle: d.cycle.map(|c| c.to_string()).unwrap_or_else(|| "-".into()),
221 });
222 jsons.push(OptionExpiryJson {
223 strike_time: d.strike_time.clone(),
224 distance_days: d.option_expiry_date_distance,
225 cycle: d.cycle,
226 });
227 }
228 format.print_rows(&rows, &jsons)?;
229 Ok(())
230}