futu_backend/
option_analytics.rs1use 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 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;