diff --git a/crates/brk_bindgen/src/generators/javascript/client.rs b/crates/brk_bindgen/src/generators/javascript/client.rs index ca9d9ba1d..eca97c824 100644 --- a/crates/brk_bindgen/src/generators/javascript/client.rs +++ b/crates/brk_bindgen/src/generators/javascript/client.rs @@ -81,7 +81,7 @@ function indexToDate(index, i) {{ case 'hour4': return new Date(_EPOCH_MS + i * 14400000); case 'hour12': return new Date(_EPOCH_MS + i * 43200000); case 'day1': return i === 0 ? _GENESIS : new Date(_DAY_ONE.getTime() + (i - 1) * _MS_PER_DAY); - case 'day3': return new Date(_EPOCH_MS + i * 259200000); + case 'day3': return new Date(_EPOCH_MS - 86400000 + i * 259200000); case 'week1': return new Date(_GENESIS.getTime() + i * _MS_PER_WEEK); case 'month1': return _addMonths(i); case 'month3': return _addMonths(i * 3); @@ -113,7 +113,7 @@ function dateToIndex(index, d) {{ if (ms < _DAY_ONE.getTime()) return 0; return 1 + Math.floor((ms - _DAY_ONE.getTime()) / _MS_PER_DAY); }} - case 'day3': return Math.floor((ms - _EPOCH_MS) / 259200000); + case 'day3': return Math.floor((ms - _EPOCH_MS + 86400000) / 259200000); case 'week1': return Math.floor((ms - _GENESIS.getTime()) / _MS_PER_WEEK); case 'month1': return (d.getFullYear() - 2009) * 12 + d.getMonth(); case 'month3': return (d.getFullYear() - 2009) * 4 + Math.floor(d.getMonth() / 3); diff --git a/crates/brk_bindgen/src/generators/python/client.rs b/crates/brk_bindgen/src/generators/python/client.rs index 9fc7148f5..f76db7b5d 100644 --- a/crates/brk_bindgen/src/generators/python/client.rs +++ b/crates/brk_bindgen/src/generators/python/client.rs @@ -162,7 +162,7 @@ def _index_to_date(index: str, i: int) -> Union[date, datetime]: elif index == 'day1': return _GENESIS if i == 0 else _DAY_ONE + timedelta(days=i - 1) elif index == 'day3': - return _EPOCH.date() + timedelta(days=i * 3) + return _EPOCH.date() - timedelta(days=1) + timedelta(days=i * 3) elif index == 'week1': return _GENESIS + timedelta(weeks=i) elif index == 'month1': @@ -202,7 +202,7 @@ def _date_to_index(index: str, d: Union[date, datetime]) -> int: return 0 return 1 + (dd - _DAY_ONE).days elif index == 'day3': - return (dd - date(2009, 1, 1)).days // 3 + return (dd - date(2008, 12, 31)).days // 3 elif index == 'week1': return (dd - _GENESIS).days // 7 elif index == 'month1': diff --git a/crates/brk_client/src/lib.rs b/crates/brk_client/src/lib.rs index 61bce3958..0e3b41c00 100644 --- a/crates/brk_client/src/lib.rs +++ b/crates/brk_client/src/lib.rs @@ -2101,52 +2101,6 @@ impl GreedInvestedInvestorNegNetPainPeakSupplyTotalUnrealizedPattern { } } -/// Pattern struct for repeated tree structure. -pub struct Day1Day3DifficultyepochHalvingepochHour1Hour12Hour4Minute1Minute10Minute30Minute5Month1Month3Month6Week1Year1Year10Pattern { - pub day1: MetricPattern10, - pub day3: MetricPattern11, - pub difficultyepoch: MetricPattern19, - pub halvingepoch: MetricPattern18, - pub hour1: MetricPattern7, - pub hour12: MetricPattern9, - pub hour4: MetricPattern8, - pub minute1: MetricPattern3, - pub minute10: MetricPattern5, - pub minute30: MetricPattern6, - pub minute5: MetricPattern4, - pub month1: MetricPattern13, - pub month3: MetricPattern14, - pub month6: MetricPattern15, - pub week1: MetricPattern12, - pub year1: MetricPattern16, - pub year10: MetricPattern17, -} - -impl Day1Day3DifficultyepochHalvingepochHour1Hour12Hour4Minute1Minute10Minute30Minute5Month1Month3Month6Week1Year1Year10Pattern { - /// Create a new pattern node with accumulated metric name. - pub fn new(client: Arc, acc: String) -> Self { - Self { - day1: MetricPattern10::new(client.clone(), _m(&acc, "day1")), - day3: MetricPattern11::new(client.clone(), _m(&acc, "day3")), - difficultyepoch: MetricPattern19::new(client.clone(), _m(&acc, "difficultyepoch")), - halvingepoch: MetricPattern18::new(client.clone(), _m(&acc, "halvingepoch")), - hour1: MetricPattern7::new(client.clone(), _m(&acc, "hour1")), - hour12: MetricPattern9::new(client.clone(), _m(&acc, "hour12")), - hour4: MetricPattern8::new(client.clone(), _m(&acc, "hour4")), - minute1: MetricPattern3::new(client.clone(), _m(&acc, "minute1")), - minute10: MetricPattern5::new(client.clone(), _m(&acc, "minute10")), - minute30: MetricPattern6::new(client.clone(), _m(&acc, "minute30")), - minute5: MetricPattern4::new(client.clone(), _m(&acc, "minute5")), - month1: MetricPattern13::new(client.clone(), _m(&acc, "month1")), - month3: MetricPattern14::new(client.clone(), _m(&acc, "month3")), - month6: MetricPattern15::new(client.clone(), _m(&acc, "month6")), - week1: MetricPattern12::new(client.clone(), _m(&acc, "week1")), - year1: MetricPattern16::new(client.clone(), _m(&acc, "year1")), - year10: MetricPattern17::new(client.clone(), _m(&acc, "year10")), - } - } -} - /// Pattern struct for repeated tree structure. pub struct GreedInvestedInvestorNegNetPainSupplyTotalUnrealizedPattern { pub greed_index: MetricPattern1, @@ -3093,18 +3047,18 @@ impl BtcSatsUsdPattern { /// Pattern struct for repeated tree structure. pub struct CentsSatsUsdPattern { - pub cents: Day1Day3DifficultyepochHalvingepochHour1Hour12Hour4Minute1Minute10Minute30Minute5Month1Month3Month6Week1Year1Year10Pattern, - pub sats: Day1Day3DifficultyepochHalvingepochHour1Hour12Hour4Minute1Minute10Minute30Minute5Month1Month3Month6Week1Year1Year10Pattern, - pub usd: Day1Day3DifficultyepochHalvingepochHour1Hour12Hour4Minute1Minute10Minute30Minute5Month1Month3Month6Week1Year1Year10Pattern, + pub cents: MetricPattern2, + pub sats: MetricPattern2, + pub usd: MetricPattern2, } impl CentsSatsUsdPattern { /// Create a new pattern node with accumulated metric name. pub fn new(client: Arc, acc: String) -> Self { Self { - cents: Day1Day3DifficultyepochHalvingepochHour1Hour12Hour4Minute1Minute10Minute30Minute5Month1Month3Month6Week1Year1Year10Pattern::new(client.clone(), _m(&acc, "cents")), - sats: Day1Day3DifficultyepochHalvingepochHour1Hour12Hour4Minute1Minute10Minute30Minute5Month1Month3Month6Week1Year1Year10Pattern::new(client.clone(), _m(&acc, "sats")), - usd: Day1Day3DifficultyepochHalvingepochHour1Hour12Hour4Minute1Minute10Minute30Minute5Month1Month3Month6Week1Year1Year10Pattern::new(client.clone(), acc.clone()), + cents: MetricPattern2::new(client.clone(), _m(&acc, "cents")), + sats: MetricPattern2::new(client.clone(), _m(&acc, "sats")), + usd: MetricPattern2::new(client.clone(), acc.clone()), } } } @@ -3390,7 +3344,7 @@ impl MetricsTree_Blocks_Difficulty { /// Metrics tree node. pub struct MetricsTree_Blocks_Time { - pub timestamp: MetricsTree_Blocks_Time_Timestamp, + pub timestamp: MetricPattern1, pub date: MetricPattern20, pub timestamp_monotonic: MetricPattern20, } @@ -3398,60 +3352,13 @@ pub struct MetricsTree_Blocks_Time { impl MetricsTree_Blocks_Time { pub fn new(client: Arc, base_path: String) -> Self { Self { - timestamp: MetricsTree_Blocks_Time_Timestamp::new(client.clone(), format!("{base_path}_timestamp")), + timestamp: MetricPattern1::new(client.clone(), "timestamp".to_string()), date: MetricPattern20::new(client.clone(), "date".to_string()), timestamp_monotonic: MetricPattern20::new(client.clone(), "timestamp_monotonic".to_string()), } } } -/// Metrics tree node. -pub struct MetricsTree_Blocks_Time_Timestamp { - pub base: MetricPattern20, - pub minute1: MetricPattern3, - pub minute5: MetricPattern4, - pub minute10: MetricPattern5, - pub minute30: MetricPattern6, - pub hour1: MetricPattern7, - pub hour4: MetricPattern8, - pub hour12: MetricPattern9, - pub day1: MetricPattern10, - pub day3: MetricPattern11, - pub week1: MetricPattern12, - pub month1: MetricPattern13, - pub month3: MetricPattern14, - pub month6: MetricPattern15, - pub year1: MetricPattern16, - pub year10: MetricPattern17, - pub halvingepoch: MetricPattern18, - pub difficultyepoch: MetricPattern19, -} - -impl MetricsTree_Blocks_Time_Timestamp { - pub fn new(client: Arc, base_path: String) -> Self { - Self { - base: MetricPattern20::new(client.clone(), "timestamp".to_string()), - minute1: MetricPattern3::new(client.clone(), "timestamp_minute1".to_string()), - minute5: MetricPattern4::new(client.clone(), "timestamp_minute5".to_string()), - minute10: MetricPattern5::new(client.clone(), "timestamp_minute10".to_string()), - minute30: MetricPattern6::new(client.clone(), "timestamp_minute30".to_string()), - hour1: MetricPattern7::new(client.clone(), "timestamp_hour1".to_string()), - hour4: MetricPattern8::new(client.clone(), "timestamp_hour4".to_string()), - hour12: MetricPattern9::new(client.clone(), "timestamp_hour12".to_string()), - day1: MetricPattern10::new(client.clone(), "timestamp_day1".to_string()), - day3: MetricPattern11::new(client.clone(), "timestamp_day3".to_string()), - week1: MetricPattern12::new(client.clone(), "timestamp_week1".to_string()), - month1: MetricPattern13::new(client.clone(), "timestamp_month1".to_string()), - month3: MetricPattern14::new(client.clone(), "timestamp_month3".to_string()), - month6: MetricPattern15::new(client.clone(), "timestamp_month6".to_string()), - year1: MetricPattern16::new(client.clone(), "timestamp_year1".to_string()), - year10: MetricPattern17::new(client.clone(), "timestamp_year10".to_string()), - halvingepoch: MetricPattern18::new(client.clone(), "timestamp_halvingepoch".to_string()), - difficultyepoch: MetricPattern19::new(client.clone(), "timestamp_difficultyepoch".to_string()), - } - } -} - /// Metrics tree node. pub struct MetricsTree_Blocks_Weight { pub base: MetricPattern20, @@ -6039,7 +5946,7 @@ impl MetricsTree_Pools_Vecs { /// Metrics tree node. pub struct MetricsTree_Prices { pub split: MetricsTree_Prices_Split, - pub ohlc: CentsSatsUsdPattern, + pub ohlc: MetricsTree_Prices_Ohlc, pub price: MetricsTree_Prices_Price, } @@ -6047,7 +5954,7 @@ impl MetricsTree_Prices { pub fn new(client: Arc, base_path: String) -> Self { Self { split: MetricsTree_Prices_Split::new(client.clone(), format!("{base_path}_split")), - ohlc: CentsSatsUsdPattern::new(client.clone(), "price_ohlc".to_string()), + ohlc: MetricsTree_Prices_Ohlc::new(client.clone(), format!("{base_path}_ohlc")), price: MetricsTree_Prices_Price::new(client.clone(), format!("{base_path}_price")), } } @@ -6089,6 +5996,23 @@ impl MetricsTree_Prices_Split_Close { } } +/// Metrics tree node. +pub struct MetricsTree_Prices_Ohlc { + pub cents: MetricPattern2, + pub usd: MetricPattern2, + pub sats: MetricPattern2, +} + +impl MetricsTree_Prices_Ohlc { + pub fn new(client: Arc, base_path: String) -> Self { + Self { + cents: MetricPattern2::new(client.clone(), "price_ohlc_cents".to_string()), + usd: MetricPattern2::new(client.clone(), "price_ohlc".to_string()), + sats: MetricPattern2::new(client.clone(), "price_ohlc_sats".to_string()), + } + } +} + /// Metrics tree node. pub struct MetricsTree_Prices_Price { pub cents: MetricPattern20, diff --git a/crates/brk_computer/src/blocks/compute.rs b/crates/brk_computer/src/blocks/compute.rs index 293f13714..3c67fc5e2 100644 --- a/crates/brk_computer/src/blocks/compute.rs +++ b/crates/brk_computer/src/blocks/compute.rs @@ -14,12 +14,49 @@ impl Vecs { starting_indexes: &ComputeIndexes, exit: &Exit, ) -> Result<()> { - self.time.timestamp.compute_first( - starting_indexes, - &indexer.vecs.blocks.timestamp, - indexes, - exit, - )?; + { + let ts = &mut self.time.timestamp; + + macro_rules! period { + ($field:ident) => { + ts.$field.compute_transform( + starting_indexes.$field, + &indexes.$field.first_height, + |(idx, _, _)| (idx, idx.to_timestamp()), + exit, + )?; + }; + } + + period!(minute1); + period!(minute5); + period!(minute10); + period!(minute30); + period!(hour1); + period!(hour4); + period!(hour12); + period!(day1); + period!(day3); + period!(week1); + period!(month1); + period!(month3); + period!(month6); + period!(year1); + period!(year10); + + ts.halvingepoch.compute_indirect( + starting_indexes.halvingepoch, + &indexes.halvingepoch.first_height, + &indexer.vecs.blocks.timestamp, + exit, + )?; + ts.difficultyepoch.compute_indirect( + starting_indexes.difficultyepoch, + &indexes.difficultyepoch.first_height, + &indexer.vecs.blocks.timestamp, + exit, + )?; + } self.count .compute(indexer, &self.time, starting_indexes, exit)?; self.interval diff --git a/crates/brk_computer/src/distribution/compute/block_loop.rs b/crates/brk_computer/src/distribution/compute/block_loop.rs index caca96ac9..d4a1e5873 100644 --- a/crates/brk_computer/src/distribution/compute/block_loop.rs +++ b/crates/brk_computer/src/distribution/compute/block_loop.rs @@ -204,7 +204,7 @@ pub(crate) fn process_blocks( debug!("AddressCache created, entering main loop"); // Cache for day1 lookups - same day1 repeats ~140 times per day - let mut cached_day1 = Day1::default(); + let mut cached_day1: Option = None; let mut cached_date_first_height = Height::ZERO; let mut cached_date_height_count = StoredU64::default(); @@ -428,12 +428,12 @@ pub(crate) fn process_blocks( // avoiding redundant PcoVec page decompressions. let date = height_to_date_vec[offset]; let day1 = Day1::try_from(date).unwrap(); - let (date_first_height, date_height_count) = if day1 == cached_day1 { + let (date_first_height, date_height_count) = if cached_day1 == Some(day1) { (cached_date_first_height, cached_date_height_count) } else { let fh: Height = day1_to_first_height.collect_one(day1).unwrap(); let hc = day1_to_height_count.collect_one(day1).unwrap(); - cached_day1 = day1; + cached_day1 = Some(day1); cached_date_first_height = fh; cached_date_height_count = hc; (fh, hc) diff --git a/crates/brk_computer/src/internal/eager_indexes.rs b/crates/brk_computer/src/internal/eager_indexes.rs index 285373828..83997e410 100644 --- a/crates/brk_computer/src/internal/eager_indexes.rs +++ b/crates/brk_computer/src/internal/eager_indexes.rs @@ -13,7 +13,8 @@ use brk_types::{ use derive_more::{Deref, DerefMut}; use schemars::JsonSchema; use vecdb::{ - Database, EagerVec, Exit, ImportableVec, PcoVec, ReadableVec, Rw, StorageMode, VecIndex, + AnyVec, Database, EagerVec, Exit, ImportableVec, PcoVec, ReadableVec, Rw, StorageMode, + VecIndex, WritableVec, }; use crate::{ @@ -59,7 +60,7 @@ where macro_rules! period { ($idx:ident) => { - ImportableVec::forced_import(db, &format!("{name}_{}", stringify!($idx)), v)? + ImportableVec::forced_import(db, name, v)? }; } @@ -76,15 +77,10 @@ where ) -> Result<()> { macro_rules! period { ($field:ident) => { - self.0.$field.compute_transform( + self.0.$field.compute_indirect( starting_indexes.$field, &indexes.$field.first_height, - |(idx, first_h, _)| { - let v = height_source - .collect_one(first_h) - .unwrap_or_else(|| T::from(0_usize)); - (idx, v) - }, + height_source, exit, )?; }; @@ -122,25 +118,17 @@ where let src_len = height_source.len(); macro_rules! period { - ($field:ident) => {{ - let fh = &indexes.$field.first_height; - self.0.$field.compute_transform( + ($field:ident) => { + compute_period_extremum( + &mut self.0.$field, starting_indexes.$field, - fh, - |(idx, first_h, _)| { - let end_h = Height::from( - fh.collect_one_at(idx.to_usize() + 1) - .map(|h: Height| h.to_usize()) - .unwrap_or(src_len), - ); - let v = height_source - .max(first_h, end_h) - .unwrap_or_else(|| T::from(0_usize)); - (idx, v) - }, + &indexes.$field.first_height, + height_source, + src_len, + T::max, exit, )?; - }}; + }; } period!(minute1); @@ -175,25 +163,17 @@ where let src_len = height_source.len(); macro_rules! period { - ($field:ident) => {{ - let fh = &indexes.$field.first_height; - self.0.$field.compute_transform( + ($field:ident) => { + compute_period_extremum( + &mut self.0.$field, starting_indexes.$field, - fh, - |(idx, first_h, _)| { - let end_h = Height::from( - fh.collect_one_at(idx.to_usize() + 1) - .map(|h: Height| h.to_usize()) - .unwrap_or(src_len), - ); - let v = height_source - .min(first_h, end_h) - .unwrap_or_else(|| T::from(0_usize)); - (idx, v) - }, + &indexes.$field.first_height, + height_source, + src_len, + T::min, exit, )?; - }}; + }; } period!(minute1); @@ -217,3 +197,60 @@ where Ok(()) } } + +/// Compute per-period extremum (max or min) of height_source values. +/// +/// Each period's range is `[fh[i]..fh[i+1])` of height_source. +/// Uses a cursor on height_source so each page is decompressed at most once. +fn compute_period_extremum( + out: &mut EagerVec>, + starting_index: I, + fh: &impl ReadableVec, + height_source: &impl ReadableVec, + src_len: usize, + better: fn(T, T) -> T, + exit: &Exit, +) -> Result<()> { + out.validate_and_truncate(fh.version() + height_source.version(), starting_index)?; + let mut cursor = height_source.cursor(); + Ok(out.repeat_until_complete(exit, |this| { + let skip = this.len(); + let end = fh.len(); + if skip >= end { + return Ok(()); + } + + let fh_batch: Vec = fh.collect_range_at(skip, (end + 1).min(fh.len())); + + if cursor.position() < fh_batch[0].to_usize() { + cursor.advance(fh_batch[0].to_usize() - cursor.position()); + } + + for j in 0..(end - skip) { + let first_h = fh_batch[j].to_usize(); + let end_h = fh_batch.get(j + 1).map_or(src_len, |h| h.to_usize()); + + if cursor.position() < first_h { + cursor.advance(first_h - cursor.position()); + } + + let range_len = end_h.saturating_sub(first_h); + let v = if range_len > 0 { + cursor + .fold(range_len, None, |acc, b| { + Some(match acc { + Some(a) => better(a, b), + None => b, + }) + }) + .unwrap_or_else(|| T::from(0_usize)) + } else { + T::from(0_usize) + }; + + this.checked_push_at(skip + j, v)?; + } + + Ok(()) + })?) +} diff --git a/crates/brk_computer/src/internal/lazy_eager_indexes.rs b/crates/brk_computer/src/internal/lazy_eager_indexes.rs index f96389f85..30fa3f990 100644 --- a/crates/brk_computer/src/internal/lazy_eager_indexes.rs +++ b/crates/brk_computer/src/internal/lazy_eager_indexes.rs @@ -59,7 +59,7 @@ where macro_rules! period { ($idx:ident) => { LazyVecFrom1::transformed::( - &format!("{name}_{}", stringify!($idx)), + name, version, source.$idx.read_only_boxed_clone(), ) diff --git a/crates/brk_computer/src/lib.rs b/crates/brk_computer/src/lib.rs index 37f638221..7f3a02ff1 100644 --- a/crates/brk_computer/src/lib.rs +++ b/crates/brk_computer/src/lib.rs @@ -65,10 +65,17 @@ impl Computer { let i = Instant::now(); let (indexes, positions) = thread::scope(|s| -> Result<_> { let positions_handle = big_thread().spawn_scoped(s, || -> Result<_> { - Ok(Box::new(positions::Vecs::forced_import(&computed_path, VERSION)?)) + Ok(Box::new(positions::Vecs::forced_import( + &computed_path, + VERSION, + )?)) })?; - let indexes = Box::new(indexes::Vecs::forced_import(&computed_path, VERSION, indexer)?); + let indexes = Box::new(indexes::Vecs::forced_import( + &computed_path, + VERSION, + indexer, + )?); let positions = positions_handle.join().unwrap()?; Ok((indexes, positions)) @@ -79,11 +86,19 @@ impl Computer { let i = Instant::now(); let (inputs, outputs) = thread::scope(|s| -> Result<_> { let inputs_handle = big_thread().spawn_scoped(s, || -> Result<_> { - Ok(Box::new(inputs::Vecs::forced_import(&computed_path, VERSION, &indexes)?)) + Ok(Box::new(inputs::Vecs::forced_import( + &computed_path, + VERSION, + &indexes, + )?)) })?; let outputs_handle = big_thread().spawn_scoped(s, || -> Result<_> { - Ok(Box::new(outputs::Vecs::forced_import(&computed_path, VERSION, &indexes)?)) + Ok(Box::new(outputs::Vecs::forced_import( + &computed_path, + VERSION, + &indexes, + )?)) })?; let inputs = inputs_handle.join().unwrap()?; @@ -96,7 +111,11 @@ impl Computer { let i = Instant::now(); let constants = Box::new(constants::Vecs::new(VERSION, &indexes)); // Price must be created before market since market's lazy vecs reference price - let prices = Box::new(prices::Vecs::forced_import(&computed_path, VERSION, &indexes)?); + let prices = Box::new(prices::Vecs::forced_import( + &computed_path, + VERSION, + &indexes, + )?); info!("Imported price/constants in {:?}", i.elapsed()); let i = Instant::now(); @@ -104,12 +123,21 @@ impl Computer { thread::scope(|s| -> Result<_> { // Import blocks module (no longer needs prices) let blocks_handle = big_thread().spawn_scoped(s, || -> Result<_> { - Ok(Box::new(blocks::Vecs::forced_import(&computed_path, VERSION, indexer, &indexes)?)) + Ok(Box::new(blocks::Vecs::forced_import( + &computed_path, + VERSION, + indexer, + &indexes, + )?)) })?; // Import mining module (separate database) let mining_handle = big_thread().spawn_scoped(s, || -> Result<_> { - Ok(Box::new(mining::Vecs::forced_import(&computed_path, VERSION, &indexes)?)) + Ok(Box::new(mining::Vecs::forced_import( + &computed_path, + VERSION, + &indexes, + )?)) })?; // Import transactions module @@ -130,9 +158,11 @@ impl Computer { )?)) })?; - let cointime = Box::new( - cointime::Vecs::forced_import(&computed_path, VERSION, &indexes)? - ); + let cointime = Box::new(cointime::Vecs::forced_import( + &computed_path, + VERSION, + &indexes, + )?); let blocks = blocks_handle.join().unwrap()?; let mining = mining_handle.join().unwrap()?; @@ -154,16 +184,21 @@ impl Computer { // Threads inside let i = Instant::now(); - let distribution = Box::new( - distribution::Vecs::forced_import(&computed_path, VERSION, &indexes)? - ); + let distribution = Box::new(distribution::Vecs::forced_import( + &computed_path, + VERSION, + &indexes, + )?); info!("Imported distribution in {:?}", i.elapsed()); // Supply must be imported after distribution (references distribution's supply) let i = Instant::now(); - let supply = Box::new( - supply::Vecs::forced_import(&computed_path, VERSION, &indexes, &distribution)? - ); + let supply = Box::new(supply::Vecs::forced_import( + &computed_path, + VERSION, + &indexes, + &distribution, + )?); info!("Imported supply in {:?}", i.elapsed()); let i = Instant::now(); @@ -286,14 +321,25 @@ impl Computer { // Inputs → scripts → outputs (sequential) info!("Computing inputs..."); let i = Instant::now(); - self.inputs - .compute(indexer, &self.indexes, &self.blocks, &starting_indexes, exit)?; + self.inputs.compute( + indexer, + &self.indexes, + &self.blocks, + &starting_indexes, + exit, + )?; info!("Computed inputs in {:?}", i.elapsed()); info!("Computing scripts..."); let i = Instant::now(); - self.scripts - .compute(indexer, &self.blocks, &self.outputs, &self.prices, &starting_indexes, exit)?; + self.scripts.compute( + indexer, + &self.blocks, + &self.outputs, + &self.prices, + &starting_indexes, + exit, + )?; info!("Computed scripts in {:?}", i.elapsed()); info!("Computing outputs..."); diff --git a/crates/brk_computer/src/prices/compute.rs b/crates/brk_computer/src/prices/compute.rs index 0bbfa2e41..4e150118c 100644 --- a/crates/brk_computer/src/prices/compute.rs +++ b/crates/brk_computer/src/prices/compute.rs @@ -37,7 +37,6 @@ impl Vecs { &self.split.high.cents, &self.split.low.cents, &self.split.close.cents, - indexes, exit, )?; diff --git a/crates/brk_computer/src/prices/ohlcs.rs b/crates/brk_computer/src/prices/ohlcs.rs index 10d92b57c..3d4f442bd 100644 --- a/crates/brk_computer/src/prices/ohlcs.rs +++ b/crates/brk_computer/src/prices/ohlcs.rs @@ -10,18 +10,18 @@ use schemars::JsonSchema; use serde::Serialize; use vecdb::{ BytesVec, BytesVecValue, Database, EagerVec, Exit, Formattable, ImportableVec, LazyVecFrom1, - ReadableCloneableVec, ReadableVec, Rw, StorageMode, UnaryTransform, + ReadableCloneableVec, Rw, StorageMode, UnaryTransform, }; use crate::{ - ComputeIndexes, indexes, indexes_from, + ComputeIndexes, indexes_from, internal::{ComputedHeightDerivedLast, EagerIndexes, Indexes}, }; // ── EagerOhlcIndexes ───────────────────────────────────────────────── #[derive(Deref, DerefMut, Traversable)] -#[traversable(transparent)] +#[traversable(merge)] pub struct OhlcVecs( #[allow(clippy::type_complexity)] pub Indexes< @@ -58,7 +58,7 @@ where macro_rules! period { ($idx:ident) => { - ImportableVec::forced_import(db, &format!("{name}_{}", stringify!($idx)), v)? + ImportableVec::forced_import(db, name, v)? }; } @@ -67,7 +67,6 @@ where } impl OhlcVecs { - #[allow(clippy::too_many_arguments)] pub(crate) fn compute_from_split( &mut self, starting_indexes: &ComputeIndexes, @@ -75,26 +74,24 @@ impl OhlcVecs { high: &EagerIndexes, low: &EagerIndexes, close: &ComputedHeightDerivedLast, - indexes: &indexes::Vecs, exit: &Exit, ) -> Result<()> { macro_rules! period { ($field:ident) => { - self.0.$field.compute_transform( + self.0.$field.compute_transform4( starting_indexes.$field, - &indexes.$field.first_height, - |(idx, _first_h, _)| { - let o = open.$field.collect_one(idx).unwrap_or_default(); - let h = high.$field.collect_one(idx).unwrap_or_default(); - let l = low.$field.collect_one(idx).unwrap_or_default(); - let c = close.$field.collect_one(idx).flatten().unwrap_or_default(); + &open.$field, + &high.$field, + &low.$field, + &close.$field, + |(idx, o, h, l, c, _)| { ( idx, OHLCCents { open: Open::new(o), high: High::new(h), low: Low::new(l), - close: Close::new(c), + close: Close::new(c.unwrap_or_default()), }, ) }, @@ -105,14 +102,13 @@ impl OhlcVecs { macro_rules! epoch { ($field:ident) => { - self.0.$field.compute_transform( + self.0.$field.compute_transform4( starting_indexes.$field, - &indexes.$field.first_height, - |(idx, _first_h, _)| { - let o = open.$field.collect_one(idx).unwrap_or_default(); - let h = high.$field.collect_one(idx).unwrap_or_default(); - let l = low.$field.collect_one(idx).unwrap_or_default(); - let c = close.$field.collect_one(idx).unwrap_or_default(); + &open.$field, + &high.$field, + &low.$field, + &close.$field, + |(idx, o, h, l, c, _)| { ( idx, OHLCCents { @@ -153,7 +149,7 @@ impl OhlcVecs { // ── LazyOhlcIndexes ────────────────────────────────────────────────── #[derive(Clone, Deref, DerefMut, Traversable)] -#[traversable(transparent)] +#[traversable(merge)] pub struct LazyOhlcVecs( #[allow(clippy::type_complexity)] pub Indexes< @@ -193,7 +189,7 @@ where macro_rules! period { ($idx:ident) => { LazyVecFrom1::transformed::( - &format!("{name}_{}", stringify!($idx)), + name, version, source.$idx.read_only_boxed_clone(), ) diff --git a/crates/brk_types/src/date.rs b/crates/brk_types/src/date.rs index 193a9d0cd..dcc63cd5a 100644 --- a/crates/brk_types/src/date.rs +++ b/crates/brk_types/src/date.rs @@ -14,10 +14,8 @@ use super::{Day1, Year10, Month1, Month3, Month6, Timestamp, Week1, Year1}; pub struct Date(u32); impl Date { - pub const INDEX_ZERO: Self = Self(20090103); - pub const INDEX_ZERO_: Date_ = Date_::constant(2009, 1, 3); - pub const INDEX_ONE: Self = Self(20090109); - pub const INDEX_ONE_: Date_ = Date_::constant(2009, 1, 9); + pub const INDEX_ZERO: Self = Self(20090101); + pub const INDEX_ZERO_: Date_ = Date_::constant(2009, 1, 1); pub const MIN_RATIO: Self = Self(20120101); pub fn new(year: u16, month: u8, day: u8) -> Self { @@ -99,22 +97,18 @@ impl From for Date { impl From for Date { #[inline] fn from(value: Day1) -> Self { - if value == Day1::default() { - Date::INDEX_ZERO - } else { - Self::from( - Self::INDEX_ONE_ - .checked_add(Span::new().days(i64::from(value) - 1)) - .unwrap(), - ) - } + Self::from( + Self::INDEX_ZERO_ + .checked_add(Span::new().days(i64::from(value))) + .unwrap(), + ) } } impl From for Date { #[inline] fn from(value: Week1) -> Self { - // Week 0 starts at genesis (2009-01-03), add i weeks + // Week 0 starts at 2009-01-01, add i weeks Self::from( Self::INDEX_ZERO_ .checked_add(Span::new().weeks(i64::from(u16::from(value)))) @@ -279,49 +273,48 @@ mod tests { #[test] fn test_date_from_day1_zero() { - // Day1 0 is genesis: Jan 3, 2009 + // Day1 0 is Jan 1, 2009 let date = Date::from(Day1::from(0_usize)); assert_eq!(date, Date::INDEX_ZERO); assert_eq!(date.year(), 2009); assert_eq!(date.month(), 1); + assert_eq!(date.day(), 1); + } + + #[test] + fn test_date_from_day1_two() { + // Day1 2 is Jan 3, 2009 (genesis) + let date = Date::from(Day1::from(2_usize)); + assert_eq!(date.year(), 2009); + assert_eq!(date.month(), 1); assert_eq!(date.day(), 3); } #[test] - fn test_date_from_day1_one() { - // Day1 1 is Jan 9, 2009 (6 day gap after genesis) - let date = Date::from(Day1::from(1_usize)); - assert_eq!(date, Date::INDEX_ONE); + fn test_date_from_day1_eight() { + // Day1 8 is Jan 9, 2009 + let date = Date::from(Day1::from(8_usize)); assert_eq!(date.year(), 2009); assert_eq!(date.month(), 1); assert_eq!(date.day(), 9); } - #[test] - fn test_date_from_day1_two() { - // Day1 2 is Jan 10, 2009 - let date = Date::from(Day1::from(2_usize)); - assert_eq!(date.year(), 2009); - assert_eq!(date.month(), 1); - assert_eq!(date.day(), 10); - } - #[test] fn test_date_from_week1_zero() { - // Week1 0 starts at genesis: Jan 3, 2009 + // Week1 0 starts at Jan 1, 2009 let date = Date::from(Week1::from(0_usize)); assert_eq!(date.year(), 2009); assert_eq!(date.month(), 1); - assert_eq!(date.day(), 3); + assert_eq!(date.day(), 1); } #[test] fn test_date_from_week1_one() { - // Week1 1 is Jan 10, 2009 (one week after genesis) + // Week1 1 is Jan 8, 2009 (one week after epoch) let date = Date::from(Week1::from(1_usize)); assert_eq!(date.year(), 2009); assert_eq!(date.month(), 1); - assert_eq!(date.day(), 10); + assert_eq!(date.day(), 8); } #[test] diff --git a/crates/brk_types/src/day1.rs b/crates/brk_types/src/day1.rs index 09e5575c5..3e44d75a4 100644 --- a/crates/brk_types/src/day1.rs +++ b/crates/brk_types/src/day1.rs @@ -11,7 +11,7 @@ use vecdb::{CheckedSub, Formattable, Pco, PrintableIndex}; use crate::{FromCoarserIndex, Month1, Month3, Month6, Week1, Year1, Year10}; -use super::Date; +use super::{Date, Timestamp}; #[derive( Debug, @@ -31,6 +31,10 @@ pub struct Day1(u16); impl Day1 { pub const BYTES: usize = size_of::(); + + pub fn to_timestamp(&self) -> Timestamp { + Timestamp::from(Date::from(*self)) + } } impl From for usize { @@ -74,14 +78,8 @@ impl TryFrom for Day1 { let value_ = jiff::civil::Date::from(value); if value_ < Date::INDEX_ZERO_ { Err(Error::UnindexableDate) - } else if value == Date::INDEX_ZERO { - Ok(Self(0)) - } else if value_ < Date::INDEX_ONE_ { - Err(Error::UnindexableDate) - } else if value == Date::INDEX_ONE { - Ok(Self(1)) } else { - Ok(Self(Date::INDEX_ONE_.until(value_)?.get_days() as u16 + 1)) + Ok(Self(Date::INDEX_ZERO_.until(value_)?.get_days() as u16)) } } } @@ -101,40 +99,21 @@ impl Rem for Day1 { impl FromCoarserIndex for Day1 { fn min_from(coarser: Week1) -> usize { - let coarser = usize::from(coarser); - if coarser == 0 { - 0 - } else if coarser == 1 { - 1 - } else { - 4 + (coarser - 2) * 7 - } + usize::from(coarser) * 7 } fn max_from_(coarser: Week1) -> usize { - let coarser = usize::from(coarser); - if coarser == 0 { - 0 - } else if coarser == 1 { - 3 - } else { - 3 + (coarser - 1) * 7 - } + usize::from(coarser) * 7 + 6 } } impl FromCoarserIndex for Day1 { fn min_from(coarser: Month1) -> usize { - let coarser = u16::from(coarser); - if coarser == 0 { - 0 - } else { - let d = Date::new(2009, 1, 1) - .into_jiff() - .checked_add(Span::new().months(coarser)) - .unwrap(); - Day1::try_from(Date::from(d)).unwrap().into() - } + let d = Date::new(2009, 1, 1) + .into_jiff() + .checked_add(Span::new().months(u16::from(coarser))) + .unwrap(); + Day1::try_from(Date::from(d)).unwrap().into() } fn max_from_(coarser: Month1) -> usize { @@ -148,16 +127,11 @@ impl FromCoarserIndex for Day1 { impl FromCoarserIndex for Day1 { fn min_from(coarser: Month3) -> usize { - let coarser = u8::from(coarser); - if coarser == 0 { - 0 - } else { - let d = Date::new(2009, 1, 1) - .into_jiff() - .checked_add(Span::new().months(3 * coarser)) - .unwrap(); - Day1::try_from(Date::from(d)).unwrap().into() - } + let d = Date::new(2009, 1, 1) + .into_jiff() + .checked_add(Span::new().months(3 * u8::from(coarser))) + .unwrap(); + Day1::try_from(Date::from(d)).unwrap().into() } fn max_from_(coarser: Month3) -> usize { @@ -171,16 +145,11 @@ impl FromCoarserIndex for Day1 { impl FromCoarserIndex for Day1 { fn min_from(coarser: Month6) -> usize { - let coarser = u8::from(coarser); - if coarser == 0 { - 0 - } else { - let d = Date::new(2009, 1, 1) - .into_jiff() - .checked_add(Span::new().months(6 * coarser)) - .unwrap(); - Day1::try_from(Date::from(d)).unwrap().into() - } + let d = Date::new(2009, 1, 1) + .into_jiff() + .checked_add(Span::new().months(6 * u8::from(coarser))) + .unwrap(); + Day1::try_from(Date::from(d)).unwrap().into() } fn max_from_(coarser: Month6) -> usize { @@ -194,14 +163,9 @@ impl FromCoarserIndex for Day1 { impl FromCoarserIndex for Day1 { fn min_from(coarser: Year1) -> usize { - let coarser = u8::from(coarser); - if coarser == 0 { - 0 - } else { - Self::try_from(Date::new(2009 + coarser as u16, 1, 1)) - .unwrap() - .into() - } + Self::try_from(Date::new(2009 + u8::from(coarser) as u16, 1, 1)) + .unwrap() + .into() } fn max_from_(coarser: Year1) -> usize { @@ -215,6 +179,7 @@ impl FromCoarserIndex for Day1 { fn min_from(coarser: Year10) -> usize { let coarser = u8::from(coarser); if coarser == 0 { + // Decade 0 starts at 2000, before INDEX_ZERO (2009-01-01) 0 } else { Self::try_from(Date::new(2000 + 10 * coarser as u16, 1, 1)) diff --git a/crates/brk_types/src/day3.rs b/crates/brk_types/src/day3.rs index 06f97d533..4a3259ccc 100644 --- a/crates/brk_types/src/day3.rs +++ b/crates/brk_types/src/day3.rs @@ -26,7 +26,11 @@ pub struct Day3(u16); impl Day3 { pub fn from_timestamp(ts: Timestamp) -> Self { - Self(((*ts - INDEX_EPOCH) / DAY3_INTERVAL) as u16) + Self(((*ts - INDEX_EPOCH + 86400) / DAY3_INTERVAL) as u16) + } + + pub fn to_timestamp(&self) -> Timestamp { + Timestamp::new(INDEX_EPOCH - 86400 + self.0 as u32 * DAY3_INTERVAL) } } diff --git a/crates/brk_types/src/difficultyepoch.rs b/crates/brk_types/src/difficultyepoch.rs index b1f7f74e8..004fc255b 100644 --- a/crates/brk_types/src/difficultyepoch.rs +++ b/crates/brk_types/src/difficultyepoch.rs @@ -96,7 +96,7 @@ impl PrintableIndex for DifficultyEpoch { } fn to_possible_strings() -> &'static [&'static str] { - &["difficulty", "difficultyepoch"] + &["difficulty", "difficultyepoch", "diff"] } } diff --git a/crates/brk_types/src/halvingepoch.rs b/crates/brk_types/src/halvingepoch.rs index ff4b6a3c0..83827dd2d 100644 --- a/crates/brk_types/src/halvingepoch.rs +++ b/crates/brk_types/src/halvingepoch.rs @@ -102,7 +102,7 @@ impl PrintableIndex for HalvingEpoch { } fn to_possible_strings() -> &'static [&'static str] { - &["halving", "halvingepoch"] + &["halving", "halvingepoch", "halv"] } } diff --git a/crates/brk_types/src/height.rs b/crates/brk_types/src/height.rs index 9dc3fe3f9..2f5342ba1 100644 --- a/crates/brk_types/src/height.rs +++ b/crates/brk_types/src/height.rs @@ -272,7 +272,7 @@ impl PrintableIndex for Height { } fn to_possible_strings() -> &'static [&'static str] { - &["h", "height"] + &["h", "height", "blk", "block"] } } diff --git a/crates/brk_types/src/hour1.rs b/crates/brk_types/src/hour1.rs index 9fe540d9f..6d2711bee 100644 --- a/crates/brk_types/src/hour1.rs +++ b/crates/brk_types/src/hour1.rs @@ -28,6 +28,10 @@ impl Hour1 { pub fn from_timestamp(ts: Timestamp) -> Self { Self((*ts - INDEX_EPOCH) / HOUR1_INTERVAL) } + + pub fn to_timestamp(&self) -> Timestamp { + Timestamp::new(INDEX_EPOCH + self.0 * HOUR1_INTERVAL) + } } impl From for usize { diff --git a/crates/brk_types/src/hour12.rs b/crates/brk_types/src/hour12.rs index 0b64a304d..7a86e20bc 100644 --- a/crates/brk_types/src/hour12.rs +++ b/crates/brk_types/src/hour12.rs @@ -28,6 +28,10 @@ impl Hour12 { pub fn from_timestamp(ts: Timestamp) -> Self { Self((*ts - INDEX_EPOCH) / HOUR12_INTERVAL) } + + pub fn to_timestamp(&self) -> Timestamp { + Timestamp::new(INDEX_EPOCH + self.0 * HOUR12_INTERVAL) + } } impl From for usize { diff --git a/crates/brk_types/src/hour4.rs b/crates/brk_types/src/hour4.rs index c9e50b7d9..13609e397 100644 --- a/crates/brk_types/src/hour4.rs +++ b/crates/brk_types/src/hour4.rs @@ -28,6 +28,10 @@ impl Hour4 { pub fn from_timestamp(ts: Timestamp) -> Self { Self((*ts - INDEX_EPOCH) / HOUR4_INTERVAL) } + + pub fn to_timestamp(&self) -> Timestamp { + Timestamp::new(INDEX_EPOCH + self.0 * HOUR4_INTERVAL) + } } impl From for usize { diff --git a/crates/brk_types/src/index.rs b/crates/brk_types/src/index.rs index 642e6df97..dcd3d4fa1 100644 --- a/crates/brk_types/src/index.rs +++ b/crates/brk_types/src/index.rs @@ -238,7 +238,7 @@ impl Index { Self::Hour1 => HOUR1_INTERVAL, Self::Hour4 => HOUR4_INTERVAL, Self::Hour12 => HOUR12_INTERVAL, - Self::Day3 => DAY3_INTERVAL, + Self::Day3 => return Some(Day3::from(i).to_timestamp()), _ => return self.index_to_date(i).map(|d| d.into()), }; Some(Timestamp::new(INDEX_EPOCH + i as u32 * interval)) @@ -276,7 +276,7 @@ impl Index { Self::Hour1 => HOUR1_INTERVAL, Self::Hour4 => HOUR4_INTERVAL, Self::Hour12 => HOUR12_INTERVAL, - Self::Day3 => DAY3_INTERVAL, + Self::Day3 => return Some(usize::from(Day3::from_timestamp(ts))), _ => return self.date_to_index(Date::from(ts)), }; Some(((*ts - INDEX_EPOCH) / interval) as usize) @@ -442,13 +442,13 @@ mod tests { } #[test] - fn test_date_to_index_day1_genesis() { + fn test_date_to_index_day1_zero() { assert_eq!(Index::Day1.date_to_index(Date::INDEX_ZERO), Some(0)); } #[test] - fn test_date_to_index_day1_one() { - assert_eq!(Index::Day1.date_to_index(Date::INDEX_ONE), Some(1)); + fn test_date_to_index_day1_genesis() { + assert_eq!(Index::Day1.date_to_index(Date::new(2009, 1, 3)), Some(2)); } #[test] @@ -494,11 +494,11 @@ mod tests { } #[test] - fn test_date_to_index_pre_genesis_returns_none() { - let pre_genesis = Date::new(2009, 1, 2); - assert!(Index::Day1.date_to_index(pre_genesis).is_none()); - assert!(Index::Week1.date_to_index(pre_genesis).is_none()); - assert!(Index::Month1.date_to_index(pre_genesis).is_none()); + fn test_date_to_index_pre_epoch_returns_none() { + let pre_epoch = Date::new(2008, 12, 31); + assert!(Index::Day1.date_to_index(pre_epoch).is_none()); + assert!(Index::Week1.date_to_index(pre_epoch).is_none()); + assert!(Index::Month1.date_to_index(pre_epoch).is_none()); } #[test] diff --git a/crates/brk_types/src/metricdata.rs b/crates/brk_types/src/metricdata.rs index 6b3057cda..5a9ca5d53 100644 --- a/crates/brk_types/src/metricdata.rs +++ b/crates/brk_types/src/metricdata.rs @@ -237,14 +237,14 @@ mod tests { let metric = date_based_metric(); let dates: Vec<_> = metric.dates().unwrap().collect(); assert_eq!(dates.len(), 5); - // Day1 0 = Jan 3, 2009 (genesis) + // Day1 0 = Jan 1, 2009 assert_eq!(dates[0].year(), 2009); assert_eq!(dates[0].month(), 1); - assert_eq!(dates[0].day(), 3); - // Day1 1 = Jan 9, 2009 (day one) + assert_eq!(dates[0].day(), 1); + // Day1 1 = Jan 2, 2009 assert_eq!(dates[1].year(), 2009); assert_eq!(dates[1].month(), 1); - assert_eq!(dates[1].day(), 9); + assert_eq!(dates[1].day(), 2); } #[test] @@ -271,13 +271,13 @@ mod tests { let metric = date_based_metric(); let pairs: Vec<_> = metric.iter_dates().unwrap().collect(); assert_eq!(pairs.len(), 5); - // First pair: (Jan 3 2009, 100) + // First pair: (Jan 1 2009, 100) assert_eq!(pairs[0].0.year(), 2009); assert_eq!(pairs[0].0.month(), 1); - assert_eq!(pairs[0].0.day(), 3); + assert_eq!(pairs[0].0.day(), 1); assert_eq!(pairs[0].1, &100); - // Second pair: (Jan 9 2009, 200) - assert_eq!(pairs[1].0.day(), 9); + // Second pair: (Jan 2 2009, 200) + assert_eq!(pairs[1].0.day(), 2); assert_eq!(pairs[1].1, &200); } @@ -315,7 +315,7 @@ mod tests { let date_metric = DateMetricData::try_new(metric).unwrap(); let pairs: Vec<_> = date_metric.iter_dates().unwrap().collect(); assert_eq!(pairs.len(), 5); - assert_eq!(pairs[0].0.day(), 3); + assert_eq!(pairs[0].0.day(), 1); assert_eq!(pairs[0].1, &100); } @@ -545,8 +545,8 @@ mod tests { #[test] fn test_timestamp_to_index_day1_via_date_fallback() { // Day1 goes through date_to_index fallback - // 2009-01-09 = Day1 index 1 + // 2009-01-09 = Day1 index 8 let ts = Timestamp::from(Date::new(2009, 1, 9)); - assert_eq!(Index::Day1.timestamp_to_index(ts), Some(1)); + assert_eq!(Index::Day1.timestamp_to_index(ts), Some(8)); } } diff --git a/crates/brk_types/src/minute1.rs b/crates/brk_types/src/minute1.rs index 87f439b19..8a76125e3 100644 --- a/crates/brk_types/src/minute1.rs +++ b/crates/brk_types/src/minute1.rs @@ -28,6 +28,10 @@ impl Minute1 { pub fn from_timestamp(ts: Timestamp) -> Self { Self((*ts - INDEX_EPOCH) / MINUTE1_INTERVAL) } + + pub fn to_timestamp(&self) -> Timestamp { + Timestamp::new(INDEX_EPOCH + self.0 * MINUTE1_INTERVAL) + } } impl From for usize { diff --git a/crates/brk_types/src/minute10.rs b/crates/brk_types/src/minute10.rs index c71140286..e2c6f16ee 100644 --- a/crates/brk_types/src/minute10.rs +++ b/crates/brk_types/src/minute10.rs @@ -28,6 +28,10 @@ impl Minute10 { pub fn from_timestamp(ts: Timestamp) -> Self { Self((*ts - INDEX_EPOCH) / MINUTE10_INTERVAL) } + + pub fn to_timestamp(&self) -> Timestamp { + Timestamp::new(INDEX_EPOCH + self.0 * MINUTE10_INTERVAL) + } } impl From for usize { diff --git a/crates/brk_types/src/minute30.rs b/crates/brk_types/src/minute30.rs index 8a293d717..d45faba9d 100644 --- a/crates/brk_types/src/minute30.rs +++ b/crates/brk_types/src/minute30.rs @@ -28,6 +28,10 @@ impl Minute30 { pub fn from_timestamp(ts: Timestamp) -> Self { Self((*ts - INDEX_EPOCH) / MINUTE30_INTERVAL) } + + pub fn to_timestamp(&self) -> Timestamp { + Timestamp::new(INDEX_EPOCH + self.0 * MINUTE30_INTERVAL) + } } impl From for usize { diff --git a/crates/brk_types/src/minute5.rs b/crates/brk_types/src/minute5.rs index 5424eef62..7df6672c8 100644 --- a/crates/brk_types/src/minute5.rs +++ b/crates/brk_types/src/minute5.rs @@ -28,6 +28,10 @@ impl Minute5 { pub fn from_timestamp(ts: Timestamp) -> Self { Self((*ts - INDEX_EPOCH) / MINUTE5_INTERVAL) } + + pub fn to_timestamp(&self) -> Timestamp { + Timestamp::new(INDEX_EPOCH + self.0 * MINUTE5_INTERVAL) + } } impl From for usize { diff --git a/crates/brk_types/src/month1.rs b/crates/brk_types/src/month1.rs index 424258ddf..35c4e98ac 100644 --- a/crates/brk_types/src/month1.rs +++ b/crates/brk_types/src/month1.rs @@ -7,7 +7,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use vecdb::{CheckedSub, Formattable, Pco, PrintableIndex}; -use super::{Date, Day1, Year1}; +use super::{Date, Day1, Timestamp, Year1}; #[derive( Debug, @@ -25,6 +25,12 @@ use super::{Date, Day1, Year1}; )] pub struct Month1(u16); +impl Month1 { + pub fn to_timestamp(&self) -> Timestamp { + Timestamp::from(Date::from(*self)) + } +} + impl From for Month1 { #[inline] fn from(value: u16) -> Self { @@ -115,7 +121,7 @@ impl PrintableIndex for Month1 { } fn to_possible_strings() -> &'static [&'static str] { - &["month", "m", "monthly", "month1", "monthindex"] + &["month", "m", "monthly", "month1", "monthindex", "1m", "1mo"] } } diff --git a/crates/brk_types/src/month3.rs b/crates/brk_types/src/month3.rs index 4c81569b7..826ad0059 100644 --- a/crates/brk_types/src/month3.rs +++ b/crates/brk_types/src/month3.rs @@ -7,7 +7,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use vecdb::{CheckedSub, Formattable, Pco, PrintableIndex}; -use super::Month1; +use super::{Date, Month1, Timestamp}; #[derive( Debug, @@ -25,6 +25,12 @@ use super::Month1; )] pub struct Month3(u8); +impl Month3 { + pub fn to_timestamp(&self) -> Timestamp { + Timestamp::from(Date::from(*self)) + } +} + impl From for Month3 { #[inline] fn from(value: u8) -> Self { @@ -101,7 +107,7 @@ impl PrintableIndex for Month3 { } fn to_possible_strings() -> &'static [&'static str] { - &["quarter", "q", "quarterly", "month3", "quarterindex"] + &["quarter", "q", "quarterly", "month3", "quarterindex", "3m", "3mo"] } } diff --git a/crates/brk_types/src/month6.rs b/crates/brk_types/src/month6.rs index 01ec904a2..2ab1169d9 100644 --- a/crates/brk_types/src/month6.rs +++ b/crates/brk_types/src/month6.rs @@ -7,7 +7,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use vecdb::{CheckedSub, Formattable, Pco, PrintableIndex}; -use super::Month1; +use super::{Date, Month1, Timestamp}; #[derive( Debug, @@ -25,6 +25,12 @@ use super::Month1; )] pub struct Month6(u8); +impl Month6 { + pub fn to_timestamp(&self) -> Timestamp { + Timestamp::from(Date::from(*self)) + } +} + impl From for Month6 { #[inline] fn from(value: u8) -> Self { @@ -101,7 +107,7 @@ impl PrintableIndex for Month6 { } fn to_possible_strings() -> &'static [&'static str] { - &["semester", "s", "month6", "semesterindex"] + &["semester", "s", "month6", "semesterindex", "6m", "6mo"] } } diff --git a/crates/brk_types/src/week1.rs b/crates/brk_types/src/week1.rs index 2bc4c8f84..7a3716b0d 100644 --- a/crates/brk_types/src/week1.rs +++ b/crates/brk_types/src/week1.rs @@ -7,7 +7,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use vecdb::{CheckedSub, Formattable, Pco, PrintableIndex}; -use super::{Date, Day1}; +use super::{Date, Day1, Timestamp}; #[derive( Debug, @@ -25,6 +25,12 @@ use super::{Date, Day1}; )] pub struct Week1(u16); +impl Week1 { + pub fn to_timestamp(&self) -> Timestamp { + Timestamp::from(Date::from(*self)) + } +} + impl From for Week1 { #[inline] fn from(value: u16) -> Self { diff --git a/crates/brk_types/src/year1.rs b/crates/brk_types/src/year1.rs index 51879aad1..873573d8c 100644 --- a/crates/brk_types/src/year1.rs +++ b/crates/brk_types/src/year1.rs @@ -7,7 +7,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use vecdb::{CheckedSub, Formattable, Pco, PrintableIndex}; -use super::{Date, Day1, Month1}; +use super::{Date, Day1, Month1, Timestamp}; #[derive( Debug, @@ -25,6 +25,12 @@ use super::{Date, Day1, Month1}; )] pub struct Year1(u8); +impl Year1 { + pub fn to_timestamp(&self) -> Timestamp { + Timestamp::from(Date::from(*self)) + } +} + impl From for Year1 { #[inline] fn from(value: u8) -> Self { diff --git a/crates/brk_types/src/year10.rs b/crates/brk_types/src/year10.rs index 184d25d71..27749a469 100644 --- a/crates/brk_types/src/year10.rs +++ b/crates/brk_types/src/year10.rs @@ -7,7 +7,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use vecdb::{CheckedSub, Formattable, Pco, PrintableIndex}; -use super::{Date, Day1, Year1}; +use super::{Date, Day1, Timestamp, Year1}; #[derive( Debug, @@ -25,6 +25,12 @@ use super::{Date, Day1, Year1}; )] pub struct Year10(u8); +impl Year10 { + pub fn to_timestamp(&self) -> Timestamp { + Timestamp::from(Date::from(*self)) + } +} + impl From for Year10 { #[inline] fn from(value: u8) -> Self { diff --git a/modules/brk-client/index.js b/modules/brk-client/index.js index 4ea8607af..a814808bc 100644 --- a/modules/brk-client/index.js +++ b/modules/brk-client/index.js @@ -951,7 +951,7 @@ function indexToDate(index, i) { case 'hour4': return new Date(_EPOCH_MS + i * 14400000); case 'hour12': return new Date(_EPOCH_MS + i * 43200000); case 'day1': return i === 0 ? _GENESIS : new Date(_DAY_ONE.getTime() + (i - 1) * _MS_PER_DAY); - case 'day3': return new Date(_EPOCH_MS + i * 259200000); + case 'day3': return new Date(_EPOCH_MS - 86400000 + i * 259200000); case 'week1': return new Date(_GENESIS.getTime() + i * _MS_PER_WEEK); case 'month1': return _addMonths(i); case 'month3': return _addMonths(i * 3); @@ -983,7 +983,7 @@ function dateToIndex(index, d) { if (ms < _DAY_ONE.getTime()) return 0; return 1 + Math.floor((ms - _DAY_ONE.getTime()) / _MS_PER_DAY); } - case 'day3': return Math.floor((ms - _EPOCH_MS) / 259200000); + case 'day3': return Math.floor((ms - _EPOCH_MS + 86400000) / 259200000); case 'week1': return Math.floor((ms - _GENESIS.getTime()) / _MS_PER_WEEK); case 'month1': return (d.getFullYear() - 2009) * 12 + d.getMonth(); case 'month3': return (d.getFullYear() - 2009) * 4 + Math.floor(d.getMonth() / 3); @@ -2710,57 +2710,6 @@ function createGreedInvestedInvestorNegNetPainPeakSupplyTotalUnrealizedPattern(c }; } -/** - * @template T - * @typedef {Object} Day1Day3DifficultyepochHalvingepochHour1Hour12Hour4Minute1Minute10Minute30Minute5Month1Month3Month6Week1Year1Year10Pattern - * @property {MetricPattern10} day1 - * @property {MetricPattern11} day3 - * @property {MetricPattern19} difficultyepoch - * @property {MetricPattern18} halvingepoch - * @property {MetricPattern7} hour1 - * @property {MetricPattern9} hour12 - * @property {MetricPattern8} hour4 - * @property {MetricPattern3} minute1 - * @property {MetricPattern5} minute10 - * @property {MetricPattern6} minute30 - * @property {MetricPattern4} minute5 - * @property {MetricPattern13} month1 - * @property {MetricPattern14} month3 - * @property {MetricPattern15} month6 - * @property {MetricPattern12} week1 - * @property {MetricPattern16} year1 - * @property {MetricPattern17} year10 - */ - -/** - * Create a Day1Day3DifficultyepochHalvingepochHour1Hour12Hour4Minute1Minute10Minute30Minute5Month1Month3Month6Week1Year1Year10Pattern pattern node - * @template T - * @param {BrkClientBase} client - * @param {string} acc - Accumulated metric name - * @returns {Day1Day3DifficultyepochHalvingepochHour1Hour12Hour4Minute1Minute10Minute30Minute5Month1Month3Month6Week1Year1Year10Pattern} - */ -function createDay1Day3DifficultyepochHalvingepochHour1Hour12Hour4Minute1Minute10Minute30Minute5Month1Month3Month6Week1Year1Year10Pattern(client, acc) { - return { - day1: createMetricPattern10(client, _m(acc, 'day1')), - day3: createMetricPattern11(client, _m(acc, 'day3')), - difficultyepoch: createMetricPattern19(client, _m(acc, 'difficultyepoch')), - halvingepoch: createMetricPattern18(client, _m(acc, 'halvingepoch')), - hour1: createMetricPattern7(client, _m(acc, 'hour1')), - hour12: createMetricPattern9(client, _m(acc, 'hour12')), - hour4: createMetricPattern8(client, _m(acc, 'hour4')), - minute1: createMetricPattern3(client, _m(acc, 'minute1')), - minute10: createMetricPattern5(client, _m(acc, 'minute10')), - minute30: createMetricPattern6(client, _m(acc, 'minute30')), - minute5: createMetricPattern4(client, _m(acc, 'minute5')), - month1: createMetricPattern13(client, _m(acc, 'month1')), - month3: createMetricPattern14(client, _m(acc, 'month3')), - month6: createMetricPattern15(client, _m(acc, 'month6')), - week1: createMetricPattern12(client, _m(acc, 'week1')), - year1: createMetricPattern16(client, _m(acc, 'year1')), - year10: createMetricPattern17(client, _m(acc, 'year10')), - }; -} - /** * @typedef {Object} GreedInvestedInvestorNegNetPainSupplyTotalUnrealizedPattern * @property {MetricPattern1} greedIndex @@ -3818,9 +3767,9 @@ function createBtcSatsUsdPattern(client, acc) { /** * @typedef {Object} CentsSatsUsdPattern - * @property {Day1Day3DifficultyepochHalvingepochHour1Hour12Hour4Minute1Minute10Minute30Minute5Month1Month3Month6Week1Year1Year10Pattern} cents - * @property {Day1Day3DifficultyepochHalvingepochHour1Hour12Hour4Minute1Minute10Minute30Minute5Month1Month3Month6Week1Year1Year10Pattern} sats - * @property {Day1Day3DifficultyepochHalvingepochHour1Hour12Hour4Minute1Minute10Minute30Minute5Month1Month3Month6Week1Year1Year10Pattern} usd + * @property {MetricPattern2} cents + * @property {MetricPattern2} sats + * @property {MetricPattern2} usd */ /** @@ -3831,9 +3780,9 @@ function createBtcSatsUsdPattern(client, acc) { */ function createCentsSatsUsdPattern(client, acc) { return { - cents: createDay1Day3DifficultyepochHalvingepochHour1Hour12Hour4Minute1Minute10Minute30Minute5Month1Month3Month6Week1Year1Year10Pattern(client, _m(acc, 'cents')), - sats: createDay1Day3DifficultyepochHalvingepochHour1Hour12Hour4Minute1Minute10Minute30Minute5Month1Month3Month6Week1Year1Year10Pattern(client, _m(acc, 'sats')), - usd: createDay1Day3DifficultyepochHalvingepochHour1Hour12Hour4Minute1Minute10Minute30Minute5Month1Month3Month6Week1Year1Year10Pattern(client, acc), + cents: createMetricPattern2(client, _m(acc, 'cents')), + sats: createMetricPattern2(client, _m(acc, 'sats')), + usd: createMetricPattern2(client, acc), }; } @@ -4099,33 +4048,11 @@ function createRatioPattern2(client, acc) { /** * @typedef {Object} MetricsTree_Blocks_Time - * @property {MetricsTree_Blocks_Time_Timestamp} timestamp + * @property {MetricPattern1} timestamp * @property {MetricPattern20} date * @property {MetricPattern20} timestampMonotonic */ -/** - * @typedef {Object} MetricsTree_Blocks_Time_Timestamp - * @property {MetricPattern20} base - * @property {MetricPattern3} minute1 - * @property {MetricPattern4} minute5 - * @property {MetricPattern5} minute10 - * @property {MetricPattern6} minute30 - * @property {MetricPattern7} hour1 - * @property {MetricPattern8} hour4 - * @property {MetricPattern9} hour12 - * @property {MetricPattern10} day1 - * @property {MetricPattern11} day3 - * @property {MetricPattern12} week1 - * @property {MetricPattern13} month1 - * @property {MetricPattern14} month3 - * @property {MetricPattern15} month6 - * @property {MetricPattern16} year1 - * @property {MetricPattern17} year10 - * @property {MetricPattern18} halvingepoch - * @property {MetricPattern19} difficultyepoch - */ - /** * @typedef {Object} MetricsTree_Blocks_Weight * @property {MetricPattern20} base @@ -5280,7 +5207,7 @@ function createRatioPattern2(client, acc) { /** * @typedef {Object} MetricsTree_Prices * @property {MetricsTree_Prices_Split} split - * @property {CentsSatsUsdPattern} ohlc + * @property {MetricsTree_Prices_Ohlc} ohlc * @property {MetricsTree_Prices_Price} price */ @@ -5299,6 +5226,13 @@ function createRatioPattern2(client, acc) { * @property {MetricPattern2} sats */ +/** + * @typedef {Object} MetricsTree_Prices_Ohlc + * @property {MetricPattern2} cents + * @property {MetricPattern2} usd + * @property {MetricPattern2} sats + */ + /** * @typedef {Object} MetricsTree_Prices_Price * @property {MetricPattern20} cents @@ -6660,26 +6594,7 @@ class BrkClient extends BrkClientBase { daysBeforeNextAdjustment: createMetricPattern1(this, 'days_before_next_difficulty_adjustment'), }, time: { - timestamp: { - base: createMetricPattern20(this, 'timestamp'), - minute1: createMetricPattern3(this, 'timestamp_minute1'), - minute5: createMetricPattern4(this, 'timestamp_minute5'), - minute10: createMetricPattern5(this, 'timestamp_minute10'), - minute30: createMetricPattern6(this, 'timestamp_minute30'), - hour1: createMetricPattern7(this, 'timestamp_hour1'), - hour4: createMetricPattern8(this, 'timestamp_hour4'), - hour12: createMetricPattern9(this, 'timestamp_hour12'), - day1: createMetricPattern10(this, 'timestamp_day1'), - day3: createMetricPattern11(this, 'timestamp_day3'), - week1: createMetricPattern12(this, 'timestamp_week1'), - month1: createMetricPattern13(this, 'timestamp_month1'), - month3: createMetricPattern14(this, 'timestamp_month3'), - month6: createMetricPattern15(this, 'timestamp_month6'), - year1: createMetricPattern16(this, 'timestamp_year1'), - year10: createMetricPattern17(this, 'timestamp_year10'), - halvingepoch: createMetricPattern18(this, 'timestamp_halvingepoch'), - difficultyepoch: createMetricPattern19(this, 'timestamp_difficultyepoch'), - }, + timestamp: createMetricPattern1(this, 'timestamp'), date: createMetricPattern20(this, 'date'), timestampMonotonic: createMetricPattern20(this, 'timestamp_monotonic'), }, @@ -7584,7 +7499,11 @@ class BrkClient extends BrkClientBase { sats: createMetricPattern2(this, 'price_close_sats'), }, }, - ohlc: createCentsSatsUsdPattern(this, 'price_ohlc'), + ohlc: { + cents: createMetricPattern2(this, 'price_ohlc_cents'), + usd: createMetricPattern2(this, 'price_ohlc'), + sats: createMetricPattern2(this, 'price_ohlc_sats'), + }, price: { cents: createMetricPattern20(this, 'price_cents'), usd: createMetricPattern20(this, 'price'), diff --git a/packages/brk_client/brk_client/__init__.py b/packages/brk_client/brk_client/__init__.py index b147a1d93..90bc79e7d 100644 --- a/packages/brk_client/brk_client/__init__.py +++ b/packages/brk_client/brk_client/__init__.py @@ -1140,7 +1140,7 @@ def _index_to_date(index: str, i: int) -> Union[date, datetime]: elif index == 'day1': return _GENESIS if i == 0 else _DAY_ONE + timedelta(days=i - 1) elif index == 'day3': - return _EPOCH.date() + timedelta(days=i * 3) + return _EPOCH.date() - timedelta(days=1) + timedelta(days=i * 3) elif index == 'week1': return _GENESIS + timedelta(weeks=i) elif index == 'month1': @@ -1180,7 +1180,7 @@ def _date_to_index(index: str, d: Union[date, datetime]) -> int: return 0 return 1 + (dd - _DAY_ONE).days elif index == 'day3': - return (dd - date(2009, 1, 1)).days // 3 + return (dd - date(2008, 12, 31)).days // 3 elif index == 'week1': return (dd - _GENESIS).days // 7 elif index == 'month1': @@ -2645,29 +2645,6 @@ class GreedInvestedInvestorNegNetPainPeakSupplyTotalUnrealizedPattern: self.unrealized_loss: MetricPattern1[Dollars] = MetricPattern1(client, _m(acc, 'unrealized_loss')) self.unrealized_profit: MetricPattern1[Dollars] = MetricPattern1(client, _m(acc, 'unrealized_profit')) -class Day1Day3DifficultyepochHalvingepochHour1Hour12Hour4Minute1Minute10Minute30Minute5Month1Month3Month6Week1Year1Year10Pattern(Generic[T]): - """Pattern struct for repeated tree structure.""" - - def __init__(self, client: BrkClientBase, acc: str): - """Create pattern node with accumulated metric name.""" - self.day1: MetricPattern10[T] = MetricPattern10(client, _m(acc, 'day1')) - self.day3: MetricPattern11[T] = MetricPattern11(client, _m(acc, 'day3')) - self.difficultyepoch: MetricPattern19[T] = MetricPattern19(client, _m(acc, 'difficultyepoch')) - self.halvingepoch: MetricPattern18[T] = MetricPattern18(client, _m(acc, 'halvingepoch')) - self.hour1: MetricPattern7[T] = MetricPattern7(client, _m(acc, 'hour1')) - self.hour12: MetricPattern9[T] = MetricPattern9(client, _m(acc, 'hour12')) - self.hour4: MetricPattern8[T] = MetricPattern8(client, _m(acc, 'hour4')) - self.minute1: MetricPattern3[T] = MetricPattern3(client, _m(acc, 'minute1')) - self.minute10: MetricPattern5[T] = MetricPattern5(client, _m(acc, 'minute10')) - self.minute30: MetricPattern6[T] = MetricPattern6(client, _m(acc, 'minute30')) - self.minute5: MetricPattern4[T] = MetricPattern4(client, _m(acc, 'minute5')) - self.month1: MetricPattern13[T] = MetricPattern13(client, _m(acc, 'month1')) - self.month3: MetricPattern14[T] = MetricPattern14(client, _m(acc, 'month3')) - self.month6: MetricPattern15[T] = MetricPattern15(client, _m(acc, 'month6')) - self.week1: MetricPattern12[T] = MetricPattern12(client, _m(acc, 'week1')) - self.year1: MetricPattern16[T] = MetricPattern16(client, _m(acc, 'year1')) - self.year10: MetricPattern17[T] = MetricPattern17(client, _m(acc, 'year10')) - class GreedInvestedInvestorNegNetPainSupplyTotalUnrealizedPattern: """Pattern struct for repeated tree structure.""" @@ -3145,9 +3122,9 @@ class CentsSatsUsdPattern: def __init__(self, client: BrkClientBase, acc: str): """Create pattern node with accumulated metric name.""" - self.cents: Day1Day3DifficultyepochHalvingepochHour1Hour12Hour4Minute1Minute10Minute30Minute5Month1Month3Month6Week1Year1Year10Pattern[OHLCCents] = Day1Day3DifficultyepochHalvingepochHour1Hour12Hour4Minute1Minute10Minute30Minute5Month1Month3Month6Week1Year1Year10Pattern(client, _m(acc, 'cents')) - self.sats: Day1Day3DifficultyepochHalvingepochHour1Hour12Hour4Minute1Minute10Minute30Minute5Month1Month3Month6Week1Year1Year10Pattern[OHLCSats] = Day1Day3DifficultyepochHalvingepochHour1Hour12Hour4Minute1Minute10Minute30Minute5Month1Month3Month6Week1Year1Year10Pattern(client, _m(acc, 'sats')) - self.usd: Day1Day3DifficultyepochHalvingepochHour1Hour12Hour4Minute1Minute10Minute30Minute5Month1Month3Month6Week1Year1Year10Pattern[OHLCDollars] = Day1Day3DifficultyepochHalvingepochHour1Hour12Hour4Minute1Minute10Minute30Minute5Month1Month3Month6Week1Year1Year10Pattern(client, acc) + self.cents: MetricPattern2[Cents] = MetricPattern2(client, _m(acc, 'cents')) + self.sats: MetricPattern2[Sats] = MetricPattern2(client, _m(acc, 'sats')) + self.usd: MetricPattern2[Dollars] = MetricPattern2(client, acc) class HistogramLineSignalPattern: """Pattern struct for repeated tree structure.""" @@ -3251,34 +3228,11 @@ class MetricsTree_Blocks_Difficulty: self.blocks_before_next_adjustment: MetricPattern1[StoredU32] = MetricPattern1(client, 'blocks_before_next_difficulty_adjustment') self.days_before_next_adjustment: MetricPattern1[StoredF32] = MetricPattern1(client, 'days_before_next_difficulty_adjustment') -class MetricsTree_Blocks_Time_Timestamp: - """Metrics tree node.""" - - def __init__(self, client: BrkClientBase, base_path: str = ''): - self.base: MetricPattern20[Timestamp] = MetricPattern20(client, 'timestamp') - self.minute1: MetricPattern3[Timestamp] = MetricPattern3(client, 'timestamp_minute1') - self.minute5: MetricPattern4[Timestamp] = MetricPattern4(client, 'timestamp_minute5') - self.minute10: MetricPattern5[Timestamp] = MetricPattern5(client, 'timestamp_minute10') - self.minute30: MetricPattern6[Timestamp] = MetricPattern6(client, 'timestamp_minute30') - self.hour1: MetricPattern7[Timestamp] = MetricPattern7(client, 'timestamp_hour1') - self.hour4: MetricPattern8[Timestamp] = MetricPattern8(client, 'timestamp_hour4') - self.hour12: MetricPattern9[Timestamp] = MetricPattern9(client, 'timestamp_hour12') - self.day1: MetricPattern10[Timestamp] = MetricPattern10(client, 'timestamp_day1') - self.day3: MetricPattern11[Timestamp] = MetricPattern11(client, 'timestamp_day3') - self.week1: MetricPattern12[Timestamp] = MetricPattern12(client, 'timestamp_week1') - self.month1: MetricPattern13[Timestamp] = MetricPattern13(client, 'timestamp_month1') - self.month3: MetricPattern14[Timestamp] = MetricPattern14(client, 'timestamp_month3') - self.month6: MetricPattern15[Timestamp] = MetricPattern15(client, 'timestamp_month6') - self.year1: MetricPattern16[Timestamp] = MetricPattern16(client, 'timestamp_year1') - self.year10: MetricPattern17[Timestamp] = MetricPattern17(client, 'timestamp_year10') - self.halvingepoch: MetricPattern18[Timestamp] = MetricPattern18(client, 'timestamp_halvingepoch') - self.difficultyepoch: MetricPattern19[Timestamp] = MetricPattern19(client, 'timestamp_difficultyepoch') - class MetricsTree_Blocks_Time: """Metrics tree node.""" def __init__(self, client: BrkClientBase, base_path: str = ''): - self.timestamp: MetricsTree_Blocks_Time_Timestamp = MetricsTree_Blocks_Time_Timestamp(client) + self.timestamp: MetricPattern1[Timestamp] = MetricPattern1(client, 'timestamp') self.date: MetricPattern20[Date] = MetricPattern20(client, 'date') self.timestamp_monotonic: MetricPattern20[Timestamp] = MetricPattern20(client, 'timestamp_monotonic') @@ -4560,6 +4514,14 @@ class MetricsTree_Prices_Split: self.low: CentsSatsUsdPattern = CentsSatsUsdPattern(client, 'price_low') self.close: MetricsTree_Prices_Split_Close = MetricsTree_Prices_Split_Close(client) +class MetricsTree_Prices_Ohlc: + """Metrics tree node.""" + + def __init__(self, client: BrkClientBase, base_path: str = ''): + self.cents: MetricPattern2[OHLCCents] = MetricPattern2(client, 'price_ohlc_cents') + self.usd: MetricPattern2[OHLCDollars] = MetricPattern2(client, 'price_ohlc') + self.sats: MetricPattern2[OHLCSats] = MetricPattern2(client, 'price_ohlc_sats') + class MetricsTree_Prices_Price: """Metrics tree node.""" @@ -4573,7 +4535,7 @@ class MetricsTree_Prices: def __init__(self, client: BrkClientBase, base_path: str = ''): self.split: MetricsTree_Prices_Split = MetricsTree_Prices_Split(client) - self.ohlc: CentsSatsUsdPattern = CentsSatsUsdPattern(client, 'price_ohlc') + self.ohlc: MetricsTree_Prices_Ohlc = MetricsTree_Prices_Ohlc(client) self.price: MetricsTree_Prices_Price = MetricsTree_Prices_Price(client) class MetricsTree_Distribution_AnyAddressIndexes: diff --git a/packages/brk_client/tests/test_metric_data.py b/packages/brk_client/tests/test_metric_data.py index c16485e81..7b0b09c12 100644 --- a/packages/brk_client/tests/test_metric_data.py +++ b/packages/brk_client/tests/test_metric_data.py @@ -291,9 +291,9 @@ class TestIndexToDate: def test_day3(self, day3_metric): dates = day3_metric.dates() - assert dates[0] == date(2009, 1, 1) # epoch - assert dates[1] == date(2009, 1, 4) # +3 days - assert dates[2] == date(2009, 1, 7) # +6 days + assert dates[0] == date(2008, 12, 31) # epoch - 1 day (TradingView-aligned) + assert dates[1] == date(2009, 1, 3) # +3 days + assert dates[2] == date(2009, 1, 6) # +6 days def test_hour1_returns_datetime(self, hour1_metric): """Sub-daily indexes return datetime, not date.""" diff --git a/website/scripts/chart/index.js b/website/scripts/chart/index.js index e55ffee88..c62fd6e69 100644 --- a/website/scripts/chart/index.js +++ b/website/scripts/chart/index.js @@ -13,7 +13,7 @@ import { createRadios, createSelect } from "../utils/dom.js"; import { createPersistedValue } from "../utils/persisted.js"; import { onChange as onThemeChange } from "../utils/theme.js"; import { throttle, debounce } from "../utils/timing.js"; -import { serdeBool, serdeChartableIndex } from "../utils/serde.js"; +import { serdeBool, INDEX_FROM_LABEL } from "../utils/serde.js"; import { stringToId, numberToShortUSFormat } from "../utils/format.js"; import { style } from "../utils/elements.js"; import { Unit } from "../utils/units.js"; @@ -87,22 +87,23 @@ export function createChart({ parent, brk, fitContent }) { const getTimeEndpoint = (idx) => idx === "height" ? brk.metrics.blocks.time.timestampMonotonic.by[idx] - : /** @type {any} */ (brk.metrics.blocks.time.timestamp)[idx].by[idx]; + : brk.metrics.blocks.time.timestamp.by[idx]; const index = { /** @type {Set<(index: ChartableIndex) => void>} */ onChange: new Set(), get() { - return serdeChartableIndex.deserialize(index.name.value); + return INDEX_FROM_LABEL[index.name.value]; }, name: createPersistedValue({ - defaultValue: /** @type {ChartableIndexName} */ ("date"), + defaultValue: /** @type {IndexLabel} */ ("1d"), storageKey: "chart-index", urlKey: "i", serialize: (v) => v, - deserialize: (s) => /** @type {ChartableIndexName} */ (s), + deserialize: (s) => + /** @type {IndexLabel} */ (s in INDEX_FROM_LABEL ? s : "1d"), onChange: () => { range.set(null); index.onChange.forEach((cb) => cb(index.get())); @@ -323,7 +324,12 @@ export function createChart({ parent, brk, fitContent }) { ichart.applyOptions({ timeScale: { - timeVisible: index === "height", + timeVisible: + index === "height" || + index === "difficultyepoch" || + index === "halvingepoch" || + index.startsWith("minute") || + index.startsWith("hour"), ...(!fitContent ? { minBarSpacing, @@ -1420,20 +1426,22 @@ export function createChart({ parent, brk, fitContent }) { /** @type {HTMLElement | null} */ let indexField = null; - const lastTd = ichart.chartElement().querySelector("table > tr:last-child > td:nth-child(2)"); + const lastTd = ichart + .chartElement() + .querySelector("table > tr:last-child > td:nth-child(2)"); const chart = { get panes() { return blueprints.panes; }, - /** @param {ChartableIndexName[]} choices */ - setIndexChoices(choices) { + /** @param {{ choices: IndexLabel[], groups: { label: string, items: IndexLabel[] }[] }} arg */ + setIndexChoices({ choices, groups }) { if (indexField) indexField.remove(); let currentValue = choices.includes(preferredIndex) ? preferredIndex - : (choices[0] ?? "date"); + : (choices[0] ?? "1d"); if (currentValue !== index.name.value) { index.name.set(currentValue); @@ -1446,6 +1454,7 @@ export function createChart({ parent, brk, fitContent }) { index.name.set(v); }, choices, + groups, id: "index", }); const sep = document.createElement("span"); diff --git a/website/scripts/options/unused.js b/website/scripts/options/unused.js index 70308a5aa..e8937a275 100644 --- a/website/scripts/options/unused.js +++ b/website/scripts/options/unused.js @@ -1,5 +1,5 @@ import { localhost } from "../utils/env.js"; -import { serdeChartableIndex } from "../utils/serde.js"; +import { INDEX_LABEL } from "../utils/serde.js"; /** * Check if a metric pattern has at least one chartable index @@ -8,7 +8,7 @@ import { serdeChartableIndex } from "../utils/serde.js"; */ function hasChartableIndex(node) { const indexes = node.indexes(); - return indexes.some((idx) => serdeChartableIndex.serialize(idx) !== null); + return indexes.some((idx) => idx in INDEX_LABEL); } /** diff --git a/website/scripts/panes/chart.js b/website/scripts/panes/chart.js index ab8c17456..8ad1fd2e1 100644 --- a/website/scripts/panes/chart.js +++ b/website/scripts/panes/chart.js @@ -1,6 +1,6 @@ import { createHeader } from "../utils/dom.js"; import { chartElement } from "../utils/elements.js"; -import { serdeChartableIndex } from "../utils/serde.js"; +import { INDEX_FROM_LABEL } from "../utils/serde.js"; import { Unit } from "../utils/units.js"; import { createChart } from "../chart/index.js"; import { colors } from "../utils/colors.js"; @@ -45,7 +45,7 @@ export function init() { const usdPrice = { type: "Candlestick", title: "Price", - metric: brk.metrics.prices.ohlc.usd.day1, + metric: brk.metrics.prices.ohlc.usd, }; result.set(Unit.usd, [usdPrice, ...(optionTop.get(Unit.usd) ?? [])]); @@ -54,7 +54,7 @@ export function init() { const satsPrice = { type: "Candlestick", title: "Price", - metric: brk.metrics.prices.ohlc.sats.day1, + metric: brk.metrics.prices.ohlc.sats, colors: /** @type {const} */ ([colors.bi.p1[1], colors.bi.p1[0]]), }; result.set(Unit.sats, [satsPrice, ...(optionTop.get(Unit.sats) ?? [])]); @@ -104,24 +104,32 @@ export function init() { onPrice(updatePriceWithLatest); } -const ALL_CHOICES = /** @satisfies {ChartableIndexName[]} */ ([ - "timestamp", - "date", - "week", - "month", - "month3", - "month6", - "year", - "year10", -]); +/** @type {{ label: string, items: IndexLabel[] }[]} */ +const ALL_GROUPS = [ + { label: "Height", items: ["blk", "halv", "diff"] }, + { + label: "Time", + items: [ + "1mn", "5mn", "10mn", "30mn", + "1h", "4h", "12h", + "1d", "3d", "1w", + "1m", "3m", "6m", + "1y", "10y", + ], + }, +]; + +const ALL_CHOICES = /** @satisfies {IndexLabel[]} */ ( + ALL_GROUPS.flatMap((g) => g.items) +); /** * @param {ChartOption} opt - * @returns {ChartableIndexName[]} + * @returns {{ choices: IndexLabel[], groups: { label: string, items: IndexLabel[] }[] }} */ function computeChoices(opt) { if (!opt.top().size && !opt.bottom().size) { - return [...ALL_CHOICES]; + return { choices: [...ALL_CHOICES], groups: ALL_GROUPS }; } const rawIndexes = new Set( [Array.from(opt.top().values()), Array.from(opt.bottom().values())] @@ -133,8 +141,16 @@ function computeChoices(opt) { .flatMap((blueprint) => blueprint.metric.indexes()), ); - return ALL_CHOICES.filter((choice) => - rawIndexes.has(serdeChartableIndex.deserialize(choice)), - ); + const groups = ALL_GROUPS + .map(({ label, items }) => ({ + label, + items: items.filter((choice) => rawIndexes.has(INDEX_FROM_LABEL[choice])), + })) + .filter(({ items }) => items.length > 0); + + return { + choices: groups.flatMap((g) => g.items), + groups, + }; } diff --git a/website/scripts/types.js b/website/scripts/types.js index a95e645aa..8f9c247d0 100644 --- a/website/scripts/types.js +++ b/website/scripts/types.js @@ -17,7 +17,7 @@ * * @import { UnitObject as Unit } from "./utils/units.js" * - * @import { ChartableIndexName } from "./utils/serde.js"; + * @import { ChartableIndex, IndexLabel } from "./utils/serde.js"; */ // import uFuzzy = require("./modules/leeoniya-ufuzzy/1.0.19/dist/uFuzzy.d.ts"); @@ -215,6 +215,4 @@ * Generic tree node type for walking * @typedef {AnyMetricPattern | Record} TreeNode * - * Chartable index IDs (subset of IndexName that can be charted) - * @typedef {"height" | "day1" | "week1" | "month1" | "month3" | "month6" | "year1" | "year10"} ChartableIndex */ diff --git a/website/scripts/utils/array.js b/website/scripts/utils/array.js index 5ee363201..585c5b282 100644 --- a/website/scripts/utils/array.js +++ b/website/scripts/utils/array.js @@ -6,6 +6,15 @@ */ export const entries = (obj) => /** @type {[keyof T & string, T[keyof T & string]][]} */ (Object.entries(obj)); +/** + * Typed Object.fromEntries that preserves key/value types + * @template {string} K + * @template V + * @param {Iterable} pairs + * @returns {Record} + */ +export const fromEntries = (pairs) => /** @type {Record} */ (Object.fromEntries(pairs)); + /** * Type-safe includes that narrows the value type * @template T diff --git a/website/scripts/utils/dom.js b/website/scripts/utils/dom.js index 5e470b052..5f97ad1a4 100644 --- a/website/scripts/utils/dom.js +++ b/website/scripts/utils/dom.js @@ -238,10 +238,12 @@ export function createRadios({ * @param {(choice: T) => string} [args.toKey] * @param {(choice: T) => string} [args.toLabel] * @param {boolean} [args.sorted] + * @param {{ label: string, items: T[] }[]} [args.groups] */ export function createSelect({ id, choices: unsortedChoices, + groups, initialValue, onChange, sorted, @@ -271,15 +273,27 @@ export function createSelect({ select.name = id ?? ""; field.append(select); - choices.forEach((choice) => { + /** @param {T} choice */ + const createOption = (choice) => { const option = window.document.createElement("option"); option.value = toKey(choice); option.textContent = toLabel(choice); if (toKey(choice) === initialKey) { option.selected = true; } - select.append(option); - }); + return option; + }; + + if (groups) { + groups.forEach(({ label, items }) => { + const optgroup = window.document.createElement("optgroup"); + optgroup.label = label; + items.forEach((choice) => optgroup.append(createOption(choice))); + select.append(optgroup); + }); + } else { + choices.forEach((choice) => select.append(createOption(choice))); + } select.addEventListener("change", () => { onChange?.(fromKey(select.value)); diff --git a/website/scripts/utils/serde.js b/website/scripts/utils/serde.js index ac34c3f21..43962e571 100644 --- a/website/scripts/utils/serde.js +++ b/website/scripts/utils/serde.js @@ -1,3 +1,5 @@ +import { entries, fromEntries } from "./array.js"; + export const serdeBool = { /** * @param {boolean} v @@ -17,64 +19,21 @@ export const serdeBool = { }, }; -/** - * @typedef {"timestamp" | "date" | "week" | "month" | "month3" | "month6" | "year" | "year10"} ChartableIndexName - */ +export const INDEX_LABEL = /** @type {const} */ ({ + height: "blk", + minute1: "1mn", minute5: "5mn", minute10: "10mn", minute30: "30mn", + hour1: "1h", hour4: "4h", hour12: "12h", + day1: "1d", day3: "3d", week1: "1w", + month1: "1m", month3: "3m", month6: "6m", + year1: "1y", year10: "10y", + halvingepoch: "halv", difficultyepoch: "diff", +}); -export const serdeChartableIndex = { - /** - * @param {IndexName} v - * @returns {ChartableIndexName | null} - */ - serialize(v) { - switch (v) { - case "day1": - return "date"; - case "year10": - return "year10"; - case "height": - return "timestamp"; - case "month1": - return "month"; - case "month3": - return "month3"; - case "month6": - return "month6"; - case "week1": - return "week"; - case "year1": - return "year"; - default: - return null; - } - }, - /** - * @param {ChartableIndexName} v - * @returns {ChartableIndex} - */ - deserialize(v) { - switch (v) { - case "timestamp": - return "height"; - case "date": - return "day1"; - case "week": - return "week1"; - case "month": - return "month1"; - case "month3": - return "month3"; - case "month6": - return "month6"; - case "year": - return "year1"; - case "year10": - return "year10"; - default: - throw Error("todo"); - } - }, -}; +/** @typedef {typeof INDEX_LABEL} IndexLabelMap */ +/** @typedef {keyof IndexLabelMap} ChartableIndex */ +/** @typedef {IndexLabelMap[ChartableIndex]} IndexLabel */ + +export const INDEX_FROM_LABEL = fromEntries(entries(INDEX_LABEL).map(([k, v]) => [v, k])); /** * @typedef {"" |