1use std::fmt;
35use std::time::Duration;
36
37use brokapi_core::{
38 Candle, Error, InstrumentId, MarketDataSource, TimeRange, Timeframe, UnixNanos,
39};
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub struct HistoryOpts {
55 pub window: Option<Duration>,
58 pub max_rate_waits: u32,
61 pub max_rate_wait: Duration,
64 pub transport_retries: u32,
66 pub transport_backoff: Duration,
69}
70
71impl Default for HistoryOpts {
72 fn default() -> Self {
73 Self {
74 window: None,
75 max_rate_waits: 10,
76 max_rate_wait: Duration::from_secs(60),
77 transport_retries: 2,
78 transport_backoff: Duration::from_secs(1),
79 }
80 }
81}
82
83#[derive(Debug, Clone)]
95pub struct HistoryError {
96 pub error: Error,
98 pub resume_from: UnixNanos,
101}
102
103impl fmt::Display for HistoryError {
104 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105 write!(
106 f,
107 "history read stopped at {} ns: {}",
108 self.resume_from.as_nanos(),
109 self.error
110 )
111 }
112}
113
114impl std::error::Error for HistoryError {
115 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
116 Some(&self.error)
117 }
118}
119
120pub struct CandlePages<'a, S: MarketDataSource + ?Sized> {
134 src: &'a S,
135 id: &'a InstrumentId,
136 tf: Timeframe,
137 next_from: UnixNanos,
138 to: UnixNanos,
139 window: Option<Duration>,
140 opts: HistoryOpts,
141 last_open: Option<UnixNanos>,
142 requests: u32,
143 rate_waits: u32,
144}
145
146impl<'a, S: MarketDataSource + ?Sized> CandlePages<'a, S> {
147 pub fn new(
149 src: &'a S,
150 id: &'a InstrumentId,
151 tf: Timeframe,
152 range: TimeRange,
153 opts: HistoryOpts,
154 ) -> Self {
155 let window = opts.window.or_else(|| src.candles_max_span(tf));
156 Self {
157 src,
158 id,
159 tf,
160 next_from: range.from,
161 to: range.to,
162 window,
163 opts,
164 last_open: None,
165 requests: 0,
166 rate_waits: 0,
167 }
168 }
169
170 pub fn resume_from(&self) -> UnixNanos {
173 self.next_from
174 }
175
176 pub fn requests(&self) -> u32 {
180 self.requests
181 }
182
183 pub fn rate_waits(&self) -> u32 {
185 self.rate_waits
186 }
187
188 pub async fn next_page(&mut self) -> Result<Option<Vec<Candle>>, HistoryError> {
200 if self.next_from >= self.to {
201 return Ok(None);
202 }
203 let from = self.next_from;
204 let end = match self.window {
205 Some(w) => {
206 let step = i64::try_from(w.as_nanos()).unwrap_or(i64::MAX).max(1);
207 UnixNanos::from_nanos(from.as_nanos().saturating_add(step).min(self.to.as_nanos()))
208 }
209 None => self.to,
210 };
211 let mut bars = self.read_window(TimeRange::new(from, end)).await?;
212 bars.sort_by_key(|c| c.open_ts);
213 if let Some(last) = self.last_open {
214 bars.retain(|c| c.open_ts > last);
215 }
216 bars.dedup_by_key(|c| c.open_ts);
217 if let Some(c) = bars.last() {
218 self.last_open = Some(c.open_ts);
219 }
220 self.next_from = end;
221 Ok(Some(bars))
222 }
223
224 async fn read_window(&mut self, range: TimeRange) -> Result<Vec<Candle>, HistoryError> {
226 let mut waits = 0u32;
227 let mut retries = 0u32;
228 loop {
229 self.requests += 1;
230 match self.src.candles(self.id, self.tf, range).await {
231 Ok(bars) => return Ok(bars),
232 Err(Error::RateLimited { retry_after }) if waits < self.opts.max_rate_waits => {
233 waits += 1;
234 self.rate_waits += 1;
235 tokio::time::sleep(retry_after.min(self.opts.max_rate_wait)).await;
236 }
237 Err(Error::Transport(_)) if retries < self.opts.transport_retries => {
238 let pause = self.opts.transport_backoff * 2u32.saturating_pow(retries);
239 retries += 1;
240 tokio::time::sleep(pause).await;
241 }
242 Err(error) => {
243 return Err(HistoryError {
244 error,
245 resume_from: range.from,
246 });
247 }
248 }
249 }
250 }
251}
252
253pub async fn load_candles<S: MarketDataSource + ?Sized>(
274 src: &S,
275 id: &InstrumentId,
276 tf: Timeframe,
277 range: TimeRange,
278 opts: HistoryOpts,
279) -> Result<Vec<Candle>, HistoryError> {
280 let mut pages = CandlePages::new(src, id, tf, range, opts);
281 let mut all = Vec::new();
282 while let Some(mut page) = pages.next_page().await? {
283 all.append(&mut page);
284 }
285 Ok(all)
286}
287
288#[cfg(test)]
289mod tests {
290 use std::sync::Mutex;
291 use std::sync::atomic::{AtomicU32, Ordering};
292
293 use async_trait::async_trait;
294 use brokapi_core::{
295 InstrumentKey, MarketTrade, NativeId, Price, Qty, SubscribeOpts, SubscriptionId, Timestamps,
296 };
297
298 use super::*;
299
300 struct Fake {
302 calls: Mutex<Vec<TimeRange>>,
303 rate_limited_first: AtomicU32,
304 transport_first: AtomicU32,
305 max_span: Option<Duration>,
306 inclusive_end: bool,
308 }
309
310 impl Fake {
311 fn new(max_span: Option<Duration>) -> Self {
312 Self {
313 calls: Mutex::new(Vec::new()),
314 rate_limited_first: AtomicU32::new(0),
315 transport_first: AtomicU32::new(0),
316 max_span,
317 inclusive_end: false,
318 }
319 }
320 }
321
322 fn id() -> InstrumentId {
323 InstrumentId::new(
324 InstrumentKey::new("SBER", "TQBR"),
325 NativeId::TickerClass {
326 ticker: "SBER".into(),
327 class_code: "TQBR".into(),
328 },
329 )
330 }
331
332 const HOUR: i64 = 3_600_000_000_000;
333
334 fn bar(open: i64) -> Candle {
335 Candle {
336 instrument: id(),
337 timeframe: Timeframe::H1,
338 open: Price::from(1),
339 high: Price::from(2),
340 low: Price::from(1),
341 close: Price::from(2),
342 volume: Qty::from(1),
343 open_ts: UnixNanos::from_nanos(open),
344 close_ts: UnixNanos::from_nanos(open + HOUR),
345 ts: Timestamps::new(UnixNanos::ZERO, std::time::Instant::now()),
346 }
347 }
348
349 #[async_trait]
350 impl MarketDataSource for Fake {
351 async fn last_trades(&self, _: &InstrumentId, _: u32) -> Result<Vec<MarketTrade>, Error> {
352 unimplemented!()
353 }
354 async fn subscribe_market_trades(
355 &self,
356 _: &InstrumentId,
357 _: SubscribeOpts,
358 ) -> Result<SubscriptionId, Error> {
359 unimplemented!()
360 }
361 async fn subscribe_quotes(
362 &self,
363 _: &InstrumentId,
364 _: SubscribeOpts,
365 ) -> Result<SubscriptionId, Error> {
366 unimplemented!()
367 }
368 async fn candles(
369 &self,
370 _: &InstrumentId,
371 _: Timeframe,
372 range: TimeRange,
373 ) -> Result<Vec<Candle>, Error> {
374 if let Some(max) = self.max_span {
375 let span = (range.to.as_nanos() - range.from.as_nanos()) as u128;
376 if span > max.as_nanos() {
377 return Err(Error::Rejected {
378 code: "span".into(),
379 message: "window too long".into(),
380 });
381 }
382 }
383 if self
384 .rate_limited_first
385 .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |n| n.checked_sub(1))
386 .is_ok()
387 {
388 return Err(Error::RateLimited {
389 retry_after: Duration::from_millis(5),
390 });
391 }
392 if self
393 .transport_first
394 .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |n| n.checked_sub(1))
395 .is_ok()
396 {
397 return Err(Error::Transport(brokapi_core::TransportError::Message(
398 "reset".into(),
399 )));
400 }
401 self.calls.lock().unwrap().push(range);
402 let mut open = (range.from.as_nanos() + HOUR - 1) / HOUR * HOUR;
403 let mut out = Vec::new();
404 while open < range.to.as_nanos() || (self.inclusive_end && open == range.to.as_nanos())
405 {
406 out.push(bar(open));
407 open += HOUR;
408 }
409 Ok(out)
410 }
411 async fn subscribe_candles(
412 &self,
413 _: &InstrumentId,
414 _: Timeframe,
415 _: SubscribeOpts,
416 ) -> Result<SubscriptionId, Error> {
417 unimplemented!()
418 }
419 fn candles_max_span(&self, _: Timeframe) -> Option<Duration> {
420 self.max_span
421 }
422 }
423
424 fn hours(n: i64) -> TimeRange {
425 TimeRange::new(UnixNanos::ZERO, UnixNanos::from_nanos(n * HOUR))
426 }
427
428 fn fast() -> HistoryOpts {
429 HistoryOpts {
430 transport_backoff: Duration::from_millis(1),
431 ..HistoryOpts::default()
432 }
433 }
434
435 #[tokio::test]
436 async fn cuts_the_range_into_windows_the_source_accepts() {
437 let fake = Fake::new(Some(Duration::from_secs(10 * 3600)));
438 let bars = load_candles(&fake, &id(), Timeframe::H1, hours(35), fast())
439 .await
440 .unwrap();
441 assert_eq!(bars.len(), 35);
442 assert!(bars.windows(2).all(|w| w[0].open_ts < w[1].open_ts));
443 assert_eq!(fake.calls.lock().unwrap().len(), 4);
445 }
446
447 #[tokio::test]
448 async fn without_a_known_span_the_range_goes_in_one_request() {
449 let fake = Fake::new(None);
450 let bars = load_candles(&fake, &id(), Timeframe::H1, hours(35), fast())
451 .await
452 .unwrap();
453 assert_eq!(bars.len(), 35);
454 assert_eq!(fake.calls.lock().unwrap().len(), 1);
455 }
456
457 #[tokio::test]
458 async fn the_window_option_overrides_the_source() {
459 let fake = Fake::new(Some(Duration::from_secs(100 * 3600)));
460 let opts = HistoryOpts {
461 window: Some(Duration::from_secs(12 * 3600)),
462 ..fast()
463 };
464 load_candles(&fake, &id(), Timeframe::H1, hours(36), opts)
465 .await
466 .unwrap();
467 assert_eq!(fake.calls.lock().unwrap().len(), 3);
468 }
469
470 #[tokio::test]
471 async fn a_bar_on_the_edge_of_two_windows_is_delivered_once() {
472 let mut fake = Fake::new(Some(Duration::from_secs(10 * 3600)));
473 fake.inclusive_end = true;
474 let bars = load_candles(&fake, &id(), Timeframe::H1, hours(30), fast())
475 .await
476 .unwrap();
477 let mut opens: Vec<i64> = bars.iter().map(|b| b.open_ts.as_nanos()).collect();
478 let before = opens.len();
479 opens.dedup();
480 assert_eq!(before, opens.len(), "no bar twice");
481 assert_eq!(opens.len(), 31);
483 }
484
485 #[tokio::test]
486 async fn a_rate_limit_is_waited_out_and_counted() {
487 let fake = Fake::new(Some(Duration::from_secs(10 * 3600)));
488 fake.rate_limited_first.store(3, Ordering::SeqCst);
489 let inst = id();
490 let mut pages = CandlePages::new(&fake, &inst, Timeframe::H1, hours(20), fast());
491 while pages.next_page().await.unwrap().is_some() {}
492 assert_eq!(pages.rate_waits(), 3);
493 assert_eq!(pages.requests(), 5, "2 windows and 3 refused attempts");
494 assert_eq!(pages.resume_from(), hours(20).to);
495 }
496
497 #[tokio::test]
498 async fn too_many_rate_limits_stop_the_read_at_the_failed_window() {
499 let fake = Fake::new(Some(Duration::from_secs(10 * 3600)));
500 fake.rate_limited_first.store(100, Ordering::SeqCst);
501 let opts = HistoryOpts {
502 max_rate_waits: 2,
503 ..fast()
504 };
505 let err = load_candles(&fake, &id(), Timeframe::H1, hours(20), opts)
506 .await
507 .unwrap_err();
508 assert!(matches!(err.error, Error::RateLimited { .. }));
509 assert_eq!(err.resume_from, UnixNanos::ZERO);
510 }
511
512 #[tokio::test]
513 async fn a_network_error_is_repeated_a_few_times_then_reported() {
514 let fake = Fake::new(None);
515 fake.transport_first.store(2, Ordering::SeqCst);
516 let bars = load_candles(&fake, &id(), Timeframe::H1, hours(5), fast())
517 .await
518 .unwrap();
519 assert_eq!(bars.len(), 5);
520
521 let fake = Fake::new(None);
522 fake.transport_first.store(3, Ordering::SeqCst);
523 let err = load_candles(&fake, &id(), Timeframe::H1, hours(5), fast())
524 .await
525 .unwrap_err();
526 assert!(matches!(err.error, Error::Transport(_)));
527 }
528
529 #[tokio::test]
530 async fn a_refusal_is_not_repeated_and_tells_where_to_resume() {
531 let fake = Fake::new(Some(Duration::from_secs(5 * 3600)));
533 let opts = HistoryOpts {
534 window: Some(Duration::from_secs(10 * 3600)),
535 ..fast()
536 };
537 let inst = id();
538 let mut pages = CandlePages::new(&fake, &inst, Timeframe::H1, hours(20), opts);
539 let err = pages.next_page().await.unwrap_err();
540 assert!(matches!(err.error, Error::Rejected { .. }));
541 assert_eq!(pages.requests(), 1, "a refusal is final");
542 assert_eq!(err.resume_from, UnixNanos::ZERO);
543 }
544
545 #[tokio::test]
546 async fn an_empty_range_is_finished_at_once() {
547 let fake = Fake::new(None);
548 let inst = id();
549 let mut pages = CandlePages::new(&fake, &inst, Timeframe::H1, hours(0), fast());
550 assert!(pages.next_page().await.unwrap().is_none());
551 assert_eq!(pages.requests(), 0);
552 }
553}