Skip to main content

futu_mcp/tool_args/
push.rs

1//! v1.4.110 P1-1: 拆自 `tool_args.rs` 按 handler 域分组.
2
3use 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    // v1.4.84 §5 B1
37    #[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    // v1.4.84 §5 B2 field migration: Vec<SubTypeEnum> 双接
56    #[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/// v1.4.74 A1 BUG-011 fix: MCP `futu_subscribe` request body(对称于
70/// `UnsubscribeReq`)。
71#[derive(Debug, Deserialize, schemars::JsonSchema)]
72#[serde(deny_unknown_fields)]
73pub struct SubscribeReq {
74    #[schemars(
75        description = "Security symbols to subscribe, e.g. [\"HK.00700\", \"US.AAPL\"]. Alias: stocks / code_list / symbol_list / security_list"
76    )]
77    // v1.4.84 §5 B1
78    #[serde(
79        alias = "stocks",
80        alias = "code_list",
81        alias = "symbol_list",
82        alias = "security_list"
83    )]
84    pub symbols: Vec<String>,
85    #[schemars(
86        description = "Sub-type ids to subscribe. Accept int (1=Basic, 2=OrderBook, 4=Ticker, \
87                       5=RT, 6=KL_Day, 7=KL_5Min, 8=KL_15Min, 9=KL_30Min, 10=KL_60Min, 11=KL_1Min, \
88                       12=KL_Week, 13=KL_Month, 14=Broker, 15=KL_Quarter, 16=KL_Year, 17=KL_3Min, \
89                       18=KL_10Min, 19=KL_120Min, 20=KL_180Min, 21=KL_240Min, 22=OrderBook_Odd) \
90                       OR string (\"Basic\" / \"OrderBook\" / \"KL_Day\" / \"day\" / ...). Alias: \
91                       sub_type_list. Uses the daemon proto mapping: 3 is reserved/None, 4=Ticker, \
92                       10=KL_60Min, 13=KL_Month, 18=KL_10Min."
93    )]
94    #[schemars(schema_with = "sub_type_list_schema")]
95    // v1.4.84 §5 B2 field migration
96    #[serde(
97        alias = "sub_type_list",
98        deserialize_with = "tool_enums::deser_subtype_list_as_vec_i32"
99    )]
100    pub sub_types: Vec<i32>,
101    #[schemars(
102        description = "If true, backend pushes current snapshot immediately after subscribe (useful for agents needing warm state). Default true."
103    )]
104    #[serde(default = "default_is_first_push")]
105    pub is_first_push: bool,
106    #[schemars(
107        description = "If true, register push on this connection (agent will receive push via SSE notification in HTTP mode). Default true."
108    )]
109    #[serde(default = "default_is_reg_push")]
110    pub is_reg_push: bool,
111    #[schemars(
112        description = "Qot_Sub.extendedTime: include US pre/post-market data for supported real-time K/RT/Ticker subscriptions. Default false."
113    )]
114    #[serde(default, alias = "extendedTime")]
115    pub extended_time: Option<bool>,
116    #[schemars(
117        description = "Qot_Sub.session: 0=NONE, 1=RTH, 2=ETH, 3=ALL. 4=OVERNIGHT is rejected by C++ OpenD for QOT subscribe."
118    )]
119    #[serde(default)]
120    pub session: Option<i32>,
121    #[schemars(
122        description = "Qot_Sub.isSubOrderBookDetail: subscribe order-book detail when available. Default false."
123    )]
124    #[serde(
125        default,
126        alias = "is_sub_order_book_detail",
127        alias = "orderbook_detail"
128    )]
129    pub is_sub_order_book_detail: Option<bool>,
130}
131
132impl SubscribeReq {
133    pub fn validate(&self) -> Result<(), String> {
134        match futu_qot::subscription_plan::SubscribeOptionsPlan::from_raw(
135            self.session,
136            self.extended_time,
137            self.is_sub_order_book_detail,
138        ) {
139            Ok(_) => Ok(()),
140            Err(futu_qot::subscription_plan::SubscribePlanError::OvernightSessionUnsupported) => {
141                Err(
142                    "SubscribeReq.session=4 (OVERNIGHT) is not supported by QOT subscribe; \
143                 use 2=ETH or 3=ALL"
144                        .to_string(),
145                )
146            }
147        }
148    }
149}
150
151#[derive(Debug, Deserialize, schemars::JsonSchema)]
152#[serde(deny_unknown_fields)]
153pub struct SubAccPushReq {
154    #[schemars(description = "Array of account IDs (u64) to receive order/deal push \
155                       for. ⚠️ Call `futu_list_accounts` first to discover real \
156                       `acc_id` values; do NOT hallucinate 18-digit numbers — \
157                       invalid ids will silently fail to receive push. Alias: account_ids / accounts")]
158    // v1.4.84 §5 B1
159    #[serde(alias = "account_ids", alias = "accounts")]
160    pub acc_ids: Vec<u64>,
161    #[schemars(
162        description = "Optional per-call API key plaintext. Priority: tool args > HTTP Bearer > startup key. Use to scope this push subscription to a specific narrow key."
163    )]
164    #[serde(default)]
165    pub api_key: Option<String>,
166}
167
168/// v1.4.58 Phase A: `futu_unsub_acc_push` request body.
169#[derive(Debug, Deserialize, schemars::JsonSchema)]
170#[serde(deny_unknown_fields)]
171pub struct UnsubAccPushReq {
172    /// session_id from previous `futu_sub_acc_push` response — **required**.
173    /// rmcp 的 Peer<RoleServer> 不实装 PartialEq,无法按 peer 身份批量撤销。
174    #[schemars(description = "Required: session_id returned by `futu_sub_acc_push` \
175                       response (session_id field or unsub_hint). If omitted, \
176                       handler returns an error. If session_id not found (e.g. \
177                       4h auto-purged), removed_count=0 is returned.")]
178    #[serde(default)]
179    pub session_id: Option<String>,
180    #[schemars(
181        description = "Optional per-call API key plaintext. Priority: tool args > HTTP Bearer > startup key. Use the same key that created the push subscription when running in stdio mode with multiple keys."
182    )]
183    #[serde(default)]
184    pub api_key: Option<String>,
185}