Skip to main content

futu_backend/quote_sub/
ticker.rs

1use prost::Message;
2
3use futu_core::error::{FutuError, Result};
4
5use crate::conn::BackendConn;
6use crate::proto_internal::ft_cmd_tick;
7
8use super::{CMD_QOT_PULL_TICKER, ftapi_market_to_quote_mkt};
9
10/// C++ `NN_QuoteTickerKey_FetchLatest ((u64_t)-1)`.
11const TICKER_FETCH_LATEST_KEY: u64 = u64::MAX;
12/// C++ `NN_QuotePullTicker_LatestTime ((u32_t)-1)`.
13const TICKER_LATEST_DATE_TIME_S: u32 = u32::MAX;
14
15pub(super) mod nn_quote_session {
16    pub const RTH: i32 = 0;
17    pub const ETH: i32 = 1;
18    pub const ALL: i32 = 2;
19}
20
21pub(super) mod tick_period_type {
22    // Ref: C++ `FTCmdTick.proto` TickPeriodType.
23    pub const NORMAL: u32 = 0;
24    pub const BEFORE: u32 = 1;
25    pub const AFTER: u32 = 2;
26    pub const OVERNIGHT: u32 = 4;
27}
28
29pub(super) fn common_session_to_nn(session: i32) -> i32 {
30    match session {
31        // FTAPI Common.Session: 1=RTH, 2=ETH, 3=ALL.
32        2 => nn_quote_session::ETH,
33        3 => nn_quote_session::ALL,
34        _ => nn_quote_session::RTH,
35    }
36}
37
38pub(super) fn ticker_periods_for_nn_session(nn_session: i32) -> Vec<u32> {
39    match nn_session {
40        nn_quote_session::ALL => vec![
41            tick_period_type::NORMAL,
42            tick_period_type::BEFORE,
43            tick_period_type::AFTER,
44            tick_period_type::OVERNIGHT,
45        ],
46        nn_quote_session::ETH => vec![
47            tick_period_type::NORMAL,
48            tick_period_type::BEFORE,
49            tick_period_type::AFTER,
50        ],
51        _ => vec![tick_period_type::NORMAL],
52    }
53}
54
55/// 拉取最新逐笔,用于对齐 C++ Qot_Sub 成功后“订阅逐笔要提前拉一根”。
56///
57/// Ref:
58/// - `APIServer_Qot_Sub.cpp:265-280`: subscribe Ticker 后调用
59///   `PullNewestTickerList_Lot(..., 1, false, SessionToNN(enSession))`.
60/// - `NNBiz_Qot_PullQot.cpp:126-129`: 非 US 强制 RTH,再从 latest key 拉取。
61/// - `NNBiz_Qot_PullQot.cpp:327-365`: CMD6128 body + reserved[0]=head market,
62///   reserved[1]=NN_QuoteExType_SECURITY(0).
63pub async fn pull_latest_ticker(
64    backend: &BackendConn,
65    stock_id: u64,
66    nn_mkt_type: u8,
67    common_session: i32,
68    pull_count: u32,
69    broker_id: Option<i32>,
70) -> Result<ft_cmd_tick::TickRsp> {
71    if stock_id == 0 || pull_count == 0 {
72        return Err(FutuError::Codec(format!(
73            "PullLatestTicker: invalid stock_id={stock_id} pull_count={pull_count}"
74        )));
75    }
76
77    // C++ `PullNewestTickerList`: only US keeps ETH/ALL; other markets collapse
78    // to RTH because backend does not distinguish pre/after sessions there.
79    let nn_session = if nn_mkt_type == ftapi_market_to_quote_mkt(11) {
80        common_session_to_nn(common_session)
81    } else {
82        nn_quote_session::RTH
83    };
84
85    let req = ft_cmd_tick::TickReq {
86        security_id: Some(stock_id),
87        date_time_s: Some(TICKER_LATEST_DATE_TIME_S),
88        begin_tick_key: Some(TICKER_FETCH_LATEST_KEY),
89        tick_count: Some(pull_count),
90        tick_period_type: None,
91        tick_period_type_ex: ticker_periods_for_nn_session(nn_session),
92        req_auth: None,
93        end_tick_key: None,
94        date_time_s_v2: None,
95        // v1.4.110 codex Phase 3 Slice 6a: caller-provided broker_id (crypto-only).
96        // 对齐 C++ NNBiz_Qot_PullQot.cpp:344-349 `pbReq.set_broker_id(...)`.
97        broker_id,
98    };
99
100    let mut reserved = [0u8; 10];
101    reserved[0] = nn_mkt_type;
102    // reserved[1] = 0 == NN_QuoteExType_SECURITY.
103
104    let frame = backend
105        .request_with_reserved(CMD_QOT_PULL_TICKER, req.encode_to_vec(), reserved)
106        .await?;
107    let rsp: ft_cmd_tick::TickRsp = Message::decode(frame.body.as_ref())?;
108    let result = rsp.result.unwrap_or(-1);
109    if result != 0 {
110        return Err(FutuError::ServerError {
111            ret_type: result,
112            msg: format!("CMD6128 PullLatestTicker result={result}"),
113        });
114    }
115    Ok(rsp)
116}