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}