Skip to main content

futu_backend/
option_analytics.rs

1use bytes::Bytes;
2use prost::Message;
3
4use futu_command_spec::QotReadOperation;
5use futu_core::error::{FutuError, Result};
6use futu_domain_qot_option::{
7    OptionMarketStatisticPlan, OptionOverviewBatch, OptionUnderlyingHistoryPlan,
8    OptionUnderlyingVolatilityPlan,
9};
10
11use crate::command_runtime::execute_qot_read_with_reserved;
12use crate::conn::BackendConn;
13use crate::proto_internal::option_statistic_service as proto;
14
15#[derive(Debug)]
16pub struct OptionAnalyticsBackendPage<T> {
17    pub request_serial_no: u32,
18    pub response: T,
19}
20
21#[must_use]
22pub fn build_market_statistic_request(
23    plan: &OptionMarketStatisticPlan,
24) -> proto::GetMarketStatisticDatasReq {
25    proto::GetMarketStatisticDatasReq {
26        market_type: Some(plan.backend_market),
27        underlying_type: Some(plan.underlying_type),
28        statistic_data_type: Some(plan.data_type),
29        from: None,
30        count: None,
31        end_time: Some(plan.end_time),
32        start_time: Some(plan.start_time),
33        user_rights: None,
34    }
35}
36
37#[must_use]
38pub fn build_underlying_history_request(
39    plan: &OptionUnderlyingHistoryPlan,
40) -> proto::GetStatisticDatasReq {
41    proto::GetStatisticDatasReq {
42        stock_id: Some(plan.owner.stock_id),
43        index_option_type: plan.backend_index_option_type,
44        market_type: Some(plan.backend_market),
45        start_time: Some(plan.start_time),
46        end_time: Some(plan.end_time),
47        count: None,
48        user_rights: None,
49    }
50}
51
52#[must_use]
53pub fn build_overview_request(batch: &OptionOverviewBatch) -> proto::GetUnderlyingsNewDataReq {
54    proto::GetUnderlyingsNewDataReq {
55        underlying_key_list: batch
56            .keys
57            .iter()
58            .map(|key| proto::UnderlyingKey {
59                stock_id: Some(key.stock_id),
60                index_option_type: key.index_option_type,
61            })
62            .collect(),
63        user_rights: None,
64        not_history_iv: None,
65        not_history_hv: None,
66    }
67}
68
69#[must_use]
70pub fn build_underlying_volatility_request(
71    plan: &OptionUnderlyingVolatilityPlan,
72) -> proto::GetVolatilityDatasReq {
73    proto::GetVolatilityDatasReq {
74        stock_id: Some(plan.owner.stock_id),
75        index_option_type: plan.backend_index_option_type,
76        market_type: Some(plan.backend_market),
77        end_time: (plan.end_time > 0).then_some(plan.end_time),
78        count: (plan.count > 0).then_some(plan.count),
79        user_rights: None,
80    }
81}
82
83pub async fn pull_market_statistic(
84    backend: &BackendConn,
85    plan: &OptionMarketStatisticPlan,
86) -> Result<OptionAnalyticsBackendPage<proto::GetMarketStatisticDatasRsp>> {
87    pull(
88        backend,
89        QotReadOperation::OptionMarketStatistic,
90        build_market_statistic_request(plan),
91        plan.quote_mkt_type,
92        "option market statistic",
93    )
94    .await
95}
96
97pub async fn pull_underlying_history(
98    backend: &BackendConn,
99    plan: &OptionUnderlyingHistoryPlan,
100) -> Result<OptionAnalyticsBackendPage<proto::GetStatisticDatasRsp>> {
101    pull(
102        backend,
103        QotReadOperation::OptionUnderlyingHisStatistic,
104        build_underlying_history_request(plan),
105        plan.quote_mkt_type,
106        "option underlying history",
107    )
108    .await
109}
110
111pub async fn pull_overview_batch(
112    backend: &BackendConn,
113    batch: &OptionOverviewBatch,
114) -> Result<OptionAnalyticsBackendPage<proto::GetUnderlyingsNewDataRsp>> {
115    pull(
116        backend,
117        QotReadOperation::OptionUnderlyingOverview,
118        build_overview_request(batch),
119        batch.quote_mkt_type,
120        "option underlying overview",
121    )
122    .await
123}
124
125pub async fn pull_underlying_volatility(
126    backend: &BackendConn,
127    plan: &OptionUnderlyingVolatilityPlan,
128) -> Result<OptionAnalyticsBackendPage<proto::GetVolatilityDatasRsp>> {
129    pull(
130        backend,
131        QotReadOperation::OptionUnderlyingHisVolatility,
132        build_underlying_volatility_request(plan),
133        plan.quote_mkt_type,
134        "option underlying volatility",
135    )
136    .await
137}
138
139async fn pull<Req, Rsp>(
140    backend: &BackendConn,
141    operation: QotReadOperation,
142    request: Req,
143    quote_mkt_type: u8,
144    label: &'static str,
145) -> Result<OptionAnalyticsBackendPage<Rsp>>
146where
147    Req: Message,
148    Rsp: Message + Default + BackendStatus,
149{
150    // Ref: NNBiz_Qot_OptionStatistic.cpp:40,129,236,384. C++ writes
151    // NN_QuoteMktType_*_OPTIONS to reserved[0] and SECURITY(0) to reserved[1].
152    let mut reserved = [0_u8; 10];
153    reserved[0] = quote_mkt_type;
154    let response = execute_qot_read_with_reserved(
155        backend,
156        operation,
157        Bytes::from(request.encode_to_vec()),
158        reserved,
159    )
160    .await?;
161    let decoded = Rsp::decode(response.body.as_ref()).map_err(FutuError::Proto)?;
162    match decoded.backend_code() {
163        Some(0) => Ok(OptionAnalyticsBackendPage {
164            request_serial_no: response.request_serial_no,
165            response: decoded,
166        }),
167        Some(code) => Err(FutuError::ServerError {
168            ret_type: code,
169            msg: decoded
170                .backend_message()
171                .unwrap_or_else(|| format!("{label} backend rejected request")),
172        }),
173        None => Err(FutuError::Codec(format!(
174            "{label} backend response missing code"
175        ))),
176    }
177}
178
179trait BackendStatus {
180    fn backend_code(&self) -> Option<i32>;
181    fn backend_message(&self) -> Option<String>;
182}
183
184macro_rules! impl_backend_status {
185    ($type:ty) => {
186        impl BackendStatus for $type {
187            fn backend_code(&self) -> Option<i32> {
188                self.code
189            }
190
191            fn backend_message(&self) -> Option<String> {
192                self.msg.clone()
193            }
194        }
195    };
196}
197
198impl_backend_status!(proto::GetMarketStatisticDatasRsp);
199impl_backend_status!(proto::GetStatisticDatasRsp);
200impl_backend_status!(proto::GetUnderlyingsNewDataRsp);
201impl_backend_status!(proto::GetVolatilityDatasRsp);
202
203#[cfg(test)]
204#[path = "option_analytics/tests.rs"]
205mod tests;