Skip to main content

futu_mcp/tool_args/qot/
price_reminder.rs

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    /// Reminder session list controlling which trading sessions trigger the alert.
114    ///
115    /// `Qot_Common::PriceReminderMarketStatus`: 1=Open, 2=USPre, 3=USAfter, 4=USOverNight.
116    /// US stocks (with pre-/after-/overnight sessions) default to `[Open, USPre, USAfter]`
117    /// when this list is empty. Non-US securities ignore the list entirely.
118    #[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    /// Runtime validation for op-conditional required fields.
129    ///
130    /// Required fields by op:
131    /// - op=1 (Add): `reminder_type` + `freq` + `value`
132    /// - op=5 (Modify): `key` (other fields are optional and the backend
133    ///   preserves the existing value when omitted)
134    /// - op=2/3/4 (Del / Enable / Disable): `key`
135    /// - op=6 (DeleteAll): no extra required fields
136    ///
137    /// Returns `Err(String)` with a human-readable hint for the agent /
138    /// SDK user when validation fails.
139    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                // v1.4.106 codex 0450 F2 (P2): freq schema/runtime sync —
150                // gateway 对齐 C++ NN_PriceReminderFreq_None check 在 Add 路径
151                // 强制要求 freq, MCP/REST schema 必须同步要求, 否则 caller 按
152                // schema 不传 freq 会触发 runtime reject (silent ship-blocker
153                // 见 pitfall #54 schema-only fix).
154                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 => {} // DeleteAll: 无必填
191            _ => {
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        // v1.4.106 codex 0450 F5 (P3): strict reject invalid session list values
200        // (defense-in-depth — gateway also rejects but agent-side feedback is
201        // faster than wire round-trip). Deliberate deviation from C++ silent
202        // drop (see handler comment).
203        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    // v1.4.84 §5 B1
225    #[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    // v1.4.84 §5 B2 field migration (Option variant)
233    #[serde(
234        default,
235        deserialize_with = "deser_price_reminder_market_as_option_i32"
236    )]
237    pub market: Option<i32>,
238}
239
240impl GetPriceReminderReq {
241    /// Runtime validation: symbol XOR market is required.
242    ///
243    /// Schema-level oneOf is not enforceable through this serde shape, so the
244    /// handler returns a clear runtime validation error.
245    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        // symbol + market 同时传: symbol wins (schema 已说明), 不 error
254        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    // v1.4.84 §5 B1
265    #[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}