diff --git a/crates/brk_computer/src/cointime/reserve_risk/compute.rs b/crates/brk_computer/src/cointime/reserve_risk/compute.rs index c1520b1f8..e871cb216 100644 --- a/crates/brk_computer/src/cointime/reserve_risk/compute.rs +++ b/crates/brk_computer/src/cointime/reserve_risk/compute.rs @@ -23,7 +23,7 @@ impl Vecs { self.hodl_bank.compute_cumulative_transformed_binary( starting_indexes.height, - &prices.usd.price, + &prices.price.usd, &self.vocdd_365d_median, |price, median| StoredF64::from(f64::from(price) - f64::from(median)), exit, @@ -31,7 +31,7 @@ impl Vecs { self.reserve_risk.height.compute_divide( starting_indexes.height, - &prices.usd.price, + &prices.price.usd, &self.hodl_bank, exit, )?; diff --git a/crates/brk_computer/src/cointime/value/compute.rs b/crates/brk_computer/src/cointime/value/compute.rs index 896c7fd38..dd3326d60 100644 --- a/crates/brk_computer/src/cointime/value/compute.rs +++ b/crates/brk_computer/src/cointime/value/compute.rs @@ -45,7 +45,7 @@ impl Vecs { .compute(starting_indexes.height, &window_starts, exit, |vec| { vec.compute_multiply( starting_indexes.height, - &prices.usd.price, + &prices.price.usd, &coinblocks_destroyed.height, exit, )?; @@ -56,7 +56,7 @@ impl Vecs { .compute(starting_indexes.height, &window_starts, exit, |vec| { vec.compute_multiply( starting_indexes.height, - &prices.usd.price, + &prices.price.usd, &activity.coinblocks_created.height, exit, )?; @@ -67,7 +67,7 @@ impl Vecs { .compute(starting_indexes.height, &window_starts, exit, |vec| { vec.compute_multiply( starting_indexes.height, - &prices.usd.price, + &prices.price.usd, &activity.coinblocks_stored.height, exit, )?; @@ -81,7 +81,7 @@ impl Vecs { .compute(starting_indexes.height, &window_starts, exit, |vec| { vec.compute_transform3( starting_indexes.height, - &prices.usd.price, + &prices.price.usd, &coindays_destroyed.height, circulating_supply, |(i, price, cdd, supply, _): (_, Dollars, StoredF64, Bitcoin, _)| { diff --git a/crates/brk_computer/src/distribution/compute/block_loop.rs b/crates/brk_computer/src/distribution/compute/block_loop.rs index 3c23f9e06..f9a3c647d 100644 --- a/crates/brk_computer/src/distribution/compute/block_loop.rs +++ b/crates/brk_computer/src/distribution/compute/block_loop.rs @@ -31,7 +31,7 @@ use super::{ }, BIP30_DUPLICATE_HEIGHT_1, BIP30_DUPLICATE_HEIGHT_2, BIP30_ORIGINAL_HEIGHT_1, BIP30_ORIGINAL_HEIGHT_2, ComputeContext, FLUSH_INTERVAL, TxInReaders, TxOutReaders, - VecsReaders, build_txinindex_to_txindex, build_txoutindex_to_txindex, + IndexToTxIndexBuf, VecsReaders, }; /// Process all blocks from starting_height to last_height. @@ -78,7 +78,7 @@ pub(crate) fn process_blocks( let txindex_to_input_count = &indexes.txindex.input_count; // From price - use cents for computation: - let height_to_price = &prices.cents.price; + let height_to_price = &prices.price.cents; // Access pre-computed vectors from context for thread-safe access let height_to_price_vec = &ctx.height_to_price; @@ -127,9 +127,11 @@ pub(crate) fn process_blocks( }; debug!("txindex_to_height RangeMap built"); - // Create reusable iterators for sequential txout/txin reads (16KB buffered) + // Create reusable iterators and buffers for per-block reads let mut txout_iters = TxOutReaders::new(indexer); let mut txin_iters = TxInReaders::new(indexer, inputs, &mut txindex_to_height); + let mut txout_to_txindex_buf = IndexToTxIndexBuf::new(); + let mut txin_to_txindex_buf = IndexToTxIndexBuf::new(); // Pre-collect first address indexes per type for the block range let first_p2a_vec = indexer @@ -230,11 +232,11 @@ pub(crate) fn process_blocks( debug_assert_eq!(ctx.timestamp_at(height), timestamp); debug_assert_eq!(ctx.price_at(height), block_price); - // Build txindex mappings for this block (pass ReadableVec refs directly) + // Build txindex mappings for this block (reuses internal buffers) let txoutindex_to_txindex = - build_txoutindex_to_txindex(first_txindex, tx_count, txindex_to_output_count); + txout_to_txindex_buf.build(first_txindex, tx_count, txindex_to_output_count); let txinindex_to_txindex = - build_txinindex_to_txindex(first_txindex, tx_count, txindex_to_input_count); + txin_to_txindex_buf.build(first_txindex, tx_count, txindex_to_input_count); // Get first address indexes for this height from pre-collected vecs let first_addressindexes = ByAddressType { diff --git a/crates/brk_computer/src/distribution/compute/context.rs b/crates/brk_computer/src/distribution/compute/context.rs index 6b7410db2..f3bcf81e0 100644 --- a/crates/brk_computer/src/distribution/compute/context.rs +++ b/crates/brk_computer/src/distribution/compute/context.rs @@ -125,7 +125,7 @@ impl ComputeContext { blocks.time.timestamp_monotonic.collect(); let height_to_price: Vec = - prices.cents.price.collect(); + prices.price.cents.collect(); // Build sparse table for O(1) range max queries on prices // Used for computing peak price during UTXO holding periods (peak regret) diff --git a/crates/brk_computer/src/distribution/compute/mod.rs b/crates/brk_computer/src/distribution/compute/mod.rs index 2a7c9c377..85d7c51e1 100644 --- a/crates/brk_computer/src/distribution/compute/mod.rs +++ b/crates/brk_computer/src/distribution/compute/mod.rs @@ -7,10 +7,7 @@ mod write; pub(crate) use block_loop::process_blocks; pub(crate) use context::{ComputeContext, PriceRangeMax}; -pub(crate) use readers::{ - TxInReaders, TxOutData, TxOutReaders, VecsReaders, build_txinindex_to_txindex, - build_txoutindex_to_txindex, -}; +pub(crate) use readers::{IndexToTxIndexBuf, TxInReaders, TxOutData, TxOutReaders, VecsReaders}; pub(crate) use recover::{StartMode, determine_start_mode, recover_state, reset_state}; /// Flush checkpoint interval (every N blocks). diff --git a/crates/brk_computer/src/distribution/compute/readers.rs b/crates/brk_computer/src/distribution/compute/readers.rs index 84659e8df..1a0d27a70 100644 --- a/crates/brk_computer/src/distribution/compute/readers.rs +++ b/crates/brk_computer/src/distribution/compute/readers.rs @@ -153,42 +153,39 @@ impl VecsReaders { } } -/// Build txoutindex -> txindex mapping for a block. -pub(crate) fn build_txoutindex_to_txindex( - block_first_txindex: TxIndex, - block_tx_count: u64, - txindex_to_count: &impl ReadableVec, -) -> Vec { - build_index_to_txindex(block_first_txindex, block_tx_count, txindex_to_count) +/// Reusable buffers for per-block txindex mapping construction. +pub(crate) struct IndexToTxIndexBuf { + counts: Vec, + result: Vec, } -/// Build txinindex -> txindex mapping for a block. -pub(crate) fn build_txinindex_to_txindex( - block_first_txindex: TxIndex, - block_tx_count: u64, - txindex_to_count: &impl ReadableVec, -) -> Vec { - build_index_to_txindex(block_first_txindex, block_tx_count, txindex_to_count) -} - -/// Build index -> txindex mapping for a block (shared implementation). -fn build_index_to_txindex( - block_first_txindex: TxIndex, - block_tx_count: u64, - txindex_to_count: &impl ReadableVec, -) -> Vec { - let first = block_first_txindex.to_usize(); - - let counts: Vec = - txindex_to_count.collect_range_at(first, first + block_tx_count as usize); - - let total: u64 = counts.iter().map(|c| u64::from(*c)).sum(); - let mut result = Vec::with_capacity(total as usize); - - for (offset, count) in counts.iter().enumerate() { - let txindex = TxIndex::from(first + offset); - result.extend(std::iter::repeat_n(txindex, u64::from(*count) as usize)); +impl IndexToTxIndexBuf { + pub(crate) fn new() -> Self { + Self { + counts: Vec::new(), + result: Vec::new(), + } } - result + /// Build index -> txindex mapping for a block, reusing internal buffers. + pub(crate) fn build( + &mut self, + block_first_txindex: TxIndex, + block_tx_count: u64, + txindex_to_count: &impl ReadableVec, + ) -> &[TxIndex] { + let first = block_first_txindex.to_usize(); + txindex_to_count.collect_range_into_at(first, first + block_tx_count as usize, &mut self.counts); + + let total: u64 = self.counts.iter().map(|c| u64::from(*c)).sum(); + self.result.clear(); + self.result.reserve(total as usize); + + for (offset, count) in self.counts.iter().enumerate() { + let txindex = TxIndex::from(first + offset); + self.result.extend(std::iter::repeat_n(txindex, u64::from(*count) as usize)); + } + + &self.result + } } diff --git a/crates/brk_computer/src/distribution/metrics/realized/base.rs b/crates/brk_computer/src/distribution/metrics/realized/base.rs index 41bbfebac..a0318012e 100644 --- a/crates/brk_computer/src/distribution/metrics/realized/base.rs +++ b/crates/brk_computer/src/distribution/metrics/realized/base.rs @@ -697,15 +697,26 @@ impl RealizedBase { .min(self.investor_price_cents.height.len()); let end = others.iter().map(|o| o.cap_raw.len()).min().unwrap_or(0); + // Pre-collect all cohort data to avoid per-element BytesVec reads in nested loop + let cap_ranges: Vec> = others + .iter() + .map(|o| o.cap_raw.collect_range_at(start, end)) + .collect(); + let investor_cap_ranges: Vec> = others + .iter() + .map(|o| o.investor_cap_raw.collect_range_at(start, end)) + .collect(); + for i in start..end { let height = Height::from(i); + let local_i = i - start; let mut sum_cap = CentsSats::ZERO; let mut sum_investor_cap = CentsSquaredSats::ZERO; - for o in others.iter() { - sum_cap += o.cap_raw.collect_one_at(i).unwrap(); - sum_investor_cap += o.investor_cap_raw.collect_one_at(i).unwrap(); + for idx in 0..others.len() { + sum_cap += cap_ranges[idx][local_i]; + sum_investor_cap += investor_cap_ranges[idx][local_i]; } self.cap_raw.truncate_push(height, sum_cap)?; @@ -842,14 +853,14 @@ impl RealizedBase { self.realized_price_extra.compute_ratio( starting_indexes, - &prices.usd.price, + &prices.price.usd, &self.realized_price.usd.height, exit, )?; self.investor_price_extra.compute_ratio( starting_indexes, - &prices.usd.price, + &prices.price.usd, &self.investor_price.usd.height, exit, )?; diff --git a/crates/brk_computer/src/distribution/metrics/unrealized/base.rs b/crates/brk_computer/src/distribution/metrics/unrealized/base.rs index ae7227f82..7b9310400 100644 --- a/crates/brk_computer/src/distribution/metrics/unrealized/base.rs +++ b/crates/brk_computer/src/distribution/metrics/unrealized/base.rs @@ -331,31 +331,38 @@ impl UnrealizedBase { .min() .unwrap_or(0); + // Pre-collect all cohort data to avoid per-element BytesVec reads in nested loop + let invested_profit_ranges: Vec> = others + .iter() + .map(|o| o.invested_capital_in_profit_raw.collect_range_at(start, end)) + .collect(); + let invested_loss_ranges: Vec> = others + .iter() + .map(|o| o.invested_capital_in_loss_raw.collect_range_at(start, end)) + .collect(); + let investor_profit_ranges: Vec> = others + .iter() + .map(|o| o.investor_cap_in_profit_raw.collect_range_at(start, end)) + .collect(); + let investor_loss_ranges: Vec> = others + .iter() + .map(|o| o.investor_cap_in_loss_raw.collect_range_at(start, end)) + .collect(); + for i in start..end { let height = Height::from(i); + let local_i = i - start; let mut sum_invested_profit = CentsSats::ZERO; let mut sum_invested_loss = CentsSats::ZERO; let mut sum_investor_profit = CentsSquaredSats::ZERO; let mut sum_investor_loss = CentsSquaredSats::ZERO; - for o in others.iter() { - sum_invested_profit += o - .invested_capital_in_profit_raw - .collect_one_at(i) - .unwrap(); - sum_invested_loss += o - .invested_capital_in_loss_raw - .collect_one_at(i) - .unwrap(); - sum_investor_profit += o - .investor_cap_in_profit_raw - .collect_one_at(i) - .unwrap(); - sum_investor_loss += o - .investor_cap_in_loss_raw - .collect_one_at(i) - .unwrap(); + for idx in 0..others.len() { + sum_invested_profit += invested_profit_ranges[idx][local_i]; + sum_invested_loss += invested_loss_ranges[idx][local_i]; + sum_investor_profit += investor_profit_ranges[idx][local_i]; + sum_investor_loss += investor_loss_ranges[idx][local_i]; } self.invested_capital_in_profit_raw @@ -383,7 +390,7 @@ impl UnrealizedBase { starting_indexes.height, &self.investor_cap_in_loss_raw, &self.invested_capital_in_loss_raw, - &prices.cents.price, + &prices.price.cents, |(h, investor_cap, invested_cap, spot, ..)| { if invested_cap.inner() == 0 { return (h, Dollars::ZERO); @@ -403,7 +410,7 @@ impl UnrealizedBase { starting_indexes.height, &self.investor_cap_in_profit_raw, &self.invested_capital_in_profit_raw, - &prices.cents.price, + &prices.price.cents, |(h, investor_cap, invested_cap, spot, ..)| { if invested_cap.inner() == 0 { return (h, Dollars::ZERO); diff --git a/crates/brk_computer/src/distribution/vecs.rs b/crates/brk_computer/src/distribution/vecs.rs index 415413d19..d7ea05b30 100644 --- a/crates/brk_computer/src/distribution/vecs.rs +++ b/crates/brk_computer/src/distribution/vecs.rs @@ -249,7 +249,7 @@ impl Vecs { // Recover chain_state from stored values debug!("recovering chain_state from stored values"); let height_to_timestamp = &blocks.time.timestamp_monotonic; - let height_to_price = &prices.cents.price; + let height_to_price = &prices.price.cents; let end = usize::from(recovered_height); let timestamp_data: Vec<_> = height_to_timestamp.collect_range_at(0, end); diff --git a/crates/brk_computer/src/internal/multi/from_height/ratio/extended.rs b/crates/brk_computer/src/internal/multi/from_height/ratio/extended.rs index 15dca546a..24f2b525a 100644 --- a/crates/brk_computer/src/internal/multi/from_height/ratio/extended.rs +++ b/crates/brk_computer/src/internal/multi/from_height/ratio/extended.rs @@ -40,7 +40,7 @@ impl ComputedFromHeightRatioExtended { exit: &Exit, metric_price: &impl ReadableVec, ) -> Result<()> { - let close_price = &prices.usd.price; + let close_price = &prices.price.usd; self.base .compute_ratio(starting_indexes, close_price, metric_price, exit)?; self.extended diff --git a/crates/brk_computer/src/internal/multi/from_height/value_full.rs b/crates/brk_computer/src/internal/multi/from_height/value_full.rs index 94fe8e4a3..c4c466f4c 100644 --- a/crates/brk_computer/src/internal/multi/from_height/value_full.rs +++ b/crates/brk_computer/src/internal/multi/from_height/value_full.rs @@ -64,7 +64,7 @@ impl ValueFromHeightFull { Ok(vec.compute_binary::( max_from, &self.sats.height, - &prices.usd.price, + &prices.price.usd, exit, )?) }) diff --git a/crates/brk_computer/src/internal/multi/from_height/value_last.rs b/crates/brk_computer/src/internal/multi/from_height/value_last.rs index 58077dbc6..eb63b59e5 100644 --- a/crates/brk_computer/src/internal/multi/from_height/value_last.rs +++ b/crates/brk_computer/src/internal/multi/from_height/value_last.rs @@ -59,7 +59,7 @@ impl ValueFromHeightLast { self.usd.compute_binary::( max_from, &self.sats.height, - &prices.usd.price, + &prices.price.usd, exit, )?; Ok(()) diff --git a/crates/brk_computer/src/internal/multi/from_height/value_lazy_computed_cumulative.rs b/crates/brk_computer/src/internal/multi/from_height/value_lazy_computed_cumulative.rs index 06ce85166..385a39f53 100644 --- a/crates/brk_computer/src/internal/multi/from_height/value_lazy_computed_cumulative.rs +++ b/crates/brk_computer/src/internal/multi/from_height/value_lazy_computed_cumulative.rs @@ -63,7 +63,7 @@ impl LazyComputedValueFromHeightCumulative { self.usd.compute_binary::( max_from, &self.sats.height, - &prices.usd.price, + &prices.price.usd, exit, )?; Ok(()) diff --git a/crates/brk_computer/src/internal/multi/from_height/value_sum_cumulative.rs b/crates/brk_computer/src/internal/multi/from_height/value_sum_cumulative.rs index 42a22ee02..5a19d0acc 100644 --- a/crates/brk_computer/src/internal/multi/from_height/value_sum_cumulative.rs +++ b/crates/brk_computer/src/internal/multi/from_height/value_sum_cumulative.rs @@ -63,7 +63,7 @@ impl ValueFromHeightSumCumulative { Ok(vec.compute_binary::( max_from, &self.sats.height, - &prices.usd.price, + &prices.price.usd, exit, )?) }) diff --git a/crates/brk_computer/src/internal/single/height/value.rs b/crates/brk_computer/src/internal/single/height/value.rs index 1e4eea2d2..371660e22 100644 --- a/crates/brk_computer/src/internal/single/height/value.rs +++ b/crates/brk_computer/src/internal/single/height/value.rs @@ -51,7 +51,7 @@ impl ValueFromHeight { self.usd.compute_binary::( max_from, &self.sats, - &prices.usd.price, + &prices.price.usd, exit, )?; Ok(()) diff --git a/crates/brk_computer/src/market/ath/compute.rs b/crates/brk_computer/src/market/ath/compute.rs index 86f4f544b..1e09c788b 100644 --- a/crates/brk_computer/src/market/ath/compute.rs +++ b/crates/brk_computer/src/market/ath/compute.rs @@ -14,7 +14,7 @@ impl Vecs { ) -> Result<()> { self.price_ath.usd.height.compute_all_time_high( starting_indexes.height, - &prices.usd.price, + &prices.price.usd, exit, )?; @@ -22,7 +22,7 @@ impl Vecs { self.days_since_price_ath.height.compute_transform2( starting_indexes.height, &self.price_ath.usd.height, - &prices.usd.price, + &prices.price.usd, |(i, ath, price, slf)| { if prev.is_none() { let i = i.to_usize(); diff --git a/crates/brk_computer/src/market/dca/compute.rs b/crates/brk_computer/src/market/dca/compute.rs index 604da7f1f..6b8b37bcb 100644 --- a/crates/brk_computer/src/market/dca/compute.rs +++ b/crates/brk_computer/src/market/dca/compute.rs @@ -23,7 +23,7 @@ impl Vecs { exit: &Exit, ) -> Result<()> { let h2d = &indexes.height.day1; - let close = &prices.usd.split.close.day1; + let close = &prices.split.close.usd.day1; let first_price_di = Day1::try_from(Date::new(2010, 7, 12)) .unwrap() @@ -97,7 +97,7 @@ impl Vecs { { returns.compute_binary::( starting_indexes.height, - &prices.usd.price, + &prices.price.usd, &average_price.usd.height, exit, )?; @@ -165,7 +165,7 @@ impl Vecs { { returns.compute_binary::( starting_indexes.height, - &prices.usd.price, + &prices.price.usd, &lookback_price.usd.height, exit, )?; @@ -188,11 +188,16 @@ impl Vecs { let start_days = super::ByDcaClass::<()>::start_days(); for (stack, day1) in self.class_stack.iter_mut().zip(start_days) { let mut last_di: Option = None; + let mut prev_value = if sh > 0 { + stack.sats.height.collect_one_at(sh - 1).unwrap_or_default() + } else { + Sats::ZERO + }; stack.sats.height.compute_transform( starting_indexes.height, h2d, - |(h, di, this)| { + |(h, di, _)| { let hi = h.to_usize(); if last_di.is_none() && hi > 0 { @@ -201,6 +206,7 @@ impl Vecs { if di < day1 { last_di = Some(di); + prev_value = Sats::ZERO; return (h, Sats::ZERO); } @@ -208,17 +214,19 @@ impl Vecs { last_di = Some(di); let same_day = prev_di.is_some_and(|prev| prev == di); - if same_day { - (h, this.collect_one_at(hi - 1).unwrap_or_default()) + let result = if same_day { + prev_value } else { let prev = if hi > 0 && prev_di.is_some_and(|pd| pd >= day1) { - this.collect_one_at(hi - 1).unwrap_or_default() + prev_value } else { Sats::ZERO }; let s = close.collect_one_flat(di).map(sats_from_dca).unwrap_or(Sats::ZERO); - (h, prev + s) - } + prev + s + }; + prev_value = result; + (h, result) }, exit, )?; @@ -260,7 +268,7 @@ impl Vecs { returns.compute_binary::( starting_indexes.height, - &prices.usd.price, + &prices.price.usd, &average_price.usd.height, exit, )?; @@ -457,11 +465,17 @@ fn compute_cumulative( mut accumulate: impl FnMut(T, StoredF32) -> T, ) -> Result<()> { let mut last_di: Option = None; + let sh = starting_height.to_usize(); + let mut prev_value = if sh > 0 { + output.collect_one_at(sh - 1).unwrap_or_default() + } else { + T::default() + }; output.compute_transform( starting_height, h2d, - |(h, di, this)| { + |(h, di, _)| { let hi = h.to_usize(); if last_di.is_none() && hi > 0 { @@ -470,6 +484,7 @@ fn compute_cumulative( if di < from_day1 { last_di = Some(di); + prev_value = T::default(); return (h, T::default()); } @@ -477,17 +492,19 @@ fn compute_cumulative( last_di = Some(di); let same_day = prev_di.is_some_and(|prev| prev == di); - if same_day { - (h, this.collect_one_at(hi - 1).unwrap_or_default()) + let result = if same_day { + prev_value } else { let prev = if hi > 0 && prev_di.is_some_and(|pd| pd >= from_day1) { - this.collect_one_at(hi - 1).unwrap_or_default() + prev_value } else { initial }; let ret = returns.collect_one_flat(di).unwrap_or_default(); - (h, accumulate(prev, ret)) - } + accumulate(prev, ret) + }; + prev_value = result; + (h, result) }, exit, )?; diff --git a/crates/brk_computer/src/market/indicators/compute.rs b/crates/brk_computer/src/market/indicators/compute.rs index 8e10dc339..0752ef24f 100644 --- a/crates/brk_computer/src/market/indicators/compute.rs +++ b/crates/brk_computer/src/market/indicators/compute.rs @@ -34,7 +34,7 @@ impl Vecs { // Stochastic Oscillator: K = (close - low_2w) / (high_2w - low_2w) * 100 { - let price = &prices.usd.price; + let price = &prices.price.usd; self.stoch_k.height.compute_transform3( starting_indexes.height, price, diff --git a/crates/brk_computer/src/market/indicators/macd.rs b/crates/brk_computer/src/market/indicators/macd.rs index 4dbe10692..e25a5abbf 100644 --- a/crates/brk_computer/src/market/indicators/macd.rs +++ b/crates/brk_computer/src/market/indicators/macd.rs @@ -15,7 +15,7 @@ pub(super) fn compute( starting_indexes: &ComputeIndexes, exit: &Exit, ) -> Result<()> { - let source_version = prices.usd.price.version(); + let source_version = prices.price.usd.version(); chain .line diff --git a/crates/brk_computer/src/market/indicators/timeframe.rs b/crates/brk_computer/src/market/indicators/timeframe.rs index 90cbedf8d..1a5bdf0e3 100644 --- a/crates/brk_computer/src/market/indicators/timeframe.rs +++ b/crates/brk_computer/src/market/indicators/timeframe.rs @@ -16,10 +16,10 @@ pub(super) fn collect_returns(tf: &str, returns: &ReturnsVecs) -> Vec { pub(super) fn collect_closes(tf: &str, prices: &prices::Vecs) -> Vec { match tf { - "1d" => prices.usd.split.close.day1.collect_or_default(), - "1w" => prices.usd.split.close.week1.collect_or_default(), - "1m" => prices.usd.split.close.month1.collect_or_default(), - "1y" => prices.usd.split.close.year1.collect_or_default(), + "1d" => prices.split.close.usd.day1.collect_or_default(), + "1w" => prices.split.close.usd.week1.collect_or_default(), + "1m" => prices.split.close.usd.month1.collect_or_default(), + "1y" => prices.split.close.usd.year1.collect_or_default(), _ => unreachable!(), } } diff --git a/crates/brk_computer/src/market/lookback/compute.rs b/crates/brk_computer/src/market/lookback/compute.rs index d3e681882..5415a326b 100644 --- a/crates/brk_computer/src/market/lookback/compute.rs +++ b/crates/brk_computer/src/market/lookback/compute.rs @@ -13,7 +13,7 @@ impl Vecs { starting_indexes: &ComputeIndexes, exit: &Exit, ) -> Result<()> { - let close_data: Vec = prices.usd.price.collect(); + let close_data: Vec = prices.price.usd.collect(); for (price_ago, days) in self.price_ago.iter_mut_with_days() { let window_starts = blocks.count.start_vec(days as usize); diff --git a/crates/brk_computer/src/market/moving_average/compute.rs b/crates/brk_computer/src/market/moving_average/compute.rs index 6f0d127cd..22e225274 100644 --- a/crates/brk_computer/src/market/moving_average/compute.rs +++ b/crates/brk_computer/src/market/moving_average/compute.rs @@ -14,7 +14,7 @@ impl Vecs { starting_indexes: &ComputeIndexes, exit: &Exit, ) -> Result<()> { - let close = &prices.usd.price; + let close = &prices.price.usd; for (sma, period) in [ (&mut self.price_1w_sma, 7), @@ -42,7 +42,7 @@ impl Vecs { } let h2d = &indexes.height.day1; - let closes: Vec = prices.usd.split.close.day1.collect_or_default(); + let closes: Vec = prices.split.close.usd.day1.collect_or_default(); for (ema, period) in [ (&mut self.price_1w_ema, 7), diff --git a/crates/brk_computer/src/market/range/compute.rs b/crates/brk_computer/src/market/range/compute.rs index d8a2fb51b..956325546 100644 --- a/crates/brk_computer/src/market/range/compute.rs +++ b/crates/brk_computer/src/market/range/compute.rs @@ -16,7 +16,7 @@ impl Vecs { starting_indexes: &ComputeIndexes, exit: &Exit, ) -> Result<()> { - let price = &prices.usd.price; + let price = &prices.price.usd; self.price_1w_min.usd.height.compute_rolling_min_from_starts( starting_indexes.height, diff --git a/crates/brk_computer/src/market/returns/compute.rs b/crates/brk_computer/src/market/returns/compute.rs index 43118200a..11da8cbc3 100644 --- a/crates/brk_computer/src/market/returns/compute.rs +++ b/crates/brk_computer/src/market/returns/compute.rs @@ -23,7 +23,7 @@ impl Vecs { { returns.compute_binary::( starting_indexes.height, - &prices.usd.price, + &prices.price.usd, &lookback_price.usd.height, exit, )?; diff --git a/crates/brk_computer/src/prices/by_unit.rs b/crates/brk_computer/src/prices/by_unit.rs new file mode 100644 index 000000000..8b1234682 --- /dev/null +++ b/crates/brk_computer/src/prices/by_unit.rs @@ -0,0 +1,49 @@ +use brk_traversable::Traversable; +use brk_types::{Cents, Dollars, Height, OHLCCents, OHLCDollars, OHLCSats, Sats}; +use vecdb::{LazyVecFrom1, PcoVec, Rw, StorageMode}; + +use crate::internal::{ComputedHeightDerivedLast, EagerIndexes, LazyEagerIndexes}; + +use super::ohlcs::{LazyOhlcVecs, OhlcVecs}; + +// ── SplitByUnit ───────────────────────────────────────────────────── + +#[derive(Traversable)] +pub struct SplitByUnit { + pub open: SplitIndexesByUnit, + pub high: SplitIndexesByUnit, + pub low: SplitIndexesByUnit, + pub close: SplitCloseByUnit, +} + +#[derive(Traversable)] +pub struct SplitIndexesByUnit { + pub cents: EagerIndexes, + pub usd: LazyEagerIndexes, + pub sats: LazyEagerIndexes, +} + +#[derive(Clone, Traversable)] +pub struct SplitCloseByUnit { + pub cents: ComputedHeightDerivedLast, + pub usd: ComputedHeightDerivedLast, + pub sats: ComputedHeightDerivedLast, +} + +// ── OhlcByUnit ────────────────────────────────────────────────────── + +#[derive(Traversable)] +pub struct OhlcByUnit { + pub cents: OhlcVecs, + pub usd: LazyOhlcVecs, + pub sats: LazyOhlcVecs, +} + +// ── PriceByUnit ───────────────────────────────────────────────────── + +#[derive(Traversable)] +pub struct PriceByUnit { + pub cents: M::Stored>, + pub usd: LazyVecFrom1, + pub sats: LazyVecFrom1, +} diff --git a/crates/brk_computer/src/prices/cents/compute.rs b/crates/brk_computer/src/prices/cents/compute.rs deleted file mode 100644 index f4bb6c7a2..000000000 --- a/crates/brk_computer/src/prices/cents/compute.rs +++ /dev/null @@ -1,207 +0,0 @@ -use std::ops::Range; - -use brk_error::Result; -use brk_indexer::Indexer; -use brk_oracle::{Config, NUM_BINS, Oracle, START_HEIGHT, bin_to_cents, cents_to_bin}; -use brk_types::{Cents, OutputType, Sats, TxIndex, TxOutIndex}; -use tracing::info; -use vecdb::{AnyStoredVec, AnyVec, Exit, ReadableVec, StorageMode, VecIndex, WritableVec}; - -use super::Vecs; -use crate::{ComputeIndexes, indexes}; - -impl Vecs { - pub(crate) fn compute( - &mut self, - indexer: &Indexer, - indexes: &indexes::Vecs, - starting_indexes: &ComputeIndexes, - exit: &Exit, - ) -> Result<()> { - self.compute_prices(indexer, starting_indexes, exit)?; - self.split - .open - .compute_first(starting_indexes, &self.price, indexes, exit)?; - self.split - .high - .compute_max(starting_indexes, &self.price, indexes, exit)?; - self.split - .low - .compute_min(starting_indexes, &self.price, indexes, exit)?; - self.ohlc.compute_from_split( - starting_indexes, - &self.split.open, - &self.split.high, - &self.split.low, - &self.split.close, - indexes, - exit, - )?; - Ok(()) - } - - fn compute_prices( - &mut self, - indexer: &Indexer, - starting_indexes: &ComputeIndexes, - exit: &Exit, - ) -> Result<()> { - let source_version = - indexer.vecs.outputs.value.version() + indexer.vecs.outputs.outputtype.version(); - self.price - .validate_computed_version_or_reset(source_version)?; - - let total_heights = indexer.vecs.blocks.timestamp.len(); - - if total_heights <= START_HEIGHT { - return Ok(()); - } - - // Reorg: truncate to starting_indexes - let truncate_to = self.price.len().min(starting_indexes.height.to_usize()); - self.price.truncate_if_needed_at(truncate_to)?; - - if self.price.len() < START_HEIGHT { - for line in brk_oracle::PRICES.lines().skip(self.price.len()) { - if self.price.len() >= START_HEIGHT { - break; - } - let dollars: f64 = line.parse().unwrap_or(0.0); - let cents = (dollars * 100.0).round() as u64; - self.price.push(Cents::new(cents)); - } - } - - if self.price.len() >= total_heights { - return Ok(()); - } - - let config = Config::default(); - let committed = self.price.len(); - let prev_cents = self.price.collect_one_at(committed - 1).unwrap(); - let seed_bin = cents_to_bin(prev_cents.inner() as f64); - let warmup = config.window_size.min(committed - START_HEIGHT); - let mut oracle = Oracle::from_checkpoint(seed_bin, config, |o| { - Self::feed_blocks(o, indexer, (committed - warmup)..committed); - }); - - let num_new = total_heights - committed; - info!( - "Computing oracle prices: {} to {} ({warmup} warmup)", - committed, total_heights - ); - - let ref_bins = Self::feed_blocks(&mut oracle, indexer, committed..total_heights); - - for (i, ref_bin) in ref_bins.into_iter().enumerate() { - self.price.push(Cents::new(bin_to_cents(ref_bin))); - - let progress = ((i + 1) * 100 / num_new) as u8; - if i > 0 && progress > ((i * 100 / num_new) as u8) { - info!("Oracle price computation: {}%", progress); - } - } - - { - let _lock = exit.lock(); - self.price.write()?; - } - - info!("Oracle prices complete: {} committed", self.price.len()); - - Ok(()) - } - - /// Feed a range of blocks from the indexer into an Oracle (skipping coinbase), - /// returning per-block ref_bin values. - fn feed_blocks( - oracle: &mut Oracle, - indexer: &Indexer, - range: Range, - ) -> Vec { - let total_txs = indexer.vecs.transactions.height.len(); - let total_outputs = indexer.vecs.outputs.value.len(); - - // Pre-collect height-indexed data for the range (plus one extra for next-block lookups) - let collect_end = (range.end + 1).min(indexer.vecs.transactions.first_txindex.len()); - let first_txindexes: Vec = indexer - .vecs - .transactions - .first_txindex - .collect_range_at(range.start, collect_end); - - let out_firsts: Vec = indexer - .vecs - .outputs - .first_txoutindex - .collect_range_at(range.start, collect_end); - - let mut ref_bins = Vec::with_capacity(range.len()); - - // Cursor avoids per-block PcoVec page decompression for - // the tx-indexed first_txoutindex lookup. The accessed - // txindex values (first_txindex + 1) are strictly increasing - // across blocks, so the cursor only advances forward. - let mut txout_cursor = indexer.vecs.transactions.first_txoutindex.cursor(); - - // Reusable buffers — avoid per-block allocation - let mut values: Vec = Vec::new(); - let mut output_types: Vec = Vec::new(); - - for (idx, _h) in range.enumerate() { - let first_txindex = first_txindexes[idx]; - let next_first_txindex = first_txindexes - .get(idx + 1) - .copied() - .unwrap_or(TxIndex::from(total_txs)); - - let out_start = if first_txindex.to_usize() + 1 < next_first_txindex.to_usize() { - let target = first_txindex.to_usize() + 1; - txout_cursor.advance(target - txout_cursor.position()); - txout_cursor.next().unwrap().to_usize() - } else { - out_firsts - .get(idx + 1) - .copied() - .unwrap_or(TxOutIndex::from(total_outputs)) - .to_usize() - }; - let out_end = out_firsts - .get(idx + 1) - .copied() - .unwrap_or(TxOutIndex::from(total_outputs)) - .to_usize(); - - indexer.vecs.outputs.value.collect_range_into_at(out_start, out_end, &mut values); - indexer.vecs.outputs.outputtype.collect_range_into_at(out_start, out_end, &mut output_types); - - let mut hist = [0u32; NUM_BINS]; - for i in 0..values.len() { - if let Some(bin) = oracle.output_to_bin(values[i], output_types[i]) { - hist[bin] += 1; - } - } - - ref_bins.push(oracle.process_histogram(&hist)); - } - - ref_bins - } -} - -impl Vecs { - /// Returns an Oracle seeded from the last committed price, with the last - /// window_size blocks already processed. Ready for additional blocks (e.g. mempool). - pub fn live_oracle(&self, indexer: &Indexer) -> Result { - let config = Config::default(); - let height = indexer.vecs.blocks.timestamp.len(); - let last_cents = self.price.collect_one_at(self.price.len() - 1).unwrap(); - let seed_bin = cents_to_bin(last_cents.inner() as f64); - let window_size = config.window_size; - let oracle = Oracle::from_checkpoint(seed_bin, config, |o| { - Vecs::feed_blocks(o, indexer, height.saturating_sub(window_size)..height); - }); - - Ok(oracle) - } -} diff --git a/crates/brk_computer/src/prices/cents/import.rs b/crates/brk_computer/src/prices/cents/import.rs deleted file mode 100644 index 466353e38..000000000 --- a/crates/brk_computer/src/prices/cents/import.rs +++ /dev/null @@ -1,42 +0,0 @@ -use brk_error::Result; -use brk_types::Version; -use vecdb::{Database, ImportableVec, PcoVec, ReadableCloneableVec}; - -use super::Vecs; -use crate::indexes; -use crate::internal::{ComputedHeightDerivedLast, EagerIndexes}; -use crate::prices::{ohlcs::OhlcVecs, split::SplitOhlc}; - -impl Vecs { - pub(crate) fn forced_import( - db: &Database, - parent_version: Version, - indexes: &indexes::Vecs, - ) -> Result { - let version = parent_version + Version::new(11); - - let price = PcoVec::forced_import(db, "price_cents", version)?; - - let open = EagerIndexes::forced_import(db, "price_open_cents", version)?; - let high = EagerIndexes::forced_import(db, "price_high_cents", version)?; - let low = EagerIndexes::forced_import(db, "price_low_cents", version)?; - - let close = ComputedHeightDerivedLast::forced_import( - "price_close_cents", - price.read_only_boxed_clone(), - version, - indexes, - ); - - let split = SplitOhlc { - open, - high, - low, - close, - }; - - let ohlc = OhlcVecs::forced_import(db, "price_ohlc_cents", version)?; - - Ok(Self { split, ohlc, price }) - } -} diff --git a/crates/brk_computer/src/prices/cents/mod.rs b/crates/brk_computer/src/prices/cents/mod.rs deleted file mode 100644 index 1136f9ebd..000000000 --- a/crates/brk_computer/src/prices/cents/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -mod compute; -mod import; -mod vecs; - -pub use vecs::Vecs; diff --git a/crates/brk_computer/src/prices/cents/vecs.rs b/crates/brk_computer/src/prices/cents/vecs.rs deleted file mode 100644 index cd9f02eec..000000000 --- a/crates/brk_computer/src/prices/cents/vecs.rs +++ /dev/null @@ -1,19 +0,0 @@ -use brk_traversable::Traversable; -use brk_types::{Cents, Height, OHLCCents}; -use vecdb::{PcoVec, Rw, StorageMode}; - -use crate::internal::{ComputedHeightDerivedLast, EagerIndexes}; -use crate::prices::{ohlcs::OhlcVecs, split::SplitOhlc}; - -#[derive(Traversable)] -pub struct Vecs { - #[allow(clippy::type_complexity)] - pub split: SplitOhlc< - EagerIndexes, - EagerIndexes, - EagerIndexes, - ComputedHeightDerivedLast, - >, - pub ohlc: OhlcVecs, - pub price: M::Stored>, -} diff --git a/crates/brk_computer/src/prices/compute.rs b/crates/brk_computer/src/prices/compute.rs index 746ec0385..0bbfa2e41 100644 --- a/crates/brk_computer/src/prices/compute.rs +++ b/crates/brk_computer/src/prices/compute.rs @@ -1,6 +1,11 @@ +use std::ops::Range; + use brk_error::Result; use brk_indexer::Indexer; -use vecdb::Exit; +use brk_oracle::{Config, NUM_BINS, Oracle, START_HEIGHT, bin_to_cents, cents_to_bin}; +use brk_types::{Cents, OutputType, Sats, TxIndex, TxOutIndex}; +use tracing::info; +use vecdb::{AnyStoredVec, AnyVec, Exit, ReadableVec, StorageMode, VecIndex, WritableVec}; use super::Vecs; use crate::{ComputeIndexes, indexes}; @@ -13,11 +18,197 @@ impl Vecs { starting_indexes: &ComputeIndexes, exit: &Exit, ) -> Result<()> { - self.cents - .compute(indexer, indexes, starting_indexes, exit)?; + self.compute_prices(indexer, starting_indexes, exit)?; + self.split + .open + .cents + .compute_first(starting_indexes, &self.price.cents, indexes, exit)?; + self.split + .high + .cents + .compute_max(starting_indexes, &self.price.cents, indexes, exit)?; + self.split + .low + .cents + .compute_min(starting_indexes, &self.price.cents, indexes, exit)?; + self.ohlc.cents.compute_from_split( + starting_indexes, + &self.split.open.cents, + &self.split.high.cents, + &self.split.low.cents, + &self.split.close.cents, + indexes, + exit, + )?; let _lock = exit.lock(); self.db().compact()?; Ok(()) } + + fn compute_prices( + &mut self, + indexer: &Indexer, + starting_indexes: &ComputeIndexes, + exit: &Exit, + ) -> Result<()> { + let source_version = + indexer.vecs.outputs.value.version() + indexer.vecs.outputs.outputtype.version(); + self.price + .cents + .validate_computed_version_or_reset(source_version)?; + + let total_heights = indexer.vecs.blocks.timestamp.len(); + + if total_heights <= START_HEIGHT { + return Ok(()); + } + + // Reorg: truncate to starting_indexes + let truncate_to = self.price.cents.len().min(starting_indexes.height.to_usize()); + self.price.cents.truncate_if_needed_at(truncate_to)?; + + if self.price.cents.len() < START_HEIGHT { + for line in brk_oracle::PRICES.lines().skip(self.price.cents.len()) { + if self.price.cents.len() >= START_HEIGHT { + break; + } + let dollars: f64 = line.parse().unwrap_or(0.0); + let cents = (dollars * 100.0).round() as u64; + self.price.cents.push(Cents::new(cents)); + } + } + + if self.price.cents.len() >= total_heights { + return Ok(()); + } + + let config = Config::default(); + let committed = self.price.cents.len(); + let prev_cents = self.price.cents.collect_one_at(committed - 1).unwrap(); + let seed_bin = cents_to_bin(prev_cents.inner() as f64); + let warmup = config.window_size.min(committed - START_HEIGHT); + let mut oracle = Oracle::from_checkpoint(seed_bin, config, |o| { + Self::feed_blocks(o, indexer, (committed - warmup)..committed); + }); + + let num_new = total_heights - committed; + info!( + "Computing oracle prices: {} to {} ({warmup} warmup)", + committed, total_heights + ); + + let ref_bins = Self::feed_blocks(&mut oracle, indexer, committed..total_heights); + + for (i, ref_bin) in ref_bins.into_iter().enumerate() { + self.price.cents.push(Cents::new(bin_to_cents(ref_bin))); + + let progress = ((i + 1) * 100 / num_new) as u8; + if i > 0 && progress > ((i * 100 / num_new) as u8) { + info!("Oracle price computation: {}%", progress); + } + } + + { + let _lock = exit.lock(); + self.price.cents.write()?; + } + + info!("Oracle prices complete: {} committed", self.price.cents.len()); + + Ok(()) + } + + /// Feed a range of blocks from the indexer into an Oracle (skipping coinbase), + /// returning per-block ref_bin values. + fn feed_blocks( + oracle: &mut Oracle, + indexer: &Indexer, + range: Range, + ) -> Vec { + let total_txs = indexer.vecs.transactions.height.len(); + let total_outputs = indexer.vecs.outputs.value.len(); + + // Pre-collect height-indexed data for the range (plus one extra for next-block lookups) + let collect_end = (range.end + 1).min(indexer.vecs.transactions.first_txindex.len()); + let first_txindexes: Vec = indexer + .vecs + .transactions + .first_txindex + .collect_range_at(range.start, collect_end); + + let out_firsts: Vec = indexer + .vecs + .outputs + .first_txoutindex + .collect_range_at(range.start, collect_end); + + let mut ref_bins = Vec::with_capacity(range.len()); + + // Cursor avoids per-block PcoVec page decompression for + // the tx-indexed first_txoutindex lookup. The accessed + // txindex values (first_txindex + 1) are strictly increasing + // across blocks, so the cursor only advances forward. + let mut txout_cursor = indexer.vecs.transactions.first_txoutindex.cursor(); + + // Reusable buffers — avoid per-block allocation + let mut values: Vec = Vec::new(); + let mut output_types: Vec = Vec::new(); + + for (idx, _h) in range.enumerate() { + let first_txindex = first_txindexes[idx]; + let next_first_txindex = first_txindexes + .get(idx + 1) + .copied() + .unwrap_or(TxIndex::from(total_txs)); + + let out_start = if first_txindex.to_usize() + 1 < next_first_txindex.to_usize() { + let target = first_txindex.to_usize() + 1; + txout_cursor.advance(target - txout_cursor.position()); + txout_cursor.next().unwrap().to_usize() + } else { + out_firsts + .get(idx + 1) + .copied() + .unwrap_or(TxOutIndex::from(total_outputs)) + .to_usize() + }; + let out_end = out_firsts + .get(idx + 1) + .copied() + .unwrap_or(TxOutIndex::from(total_outputs)) + .to_usize(); + + indexer.vecs.outputs.value.collect_range_into_at(out_start, out_end, &mut values); + indexer.vecs.outputs.outputtype.collect_range_into_at(out_start, out_end, &mut output_types); + + let mut hist = [0u32; NUM_BINS]; + for i in 0..values.len() { + if let Some(bin) = oracle.output_to_bin(values[i], output_types[i]) { + hist[bin] += 1; + } + } + + ref_bins.push(oracle.process_histogram(&hist)); + } + + ref_bins + } +} + +impl Vecs { + /// Returns an Oracle seeded from the last committed price, with the last + /// window_size blocks already processed. Ready for additional blocks (e.g. mempool). + pub fn live_oracle(&self, indexer: &Indexer) -> Result { + let config = Config::default(); + let height = indexer.vecs.blocks.timestamp.len(); + let last_cents = self.price.cents.collect_one_at(self.price.cents.len() - 1).unwrap(); + let seed_bin = cents_to_bin(last_cents.inner() as f64); + let window_size = config.window_size; + let oracle = Oracle::from_checkpoint(seed_bin, config, |o| { + Vecs::feed_blocks(o, indexer, height.saturating_sub(window_size)..height); + }); + + Ok(oracle) + } } diff --git a/crates/brk_computer/src/prices/mod.rs b/crates/brk_computer/src/prices/mod.rs index 0f364caa7..a05754127 100644 --- a/crates/brk_computer/src/prices/mod.rs +++ b/crates/brk_computer/src/prices/mod.rs @@ -1,22 +1,28 @@ +pub(crate) mod by_unit; mod compute; pub(crate) mod ohlcs; -pub(crate) mod split; - -pub mod cents; -pub mod sats; -pub mod usd; - -pub use cents::Vecs as CentsVecs; -pub use sats::Vecs as SatsVecs; -pub use usd::Vecs as UsdVecs; use std::path::Path; use brk_traversable::Traversable; use brk_types::Version; -use vecdb::{Database, Rw, StorageMode, PAGE_SIZE}; +use vecdb::{ + Database, ImportableVec, LazyVecFrom1, PcoVec, ReadableCloneableVec, Rw, StorageMode, + PAGE_SIZE, +}; -use crate::indexes; +use crate::{ + indexes, + internal::{ + CentsUnsignedToDollars, CentsUnsignedToSats, ComputedHeightDerivedLast, EagerIndexes, + LazyEagerIndexes, OhlcCentsToDollars, OhlcCentsToSats, + }, +}; + +use by_unit::{ + OhlcByUnit, PriceByUnit, SplitByUnit, SplitCloseByUnit, SplitIndexesByUnit, +}; +use ohlcs::{LazyOhlcVecs, OhlcVecs}; pub const DB_NAME: &str = "prices"; @@ -25,9 +31,9 @@ pub struct Vecs { #[traversable(skip)] pub(crate) db: Database, - pub cents: CentsVecs, - pub usd: UsdVecs, - pub sats: SatsVecs, + pub split: SplitByUnit, + pub ohlc: OhlcByUnit, + pub price: PriceByUnit, } impl Vecs { @@ -56,15 +62,143 @@ impl Vecs { version: Version, indexes: &indexes::Vecs, ) -> brk_error::Result { - let cents = CentsVecs::forced_import(db, version, indexes)?; - let usd = UsdVecs::forced_import(version, indexes, ¢s); - let sats = SatsVecs::forced_import(version, indexes, ¢s); + let version = version + Version::new(11); + + // ── Cents (eager, stored) ─────────────────────────────────── + + let price_cents = PcoVec::forced_import(db, "price_cents", version)?; + + let open_cents = EagerIndexes::forced_import(db, "price_open_cents", version)?; + let high_cents = EagerIndexes::forced_import(db, "price_high_cents", version)?; + let low_cents = EagerIndexes::forced_import(db, "price_low_cents", version)?; + + let close_cents = ComputedHeightDerivedLast::forced_import( + "price_close_cents", + price_cents.read_only_boxed_clone(), + version, + indexes, + ); + + let ohlc_cents = OhlcVecs::forced_import(db, "price_ohlc_cents", version)?; + + // ── USD (lazy from cents) ─────────────────────────────────── + + let price_usd = LazyVecFrom1::transformed::( + "price", + version, + price_cents.read_only_boxed_clone(), + ); + + let open_usd = LazyEagerIndexes::from_eager_indexes::( + "price_open", + version, + &open_cents, + ); + let high_usd = LazyEagerIndexes::from_eager_indexes::( + "price_high", + version, + &high_cents, + ); + let low_usd = LazyEagerIndexes::from_eager_indexes::( + "price_low", + version, + &low_cents, + ); + + let close_usd = ComputedHeightDerivedLast::forced_import( + "price_close", + price_usd.read_only_boxed_clone(), + version, + indexes, + ); + + let ohlc_usd = LazyOhlcVecs::from_eager_ohlc_indexes::( + "price_ohlc", + version, + &ohlc_cents, + ); + + // ── Sats (lazy from cents, high↔low swapped) ─────────────── + + let price_sats = LazyVecFrom1::transformed::( + "price_sats", + version, + price_cents.read_only_boxed_clone(), + ); + + let open_sats = LazyEagerIndexes::from_eager_indexes::( + "price_open_sats", + version, + &open_cents, + ); + // Sats are inversely related to cents (sats = 10B/cents), so high↔low are swapped + let high_sats = LazyEagerIndexes::from_eager_indexes::( + "price_high_sats", + version, + &low_cents, + ); + let low_sats = LazyEagerIndexes::from_eager_indexes::( + "price_low_sats", + version, + &high_cents, + ); + + let close_sats = ComputedHeightDerivedLast::forced_import( + "price_close_sats", + price_sats.read_only_boxed_clone(), + version, + indexes, + ); + + // OhlcCentsToSats handles the high↔low swap internally + let ohlc_sats = LazyOhlcVecs::from_eager_ohlc_indexes::( + "price_ohlc_sats", + version, + &ohlc_cents, + ); + + // ── Assemble pivoted structure ────────────────────────────── + + let split = SplitByUnit { + open: SplitIndexesByUnit { + cents: open_cents, + usd: open_usd, + sats: open_sats, + }, + high: SplitIndexesByUnit { + cents: high_cents, + usd: high_usd, + sats: high_sats, + }, + low: SplitIndexesByUnit { + cents: low_cents, + usd: low_usd, + sats: low_sats, + }, + close: SplitCloseByUnit { + cents: close_cents, + usd: close_usd, + sats: close_sats, + }, + }; + + let ohlc = OhlcByUnit { + cents: ohlc_cents, + usd: ohlc_usd, + sats: ohlc_sats, + }; + + let price = PriceByUnit { + cents: price_cents, + usd: price_usd, + sats: price_sats, + }; Ok(Self { db: db.clone(), - cents, - usd, - sats, + split, + ohlc, + price, }) } diff --git a/crates/brk_computer/src/prices/sats/import.rs b/crates/brk_computer/src/prices/sats/import.rs deleted file mode 100644 index afe85f5bb..000000000 --- a/crates/brk_computer/src/prices/sats/import.rs +++ /dev/null @@ -1,64 +0,0 @@ -use brk_types::Version; -use vecdb::{LazyVecFrom1, ReadableCloneableVec}; - -use super::super::cents; -use super::Vecs; -use crate::prices::{ohlcs::LazyOhlcVecs, split::SplitOhlc}; -use crate::{ - indexes, - internal::{CentsUnsignedToSats, ComputedHeightDerivedLast, LazyEagerIndexes, OhlcCentsToSats}, -}; - -impl Vecs { - pub(crate) fn forced_import( - version: Version, - indexes: &indexes::Vecs, - cents: ¢s::Vecs, - ) -> Self { - let price = LazyVecFrom1::transformed::( - "price_sats", - version, - cents.price.read_only_boxed_clone(), - ); - - // Sats are inversely related to cents (sats = 10B/cents), so high↔low are swapped - let open = LazyEagerIndexes::from_eager_indexes::( - "price_open_sats", - version, - ¢s.split.open, - ); - let high = LazyEagerIndexes::from_eager_indexes::( - "price_high_sats", - version, - ¢s.split.low, - ); - let low = LazyEagerIndexes::from_eager_indexes::( - "price_low_sats", - version, - ¢s.split.high, - ); - - let close = ComputedHeightDerivedLast::forced_import( - "price_close_sats", - price.read_only_boxed_clone(), - version, - indexes, - ); - - let split = SplitOhlc { - open, - high, - low, - close, - }; - - // OhlcCentsToSats handles the high↔low swap internally - let ohlc = LazyOhlcVecs::from_eager_ohlc_indexes::( - "price_ohlc_sats", - version, - ¢s.ohlc, - ); - - Self { split, ohlc, price } - } -} diff --git a/crates/brk_computer/src/prices/sats/mod.rs b/crates/brk_computer/src/prices/sats/mod.rs deleted file mode 100644 index f8623047a..000000000 --- a/crates/brk_computer/src/prices/sats/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -mod import; -mod vecs; - -pub use vecs::Vecs; diff --git a/crates/brk_computer/src/prices/sats/vecs.rs b/crates/brk_computer/src/prices/sats/vecs.rs deleted file mode 100644 index 64e23e78c..000000000 --- a/crates/brk_computer/src/prices/sats/vecs.rs +++ /dev/null @@ -1,19 +0,0 @@ -use brk_traversable::Traversable; -use brk_types::{Cents, Height, OHLCCents, OHLCSats, Sats}; -use vecdb::LazyVecFrom1; - -use crate::internal::{ComputedHeightDerivedLast, LazyEagerIndexes}; -use crate::prices::{ohlcs::LazyOhlcVecs, split::SplitOhlc}; - -#[derive(Clone, Traversable)] -pub struct Vecs { - #[allow(clippy::type_complexity)] - pub split: SplitOhlc< - LazyEagerIndexes, - LazyEagerIndexes, - LazyEagerIndexes, - ComputedHeightDerivedLast, - >, - pub ohlc: LazyOhlcVecs, - pub price: LazyVecFrom1, -} diff --git a/crates/brk_computer/src/prices/split.rs b/crates/brk_computer/src/prices/split.rs deleted file mode 100644 index b89632e29..000000000 --- a/crates/brk_computer/src/prices/split.rs +++ /dev/null @@ -1,9 +0,0 @@ -use brk_traversable::Traversable; - -#[derive(Clone, Traversable)] -pub struct SplitOhlc { - pub open: O, - pub high: H, - pub low: L, - pub close: C, -} diff --git a/crates/brk_computer/src/prices/usd/import.rs b/crates/brk_computer/src/prices/usd/import.rs deleted file mode 100644 index 8e4e291d5..000000000 --- a/crates/brk_computer/src/prices/usd/import.rs +++ /dev/null @@ -1,65 +0,0 @@ -use brk_types::Version; -use vecdb::{LazyVecFrom1, ReadableCloneableVec}; - -use super::super::cents; -use super::Vecs; -use crate::prices::{ohlcs::LazyOhlcVecs, split::SplitOhlc}; -use crate::{ - indexes, - internal::{ - CentsUnsignedToDollars, ComputedHeightDerivedLast, LazyEagerIndexes, OhlcCentsToDollars, - }, -}; - -impl Vecs { - pub(crate) fn forced_import( - version: Version, - indexes: &indexes::Vecs, - cents: ¢s::Vecs, - ) -> Self { - let price = LazyVecFrom1::transformed::( - "price", - version, - cents.price.read_only_boxed_clone(), - ); - - // Dollars are monotonically increasing from cents, so open→open, high→high, low→low - let open = LazyEagerIndexes::from_eager_indexes::( - "price_open", - version, - ¢s.split.open, - ); - let high = LazyEagerIndexes::from_eager_indexes::( - "price_high", - version, - ¢s.split.high, - ); - let low = LazyEagerIndexes::from_eager_indexes::( - "price_low", - version, - ¢s.split.low, - ); - - let close = ComputedHeightDerivedLast::forced_import( - "price_close", - price.read_only_boxed_clone(), - version, - indexes, - ); - - let split = SplitOhlc { - open, - high, - low, - close, - }; - - let ohlc = LazyOhlcVecs::from_eager_ohlc_indexes::( - "price_ohlc", - version, - ¢s.ohlc, - ); - - Self { split, ohlc, price } - } -} diff --git a/crates/brk_computer/src/prices/usd/mod.rs b/crates/brk_computer/src/prices/usd/mod.rs deleted file mode 100644 index f8623047a..000000000 --- a/crates/brk_computer/src/prices/usd/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -mod import; -mod vecs; - -pub use vecs::Vecs; diff --git a/crates/brk_computer/src/prices/usd/vecs.rs b/crates/brk_computer/src/prices/usd/vecs.rs deleted file mode 100644 index f156229e5..000000000 --- a/crates/brk_computer/src/prices/usd/vecs.rs +++ /dev/null @@ -1,19 +0,0 @@ -use brk_traversable::Traversable; -use brk_types::{Cents, Dollars, Height, OHLCCents, OHLCDollars}; -use vecdb::LazyVecFrom1; - -use crate::internal::{ComputedHeightDerivedLast, LazyEagerIndexes}; -use crate::prices::{ohlcs::LazyOhlcVecs, split::SplitOhlc}; - -#[derive(Clone, Traversable)] -pub struct Vecs { - #[allow(clippy::type_complexity)] - pub split: SplitOhlc< - LazyEagerIndexes, - LazyEagerIndexes, - LazyEagerIndexes, - ComputedHeightDerivedLast, - >, - pub ohlc: LazyOhlcVecs, - pub price: LazyVecFrom1, -} diff --git a/crates/brk_query/src/impl/cost_basis.rs b/crates/brk_query/src/impl/cost_basis.rs index 3384995a5..c2411e059 100644 --- a/crates/brk_query/src/impl/cost_basis.rs +++ b/crates/brk_query/src/impl/cost_basis.rs @@ -84,9 +84,9 @@ impl Query { let day1 = Day1::try_from(date).map_err(|e| Error::Parse(e.to_string()))?; let price = &self.computer().prices; let spot = price - .cents .split .close + .cents .day1 .collect_one_flat(day1) .ok_or_else(|| Error::NotFound(format!("No price data for {date}")))?; diff --git a/crates/brk_query/src/impl/price.rs b/crates/brk_query/src/impl/price.rs index 095c5d1d8..43d0c4d08 100644 --- a/crates/brk_query/src/impl/price.rs +++ b/crates/brk_query/src/impl/price.rs @@ -5,7 +5,7 @@ use crate::Query; impl Query { pub fn live_price(&self) -> Result { - let mut oracle = self.computer().prices.cents.live_oracle(self.indexer())?; + let mut oracle = self.computer().prices.live_oracle(self.indexer())?; if let Some(mempool) = self.mempool() { let txs = mempool.get_txs(); diff --git a/modules/brk-client/tests/basic.js b/modules/brk-client/tests/basic.js index e3d287cbd..9112ac42d 100644 --- a/modules/brk-client/tests/basic.js +++ b/modules/brk-client/tests/basic.js @@ -6,40 +6,40 @@ console.log("Testing idiomatic API...\n"); // Test getter access (property) console.log("1. Getter access (.by.dateindex):"); -const all = await client.metrics.prices.usd.split.close.by.day1; +const all = await client.metrics.prices.split.close.usd.by.day1; console.log(` Total: ${all.total}, Got: ${all.data.length} items\n`); // Test dynamic access (bracket notation) console.log("2. Dynamic access (.by['dateindex']):"); -const allDynamic = await client.metrics.prices.usd.split.close.by.day1; +const allDynamic = await client.metrics.prices.split.close.usd.by.day1; console.log( ` Total: ${allDynamic.total}, Got: ${allDynamic.data.length} items\n`, ); // Test fetch all (explicit .fetch()) console.log("3. Explicit .fetch():"); -const allExplicit = await client.metrics.prices.usd.split.close.by.day1.fetch(); +const allExplicit = await client.metrics.prices.split.close.usd.by.day1.fetch(); console.log( ` Total: ${allExplicit.total}, Got: ${allExplicit.data.length} items\n`, ); // Test first(n) console.log("4. First 5 items (.first(5)):"); -const first5 = await client.metrics.prices.usd.split.close.by.day1.first(5); +const first5 = await client.metrics.prices.split.close.usd.by.day1.first(5); console.log( ` Total: ${first5.total}, Start: ${first5.start}, End: ${first5.end}, Got: ${first5.data.length} items\n`, ); // Test last(n) console.log("5. Last 5 items (.last(5)):"); -const last5 = await client.metrics.prices.usd.split.close.by.day1.last(5); +const last5 = await client.metrics.prices.split.close.usd.by.day1.last(5); console.log( ` Total: ${last5.total}, Start: ${last5.start}, End: ${last5.end}, Got: ${last5.data.length} items\n`, ); // Test slice(start, end) console.log("6. Slice 10-20 (.slice(10, 20)):"); -const sliced = await client.metrics.prices.usd.split.close.by.day1.slice( +const sliced = await client.metrics.prices.split.close.usd.by.day1.slice( 10, 20, ); @@ -49,14 +49,14 @@ console.log( // Test get(index) - single item console.log("7. Single item (.get(100)):"); -const single = await client.metrics.prices.usd.split.close.by.day1.get(100); +const single = await client.metrics.prices.split.close.usd.by.day1.get(100); console.log( ` Total: ${single.total}, Start: ${single.start}, End: ${single.end}, Got: ${single.data.length} item(s)\n`, ); // Test skip(n).take(m) chaining console.log("8. Skip and take (.skip(100).take(10)):"); -const skipTake = await client.metrics.prices.usd.split.close.by.day1 +const skipTake = await client.metrics.prices.split.close.usd.by.day1 .skip(100) .take(10); console.log( @@ -65,7 +65,7 @@ console.log( // Test fetchCsv console.log("9. Fetch as CSV (.last(3).fetchCsv()):"); -const csv = await client.metrics.prices.usd.split.close.by.day1 +const csv = await client.metrics.prices.split.close.usd.by.day1 .last(3) .fetchCsv(); console.log(` CSV preview: ${csv.substring(0, 100)}...\n`); diff --git a/modules/brk-client/tests/metric_data.js b/modules/brk-client/tests/metric_data.js index 1bed7ec96..cb44041a8 100644 --- a/modules/brk-client/tests/metric_data.js +++ b/modules/brk-client/tests/metric_data.js @@ -11,7 +11,7 @@ console.log("Testing MetricData helpers...\n"); // Fetch a date-based metric console.log("1. Fetching price data (day1):"); -const price = await client.metrics.prices.usd.split.close.by.day1.first(5); +const price = await client.metrics.prices.split.close.usd.by.day1.first(5); console.log( ` Total: ${price.total}, Start: ${price.start}, End: ${price.end}`, ); @@ -95,7 +95,7 @@ if (count !== 5) throw new Error("Expected 5 iterations"); // Test with non-date-based index (height) console.log("\n11. Testing height-based metric:"); -const heightMetric = await client.metrics.prices.usd.price.by.height.last(3); +const heightMetric = await client.metrics.prices.price.usd.by.height.last(3); console.log( ` Total: ${heightMetric.total}, Start: ${heightMetric.start}, End: ${heightMetric.end}`, ); @@ -135,7 +135,7 @@ console.log(` Iterated ${heightCount} items`); // Test different date indexes console.log("\n13. Testing month1:"); const monthMetric = - await client.metrics.prices.usd.split.close.by.month1.first(3); + await client.metrics.prices.split.close.usd.by.month1.first(3); const monthDates = monthMetric.dates(); console.log(` First month: ${monthDates[0].toISOString()}`); // MonthIndex 0 = Jan 1, 2009 @@ -238,7 +238,7 @@ console.log(` Roundtrip day1 100: ${testDate.toISOString()} -> ${roundtrip}`); // Test slice with Date console.log("\n16. Testing slice with Date:"); -const dateSlice = await client.metrics.prices.usd.split.close.by.day1 +const dateSlice = await client.metrics.prices.split.close.usd.by.day1 .slice(new Date(Date.UTC(2020, 0, 1)), new Date(Date.UTC(2020, 0, 4))) .fetch(); console.log( diff --git a/packages/brk_client/tests/test_basic.py b/packages/brk_client/tests/test_basic.py index 6558d8228..d68a9ad46 100644 --- a/packages/brk_client/tests/test_basic.py +++ b/packages/brk_client/tests/test_basic.py @@ -41,7 +41,7 @@ def test_fetch_typed_metric(): print(a) b = client.metrics.outputs.count.utxo_count.by.height().tail(10).fetch() print(b) - c = client.metrics.prices.usd.split.close.by.day1().tail(10).fetch() + c = client.metrics.prices.split.close.usd.by.day1().tail(10).fetch() print(c) d = ( client.metrics.market.dca.period_lump_sum_stack._10y.usd.by.day1() @@ -61,5 +61,5 @@ def test_fetch_typed_metric(): .fetch() ) print(f) - g = client.metrics.prices.usd.ohlc.by.day1().tail(10).fetch() + g = client.metrics.prices.ohlc.usd.by.day1().tail(10).fetch() print(g) diff --git a/website/scripts/options/market.js b/website/scripts/options/market.js index 7f311806a..e6ad162a8 100644 --- a/website/scripts/options/market.js +++ b/website/scripts/options/market.js @@ -370,7 +370,7 @@ export function createMarketSection() { title: "Sats per Dollar", bottom: [ line({ - metric: prices.sats.split.close, + metric: prices.split.close.sats, name: "Sats/$", unit: Unit.sats, }), diff --git a/website/scripts/panes/chart.js b/website/scripts/panes/chart.js index 6d97ef2f2..1beb3b52a 100644 --- a/website/scripts/panes/chart.js +++ b/website/scripts/panes/chart.js @@ -45,7 +45,7 @@ export function init() { const usdPrice = { type: "Candlestick", title: "Price", - metric: brk.metrics.prices.usd.ohlc, + 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.sats.ohlc, + 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) ?? [])]);