use std::collections::BTreeMap; use brk_error::{Error, Result}; use brk_types::{ Cents, CheckedSub, Close, Date, Day1, Dollars, Height, High, Low, OHLCCents, Open, Timestamp, }; use serde_json::Value; use tracing::info; use ureq::Agent; use crate::{PriceSource, checked_get, default_retry}; #[derive(Clone)] #[allow(clippy::upper_case_acronyms)] pub struct BRK { agent: Agent, height_to_ohlc: BTreeMap>, day1_to_ohlc: BTreeMap>, } impl BRK { #[allow(clippy::new_without_default)] pub fn new() -> Self { Self::new_with_agent(crate::new_agent(30)) } pub fn new_with_agent(agent: Agent) -> Self { Self { agent, height_to_ohlc: BTreeMap::new(), day1_to_ohlc: BTreeMap::new(), } } } const API_URL: &str = "https://bitview.space/api/vecs"; const CHUNK_SIZE: usize = 10_000; impl BRK { pub fn get_from_height(&mut self, height: Height) -> Result { let key = height.checked_sub(height % CHUNK_SIZE).unwrap(); #[allow(clippy::map_entry)] if !self.height_to_ohlc.contains_key(&key) || ((key + self.height_to_ohlc.get(&key).unwrap().len()) <= height) { self.height_to_ohlc .insert(key, self.fetch_height_prices(key)?); } self.height_to_ohlc .get(&key) .unwrap() .get(usize::from(height.checked_sub(key).unwrap())) .cloned() .ok_or(Error::NotFound("Couldn't find height in BRK".into())) } fn fetch_height_prices(&self, height: Height) -> Result> { let agent = &self.agent; default_retry(|_| { let url = format!( "{API_URL}/height-to-price-ohlc?from={}&to={}", height, height + CHUNK_SIZE ); info!("Fetching {url} ..."); let bytes = checked_get(agent, &url)?; let body: Value = serde_json::from_slice(&bytes)?; body.as_array() .ok_or(Error::Parse("Expected JSON array".into()))? .iter() .map(Self::value_to_ohlc) .collect::, _>>() }) } pub fn get_from_date(&mut self, date: Date) -> Result { let day1 = Day1::try_from(date)?; let key = day1.checked_sub(day1 % CHUNK_SIZE).unwrap(); #[allow(clippy::map_entry)] if !self.day1_to_ohlc.contains_key(&key) || ((key + self.day1_to_ohlc.get(&key).unwrap().len()) <= day1) { self.day1_to_ohlc.insert(key, self.fetch_date_prices(key)?); } self.day1_to_ohlc .get(&key) .unwrap() .get(usize::from(day1.checked_sub(key).unwrap())) .cloned() .ok_or(Error::NotFound("Couldn't find date in BRK".into())) } fn fetch_date_prices(&self, day1: Day1) -> Result> { let agent = &self.agent; default_retry(|_| { let url = format!( "{API_URL}/day1-to-price-ohlc?from={}&to={}", day1, day1 + CHUNK_SIZE ); info!("Fetching {url}..."); let bytes = checked_get(agent, &url)?; let body: Value = serde_json::from_slice(&bytes)?; body.as_array() .ok_or(Error::Parse("Expected JSON array".into()))? .iter() .map(Self::value_to_ohlc) .collect::, _>>() }) } fn value_to_ohlc(value: &Value) -> Result { let ohlc = value .as_array() .ok_or(Error::Parse("Expected OHLC array".into()))?; let get_value = |index: usize| -> Result<_> { Ok(Cents::from(Dollars::from( ohlc.get(index) .ok_or(Error::Parse("Missing OHLC value at index".into()))? .as_f64() .ok_or(Error::Parse("Invalid OHLC value type".into()))?, ))) }; Ok(OHLCCents::from(( Open::new(get_value(0)?), High::new(get_value(1)?), Low::new(get_value(2)?), Close::new(get_value(3)?), ))) } pub fn ping(&self) -> Result<()> { self.agent.get(API_URL).call()?; Ok(()) } } impl PriceSource for BRK { fn name(&self) -> &'static str { "BRK" } fn get_date(&mut self, date: Date) -> Option> { Some(self.get_from_date(date)) } fn get_1mn( &mut self, _timestamp: Timestamp, _previous_timestamp: Option, ) -> Option> { None // BRK doesn't support timestamp-based queries } fn get_height(&mut self, height: Height) -> Option> { Some(self.get_from_height(height)) } fn ping(&self) -> Result<()> { self.ping() } fn clear(&mut self) { self.height_to_ohlc.clear(); self.day1_to_ohlc.clear(); } }