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