1use rmcp::schemars;
2use serde::Deserialize;
3
4use futu_core::qot_price_reminder;
5
6use crate::tool_enums;
7
8fn parse_price_reminder_market(value: i32) -> Option<i32> {
9 qot_price_reminder::is_price_reminder_market(value).then_some(value)
10}
11
12fn parse_price_reminder_market_str(value: &str) -> Option<i32> {
13 let trimmed = value.trim();
14 if let Ok(value) = trimmed.parse::<i32>() {
15 return parse_price_reminder_market(value);
16 }
17 qot_price_reminder::price_reminder_market_from_str_alias(trimmed)
18}
19
20fn deser_price_reminder_market_as_option_i32<'de, D>(
21 deserializer: D,
22) -> Result<Option<i32>, D::Error>
23where
24 D: serde::Deserializer<'de>,
25{
26 let raw: Option<serde_json::Value> = Option::deserialize(deserializer)?;
27 match raw {
28 None | Some(serde_json::Value::Null) => Ok(None),
29 Some(serde_json::Value::Number(number)) => {
30 let value = number.as_i64().ok_or_else(|| {
31 serde::de::Error::custom(format!("price reminder market number invalid: {number}"))
32 })? as i32;
33 parse_price_reminder_market(value).map(Some).ok_or_else(|| {
34 serde::de::Error::custom(format!(
35 "unknown price reminder market int {value}: valid = {}",
36 qot_price_reminder::PRICE_REMINDER_MARKET_VALID_VALUES
37 ))
38 })
39 }
40 Some(serde_json::Value::String(text)) => parse_price_reminder_market_str(&text)
41 .map(Some)
42 .ok_or_else(|| {
43 serde::de::Error::custom(format!(
44 "unknown price reminder market {text:?}: valid = {}",
45 qot_price_reminder::PRICE_REMINDER_MARKET_VALID_VALUES
46 ))
47 }),
48 Some(other) => Err(serde::de::Error::custom(format!(
49 "price reminder market must be int or string, got: {other}"
50 ))),
51 }
52}
53
54#[derive(Debug, Deserialize, schemars::JsonSchema)]
55#[serde(deny_unknown_fields)]
56pub struct SetPriceReminderReq {
57 #[schemars(description = "Security symbol (e.g. \"HK.00700\"). Field aliases: \
58 `code` / `stock` (deprecated — prefer canonical `symbol`).")]
59 #[serde(alias = "code", alias = "stock")]
60 pub symbol: String,
61 #[schemars(
62 description = "Op: 1=Add / SetAdd, 2=Del / SetDel, 3=Enable / SetEnable, \
63 4=Disable / SetDisable, 5=Modify, 6=DeleteAll / DelAll. \
64 Accepts integer code OR string form (e.g. 1 or \"Add\"). \
65 Aliases for the field name: `op_type` / `operation` \
66 (deprecated — prefer canonical `op`)."
67 )]
68 #[serde(
69 alias = "op_type",
70 alias = "operation",
71 deserialize_with = "tool_enums::deser_price_reminder_op_as_i32"
72 )]
73 pub op: i32,
74 #[schemars(
75 description = "Reminder key (from get_price_reminder; required for modify/del/enable/disable)"
76 )]
77 #[serde(default)]
78 pub key: Option<i64>,
79 #[schemars(description = "Qot_Common::PriceReminderType: \
80 1=PriceUp, 2=PriceDown, 3=ChangeRateUp, 4=ChangeRateDown, \
81 5=5MinChangeRateUp, 6=5MinChangeRateDown, 7=VolumeUp, 8=TurnoverUp, \
82 9=TurnoverRateUp, 10=BidPriceUp, 11=AskPriceDown, 12=BidVolUp, \
83 13=AskVolUp, 14=3MinChangeRateUp, 15=3MinChangeRateDown.")]
84 #[serde(default)]
85 pub reminder_type: Option<i32>,
86 #[schemars(
87 description = "Qot_Common::PriceReminderFreq: 1=Always, 2=OncePerDay, 3=Once. \
88 Required for op=1 (Add) — the gateway rejects Add without freq. \
89 Optional for op=5 (Modify) / 2/3/4 (Del/Enable/Disable) where the \
90 backend preserves the existing value when omitted."
91 )]
92 #[serde(default)]
93 pub freq: Option<i32>,
94 #[schemars(description = "Threshold value (required for Add/Modify)")]
95 #[serde(default)]
96 pub value: Option<f64>,
97 #[schemars(
98 description = "User note (optional). Maximum length: 40 half-width bytes \
99 (~20 CN characters or 40 ASCII characters) using UTF-16 \
100 half-/full-width counting (each ASCII code unit = 1 byte, \
101 each non-ASCII code unit = 2 bytes)."
102 )]
103 #[serde(default)]
104 pub note: Option<String>,
105 #[schemars(
111 description = "Reminder session list (PriceReminderMarketStatus: 1=Open, 2=USPre, \
112 3=USAfter, 4=USOverNight). For US stocks, an empty list defaults to \
113 [Open, USPre, USAfter]; for non-US securities the list is cleared."
114 )]
115 #[serde(default)]
116 pub reminder_session_list: Vec<i32>,
117}
118
119impl SetPriceReminderReq {
120 pub fn validate(&self) -> Result<(), String> {
132 match self.op {
133 1 => {
134 if self.reminder_type.is_none() {
135 return Err(
136 "SetPriceReminderReq op=1 (Add): `reminder_type` is required \
137 (PriceReminderType enum 1-15)"
138 .to_string(),
139 );
140 }
141 if self.freq.is_none() {
147 return Err("SetPriceReminderReq op=1 (Add): `freq` is required \
148 (PriceReminderFreq enum 1=Always / 2=OncePerDay / 3=Once)"
149 .to_string());
150 }
151 if self.value.is_none() {
152 return Err(
153 "SetPriceReminderReq op=1 (Add): `value` is required (threshold \
154 value for reminder_type)"
155 .to_string(),
156 );
157 }
158 }
159 2..=4 => {
160 if self.key.is_none() {
161 return Err(format!(
162 "SetPriceReminderReq op={} ({}): `key` is required (from \
163 get_price_reminder response)",
164 self.op,
165 match self.op {
166 2 => "Del",
167 3 => "Enable",
168 _ => "Disable",
169 }
170 ));
171 }
172 }
173 5 => {
174 if self.key.is_none() {
175 return Err(
176 "SetPriceReminderReq op=5 (Modify): `key` is required (from \
177 get_price_reminder response)"
178 .to_string(),
179 );
180 }
181 }
182 6 => {} _ => {
184 return Err(format!(
185 "SetPriceReminderReq: unknown op={}, expected 1=Add|2=Del|3=Enable|\
186 4=Disable|5=Modify|6=DeleteAll",
187 self.op
188 ));
189 }
190 }
191 for &session in &self.reminder_session_list {
196 if !matches!(session, 1..=4) {
197 return Err(format!(
198 "SetPriceReminderReq: invalid reminder_session_list entry \
199 {session} (PriceReminderMarketStatus 1=Open / 2=USPre / \
200 3=USAfter / 4=USOverNight)"
201 ));
202 }
203 }
204 Ok(())
205 }
206}
207
208#[derive(Debug, Deserialize, schemars::JsonSchema)]
209#[serde(deny_unknown_fields)]
210pub struct GetPriceReminderReq {
211 #[schemars(description = "Security symbol (MARKET.CODE, e.g. HK.00700). \
212 **Exactly one of `symbol` or `market` must be set.** \
213 Passing both: symbol wins. Passing neither returns an error. \
214 The exactly-one rule is enforced at runtime. \
215 Alias: code / stock")]
216 #[serde(default, alias = "code", alias = "stock")]
218 pub symbol: Option<String>,
219 #[schemars(
220 description = "Market code — price-reminder QotMarket subset. Accept int 1=HK, 2=HK_FUTURE, 11=US, 21=SH/CN, 22=SZ, 31=SG, 41=JP, 61=MY, 91=CC \
221 OR string (\"HK\" / \"HK_FUTURE\" / \"US\" / \"SH\" / \"SZ\" / \"CN\" / \"SG\" / \"JP\" / \"MY\" / \"CC\"). \
222 **Exactly one of `symbol` or `market` required** (see symbol field doc)."
223 )]
224 #[serde(
226 default,
227 deserialize_with = "deser_price_reminder_market_as_option_i32"
228 )]
229 pub market: Option<i32>,
230}
231
232impl GetPriceReminderReq {
233 pub fn validate(&self) -> Result<(), String> {
238 if self.symbol.is_none() && self.market.is_none() {
239 return Err(
240 "GetPriceReminderReq: exactly one of `symbol` or `market` is required \
241 (neither provided)"
242 .to_string(),
243 );
244 }
245 Ok(())
247 }
248}
249
250#[derive(Debug, Deserialize, schemars::JsonSchema)]
251#[serde(deny_unknown_fields)]
252pub struct OptionExpirationDateReq {
253 #[schemars(
254 description = "Underlying stock symbol (HK/US equities + HSI/HSCEI only); alias: symbol / owner / code / stock"
255 )]
256 #[serde(alias = "symbol", alias = "owner", alias = "code", alias = "stock")]
258 pub owner_symbol: String,
259 #[schemars(description = "For index options only: Qot_Common::IndexOptionType (optional)")]
260 #[serde(default)]
261 pub index_option_type: Option<i32>,
262}