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
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#[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#[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#[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}