futu_rest/routes/qot/
quotes.rs1use axum::extract::{Extension, Json, State};
6use futu_auth::KeyRecord;
7use serde_json::Value;
8use std::sync::Arc;
9
10use crate::caller_context::CallerContext;
11use futu_core::proto_id;
12
13use super::*;
14
15pub async fn get_basic_qot(
16 State(state): State<RestState>,
17 rec: Option<Extension<Arc<KeyRecord>>>,
18 Json(body): Json<Value>,
19) -> ApiResult {
20 let ctx = CallerContext::from_key_record(rec.as_deref().map(|r| r.as_ref()));
21 let resp =
22 proto_request_shared_conn::<qot_get_basic_qot::Request, qot_get_basic_qot::Response>(
23 &state,
24 proto_id::QOT_GET_BASIC_QOT,
25 Some(body),
26 Some(&ctx),
27 )
28 .await?;
29 Ok(Json(annotate_quote_cache_miss(resp.0)))
30}
31
32pub(super) fn annotate_quote_cache_miss(mut v: Value) -> Value {
38 let should_trigger = (|v: &Value| -> bool {
39 let ret_ok = v
40 .as_object()
41 .and_then(|o| o.get("ret_type"))
42 .and_then(|t| t.as_i64())
43 == Some(0);
44 if !ret_ok {
45 return false;
46 }
47 let s2c_obj = match v.get("s2c").and_then(|s| s.as_object()) {
48 Some(o) => o,
49 None => return false,
50 };
51 let list_arr = match s2c_obj.get("basic_qot_list").and_then(|l| l.as_array()) {
52 Some(a) => a,
53 None => return false,
54 };
55 list_arr.is_empty()
56 })(&v);
57 if !should_trigger {
58 return v;
59 }
60 if let Some(obj) = v.as_object_mut() {
61 let hint = "Quote cache miss (无 push 数据). \
62 如果未订阅, 先 POST /api/subscribe with sub_type=1 (Basic). \
63 若已订阅, 等首条 push 到达 (~1-3s) 再重试 /api/quote. \
64 (push_cache 全局, REST/MCP/WS 任一 conn 订阅都会填同一 cache)";
65 obj.insert("ret_msg".to_string(), Value::String(hint.to_string()));
66 }
67 v
68}
69
70pub async fn get_kl(
72 State(state): State<RestState>,
73 rec: Option<Extension<Arc<KeyRecord>>>,
74 Json(body): Json<Value>,
75) -> ApiResult {
76 let ctx = CallerContext::from_key_record(rec.as_deref().map(|r| r.as_ref()));
77 proto_request_shared_conn::<qot_get_kl::Request, qot_get_kl::Response>(
78 &state,
79 proto_id::QOT_GET_KL,
80 Some(body),
81 Some(&ctx),
82 )
83 .await
84}
85
86pub async fn get_order_book(
107 State(state): State<RestState>,
108 rec: Option<Extension<Arc<KeyRecord>>>,
109 Json(body): Json<Value>,
110) -> ApiResult {
111 let ctx = CallerContext::from_key_record(rec.as_deref().map(|r| r.as_ref()));
112 let resp =
113 proto_request_shared_conn::<qot_get_order_book::Request, qot_get_order_book::Response>(
114 &state,
115 proto_id::QOT_GET_ORDER_BOOK,
116 Some(body),
117 Some(&ctx),
118 )
119 .await?;
120 Ok(Json(orderbook_loud_unsub_hint(resp.0)))
121}
122
123pub(super) fn orderbook_loud_unsub_hint(mut v: Value) -> Value {
131 let should_trigger = (|v: &Value| -> bool {
134 let ret_ok = v
135 .as_object()
136 .and_then(|o| o.get("ret_type"))
137 .and_then(|t| t.as_i64())
138 == Some(0);
139 if !ret_ok {
140 return false;
141 }
142 let s2c_obj = match v.get("s2c").and_then(|s| s.as_object()) {
143 Some(o) => o,
144 None => return false,
145 };
146 let ask_arr = match s2c_obj
150 .get("order_book_ask_list")
151 .and_then(|l| l.as_array())
152 {
153 Some(a) => a,
154 None => return false,
155 };
156 let bid_arr = match s2c_obj
157 .get("order_book_bid_list")
158 .and_then(|l| l.as_array())
159 {
160 Some(a) => a,
161 None => return false,
162 };
163 ask_arr.is_empty() && bid_arr.is_empty()
164 })(&v);
165 if !should_trigger {
166 return v;
167 }
168 let hint = "OrderBook 未订阅或 push 未到. \
170 Please call POST /api/subscribe with sub_type=2 (OrderBook) first \
171 for the requested security. \
172 若已订阅, 等首条 push 到达 (~1-3s) 再重试 /api/orderbook. \
173 对齐 C++ OpenD: 未订阅 OrderBook 返 ret=-1.";
174 if let Some(obj) = v.as_object_mut() {
175 obj.insert("ret_type".to_string(), Value::from(-1_i64));
176 obj.insert("ret_msg".to_string(), Value::String(hint.to_string()));
177 }
178 wrap_err_code_prefix_inline(&mut v);
182 v
183}
184
185pub async fn get_broker(State(state): State<RestState>, Json(body): Json<Value>) -> ApiResult {
187 adapter::proto_request::<qot_get_broker::Request, qot_get_broker::Response>(
188 &state,
189 proto_id::QOT_GET_BROKER,
190 Some(body),
191 )
192 .await
193}
194
195pub async fn get_ticker(
197 State(state): State<RestState>,
198 rec: Option<Extension<Arc<KeyRecord>>>,
199 Json(body): Json<Value>,
200) -> ApiResult {
201 let ctx = CallerContext::from_key_record(rec.as_deref().map(|r| r.as_ref()));
202 proto_request_shared_conn::<qot_get_ticker::Request, qot_get_ticker::Response>(
203 &state,
204 proto_id::QOT_GET_TICKER,
205 Some(body),
206 Some(&ctx),
207 )
208 .await
209}
210
211pub async fn get_rt(
213 State(state): State<RestState>,
214 rec: Option<Extension<Arc<KeyRecord>>>,
215 Json(body): Json<Value>,
216) -> ApiResult {
217 let ctx = CallerContext::from_key_record(rec.as_deref().map(|r| r.as_ref()));
218 proto_request_shared_conn::<qot_get_rt::Request, qot_get_rt::Response>(
219 &state,
220 proto_id::QOT_GET_RT,
221 Some(body),
222 Some(&ctx),
223 )
224 .await
225}