futu_rest/routes/qot/
misc.rs1use axum::extract::{Extension, Json, State};
4use axum::http::StatusCode;
5use futu_auth::KeyRecord;
6use serde_json::Value;
7use std::sync::Arc;
8
9use crate::caller_context::CallerContext;
10use futu_core::proto_id;
11
12use super::*;
13
14use super::subscribe::body_requests_unsub_all;
15
16pub(super) fn normalize_financial_calendar_rest_body_for_internal_proto(body: Value) -> Value {
17 match body {
18 Value::Object(mut obj) if obj.len() == 1 => match obj.remove("c2s") {
19 Some(Value::Object(c2s)) => Value::Object(c2s),
20 Some(c2s) => {
21 let mut restored = serde_json::Map::new();
22 restored.insert("c2s".to_string(), c2s);
23 Value::Object(restored)
24 }
25 None => Value::Object(obj),
26 },
27 other => other,
28 }
29}
30
31pub async fn unsubscribe(
46 State(state): State<RestState>,
47 rec: Option<Extension<Arc<KeyRecord>>>,
48 Json(body): Json<Value>,
49) -> RawApiResult {
50 if body_requests_unsub_all(&body) {
53 return Err((
54 StatusCode::BAD_REQUEST,
55 Json(serde_json::json!({
56 "error": "/api/unsubscribe with is_unsub_all=true is **REST process-wide** — \
57 all REST callers share REST_SHARED_CONN (v1.4.90 P0-B), so \
58 清掉一个 = 清掉**所有 REST clients** 的 qot 订阅 (跨 caller \
59 影响). v1.4.104 codex round 3 F1 P1 fix: 默认 reject (与 \
60 /api/subscribe is_unsub_all reject 一致, 防 sibling-route bypass). \
61 替代方案:\n \
62 (a) 单 symbol unsubscribe: 列具体 sec_list + sub_type_list + \
63 is_sub_or_un_sub=false (per-key safe);\n \
64 (b) MCP / gRPC / WS surface 调 unsub_all (各 caller 有自己 conn_id). \
65 REST 当前没有 process-wide opt-in 或 admin clear endpoint.",
66 "v1.4.104_audit_round3_f1_fix": true,
67 "alternatives": [
68 "use explicit security_list + sub_type_list + is_sub_or_un_sub=false",
69 "use MCP / gRPC / WS for per-caller unsub_all",
70 ],
71 })),
72 ));
73 }
74 let ctx = CallerContext::from_key_record(rec.as_deref().map(|r| r.as_ref()));
76 proto_request_shared_conn_raw::<qot_sub::Request, qot_sub::Response>(
77 &state,
78 proto_id::QOT_SUB,
79 Some(body),
80 Some(&ctx),
81 )
82 .await
83}
84
85pub async fn query_subscription(
101 State(state): State<RestState>,
102 rec: Option<Extension<Arc<KeyRecord>>>,
103 Json(mut body): Json<Value>,
104) -> RawApiResult {
105 inject_default_is_req_all_conn(&mut body, true);
106 let ctx = CallerContext::from_key_record(rec.as_deref().map(|r| r.as_ref()));
110 proto_request_shared_conn_raw::<qot_get_sub_info::Request, qot_get_sub_info::Response>(
111 &state,
112 proto_id::QOT_GET_SUB_INFO,
113 Some(body),
114 Some(&ctx),
115 )
116 .await
117}
118
119pub(super) fn inject_default_is_req_all_conn(body: &mut Value, default: bool) {
122 let obj = match body.as_object_mut() {
123 Some(o) => o,
124 None => return, };
126 if let Some(Value::Object(c2s)) = obj.get_mut("c2s") {
128 c2s.entry("is_req_all_conn").or_insert(Value::Bool(default));
129 return;
130 }
131 obj.entry("is_req_all_conn").or_insert(Value::Bool(default));
134}
135
136pub async fn list_plates(State(state): State<RestState>, Json(body): Json<Value>) -> RawApiResult {
140 adapter::proto_request_raw::<
141 futu_proto::qot_get_plate_set::Request,
142 futu_proto::qot_get_plate_set::Response,
143 >(&state, proto_id::QOT_GET_PLATE_SET, Some(body))
144 .await
145}
146
147pub async fn get_risk_free_rate(
158 State(state): State<RestState>,
159 body: Option<Json<Value>>,
160) -> RawApiResult {
161 let body_val = body.map(|Json(v)| v);
162 adapter::proto_request_raw::<
163 futu_backend::proto_internal::risk_free_rate::DaemonGetRiskFreeRateReq,
164 futu_backend::proto_internal::risk_free_rate::DaemonGetRiskFreeRateRsp,
165 >(&state, proto_id::QOT_GET_RISK_FREE_RATE, body_val)
166 .await
167}
168
169pub async fn get_financial_calendar(
175 State(state): State<RestState>,
176 Json(body): Json<Value>,
177) -> RawApiResult {
178 adapter::proto_request_raw_spec_body::<
179 futu_backend::proto_internal::financial_calendar::GetFinancialStatementCalendarViewReq,
180 futu_backend::proto_internal::financial_calendar::GetFinancialStatementCalendarViewRsp,
181 >(
182 &state,
183 futu_core::proto_id::QOT_GET_FINANCIAL_CALENDAR_VIEW_INTERNAL,
184 Some(normalize_financial_calendar_rest_body_for_internal_proto(
185 body,
186 )),
187 )
188 .await
189}
190
191pub async fn search_target_financial_calendar(
196 State(state): State<RestState>,
197 Json(body): Json<Value>,
198) -> RawApiResult {
199 adapter::proto_request_raw_spec_body::<
200 futu_backend::proto_internal::financial_calendar::SearchTargetStockFinancialCalendarReq,
201 futu_backend::proto_internal::financial_calendar::SearchTargetStockFinancialCalendarRsp,
202 >(
203 &state,
204 futu_core::proto_id::QOT_SEARCH_TARGET_FINANCIAL_CALENDAR,
205 Some(normalize_financial_calendar_rest_body_for_internal_proto(
206 body,
207 )),
208 )
209 .await
210}
211
212pub async fn get_spread_table(
218 State(state): State<RestState>,
219 body: Option<Json<Value>>,
220) -> RawApiResult {
221 let body_val = body.map(|Json(v)| v);
222 adapter::proto_request_raw::<
223 futu_backend::proto_internal::spread_table_6503::DaemonGetSpreadTableReq,
224 futu_backend::proto_internal::spread_table_6503::DaemonGetSpreadTableRsp,
225 >(&state, proto_id::QOT_GET_SPREAD_TABLE, body_val)
226 .await
227}
228
229pub async fn get_ticker_statistic(
237 State(state): State<RestState>,
238 Json(body): Json<Value>,
239) -> RawApiResult {
240 adapter::proto_request_raw::<
241 futu_backend::proto_internal::ticker_statistic_daemon::DaemonGetTickerStatisticReq,
242 futu_backend::proto_internal::ticker_statistic_daemon::DaemonGetTickerStatisticRsp,
243 >(&state, proto_id::QOT_GET_TICKER_STATISTIC, Some(body))
244 .await
245}
246
247pub async fn get_ticker_statistic_detail(
260 State(state): State<RestState>,
261 Json(body): Json<Value>,
262) -> RawApiResult {
263 adapter::proto_request_raw::<
264 futu_backend::proto_internal::ticker_statistic_daemon::DaemonGetTickerStatisticDetailReq,
265 futu_backend::proto_internal::ticker_statistic_daemon::DaemonGetTickerStatisticDetailRsp,
266 >(
267 &state,
268 proto_id::QOT_GET_TICKER_STATISTIC_DETAIL,
269 Some(body),
270 )
271 .await
272}