1use bytes::Bytes;
2use prost::Message as _;
3
4use futu_command_spec::{QotReadOperation, QotWriteOperation};
5use futu_core::error::{FutuError, Result};
6use futu_domain_qot_option_event::{AlertInterval, AlertItem, AlertOp, AlertWritePlan};
7
8use crate::command_runtime::{execute_qot_read_with_reserved, execute_qot_write};
9use crate::conn::BackendConn;
10use crate::proto_internal::option_move_warn_svc as proto;
11
12pub struct AlertBackendPage {
13 pub items: Vec<AlertItem>,
14 pub next_page: Option<String>,
15 pub total: Option<i32>,
16}
17
18pub async fn pull_alerts(
19 backend: &BackendConn,
20 count: Option<i32>,
21 from: Option<i64>,
22) -> Result<AlertBackendPage> {
23 let request = proto::OptionMoveWarnGetNewReq {
24 start_idx: from,
25 query_count: count,
26 key_list: Vec::new(),
27 };
28 let mut reserved = [0; 10];
29 reserved[0] = 7;
32 let response = execute_qot_read_with_reserved(
33 backend,
34 QotReadOperation::OptionEventAlert,
35 Bytes::from(request.encode_to_vec()),
36 reserved,
37 )
38 .await?;
39 let decoded =
40 proto::OptionMoveWarnGetNewRsp::decode(response.body.as_ref()).map_err(FutuError::Proto)?;
41 require_success(decoded.ret_code, decoded.err_msg, "option event alert get")?;
42 Ok(AlertBackendPage {
43 items: decoded
44 .warn_items
45 .into_iter()
46 .map(alert_from_backend)
47 .collect(),
48 next_page: (decoded.has_more == Some(true))
49 .then(|| decoded.next_idx.map(|value| value.to_string()))
50 .flatten(),
51 total: decoded
52 .total_count
53 .map(|value| value as i32)
54 .filter(|value| *value > 0),
55 })
56}
57
58pub async fn set_alerts(backend: &BackendConn, op: AlertOp, plan: &AlertWritePlan) -> Result<()> {
59 let backend_op = match op {
60 AlertOp::Add => 1,
61 AlertOp::Delete | AlertOp::DeleteAll => 2,
62 AlertOp::Modify | AlertOp::Enable | AlertOp::Disable => 3,
63 };
64 let request = proto::OptionMoveWarnSetNewReq {
65 oper_type: Some(backend_op),
66 warn_items: plan.backend_items.iter().map(alert_to_backend).collect(),
67 };
68 let mut reserved = [0; 10];
69 reserved[0] = if matches!(plan.route_market, Some(3 | 4)) {
70 9
71 } else {
72 7
73 };
74 let response = execute_qot_write(
75 backend,
76 QotWriteOperation::OptionEventAlert,
77 Bytes::from(request.encode_to_vec()),
78 reserved,
79 )
80 .await?;
81 let decoded =
82 proto::OptionMoveWarnSetNewRsp::decode(response.body.as_ref()).map_err(FutuError::Proto)?;
83 require_success(decoded.ret_code, decoded.err_msg, "option event alert set")
84}
85
86fn alert_from_backend(item: proto::NewWarnItem) -> AlertItem {
87 AlertItem {
88 key: item.key.unwrap_or(0),
89 option_market: item.market_category.and_then(category_to_option_market),
90 enable: item.enable,
91 watchlist_group_id: item.wch_lst_group_id.filter(|value| *value != 0),
92 underlying_stock_id: item.underlying_id.filter(|value| *value != 0),
93 option_type: item.option_type.filter(|value| *value != 0),
94 side_types: item.side_types.into_iter().take(8).collect(),
95 order_types: item
96 .order_types
97 .into_iter()
98 .take(8)
99 .map(order_type_to_api)
100 .collect(),
101 market_cap: item.market_cap.map(interval_from_backend),
102 expiry_days: item.left_day.map(interval_from_backend),
103 price: item.price.map(interval_from_backend),
104 size: item.size.map(interval_from_backend),
105 premium: item.premium.map(interval_from_backend),
106 iv: item.iv.map(interval_from_backend),
107 earnings_utc: item.earning_report_utc.map(interval_from_backend),
108 note: item.note.filter(|value| !value.is_empty()),
109 }
110}
111
112fn alert_to_backend(item: &AlertItem) -> proto::NewWarnItem {
113 proto::NewWarnItem {
114 key: (item.key != 0).then_some(item.key),
115 market_category: item.option_market.and_then(option_market_to_category),
116 wch_lst_group_id: item.watchlist_group_id,
117 underlying_id: item.underlying_stock_id,
118 enable: item.enable,
119 market_cap: item.market_cap.map(interval_to_backend),
120 earning_report: None,
121 option_type: item.option_type,
122 side_types: item.side_types.clone(),
123 order_types: item
124 .order_types
125 .iter()
126 .copied()
127 .map(order_type_to_backend)
128 .collect(),
129 left_day: item.expiry_days.map(interval_to_backend),
130 price: item.price.map(interval_to_backend),
131 size: item.size.map(interval_to_backend),
132 premium: item.premium.map(interval_to_backend),
133 iv: item.iv.map(interval_to_backend),
134 note: item.note.clone(),
135 earning_report_period_type: None,
136 earning_report_utc: item.earnings_utc.map(interval_to_backend),
137 }
138}
139
140fn interval_from_backend(value: proto::Interval) -> AlertInterval {
141 AlertInterval {
142 min: value.min_value,
143 max: value.max_value,
144 exclude_min: value.exclude_min,
145 exclude_max: value.exclude_max,
146 }
147}
148
149fn interval_to_backend(value: AlertInterval) -> proto::Interval {
150 proto::Interval {
151 min_value: value.min,
152 max_value: value.max,
153 exclude_min: value.exclude_min,
154 exclude_max: value.exclude_max,
155 }
156}
157
158fn option_market_to_category(value: i32) -> Option<i32> {
159 match value {
160 1 => Some(1),
161 2 => Some(2),
162 3 => Some(4),
163 4 => Some(5),
164 _ => None,
165 }
166}
167
168fn category_to_option_market(value: i32) -> Option<i32> {
169 match value {
170 1 => Some(1),
171 2 => Some(2),
172 4 => Some(3),
173 5 => Some(4),
174 _ => None,
175 }
176}
177
178fn order_type_to_backend(value: i32) -> i32 {
179 match value {
180 0 => 4,
181 1 => 1,
182 2 => 3,
183 4 => 2,
184 _ => value,
185 }
186}
187
188fn order_type_to_api(value: i32) -> i32 {
189 match value {
190 1 => 1,
191 2 => 4,
192 3 => 2,
193 4 => 0,
194 _ => value,
195 }
196}
197
198fn require_success(code: Option<i32>, message: Option<String>, label: &str) -> Result<()> {
199 match code {
200 Some(0) => Ok(()),
201 Some(code) => Err(FutuError::ServerError {
202 ret_type: code,
203 msg: message.unwrap_or_else(|| format!("{label} backend rejected request")),
204 }),
205 None => Err(FutuError::Codec(format!(
206 "{label} backend response missing ret_code"
207 ))),
208 }
209}