Skip to main content

brokapi_tbank/
config.rs

1//! Connector configuration
2//!
3//! [`MarketDataConfig`] and [`UserDataConfig`] wrap a [`TbankConfig`] (endpoint, token, account) and
4//! add what the connector itself needs: the label of its events and the ping period of its streams.
5//!
6//! # Examples
7//!
8//! ```
9//! use std::time::Duration;
10//! use brokapi_tbank::{MarketDataConfig, UserDataConfig};
11//! use brokapi_tbank_protocol::TbankConfig;
12//!
13//! let token = TbankConfig::sandbox("t.my-token");
14//!
15//! // Market data does not depend on the account.
16//! let md = MarketDataConfig::new(token.clone(), "tbank-md");
17//! assert_eq!(md.ping_interval, Duration::from_secs(30));
18//!
19//! // Two accounts of one token: two configs, each with its own account and its own source label.
20//! let main = UserDataConfig::new(token.clone().with_account("acc-main"), "tbank-ud-main");
21//! let second = UserDataConfig::new(token.with_account("acc-second"), "tbank-ud-second");
22//! assert_ne!(main.source, second.source);
23//! ```
24
25use std::sync::Arc;
26use std::time::Duration;
27
28use brokapi_runtime::PriceGrid;
29use brokapi_tbank_protocol::TbankConfig;
30
31/// The shortest ping period T-Bank accepts on its streams.
32pub(crate) const MIN_PING: Duration = Duration::from_secs(5);
33/// The longest ping period on the market data stream.
34pub(crate) const MD_MAX_PING: Duration = Duration::from_secs(180);
35/// The longest ping period on the user data streams. The order state stream accepts up to 120 s, and
36/// the connector asks all its streams for the same period.
37pub(crate) const UD_MAX_PING: Duration = Duration::from_secs(120);
38
39/// The ping period the connector asks the server for, and the one its silence limit is built on:
40/// `configured` brought into `5 s..=longest`. The request and the limit use this one value, so a
41/// stream that pings as often as it was asked to is never taken for dead.
42pub(crate) fn effective_ping(configured: Duration, longest: Duration) -> Duration {
43    configured.clamp(MIN_PING, longest)
44}
45
46/// How long a stream may stay silent before it is closed and reopened: three ping periods.
47pub(crate) fn silence_limit(ping: Duration) -> Duration {
48    ping * 3
49}
50
51#[cfg(test)]
52mod tests {
53    use super::*;
54
55    #[test]
56    fn a_ping_period_is_brought_into_the_range_of_the_broker() {
57        let secs = Duration::from_secs;
58        assert_eq!(effective_ping(secs(1), MD_MAX_PING), secs(5), "too short");
59        assert_eq!(effective_ping(secs(30), MD_MAX_PING), secs(30), "in range");
60        assert_eq!(
61            effective_ping(secs(600), MD_MAX_PING),
62            secs(180),
63            "too long"
64        );
65        assert_eq!(effective_ping(secs(150), UD_MAX_PING), secs(120));
66        assert_eq!(effective_ping(Duration::ZERO, UD_MAX_PING), secs(5));
67    }
68
69    #[test]
70    fn the_silence_limit_follows_the_period_that_was_asked_for() {
71        // A period of 1 s is asked from the broker as 5 s: the limit is 15 s, not 3 s.
72        let ping = effective_ping(Duration::from_secs(1), MD_MAX_PING);
73        assert_eq!(silence_limit(ping), Duration::from_secs(15));
74        // A period of 10 minutes is asked as 3 minutes: the limit is 9 minutes, not 30.
75        let ping = effective_ping(Duration::from_secs(600), MD_MAX_PING);
76        assert_eq!(silence_limit(ping), Duration::from_secs(540));
77    }
78}
79
80/// Market data connector config. Not tied to an account: one per broker (token) is enough.
81///
82/// Build it with [`MarketDataConfig::new`] and change the public fields if needed.
83///
84/// # Examples
85///
86/// ```
87/// use brokapi_tbank::MarketDataConfig;
88/// use brokapi_tbank_protocol::TbankConfig;
89///
90/// let mut cfg = MarketDataConfig::new(TbankConfig::sandbox("t.my-token"), "tbank-md");
91/// cfg.ping_interval = std::time::Duration::from_secs(10);
92/// assert_eq!(&*cfg.source, "tbank-md");
93/// ```
94#[derive(Debug, Clone)]
95pub struct MarketDataConfig {
96    /// Endpoint, token and timeouts of the API. `tbank.account_id` is ignored by market data.
97    pub tbank: TbankConfig,
98    /// Envelope source label, e.g. `"tbank-md"`.
99    pub source: Arc<str>,
100    /// Server ping period on the stream. T-Bank accepts 5 s to 180 s: a value outside that range is
101    /// brought into it, both for the request and for the silence limit. A stream silent for three
102    /// periods is treated as dead and reconnected. Default 30 s.
103    pub ping_interval: Duration,
104}
105
106impl MarketDataConfig {
107    /// A config with the default ping period (30 s).
108    ///
109    /// `source` labels every [`Envelope`](brokapi_core::Envelope) this connector emits, so that you
110    /// can tell connectors apart when they write into one channel.
111    ///
112    /// # Examples
113    ///
114    /// ```
115    /// use brokapi_tbank::MarketDataConfig;
116    /// use brokapi_tbank_protocol::TbankConfig;
117    ///
118    /// let cfg = MarketDataConfig::new(TbankConfig::production("t.my-token"), "tbank-md");
119    /// assert_eq!(&*cfg.source, "tbank-md");
120    /// ```
121    pub fn new(tbank: TbankConfig, source: impl Into<Arc<str>>) -> Self {
122        Self {
123            tbank,
124            source: source.into(),
125            ping_interval: Duration::from_secs(30),
126        }
127    }
128}
129
130/// User data connector config. One connector = one account: `tbank.account_id`.
131/// With `None` the connector uses the token's only open account and refuses to start when there are several.
132///
133/// Several accounts of one token need several connectors, each with its own config that differs in
134/// `tbank.account_id` and in `source`. Share one rate limiter between them.
135///
136/// # Examples
137///
138/// ```
139/// use brokapi_tbank::UserDataConfig;
140/// use brokapi_tbank_protocol::TbankConfig;
141///
142/// let cfg = UserDataConfig::new(TbankConfig::sandbox("t.my-token").with_account("acc-main"), "tbank-ud-main");
143/// assert_eq!(cfg.tbank.account_id.as_deref(), Some("acc-main"));
144/// assert_eq!(cfg.order_state_buffer, 1024);
145/// ```
146#[derive(Debug, Clone)]
147pub struct UserDataConfig {
148    /// Endpoint, token, account and timeouts of the API. Set `account_id` with
149    /// [`TbankConfig::with_account`] when the token sees more than one account.
150    pub tbank: TbankConfig,
151    /// Envelope source label, e.g. `"tbank-ud-main"`. Give every account its own.
152    pub source: Arc<str>,
153    /// Lossless buffer capacity for the order state and executions subscriptions.
154    ///
155    /// Own order states and trades are never dropped. If the consumer is so slow that the buffer
156    /// fills up, an overflow event is sent and the trading gate closes until the next successful
157    /// reconcile. Default 1024.
158    pub order_state_buffer: usize,
159    /// Server ping period on the user data streams. A value outside 5 s to 120 s is brought into
160    /// that range, both for the request and for the silence limit. A stream silent for three periods
161    /// is treated as dead and reconnected. Default 30 s.
162    pub ping_interval: Duration,
163    /// Price steps the connector checks order prices against (default: empty, nothing is checked).
164    /// Fill it with [`PriceGrid::learn`] from the trading parameters of the instruments you trade. A
165    /// price off the step is then refused with `Rejected` (`price_off_step`) before anything is sent.
166    pub price_grid: PriceGrid,
167}
168
169impl UserDataConfig {
170    /// A config with the default buffer (1024 events) and ping period (30 s).
171    ///
172    /// `source` labels every [`Envelope`](brokapi_core::Envelope) of this connector. Give each account
173    /// its own label.
174    ///
175    /// # Examples
176    ///
177    /// ```
178    /// use brokapi_tbank::UserDataConfig;
179    /// use brokapi_tbank_protocol::TbankConfig;
180    ///
181    /// let cfg = UserDataConfig::new(TbankConfig::sandbox("t.my-token"), "tbank-ud");
182    /// assert_eq!(&*cfg.source, "tbank-ud");
183    /// ```
184    pub fn new(tbank: TbankConfig, source: impl Into<Arc<str>>) -> Self {
185        Self {
186            tbank,
187            source: source.into(),
188            order_state_buffer: 1024,
189            ping_interval: Duration::from_secs(30),
190            price_grid: PriceGrid::default(),
191        }
192    }
193}