Skip to main content

futucli/cmd/analysis/
option_args.rs

1//! v1.4.110+ split (from cmd/analysis.rs): option_args domain.
2//!
3//! pub items: OptionChainGreekFilterArgs + v10.6 option-analysis commands.
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
13fn display_opt(value: &Option<String>) -> String {
14    value.clone().unwrap_or_default()
15}
16
17fn display_i64(value: Option<i64>) -> String {
18    value.map(|v| v.to_string()).unwrap_or_default()
19}
20
21fn display_f64(value: Option<f64>) -> String {
22    value.map(|v| v.to_string()).unwrap_or_default()
23}
24
25// ============================================================
26// option-chain
27// ============================================================
28
29#[derive(Tabled)]
30pub(super) struct OptionChainRow {
31    #[tabled(rename = "Strike Time")]
32    pub strike_time: String,
33    #[tabled(rename = "Calls")]
34    pub call_count: usize,
35    #[tabled(rename = "Puts")]
36    pub put_count: usize,
37    #[tabled(rename = "Example Call")]
38    pub example_call: String,
39    #[tabled(rename = "Example Put")]
40    pub example_put: String,
41}
42
43#[derive(Serialize)]
44pub(super) struct OptionChainJson {
45    pub strike_time: String,
46    pub call_symbols: Vec<String>,
47    pub put_symbols: Vec<String>,
48}
49
50#[derive(Debug, Clone, Default)]
51pub struct OptionChainGreekFilterArgs {
52    pub delta_min: Option<f64>,
53    pub delta_max: Option<f64>,
54    pub iv_min: Option<f64>,
55    pub iv_max: Option<f64>,
56    pub oi_min: Option<f64>,
57    pub oi_max: Option<f64>,
58    pub gamma_min: Option<f64>,
59    pub gamma_max: Option<f64>,
60    pub vega_min: Option<f64>,
61    pub vega_max: Option<f64>,
62    pub theta_min: Option<f64>,
63    pub theta_max: Option<f64>,
64}
65
66impl OptionChainGreekFilterArgs {
67    pub fn into_data_filter(self) -> Option<futu_proto::qot_get_option_chain::DataFilter> {
68        let any_filter = self.delta_min.is_some()
69            || self.delta_max.is_some()
70            || self.iv_min.is_some()
71            || self.iv_max.is_some()
72            || self.oi_min.is_some()
73            || self.oi_max.is_some()
74            || self.gamma_min.is_some()
75            || self.gamma_max.is_some()
76            || self.vega_min.is_some()
77            || self.vega_max.is_some()
78            || self.theta_min.is_some()
79            || self.theta_max.is_some();
80
81        any_filter.then_some(futu_proto::qot_get_option_chain::DataFilter {
82            implied_volatility_min: self.iv_min,
83            implied_volatility_max: self.iv_max,
84            delta_min: self.delta_min,
85            delta_max: self.delta_max,
86            gamma_min: self.gamma_min,
87            gamma_max: self.gamma_max,
88            vega_min: self.vega_min,
89            vega_max: self.vega_max,
90            theta_min: self.theta_min,
91            theta_max: self.theta_max,
92            rho_min: None,
93            rho_max: None,
94            net_open_interest_min: None,
95            net_open_interest_max: None,
96            open_interest_min: self.oi_min,
97            open_interest_max: self.oi_max,
98            vol_min: None,
99            vol_max: None,
100        })
101    }
102}
103
104// ============================================================
105// option-volatility
106// ============================================================
107
108#[derive(Tabled)]
109struct OptionVolatilityRow {
110    #[tabled(rename = "Date")]
111    timestamp_str: String,
112    #[tabled(rename = "Timestamp")]
113    timestamp: String,
114    #[tabled(rename = "IV")]
115    implied_volatility: String,
116    #[tabled(rename = "HV")]
117    history_volatility: String,
118    #[tabled(rename = "Premium")]
119    volatility_premium: String,
120    #[tabled(rename = "Avg IV")]
121    average_impvol: String,
122    #[tabled(rename = "Status")]
123    impvol_status: String,
124    #[tabled(rename = "Analysis")]
125    analysis: String,
126}
127
128#[derive(Serialize)]
129struct OptionVolatilityJson {
130    symbol: String,
131    s2c: futu_proto::qot_get_option_volatility::S2c,
132}
133
134pub async fn run_option_volatility(
135    gateway: &str,
136    symbol: &str,
137    query_time_period: Option<i32>,
138    hv_time_period: Option<i32>,
139    format: OutputFormat,
140) -> Result<()> {
141    let sec = parse_symbol(symbol)?;
142    let (client, _rx) = connect_gateway(gateway, "futucli-option-volatility").await?;
143    let req = futu_proto::qot_get_option_volatility::Request {
144        c2s: futu_proto::qot_get_option_volatility::C2s {
145            security: futu_proto::qot_common::Security {
146                market: sec.market as i32,
147                code: sec.code.clone(),
148            },
149            query_time_period,
150            hv_time_period,
151        },
152    };
153    let frame = client
154        .request(
155            futu_core::proto_id::QOT_GET_OPTION_VOLATILITY,
156            req.encode_to_vec(),
157        )
158        .await?;
159    let resp = futu_proto::qot_get_option_volatility::Response::decode(frame.body.as_ref())
160        .map_err(|e| anyhow!("decode option_volatility: {e}"))?;
161    if resp.ret_type != 0 {
162        bail!(
163            "option_volatility ret_type={} msg={:?}",
164            resp.ret_type,
165            resp.ret_msg
166        );
167    }
168    let s2c = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
169    let rows: Vec<_> = s2c
170        .item_list
171        .iter()
172        .map(|item| OptionVolatilityRow {
173            timestamp_str: display_opt(&item.timestamp_str),
174            timestamp: display_i64(item.timestamp),
175            implied_volatility: display_f64(item.implied_volatility),
176            history_volatility: display_f64(item.history_volatility),
177            volatility_premium: display_f64(item.volatility_premium),
178            average_impvol: display_f64(s2c.average_impvol),
179            impvol_status: s2c
180                .impvol_status
181                .map(|value| value.to_string())
182                .unwrap_or_default(),
183            analysis: display_opt(&s2c.analysis),
184        })
185        .collect();
186    let json = OptionVolatilityJson {
187        symbol: symbol.to_string(),
188        s2c,
189    };
190    format.print_rows(&rows, &[json])?;
191    Ok(())
192}
193
194// ============================================================
195// option-exercise-probability
196// ============================================================
197
198#[derive(Tabled)]
199struct OptionExerciseProbabilityRow {
200    #[tabled(rename = "Date")]
201    timestamp_str: String,
202    #[tabled(rename = "Timestamp")]
203    timestamp: String,
204    #[tabled(rename = "Security Price")]
205    security_price: String,
206    #[tabled(rename = "Strike Probability")]
207    strike_probability: String,
208}
209
210#[derive(Serialize)]
211struct OptionExerciseProbabilityJson {
212    symbol: String,
213    s2c: futu_proto::qot_get_option_exercise_probability::S2c,
214}
215
216pub async fn run_option_exercise_probability(
217    gateway: &str,
218    symbol: &str,
219    format: OutputFormat,
220) -> Result<()> {
221    let sec = parse_symbol(symbol)?;
222    let (client, _rx) = connect_gateway(gateway, "futucli-option-exercise-probability").await?;
223    let req = futu_proto::qot_get_option_exercise_probability::Request {
224        c2s: futu_proto::qot_get_option_exercise_probability::C2s {
225            security: futu_proto::qot_common::Security {
226                market: sec.market as i32,
227                code: sec.code.clone(),
228            },
229        },
230    };
231    let frame = client
232        .request(
233            futu_core::proto_id::QOT_GET_OPTION_EXERCISE_PROBABILITY,
234            req.encode_to_vec(),
235        )
236        .await?;
237    let resp =
238        futu_proto::qot_get_option_exercise_probability::Response::decode(frame.body.as_ref())
239            .map_err(|e| anyhow!("decode option_exercise_probability: {e}"))?;
240    if resp.ret_type != 0 {
241        bail!(
242            "option_exercise_probability ret_type={} msg={:?}",
243            resp.ret_type,
244            resp.ret_msg
245        );
246    }
247    let s2c = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
248    let rows: Vec<_> = s2c
249        .item_list
250        .iter()
251        .map(|item| OptionExerciseProbabilityRow {
252            timestamp_str: display_opt(&item.timestamp_str),
253            timestamp: display_i64(item.timestamp),
254            security_price: display_f64(item.security_price),
255            strike_probability: display_f64(item.strike_probability),
256        })
257        .collect();
258    let json = OptionExerciseProbabilityJson {
259        symbol: symbol.to_string(),
260        s2c,
261    };
262    format.print_rows(&rows, &[json])?;
263    Ok(())
264}