Skip to main content

futucli/cmd/analysis/
price_reminder.rs

1//! v1.4.110+ split (from cmd/analysis.rs): price_reminder domain.
2//!
3//! pub items: SetPriceReminderCommand,run_set_price_reminder,run_get_price_reminder,run_option_expiration_date.
4
5use 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            // v1.4.106 codex 1116 F4 [P2]: 透传 user 提供的 session list, daemon
41            // 在 gateway 层统一处理美股默认 [Open, USPre, USAfter] / 非美股清空.
42            reminder_session_list: input.reminder_session_list.to_vec(),
43            header: None, // v1.4.110 codex Slice 1 schema 占位
44        },
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, // v1.4.110 codex Slice 1 schema 占位
198        },
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}