1use rmcp::schemars;
4use serde::Deserialize;
5
6use crate::tool_enums;
7
8use super::*;
9
10fn sub_type_list_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
11 schemars::json_schema!({
12 "type": "array",
13 "items": {
14 "oneOf": [
15 { "type": "integer", "format": "int32" },
16 { "type": "string" }
17 ]
18 }
19 })
20}
21
22#[derive(Debug, Deserialize, schemars::JsonSchema)]
23#[serde(deny_unknown_fields)]
24pub struct QuerySubscriptionReq {
25 #[schemars(description = "true=query all connections; false=only this connection (default)")]
26 #[serde(default)]
27 pub is_req_all_conn: bool,
28}
29
30#[derive(Debug, Deserialize, schemars::JsonSchema)]
31#[serde(deny_unknown_fields)]
32pub struct UnsubscribeReq {
33 #[schemars(
34 description = "Security symbols to unsubscribe (ignored if unsub_all=true); alias: stocks / code_list / symbol_list / security_list"
35 )]
36 #[serde(
38 default,
39 alias = "stocks",
40 alias = "code_list",
41 alias = "symbol_list",
42 alias = "security_list"
43 )]
44 pub symbols: Vec<String>,
45 #[schemars(
46 description = "Sub-type ids to unsubscribe. Accept int (1=Basic, 2=OrderBook, 4=Ticker, \
47 5=RT, 6=KL_Day, 7=KL_5Min, 8=KL_15Min, 9=KL_30Min, 10=KL_60Min, 11=KL_1Min, \
48 12=KL_Week, 13=KL_Month, 14=Broker, 15=KL_Quarter, 16=KL_Year, 17=KL_3Min, \
49 18=KL_10Min, 19=KL_120Min, 20=KL_180Min, 21=KL_240Min, 22=OrderBook_Odd) \
50 OR string (\"Basic\" / \"OrderBook\" / \"KL_Day\" / \"day\" / ...). Alias: \
51 sub_type_list. Uses the daemon proto mapping: 3 is reserved/None, 4=Ticker, \
52 10=KL_60Min, 13=KL_Month, 18=KL_10Min."
53 )]
54 #[schemars(schema_with = "sub_type_list_schema")]
55 #[serde(
57 default,
58 alias = "sub_type_list",
59 deserialize_with = "tool_enums::deser_subtype_list_as_vec_i32"
60 )]
61 pub sub_types: Vec<i32>,
62 #[schemars(
63 description = "true=clear all subscriptions on this connection (ignores symbols/sub_types); alias: unsubscribe_all"
64 )]
65 #[serde(default, alias = "unsubscribe_all")]
66 pub unsub_all: bool,
67}
68
69#[derive(Debug, Deserialize, schemars::JsonSchema)]
71#[serde(deny_unknown_fields)]
72pub struct SubscribeReq {
73 #[schemars(
74 description = "Security symbols to subscribe, e.g. [\"HK.00700\", \"US.AAPL\"]. Alias: stocks / code_list / symbol_list / security_list"
75 )]
76 #[serde(
78 alias = "stocks",
79 alias = "code_list",
80 alias = "symbol_list",
81 alias = "security_list"
82 )]
83 pub symbols: Vec<String>,
84 #[schemars(
85 description = "Sub-type ids to subscribe. Accept int (1=Basic, 2=OrderBook, 4=Ticker, \
86 5=RT, 6=KL_Day, 7=KL_5Min, 8=KL_15Min, 9=KL_30Min, 10=KL_60Min, 11=KL_1Min, \
87 12=KL_Week, 13=KL_Month, 14=Broker, 15=KL_Quarter, 16=KL_Year, 17=KL_3Min, \
88 18=KL_10Min, 19=KL_120Min, 20=KL_180Min, 21=KL_240Min, 22=OrderBook_Odd) \
89 OR string (\"Basic\" / \"OrderBook\" / \"KL_Day\" / \"day\" / ...). Alias: \
90 sub_type_list. Uses the daemon proto mapping: 3 is reserved/None, 4=Ticker, \
91 10=KL_60Min, 13=KL_Month, 18=KL_10Min."
92 )]
93 #[schemars(schema_with = "sub_type_list_schema")]
94 #[serde(
96 alias = "sub_type_list",
97 deserialize_with = "tool_enums::deser_subtype_list_as_vec_i32"
98 )]
99 pub sub_types: Vec<i32>,
100 #[schemars(
101 description = "If true, backend pushes current snapshot immediately after subscribe (useful for agents needing warm state). Default true."
102 )]
103 #[serde(default = "default_is_first_push")]
104 pub is_first_push: bool,
105 #[schemars(
106 description = "If true, register push on this connection (agent will receive push via SSE notification in HTTP mode). Default true."
107 )]
108 #[serde(default = "default_is_reg_push")]
109 pub is_reg_push: bool,
110 #[schemars(
111 description = "Qot_Sub.extendedTime: include US pre/post-market data for supported real-time K/RT/Ticker subscriptions. Default false."
112 )]
113 #[serde(default, alias = "extendedTime")]
114 pub extended_time: Option<bool>,
115 #[schemars(
116 description = "Session: 0=NONE, 1=RTH, 2=ETH, 3=ALL. OVERNIGHT is not supported for subscriptions."
117 )]
118 #[serde(default)]
119 pub session: Option<i32>,
120 #[schemars(
121 description = "Qot_Sub.isSubOrderBookDetail: subscribe order-book detail when available. Default false."
122 )]
123 #[serde(
124 default,
125 alias = "is_sub_order_book_detail",
126 alias = "orderbook_detail"
127 )]
128 pub is_sub_order_book_detail: Option<bool>,
129}
130
131impl SubscribeReq {
132 pub fn validate(&self) -> Result<(), String> {
133 futu_surface_spec::input::validate_subscription_session_id(self.session)
134 .map_err(|error| format!("SubscribeReq.session invalid: {error}"))?;
135 match futu_core::qot_subscription_options::SubscribeOptionsPlan::from_raw(
136 self.session,
137 self.extended_time,
138 self.is_sub_order_book_detail,
139 ) {
140 Ok(_) => Ok(()),
141 Err(futu_core::qot_subscription_options::SubscribePlanError::OvernightSessionUnsupported) => {
142 Err(
143 "SubscribeReq.session=4 (OVERNIGHT) is not supported by QOT subscribe; \
144 use 2=ETH or 3=ALL"
145 .to_string(),
146 )
147 }
148 }
149 }
150}
151
152#[derive(Debug, Deserialize, schemars::JsonSchema)]
153#[serde(deny_unknown_fields)]
154pub struct SubAccPushReq {
155 #[schemars(description = "Array of account IDs (u64) to receive order/deal push \
156 for. ⚠️ Call `futu_list_accounts` first to discover real \
157 `acc_id` values; do NOT hallucinate 18-digit numbers — \
158 invalid ids will silently fail to receive push. Alias: account_ids / accounts")]
159 #[serde(alias = "account_ids", alias = "accounts")]
161 pub acc_ids: Vec<u64>,
162 #[schemars(
163 description = "Optional per-call API key plaintext. HTTP scope mode still requires a valid Bearer on every /mcp request; this field overrides that identity for the tool handler. In stdio mode: tool argument > startup key."
164 )]
165 #[serde(default)]
166 pub api_key: Option<String>,
167}
168
169#[derive(Debug, Deserialize, schemars::JsonSchema)]
171#[serde(deny_unknown_fields)]
172pub struct UnsubAccPushReq {
173 #[schemars(description = "Required: session_id returned by `futu_sub_acc_push` \
176 response (session_id field or unsub_hint). If omitted, \
177 handler returns an error. If session_id not found (e.g. \
178 4h auto-purged), removed_count=0 is returned.")]
179 #[serde(default)]
180 pub session_id: Option<String>,
181 #[schemars(
182 description = "Optional per-call API key plaintext. HTTP scope mode still requires a valid Bearer on every /mcp request; this field overrides that identity for the tool handler. In stdio mode use the same tool/startup key that created the subscription."
183 )]
184 #[serde(default)]
185 pub api_key: Option<String>,
186}