Skip to main content

futu_backend/quote_sub/
ticker.rs

1use prost::Message;
2
3use futu_core::error::{FutuError, Result};
4use futu_domain_qot_ticker::ensure_ticker_pull_backend_success;
5
6use crate::command_runtime::execute_qot_plaintext;
7use crate::conn::BackendConn;
8use crate::proto_internal::ft_cmd_tick;
9
10use super::{CMD_QOT_PULL_TICKER, ftapi_market_to_quote_mkt};
11
12/// C++ `NN_QuoteTickerKey_FetchLatest ((u64_t)-1)`.
13const TICKER_FETCH_LATEST_KEY: u64 = u64::MAX;
14/// C++ `NN_QuotePullTicker_LatestTime ((u32_t)-1)`.
15const TICKER_LATEST_DATE_TIME_S: u32 = u32::MAX;
16pub const TICKER_PAGE_MAX_ITEMS: u32 = 750;
17
18pub(super) mod nn_quote_session {
19    pub const RTH: i32 = 0;
20    pub const ETH: i32 = 1;
21    pub const ALL: i32 = 2;
22}
23
24pub(super) mod tick_period_type {
25    // Ref: C++ `FTCmdTick.proto` TickPeriodType.
26    pub const NORMAL: u32 = 0;
27    pub const BEFORE: u32 = 1;
28    pub const AFTER: u32 = 2;
29    pub const OVERNIGHT: u32 = 4;
30}
31
32pub(super) fn common_session_to_nn(session: i32) -> i32 {
33    match session {
34        // FTAPI Common.Session: 1=RTH, 2=ETH, 3=ALL.
35        2 => nn_quote_session::ETH,
36        3 => nn_quote_session::ALL,
37        _ => nn_quote_session::RTH,
38    }
39}
40
41pub(super) fn ticker_periods_for_nn_session(nn_session: i32) -> Vec<u32> {
42    match nn_session {
43        nn_quote_session::ALL => vec![
44            tick_period_type::NORMAL,
45            tick_period_type::BEFORE,
46            tick_period_type::AFTER,
47            tick_period_type::OVERNIGHT,
48        ],
49        nn_quote_session::ETH => vec![
50            tick_period_type::NORMAL,
51            tick_period_type::BEFORE,
52            tick_period_type::AFTER,
53        ],
54        _ => vec![tick_period_type::NORMAL],
55    }
56}
57
58/// 拉取最新逐笔,用于对齐 C++ Qot_Sub 成功后“订阅逐笔要提前拉一根”。
59///
60/// Ref:
61/// - `APIServer_Qot_Sub.cpp:265-280`: subscribe Ticker 后调用
62///   `PullNewestTickerList_Lot(..., 1, false, SessionToNN(enSession))`.
63/// - `NNBiz_Qot_PullQot.cpp:126-129`: 非 US 强制 RTH,再从 latest key 拉取。
64/// - `NNBiz_Qot_PullQot.cpp:327-365`: CMD6128 body + reserved[0]=head market,
65///   reserved[1]=NN_QuoteExType_SECURITY(0).
66// The current 2607 C++ source still calls `TickReq::set_date_time_s` at
67// `NNBiz_Qot_PullQot.cpp:358`; keep this deprecated wire field until C++
68// removes that call rather than silently switching to the V2 field.
69#[allow(deprecated)]
70pub async fn pull_latest_ticker(
71    backend: &BackendConn,
72    stock_id: u64,
73    nn_mkt_type: u8,
74    common_session: i32,
75    pull_count: u32,
76    broker_id: Option<i32>,
77) -> Result<ft_cmd_tick::TickRsp> {
78    pull_ticker_page(
79        backend,
80        stock_id,
81        nn_mkt_type,
82        common_session,
83        TICKER_FETCH_LATEST_KEY,
84        TICKER_LATEST_DATE_TIME_S,
85        pull_count.min(TICKER_PAGE_MAX_ITEMS),
86        broker_id,
87    )
88    .await
89}
90
91/// Pull one exact CMD6128 page. The backend wire caps ordinary pages at 750;
92/// callers requesting more must continue with response `next_ticker_key` and
93/// `date_time_s`, matching C++ `OnReply_PullTickerList`.
94#[allow(deprecated)]
95pub async fn pull_ticker_page(
96    backend: &BackendConn,
97    stock_id: u64,
98    nn_mkt_type: u8,
99    common_session: i32,
100    begin_tick_key: u64,
101    date_time_s: u32,
102    pull_count: u32,
103    broker_id: Option<i32>,
104) -> Result<ft_cmd_tick::TickRsp> {
105    let (req, reserved) = build_ticker_page_request(
106        stock_id,
107        nn_mkt_type,
108        common_session,
109        begin_tick_key,
110        date_time_s,
111        pull_count,
112        broker_id,
113    )?;
114    let frame = execute_qot_plaintext(
115        backend,
116        CMD_QOT_PULL_TICKER,
117        req.encode_to_vec().into(),
118        reserved,
119    )
120    .await?;
121    let rsp: ft_cmd_tick::TickRsp = Message::decode(frame.body.as_ref())?;
122    if let Err(reject) = ensure_ticker_pull_backend_success(rsp.result) {
123        return Err(FutuError::ServerError {
124            ret_type: reject.result,
125            msg: format!("CMD6128 PullLatestTicker result={}", reject.result),
126        });
127    }
128    Ok(rsp)
129}
130
131#[allow(deprecated)]
132pub(super) fn build_ticker_page_request(
133    stock_id: u64,
134    nn_mkt_type: u8,
135    common_session: i32,
136    begin_tick_key: u64,
137    date_time_s: u32,
138    pull_count: u32,
139    broker_id: Option<i32>,
140) -> Result<(ft_cmd_tick::TickReq, [u8; 10])> {
141    if stock_id == 0 || pull_count == 0 {
142        return Err(FutuError::Codec(format!(
143            "PullLatestTicker: invalid stock_id={stock_id} pull_count={pull_count}"
144        )));
145    }
146    if pull_count > TICKER_PAGE_MAX_ITEMS {
147        return Err(FutuError::Codec(format!(
148            "PullLatestTicker: page pull_count={pull_count} exceeds {TICKER_PAGE_MAX_ITEMS}"
149        )));
150    }
151
152    // C++ `PullNewestTickerList`: only US keeps ETH/ALL; other markets collapse
153    // to RTH because backend does not distinguish pre/after sessions there.
154    let nn_session = if nn_mkt_type == ftapi_market_to_quote_mkt(11) {
155        common_session_to_nn(common_session)
156    } else {
157        nn_quote_session::RTH
158    };
159
160    let req = ft_cmd_tick::TickReq {
161        security_id: Some(stock_id),
162        date_time_s: Some(date_time_s),
163        begin_tick_key: Some(begin_tick_key),
164        tick_count: Some(pull_count),
165        tick_period_type: None,
166        tick_period_type_ex: ticker_periods_for_nn_session(nn_session),
167        req_auth: None,
168        end_tick_key: None,
169        date_time_s_v2: None,
170        // v1.4.110 codex Phase 3 Slice 6a: caller-provided broker_id (crypto-only).
171        // 对齐 C++ NNBiz_Qot_PullQot.cpp:344-349 `pbReq.set_broker_id(...)`.
172        broker_id,
173    };
174
175    let mut reserved = [0u8; 10];
176    reserved[0] = nn_mkt_type;
177    // reserved[1] = 0 == NN_QuoteExType_SECURITY.
178
179    Ok((req, reserved))
180}