diff --git a/crates/brk_client/src/lib.rs b/crates/brk_client/src/lib.rs index 62ebdcc50..a0c422caa 100644 --- a/crates/brk_client/src/lib.rs +++ b/crates/brk_client/src/lib.rs @@ -2161,10 +2161,10 @@ impl _1m1w1y24hPattern3 { /// Create a new pattern node with accumulated metric name. pub fn new(client: Arc, acc: String) -> Self { Self { - _1m: CentsUsdPattern::new(client.clone(), _m(&acc, "1m_change")), - _1w: CentsUsdPattern::new(client.clone(), _m(&acc, "1w_change")), - _1y: CentsUsdPattern::new(client.clone(), _m(&acc, "1y_change")), - _24h: CentsUsdPattern::new(client.clone(), _m(&acc, "24h_change")), + _1m: CentsUsdPattern::new(client.clone(), _m(&acc, "1m")), + _1w: CentsUsdPattern::new(client.clone(), _m(&acc, "1w")), + _1y: CentsUsdPattern::new(client.clone(), _m(&acc, "1y")), + _24h: CentsUsdPattern::new(client.clone(), _m(&acc, "24h")), } } } @@ -2965,8 +2965,8 @@ impl CentsUsdPattern { /// Create a new pattern node with accumulated metric name. pub fn new(client: Arc, acc: String) -> Self { Self { - cents: MetricPattern1::new(client.clone(), acc.clone()), - usd: MetricPattern1::new(client.clone(), _m(&acc, "usd")), + cents: MetricPattern1::new(client.clone(), _m(&acc, "cents")), + usd: MetricPattern1::new(client.clone(), acc.clone()), } } } diff --git a/crates/brk_computer/src/distribution/address/activity.rs b/crates/brk_computer/src/distribution/address/activity.rs index bd17a770d..556b9bcee 100644 --- a/crates/brk_computer/src/distribution/address/activity.rs +++ b/crates/brk_computer/src/distribution/address/activity.rs @@ -140,22 +140,12 @@ impl ActivityCountVecs { Ok(()) } - pub(crate) fn truncate_push_height( - &mut self, - height: Height, - counts: &BlockActivityCounts, - ) -> Result<()> { - self.reactivated - .height - .truncate_push(height, counts.reactivated.into())?; - self.sending - .height - .truncate_push(height, counts.sending.into())?; - self.receiving - .height - .truncate_push(height, counts.receiving.into())?; - self.both.height.truncate_push(height, counts.both.into())?; - Ok(()) + #[inline(always)] + pub(crate) fn push_height(&mut self, counts: &BlockActivityCounts) { + self.reactivated.height.push(counts.reactivated.into()); + self.sending.height.push(counts.sending.into()); + self.receiving.height.push(counts.receiving.into()); + self.both.height.push(counts.both.into()); } pub(crate) fn compute_rest( @@ -242,15 +232,11 @@ impl AddressTypeToActivityCountVecs { Ok(()) } - pub(crate) fn truncate_push_height( - &mut self, - height: Height, - counts: &AddressTypeToActivityCounts, - ) -> Result<()> { + #[inline(always)] + pub(crate) fn push_height(&mut self, counts: &AddressTypeToActivityCounts) { for (vecs, c) in self.0.values_mut().zip(counts.0.values()) { - vecs.truncate_push_height(height, c)?; + vecs.push_height(c); } - Ok(()) } } @@ -308,14 +294,10 @@ impl AddressActivityVecs { Ok(()) } - pub(crate) fn truncate_push_height( - &mut self, - height: Height, - counts: &AddressTypeToActivityCounts, - ) -> Result<()> { + #[inline(always)] + pub(crate) fn push_height(&mut self, counts: &AddressTypeToActivityCounts) { let totals = counts.totals(); - self.all.truncate_push_height(height, &totals)?; - self.by_address_type.truncate_push_height(height, counts)?; - Ok(()) + self.all.push_height(&totals); + self.by_address_type.push_height(counts); } } diff --git a/crates/brk_computer/src/distribution/address/address_count.rs b/crates/brk_computer/src/distribution/address/address_count.rs index 3f93ad0a0..acec2b208 100644 --- a/crates/brk_computer/src/distribution/address/address_count.rs +++ b/crates/brk_computer/src/distribution/address/address_count.rs @@ -137,19 +137,14 @@ impl AddressTypeToAddressCountVecs { .map(|v| &mut v.height as &mut dyn AnyStoredVec) } - pub(crate) fn truncate_push_height( - &mut self, - height: Height, - address_counts: &AddressTypeToAddressCount, - ) -> Result<()> { + #[inline(always)] + pub(crate) fn push_height(&mut self, address_counts: &AddressTypeToAddressCount) { for (vecs, &count) in self.0.values_mut().zip(address_counts.values()) { - vecs.height.truncate_push(height, count.into())?; + vecs.height.push(count.into()); } - Ok(()) } pub(crate) fn reset_height(&mut self) -> Result<()> { - use vecdb::WritableVec; for v in self.0.values_mut() { v.height.reset()?; } @@ -198,16 +193,10 @@ impl AddressCountsVecs { Ok(()) } - pub(crate) fn truncate_push_height( - &mut self, - height: Height, - total: u64, - address_counts: &AddressTypeToAddressCount, - ) -> Result<()> { - self.all.height.truncate_push(height, total.into())?; - self.by_address_type - .truncate_push_height(height, address_counts)?; - Ok(()) + #[inline(always)] + pub(crate) fn push_height(&mut self, total: u64, address_counts: &AddressTypeToAddressCount) { + self.all.height.push(total.into()); + self.by_address_type.push_height(address_counts); } pub(crate) fn compute_rest( diff --git a/crates/brk_computer/src/distribution/cohorts/address/vecs.rs b/crates/brk_computer/src/distribution/cohorts/address/vecs.rs index b5750b3cb..2198c5a59 100644 --- a/crates/brk_computer/src/distribution/cohorts/address/vecs.rs +++ b/crates/brk_computer/src/distribution/cohorts/address/vecs.rs @@ -155,31 +155,22 @@ impl DynCohortVecs for AddressCohortVecs { Ok(()) } - fn truncate_push(&mut self, height: Height) -> Result<()> { + fn push_state(&mut self, height: Height) { if self.starting_height.is_some_and(|h| h > height) { - return Ok(()); + return; } if let Some(state) = self.state.as_ref() { self.address_count .height - .truncate_push(height, state.address_count.into())?; - self.metrics.supply.truncate_push(height, &state.inner)?; - self.metrics.outputs.truncate_push(height, &state.inner)?; - self.metrics.realized.truncate_push(height, &state.inner)?; + .push(state.address_count.into()); + self.metrics.supply.push_state(&state.inner); + self.metrics.outputs.push_state(&state.inner); + self.metrics.realized.push_state(&state.inner); } - - Ok(()) } - fn compute_then_truncate_push_unrealized_states( - &mut self, - _height: Height, - _height_price: Cents, - _is_day_boundary: bool, - ) -> Result<()> { - Ok(()) - } + fn push_unrealized_state(&mut self, _height_price: Cents) {} fn compute_rest_part1( &mut self, diff --git a/crates/brk_computer/src/distribution/cohorts/traits.rs b/crates/brk_computer/src/distribution/cohorts/traits.rs index 60d4f6491..98acf81dc 100644 --- a/crates/brk_computer/src/distribution/cohorts/traits.rs +++ b/crates/brk_computer/src/distribution/cohorts/traits.rs @@ -20,16 +20,12 @@ pub trait DynCohortVecs: Send + Sync { /// Validate that computed vectors have correct versions. fn validate_computed_versions(&mut self, base_version: Version) -> Result<()>; - /// Push state to height-indexed vectors (truncating if needed). - fn truncate_push(&mut self, height: Height) -> Result<()>; + /// Push state to height-indexed vectors. + /// Height is used for the state_starting_height guard check. + fn push_state(&mut self, height: Height); - /// Compute and push unrealized profit/loss states and percentiles. - fn compute_then_truncate_push_unrealized_states( - &mut self, - height: Height, - height_price: Cents, - is_day_boundary: bool, - ) -> Result<()>; + /// Compute and push unrealized profit/loss states. + fn push_unrealized_state(&mut self, height_price: Cents); /// First phase of post-processing computations. fn compute_rest_part1( diff --git a/crates/brk_computer/src/distribution/cohorts/utxo/groups.rs b/crates/brk_computer/src/distribution/cohorts/utxo/groups.rs index 7366d628d..1d887ec9a 100644 --- a/crates/brk_computer/src/distribution/cohorts/utxo/groups.rs +++ b/crates/brk_computer/src/distribution/cohorts/utxo/groups.rs @@ -339,15 +339,11 @@ impl UTXOCohorts { } /// Push maturation sats to the matured vecs for the given height. - pub(crate) fn push_maturation( - &mut self, - height: Height, - matured: &AgeRange, - ) -> Result<()> { + #[inline(always)] + pub(crate) fn push_maturation(&mut self, matured: &AgeRange) { for (v, &sats) in self.matured.iter_mut().zip(matured.iter()) { - v.base.sats.height.truncate_push(height, sats)?; + v.base.sats.height.push(sats); } - Ok(()) } pub(crate) fn par_iter_separate_mut( @@ -795,8 +791,8 @@ impl UTXOCohorts { } /// Aggregate RealizedFull fields from age_range states and push to all/sth/lth. - /// Called during the block loop after separate cohorts' truncate_push but before reset. - pub(crate) fn push_overlapping_realized_full(&mut self, height: Height) -> Result<()> { + /// Called during the block loop after separate cohorts' push_state but before reset. + pub(crate) fn push_overlapping_realized_full(&mut self) { let Self { all, sth, @@ -823,11 +819,9 @@ impl UTXOCohorts { } } - all.metrics.realized.push_from_accum(&all_acc, height)?; - sth.metrics.realized.push_from_accum(&sth_acc, height)?; - lth.metrics.realized.push_from_accum(<h_acc, height)?; - - Ok(()) + all.metrics.realized.push_accum(&all_acc); + sth.metrics.realized.push_accum(&sth_acc); + lth.metrics.realized.push_accum(<h_acc); } } diff --git a/crates/brk_computer/src/distribution/cohorts/utxo/percentiles.rs b/crates/brk_computer/src/distribution/cohorts/utxo/percentiles.rs index c445a146a..53e6c493b 100644 --- a/crates/brk_computer/src/distribution/cohorts/utxo/percentiles.rs +++ b/crates/brk_computer/src/distribution/cohorts/utxo/percentiles.rs @@ -2,7 +2,7 @@ use std::{cmp::Reverse, collections::BinaryHeap, fs, path::Path}; use brk_cohort::{Filtered, PROFITABILITY_RANGE_COUNT, PROFIT_COUNT, TERM_NAMES}; use brk_error::Result; -use brk_types::{BasisPoints16, Cents, CentsCompact, CostBasisDistribution, Date, Dollars, Height, Sats}; +use brk_types::{BasisPoints16, Cents, CentsCompact, CostBasisDistribution, Date, Dollars, Sats}; use crate::distribution::metrics::{CostBasis, ProfitabilityMetrics}; @@ -16,15 +16,14 @@ impl UTXOCohorts { /// /// Percentiles and profitability are computed per-block from the Fenwick tree. /// Disk distributions are written only at day boundaries via K-way merge. - pub(crate) fn truncate_push_aggregate_percentiles( + pub(crate) fn push_aggregate_percentiles( &mut self, - height: Height, spot_price: Cents, date_opt: Option, states_path: &Path, ) -> Result<()> { if self.fenwick.is_initialized() { - self.push_fenwick_results(height, spot_price)?; + self.push_fenwick_results(spot_price); } // Disk distributions only at day boundaries @@ -36,20 +35,20 @@ impl UTXOCohorts { } /// Push all Fenwick-derived per-block results: percentiles, density, profitability. - fn push_fenwick_results(&mut self, height: Height, spot_price: Cents) -> Result<()> { + fn push_fenwick_results(&mut self, spot_price: Cents) { let (all_d, sth_d, lth_d) = self.fenwick.density(spot_price); let all = self.fenwick.percentiles_all(); - push_cost_basis(height, &all, all_d, &mut self.all.metrics.cost_basis)?; + push_cost_basis(&all, all_d, &mut self.all.metrics.cost_basis); let sth = self.fenwick.percentiles_sth(); - push_cost_basis(height, &sth, sth_d, &mut self.sth.metrics.cost_basis)?; + push_cost_basis(&sth, sth_d, &mut self.sth.metrics.cost_basis); let lth = self.fenwick.percentiles_lth(); - push_cost_basis(height, <h, lth_d, &mut self.lth.metrics.cost_basis)?; + push_cost_basis(<h, lth_d, &mut self.lth.metrics.cost_basis); let prof = self.fenwick.profitability(spot_price); - push_profitability(height, &prof, &mut self.profitability) + push_profitability(&prof, &mut self.profitability); } /// K-way merge only for writing daily cost basis distributions to disk. @@ -92,15 +91,15 @@ impl UTXOCohorts { } /// Push percentiles + density to cost basis vecs. +#[inline(always)] fn push_cost_basis( - height: Height, percentiles: &PercentileResult, density_bps: u16, cost_basis: &mut CostBasis, -) -> Result<()> { - cost_basis.truncate_push_minmax(height, percentiles.min_price, percentiles.max_price)?; - cost_basis.truncate_push_percentiles(height, &percentiles.sat_prices, &percentiles.usd_prices)?; - cost_basis.truncate_push_density(height, BasisPoints16::from(density_bps)) +) { + cost_basis.push_minmax(percentiles.min_price, percentiles.max_price); + cost_basis.push_percentiles(&percentiles.sat_prices, &percentiles.usd_prices); + cost_basis.push_density(BasisPoints16::from(density_bps)); } /// Convert raw (cents × sats) accumulator to Dollars (÷ 100 for cents→dollars, ÷ 1e8 for sats). @@ -111,13 +110,9 @@ fn raw_usd_to_dollars(raw: u128) -> Dollars { /// Push profitability range + profit/loss aggregate values to vecs. fn push_profitability( - height: Height, buckets: &[ProfitabilityRangeResult; PROFITABILITY_RANGE_COUNT], metrics: &mut ProfitabilityMetrics, -) -> Result<()> { - // Truncate all buckets once upfront to avoid per-push checks - metrics.truncate(height)?; - +) { // Push 25 range buckets for (i, bucket) in metrics.range.as_array_mut().into_iter().enumerate() { let r = &buckets[i]; @@ -170,8 +165,6 @@ fn push_profitability( raw_usd_to_dollars(cum_sth_usd), ); } - - Ok(()) } fn write_distribution( diff --git a/crates/brk_computer/src/distribution/cohorts/utxo/vecs/core.rs b/crates/brk_computer/src/distribution/cohorts/utxo/vecs/core.rs index bfd1bb28a..dbad94bb8 100644 --- a/crates/brk_computer/src/distribution/cohorts/utxo/vecs/core.rs +++ b/crates/brk_computer/src/distribution/cohorts/utxo/vecs/core.rs @@ -28,38 +28,30 @@ impl DynCohortVecs for UTXOCohortVecs { self.metrics.validate_computed_versions(base_version) } - fn truncate_push(&mut self, height: Height) -> Result<()> { + fn push_state(&mut self, height: Height) { if self.state_starting_height.is_some_and(|h| h > height) { - return Ok(()); + return; } if let Some(state) = self.state.as_ref() { - self.metrics.supply.truncate_push(height, state)?; - self.metrics.outputs.truncate_push(height, state)?; - self.metrics.activity.truncate_push(height, state)?; - self.metrics.realized.truncate_push(height, state)?; + self.metrics.supply.push_state(state); + self.metrics.outputs.push_state(state); + self.metrics.activity.push_state(state); + self.metrics.realized.push_state(state); } - - Ok(()) } - fn compute_then_truncate_push_unrealized_states( - &mut self, - height: Height, - height_price: Cents, - _is_day_boundary: bool, - ) -> Result<()> { + fn push_unrealized_state(&mut self, height_price: Cents) { if let Some(state) = self.state.as_mut() { state.apply_pending(); let unrealized_state = state.compute_unrealized_state(height_price); self.metrics .unrealized - .truncate_push(height, &unrealized_state)?; + .push_state(&unrealized_state); self.metrics .supply - .truncate_push_profitability(height, &unrealized_state)?; + .push_profitability(&unrealized_state); } - Ok(()) } fn compute_rest_part1( diff --git a/crates/brk_computer/src/distribution/cohorts/utxo/vecs/minimal.rs b/crates/brk_computer/src/distribution/cohorts/utxo/vecs/minimal.rs index 2197df160..77cb24136 100644 --- a/crates/brk_computer/src/distribution/cohorts/utxo/vecs/minimal.rs +++ b/crates/brk_computer/src/distribution/cohorts/utxo/vecs/minimal.rs @@ -31,28 +31,19 @@ impl DynCohortVecs for UTXOCohortVecs { Ok(()) } - fn truncate_push(&mut self, height: Height) -> Result<()> { + fn push_state(&mut self, height: Height) { if self.state_starting_height.is_some_and(|h| h > height) { - return Ok(()); + return; } if let Some(state) = self.state.as_ref() { - self.metrics.supply.truncate_push(height, state)?; - self.metrics.outputs.truncate_push(height, state)?; - self.metrics.realized.truncate_push(height, state)?; + self.metrics.supply.push_state(state); + self.metrics.outputs.push_state(state); + self.metrics.realized.push_state(state); } - - Ok(()) } - fn compute_then_truncate_push_unrealized_states( - &mut self, - _height: Height, - _height_price: Cents, - _is_day_boundary: bool, - ) -> Result<()> { - Ok(()) - } + fn push_unrealized_state(&mut self, _height_price: Cents) {} fn compute_rest_part1( &mut self, diff --git a/crates/brk_computer/src/distribution/cohorts/utxo/vecs/mod.rs b/crates/brk_computer/src/distribution/cohorts/utxo/vecs/mod.rs index 05d1404ed..c03fbaa2c 100644 --- a/crates/brk_computer/src/distribution/cohorts/utxo/vecs/mod.rs +++ b/crates/brk_computer/src/distribution/cohorts/utxo/vecs/mod.rs @@ -166,29 +166,21 @@ impl DynCohortVecs for UTXOCohortVecs { self.metrics.validate_computed_versions(base_version) } - fn truncate_push(&mut self, height: Height) -> Result<()> { + fn push_state(&mut self, height: Height) { if self.state_starting_height.is_some_and(|h| h > height) { - return Ok(()); + return; } if let Some(state) = self.state.as_ref() { - self.metrics.truncate_push(height, state)?; + self.metrics.push_state(state); } - - Ok(()) } - fn compute_then_truncate_push_unrealized_states( - &mut self, - height: Height, - height_price: Cents, - _is_day_boundary: bool, - ) -> Result<()> { + fn push_unrealized_state(&mut self, height_price: Cents) { if let Some(state) = self.state.as_mut() { self.metrics - .compute_and_push_unrealized(height, height_price, state)?; + .compute_and_push_unrealized(height_price, state); } - Ok(()) } fn compute_rest_part1( diff --git a/crates/brk_computer/src/distribution/cohorts/utxo/vecs/type.rs b/crates/brk_computer/src/distribution/cohorts/utxo/vecs/type.rs index 8cd82ba5d..b3872bd58 100644 --- a/crates/brk_computer/src/distribution/cohorts/utxo/vecs/type.rs +++ b/crates/brk_computer/src/distribution/cohorts/utxo/vecs/type.rs @@ -28,37 +28,29 @@ impl DynCohortVecs for UTXOCohortVecs { Ok(()) } - fn truncate_push(&mut self, height: Height) -> Result<()> { + fn push_state(&mut self, height: Height) { if self.state_starting_height.is_some_and(|h| h > height) { - return Ok(()); + return; } if let Some(state) = self.state.as_ref() { - self.metrics.supply.truncate_push(height, state)?; - self.metrics.outputs.truncate_push(height, state)?; - self.metrics.realized.truncate_push(height, state)?; + self.metrics.supply.push_state(state); + self.metrics.outputs.push_state(state); + self.metrics.realized.push_state(state); } - - Ok(()) } - fn compute_then_truncate_push_unrealized_states( - &mut self, - height: Height, - height_price: Cents, - _is_day_boundary: bool, - ) -> Result<()> { + fn push_unrealized_state(&mut self, height_price: Cents) { if let Some(state) = self.state.as_mut() { state.apply_pending(); let unrealized_state = state.compute_unrealized_state(height_price); self.metrics .unrealized - .truncate_push(height, &unrealized_state)?; + .push_state(&unrealized_state); self.metrics .supply - .truncate_push_profitability(height, &unrealized_state)?; + .push_profitability(&unrealized_state); } - Ok(()) } fn compute_rest_part1( diff --git a/crates/brk_computer/src/distribution/compute/block_loop.rs b/crates/brk_computer/src/distribution/compute/block_loop.rs index db2def5a6..f9f7fad2a 100644 --- a/crates/brk_computer/src/distribution/compute/block_loop.rs +++ b/crates/brk_computer/src/distribution/compute/block_loop.rs @@ -7,7 +7,7 @@ use brk_types::{ use rayon::prelude::*; use rustc_hash::FxHashSet; use tracing::{debug, info}; -use vecdb::{AnyVec, Exit, ReadableVec, VecIndex, WritableVec}; +use vecdb::{AnyStoredVec, AnyVec, Exit, ReadableVec, VecIndex, WritableVec}; use crate::{ distribution::{ @@ -210,6 +210,22 @@ pub(crate) fn process_blocks( // Initialize Fenwick tree from imported BTreeMap state (one-time) vecs.utxo_cohorts.init_fenwick_if_needed(); + // Pre-truncate all stored vecs to starting_height (one-time). + // This eliminates per-push truncation checks inside the block loop. + { + let start = starting_height.to_usize(); + vecs.utxo_cohorts + .par_iter_vecs_mut() + .chain(vecs.address_cohorts.par_iter_vecs_mut()) + .chain(vecs.addresses.funded.par_iter_height_mut()) + .chain(vecs.addresses.empty.par_iter_height_mut()) + .chain(vecs.addresses.activity.par_iter_height_mut()) + .chain(rayon::iter::once( + &mut vecs.coinblocks_destroyed.base.height as &mut dyn AnyStoredVec, + )) + .try_for_each(|v| v.any_truncate_if_needed_at(start))?; + } + // Reusable hashsets (avoid per-block allocation) let mut received_addresses = ByAddressType::>::default(); let mut seen_senders = ByAddressType::>::default(); @@ -364,14 +380,13 @@ pub(crate) fn process_blocks( blocks_old as u128 * u64::from(sent.spendable_supply.value) as u128 }) .sum(); - vecs.coinblocks_destroyed.base.height.truncate_push( - height, + vecs.coinblocks_destroyed.base.height.push( StoredF64::from(total_satblocks as f64 / Sats::ONE_BTC_U128 as f64), - )?; + ); } // Record maturation (sats crossing age boundaries) - vecs.utxo_cohorts.push_maturation(height, &matured)?; + vecs.utxo_cohorts.push_maturation(&matured); // Build set of addresses that received this block (for detecting "both" in sent) // Reuse pre-allocated hashsets: clear preserves capacity, avoiding reallocation @@ -437,14 +452,10 @@ pub(crate) fn process_blocks( // Push to height-indexed vectors vecs.addresses.funded - .truncate_push_height(height, address_counts.sum(), &address_counts)?; - vecs.addresses.empty.truncate_push_height( - height, - empty_address_counts.sum(), - &empty_address_counts, - )?; - vecs.addresses.activity - .truncate_push_height(height, &activity_counts)?; + .push_height(address_counts.sum(), &address_counts); + vecs.addresses.empty + .push_height(empty_address_counts.sum(), &empty_address_counts); + vecs.addresses.activity.push_height(&activity_counts); let is_last_of_day = is_last_of_day[offset]; let date_opt = is_last_of_day.then(|| Date::from(timestamp)); @@ -454,11 +465,9 @@ pub(crate) fn process_blocks( &mut vecs.address_cohorts, height, block_price, - date_opt.is_some(), - )?; + ); - vecs.utxo_cohorts.truncate_push_aggregate_percentiles( - height, + vecs.utxo_cohorts.push_aggregate_percentiles( block_price, date_opt, &vecs.states_path, @@ -521,42 +530,29 @@ fn push_cohort_states( address_cohorts: &mut AddressCohorts, height: Height, height_price: Cents, - is_day_boundary: bool, -) -> Result<()> { +) { // Phase 1: push + unrealized (no reset yet — states still needed for aggregation) - let (r1, r2) = rayon::join( + rayon::join( || { utxo_cohorts .par_iter_separate_mut() - .try_for_each(|v| -> Result<()> { - v.truncate_push(height)?; - v.compute_then_truncate_push_unrealized_states( - height, - height_price, - is_day_boundary, - )?; - Ok(()) + .for_each(|v| { + v.push_state(height); + v.push_unrealized_state(height_price); }) }, || { address_cohorts .par_iter_separate_mut() - .try_for_each(|v| -> Result<()> { - v.truncate_push(height)?; - v.compute_then_truncate_push_unrealized_states( - height, - height_price, - is_day_boundary, - )?; - Ok(()) + .for_each(|v| { + v.push_state(height); + v.push_unrealized_state(height_price); }) }, ); - r1?; - r2?; // Phase 2: aggregate age_range realized states → push to overlapping cohorts' RealizedFull - utxo_cohorts.push_overlapping_realized_full(height)?; + utxo_cohorts.push_overlapping_realized_full(); // Phase 3: reset per-block values utxo_cohorts @@ -565,6 +561,4 @@ fn push_cohort_states( address_cohorts .iter_separate_mut() .for_each(|v| v.reset_single_iteration_values()); - - Ok(()) } diff --git a/crates/brk_computer/src/distribution/metrics/activity/core.rs b/crates/brk_computer/src/distribution/metrics/activity/core.rs index 5e3039be3..21c03a913 100644 --- a/crates/brk_computer/src/distribution/metrics/activity/core.rs +++ b/crates/brk_computer/src/distribution/metrics/activity/core.rs @@ -1,6 +1,6 @@ use brk_error::Result; use brk_traversable::Traversable; -use brk_types::{Bitcoin, Height, Indexes, Sats, StoredF64, Version}; +use brk_types::{Bitcoin, Indexes, Sats, StoredF64, Version}; use vecdb::{AnyStoredVec, AnyVec, Exit, Rw, StorageMode, WritableVec}; use crate::{ @@ -40,27 +40,25 @@ impl ActivityCore { .min(self.sent_in_loss.base.sats.height.len()) } - pub(crate) fn truncate_push( + #[inline(always)] + pub(crate) fn push_state( &mut self, - height: Height, state: &CohortState, - ) -> Result<()> { - self.sent.base.height.truncate_push(height, state.sent)?; - self.coindays_destroyed.base.height.truncate_push( - height, + ) { + self.sent.base.height.push(state.sent); + self.coindays_destroyed.base.height.push( StoredF64::from(Bitcoin::from(state.satdays_destroyed)), - )?; + ); self.sent_in_profit .base .sats .height - .truncate_push(height, state.realized.sent_in_profit())?; + .push(state.realized.sent_in_profit()); self.sent_in_loss .base .sats .height - .truncate_push(height, state.realized.sent_in_loss())?; - Ok(()) + .push(state.realized.sent_in_loss()); } pub(crate) fn collect_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> { diff --git a/crates/brk_computer/src/distribution/metrics/activity/full.rs b/crates/brk_computer/src/distribution/metrics/activity/full.rs index 374fd4cb4..ad59ee9e0 100644 --- a/crates/brk_computer/src/distribution/metrics/activity/full.rs +++ b/crates/brk_computer/src/distribution/metrics/activity/full.rs @@ -1,6 +1,6 @@ use brk_error::Result; use brk_traversable::Traversable; -use brk_types::{Bitcoin, Height, Indexes, StoredF32, StoredF64, Version}; +use brk_types::{Bitcoin, Indexes, StoredF32, StoredF64, Version}; use derive_more::{Deref, DerefMut}; use vecdb::{AnyStoredVec, Exit, ReadableCloneableVec, Rw, StorageMode}; @@ -45,12 +45,12 @@ impl ActivityFull { self.inner.min_len() } - pub(crate) fn full_truncate_push( + #[inline(always)] + pub(crate) fn full_push_state( &mut self, - height: Height, state: &CohortState, - ) -> Result<()> { - self.inner.truncate_push(height, state) + ) { + self.inner.push_state(state); } pub(crate) fn collect_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> { diff --git a/crates/brk_computer/src/distribution/metrics/activity/mod.rs b/crates/brk_computer/src/distribution/metrics/activity/mod.rs index ab0577109..5b0485d3a 100644 --- a/crates/brk_computer/src/distribution/metrics/activity/mod.rs +++ b/crates/brk_computer/src/distribution/metrics/activity/mod.rs @@ -5,7 +5,7 @@ pub use self::core::ActivityCore; pub use full::ActivityFull; use brk_error::Result; -use brk_types::{Height, Indexes, Version}; +use brk_types::{Indexes, Version}; use vecdb::Exit; use crate::distribution::state::{CohortState, CostBasisOps, RealizedOps}; @@ -14,11 +14,10 @@ pub trait ActivityLike: Send + Sync { fn as_core(&self) -> &ActivityCore; fn as_core_mut(&mut self) -> &mut ActivityCore; fn min_len(&self) -> usize; - fn truncate_push( + fn push_state( &mut self, - height: Height, state: &CohortState, - ) -> Result<()>; + ); fn validate_computed_versions(&mut self, base_version: Version) -> Result<()>; fn compute_from_stateful( &mut self, @@ -37,8 +36,8 @@ impl ActivityLike for ActivityCore { fn as_core(&self) -> &ActivityCore { self } fn as_core_mut(&mut self) -> &mut ActivityCore { self } fn min_len(&self) -> usize { self.min_len() } - fn truncate_push(&mut self, height: Height, state: &CohortState) -> Result<()> { - self.truncate_push(height, state) + fn push_state(&mut self, state: &CohortState) { + self.push_state(state); } fn validate_computed_versions(&mut self, base_version: Version) -> Result<()> { self.validate_computed_versions(base_version) @@ -55,8 +54,8 @@ impl ActivityLike for ActivityFull { fn as_core(&self) -> &ActivityCore { &self.inner } fn as_core_mut(&mut self) -> &mut ActivityCore { &mut self.inner } fn min_len(&self) -> usize { self.full_min_len() } - fn truncate_push(&mut self, height: Height, state: &CohortState) -> Result<()> { - self.full_truncate_push(height, state) + fn push_state(&mut self, state: &CohortState) { + self.full_push_state(state); } fn validate_computed_versions(&mut self, base_version: Version) -> Result<()> { self.inner.validate_computed_versions(base_version) diff --git a/crates/brk_computer/src/distribution/metrics/cost_basis/mod.rs b/crates/brk_computer/src/distribution/metrics/cost_basis/mod.rs index 2cea43388..e826ab7e8 100644 --- a/crates/brk_computer/src/distribution/metrics/cost_basis/mod.rs +++ b/crates/brk_computer/src/distribution/metrics/cost_basis/mod.rs @@ -1,6 +1,6 @@ use brk_error::Result; use brk_traversable::Traversable; -use brk_types::{BasisPoints16, Cents, Height, Version}; +use brk_types::{BasisPoints16, Cents, Version}; use vecdb::{AnyStoredVec, AnyVec, Rw, StorageMode, WritableVec}; use crate::internal::{PerBlock, PercentPerBlock, PercentilesVecs, Price, PERCENTILES_LEN}; @@ -53,34 +53,25 @@ impl CostBasis { .min(self.supply_density.bps.height.len()) } - pub(crate) fn truncate_push_minmax( - &mut self, - height: Height, - min_price: Cents, - max_price: Cents, - ) -> Result<()> { - self.min.cents.height.truncate_push(height, min_price)?; - self.max.cents.height.truncate_push(height, max_price)?; - Ok(()) + #[inline(always)] + pub(crate) fn push_minmax(&mut self, min_price: Cents, max_price: Cents) { + self.min.cents.height.push(min_price); + self.max.cents.height.push(max_price); } - pub(crate) fn truncate_push_percentiles( + #[inline(always)] + pub(crate) fn push_percentiles( &mut self, - height: Height, sat_prices: &[Cents; PERCENTILES_LEN], usd_prices: &[Cents; PERCENTILES_LEN], - ) -> Result<()> { - self.percentiles.truncate_push(height, sat_prices)?; - self.invested_capital.truncate_push(height, usd_prices)?; - Ok(()) + ) { + self.percentiles.push(sat_prices); + self.invested_capital.push(usd_prices); } - pub(crate) fn truncate_push_density( - &mut self, - height: Height, - density_bps: BasisPoints16, - ) -> Result<()> { - Ok(self.supply_density.bps.height.truncate_push(height, density_bps)?) + #[inline(always)] + pub(crate) fn push_density(&mut self, density_bps: BasisPoints16) { + self.supply_density.bps.height.push(density_bps); } pub(crate) fn validate_computed_versions(&mut self, base_version: Version) -> Result<()> { diff --git a/crates/brk_computer/src/distribution/metrics/mod.rs b/crates/brk_computer/src/distribution/metrics/mod.rs index 4de276b52..cf1395e14 100644 --- a/crates/brk_computer/src/distribution/metrics/mod.rs +++ b/crates/brk_computer/src/distribution/metrics/mod.rs @@ -85,7 +85,7 @@ pub use unrealized::{ use brk_cohort::Filter; use brk_error::Result; -use brk_types::{Cents, Height, Indexes, Version}; +use brk_types::{Cents, Indexes, Version}; use vecdb::{AnyStoredVec, Exit, StorageMode}; use crate::{ @@ -183,17 +183,13 @@ pub trait CohortMetricsBase: /// Apply pending state, compute and push unrealized state. fn compute_and_push_unrealized( &mut self, - height: Height, height_price: Cents, state: &mut CohortState>, - ) -> Result<()> { + ) { state.apply_pending(); let unrealized_state = state.compute_unrealized_state(height_price); - self.unrealized_mut() - .truncate_push(height, &unrealized_state)?; - self.supply_mut() - .truncate_push_profitability(height, &unrealized_state)?; - Ok(()) + self.unrealized_mut().push_state(&unrealized_state); + self.supply_mut().push_profitability(&unrealized_state); } fn collect_all_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec>; @@ -207,16 +203,14 @@ pub trait CohortMetricsBase: .min(self.unrealized().min_stateful_len()) } - fn truncate_push( + fn push_state( &mut self, - height: Height, state: &CohortState>, - ) -> Result<()> { - self.supply_mut().truncate_push(height, state)?; - self.outputs_mut().truncate_push(height, state)?; - self.activity_mut().truncate_push(height, state)?; - self.realized_mut().truncate_push(height, state)?; - Ok(()) + ) { + self.supply_mut().push_state(state); + self.outputs_mut().push_state(state); + self.activity_mut().push_state(state); + self.realized_mut().push_state(state); } /// First phase of computed metrics (indexes from height). diff --git a/crates/brk_computer/src/distribution/metrics/outputs/base.rs b/crates/brk_computer/src/distribution/metrics/outputs/base.rs index fd9984851..803a3ae35 100644 --- a/crates/brk_computer/src/distribution/metrics/outputs/base.rs +++ b/crates/brk_computer/src/distribution/metrics/outputs/base.rs @@ -1,6 +1,6 @@ use brk_error::Result; use brk_traversable::Traversable; -use brk_types::{BasisPointsSigned32, Height, Indexes, StoredI64, StoredU64, Version}; +use brk_types::{BasisPointsSigned32, Indexes, StoredI64, StoredU64, Version}; use vecdb::{AnyStoredVec, AnyVec, Exit, Rw, StorageMode, WritableVec}; use crate::{ @@ -35,11 +35,11 @@ impl OutputsBase { self.unspent_count.height.len() } - pub(crate) fn truncate_push(&mut self, height: Height, state: &CohortState) -> Result<()> { + #[inline(always)] + pub(crate) fn push_state(&mut self, state: &CohortState) { self.unspent_count .height - .truncate_push(height, StoredU64::from(state.supply.utxo_count))?; - Ok(()) + .push(StoredU64::from(state.supply.utxo_count)); } pub(crate) fn collect_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> { diff --git a/crates/brk_computer/src/distribution/metrics/profitability.rs b/crates/brk_computer/src/distribution/metrics/profitability.rs index 97f9cb4e4..938e72cbd 100644 --- a/crates/brk_computer/src/distribution/metrics/profitability.rs +++ b/crates/brk_computer/src/distribution/metrics/profitability.rs @@ -2,7 +2,7 @@ use brk_cohort::{Loss, Profit, ProfitabilityRange}; use brk_error::Result; use brk_traversable::Traversable; use brk_types::{ - BasisPoints32, BasisPointsSigned32, Cents, Dollars, Height, Indexes, Sats, StoredF32, Version, + BasisPoints32, BasisPointsSigned32, Cents, Dollars, Indexes, Sats, StoredF32, Version, }; use vecdb::{AnyStoredVec, AnyVec, Database, Exit, Rw, StorageMode, WritableVec}; @@ -103,15 +103,6 @@ impl ProfitabilityBucket { }) } - #[inline(always)] - pub(crate) fn truncate(&mut self, height: Height) -> Result<()> { - self.supply.all.sats.height.truncate_if_needed(height)?; - self.supply.sth.sats.height.truncate_if_needed(height)?; - self.realized_cap.all.height.truncate_if_needed(height)?; - self.realized_cap.sth.height.truncate_if_needed(height)?; - Ok(()) - } - #[inline(always)] pub(crate) fn push( &mut self, @@ -223,10 +214,6 @@ impl ProfitabilityMetrics { } impl ProfitabilityMetrics { - pub(crate) fn truncate(&mut self, height: Height) -> Result<()> { - self.iter_mut().try_for_each(|b| b.truncate(height)) - } - pub(crate) fn forced_import( db: &Database, version: Version, diff --git a/crates/brk_computer/src/distribution/metrics/realized/core.rs b/crates/brk_computer/src/distribution/metrics/realized/core.rs index 4e9cec4af..b8c25ff7d 100644 --- a/crates/brk_computer/src/distribution/metrics/realized/core.rs +++ b/crates/brk_computer/src/distribution/metrics/realized/core.rs @@ -73,9 +73,9 @@ impl RealizedCore { self.minimal.min_stateful_len() } - pub(crate) fn truncate_push(&mut self, height: Height, state: &CohortState) -> Result<()> { - self.minimal.truncate_push(height, state)?; - Ok(()) + #[inline(always)] + pub(crate) fn push_state(&mut self, state: &CohortState) { + self.minimal.push_state(state); } pub(crate) fn collect_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> { diff --git a/crates/brk_computer/src/distribution/metrics/realized/full.rs b/crates/brk_computer/src/distribution/metrics/realized/full.rs index 4843714a0..98dc27ce8 100644 --- a/crates/brk_computer/src/distribution/metrics/realized/full.rs +++ b/crates/brk_computer/src/distribution/metrics/realized/full.rs @@ -234,49 +234,47 @@ impl RealizedFull { .min(self.peak_regret.value.base.height.len()) } - pub(crate) fn truncate_push( + #[inline(always)] + pub(crate) fn push_state( &mut self, - height: Height, state: &CohortState>, - ) -> Result<()> { - self.core.truncate_push(height, state)?; + ) { + self.core.push_state(state); self.profit .value_created .base .height - .truncate_push(height, state.realized.profit_value_created())?; + .push(state.realized.profit_value_created()); self.profit .value_destroyed .base .height - .truncate_push(height, state.realized.profit_value_destroyed())?; + .push(state.realized.profit_value_destroyed()); self.loss .value_created .base .height - .truncate_push(height, state.realized.loss_value_created())?; + .push(state.realized.loss_value_created()); self.loss .value_destroyed .base .height - .truncate_push(height, state.realized.loss_value_destroyed())?; + .push(state.realized.loss_value_destroyed()); self.investor .price .cents .height - .truncate_push(height, state.realized.investor_price())?; + .push(state.realized.investor_price()); self.cap_raw - .truncate_push(height, state.realized.cap_raw())?; + .push(state.realized.cap_raw()); self.investor .cap_raw - .truncate_push(height, state.realized.investor_cap_raw())?; + .push(state.realized.investor_cap_raw()); self.peak_regret .value .base .height - .truncate_push(height, state.realized.peak_regret())?; - - Ok(()) + .push(state.realized.peak_regret()); } pub(crate) fn collect_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> { @@ -304,36 +302,36 @@ impl RealizedFull { Ok(()) } - pub(crate) fn push_from_accum( + #[inline(always)] + pub(crate) fn push_accum( &mut self, accum: &RealizedFullAccum, - height: Height, - ) -> Result<()> { + ) { self.profit .value_created .base .height - .truncate_push(height, accum.profit_value_created())?; + .push(accum.profit_value_created()); self.profit .value_destroyed .base .height - .truncate_push(height, accum.profit_value_destroyed())?; + .push(accum.profit_value_destroyed()); self.loss .value_created .base .height - .truncate_push(height, accum.loss_value_created())?; + .push(accum.loss_value_created()); self.loss .value_destroyed .base .height - .truncate_push(height, accum.loss_value_destroyed())?; + .push(accum.loss_value_destroyed()); self.cap_raw - .truncate_push(height, accum.cap_raw)?; + .push(accum.cap_raw); self.investor .cap_raw - .truncate_push(height, accum.investor_cap_raw)?; + .push(accum.investor_cap_raw); let investor_price = { let cap = accum.cap_raw.as_u128(); @@ -347,15 +345,13 @@ impl RealizedFull { .price .cents .height - .truncate_push(height, investor_price)?; + .push(investor_price); self.peak_regret .value .base .height - .truncate_push(height, accum.peak_regret())?; - - Ok(()) + .push(accum.peak_regret()); } pub(crate) fn compute_rest_part1( diff --git a/crates/brk_computer/src/distribution/metrics/realized/minimal.rs b/crates/brk_computer/src/distribution/metrics/realized/minimal.rs index 6e3f4e833..c752de780 100644 --- a/crates/brk_computer/src/distribution/metrics/realized/minimal.rs +++ b/crates/brk_computer/src/distribution/metrics/realized/minimal.rs @@ -80,21 +80,21 @@ impl RealizedMinimal { .min(self.sopr.value_destroyed.base.height.len()) } - pub(crate) fn truncate_push(&mut self, height: Height, state: &CohortState) -> Result<()> { - self.cap.cents.height.truncate_push(height, state.realized.cap())?; - self.profit.base.cents.height.truncate_push(height, state.realized.profit())?; - self.loss.base.cents.height.truncate_push(height, state.realized.loss())?; + #[inline(always)] + pub(crate) fn push_state(&mut self, state: &CohortState) { + self.cap.cents.height.push(state.realized.cap()); + self.profit.base.cents.height.push(state.realized.profit()); + self.loss.base.cents.height.push(state.realized.loss()); self.sopr .value_created .base .height - .truncate_push(height, state.realized.value_created())?; + .push(state.realized.value_created()); self.sopr .value_destroyed .base .height - .truncate_push(height, state.realized.value_destroyed())?; - Ok(()) + .push(state.realized.value_destroyed()); } pub(crate) fn collect_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> { diff --git a/crates/brk_computer/src/distribution/metrics/realized/mod.rs b/crates/brk_computer/src/distribution/metrics/realized/mod.rs index 89cb3d868..70e39fe88 100644 --- a/crates/brk_computer/src/distribution/metrics/realized/mod.rs +++ b/crates/brk_computer/src/distribution/metrics/realized/mod.rs @@ -9,7 +9,7 @@ pub use full::{RealizedFull, RealizedFullAccum}; pub use minimal::RealizedMinimal; use brk_error::Result; -use brk_types::{Height, Indexes}; +use brk_types::Indexes; use vecdb::Exit; use crate::distribution::state::{WithCapital, CohortState, CostBasisData, RealizedState}; @@ -18,7 +18,7 @@ pub trait RealizedLike: Send + Sync { fn as_core(&self) -> &RealizedCore; fn as_core_mut(&mut self) -> &mut RealizedCore; fn min_stateful_len(&self) -> usize; - fn truncate_push(&mut self, height: Height, state: &CohortState>) -> Result<()>; + fn push_state(&mut self, state: &CohortState>); fn compute_rest_part1(&mut self, starting_indexes: &Indexes, exit: &Exit) -> Result<()>; fn compute_from_stateful( &mut self, @@ -32,8 +32,9 @@ impl RealizedLike for RealizedCore { fn as_core(&self) -> &RealizedCore { self } fn as_core_mut(&mut self) -> &mut RealizedCore { self } fn min_stateful_len(&self) -> usize { self.min_stateful_len() } - fn truncate_push(&mut self, height: Height, state: &CohortState>) -> Result<()> { - self.truncate_push(height, state) + #[inline(always)] + fn push_state(&mut self, state: &CohortState>) { + self.push_state(state) } fn compute_rest_part1(&mut self, starting_indexes: &Indexes, exit: &Exit) -> Result<()> { self.compute_rest_part1(starting_indexes, exit) @@ -47,8 +48,9 @@ impl RealizedLike for RealizedFull { fn as_core(&self) -> &RealizedCore { &self.core } fn as_core_mut(&mut self) -> &mut RealizedCore { &mut self.core } fn min_stateful_len(&self) -> usize { self.min_stateful_len() } - fn truncate_push(&mut self, height: Height, state: &CohortState>) -> Result<()> { - self.truncate_push(height, state) + #[inline(always)] + fn push_state(&mut self, state: &CohortState>) { + self.push_state(state) } fn compute_rest_part1(&mut self, starting_indexes: &Indexes, exit: &Exit) -> Result<()> { self.compute_rest_part1(starting_indexes, exit) diff --git a/crates/brk_computer/src/distribution/metrics/supply/base.rs b/crates/brk_computer/src/distribution/metrics/supply/base.rs index bc46637fb..5c69dabe1 100644 --- a/crates/brk_computer/src/distribution/metrics/supply/base.rs +++ b/crates/brk_computer/src/distribution/metrics/supply/base.rs @@ -50,9 +50,9 @@ impl SupplyBase { self.total.sats.height.len() } - pub(crate) fn truncate_push(&mut self, height: Height, state: &CohortState) -> Result<()> { - self.total.sats.height.truncate_push(height, state.supply.value)?; - Ok(()) + #[inline(always)] + pub(crate) fn push_state(&mut self, state: &CohortState) { + self.total.sats.height.push(state.supply.value); } pub(crate) fn collect_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> { diff --git a/crates/brk_computer/src/distribution/metrics/supply/core.rs b/crates/brk_computer/src/distribution/metrics/supply/core.rs index 43f631e26..0eddea735 100644 --- a/crates/brk_computer/src/distribution/metrics/supply/core.rs +++ b/crates/brk_computer/src/distribution/metrics/supply/core.rs @@ -43,20 +43,10 @@ impl SupplyCore { .min(self.in_loss.sats.height.len()) } - pub(crate) fn truncate_push_profitability( - &mut self, - height: Height, - state: &UnrealizedState, - ) -> Result<()> { - self.in_profit - .sats - .height - .truncate_push(height, state.supply_in_profit)?; - self.in_loss - .sats - .height - .truncate_push(height, state.supply_in_loss)?; - Ok(()) + #[inline(always)] + pub(crate) fn push_profitability(&mut self, state: &UnrealizedState) { + self.in_profit.sats.height.push(state.supply_in_profit); + self.in_loss.sats.height.push(state.supply_in_loss); } pub(crate) fn collect_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> { diff --git a/crates/brk_computer/src/distribution/metrics/unrealized/base.rs b/crates/brk_computer/src/distribution/metrics/unrealized/base.rs index e448ad49d..137a02305 100644 --- a/crates/brk_computer/src/distribution/metrics/unrealized/base.rs +++ b/crates/brk_computer/src/distribution/metrics/unrealized/base.rs @@ -55,31 +55,18 @@ impl UnrealizedBase { .min(self.investor_cap_in_loss_raw.len()) } - pub(crate) fn truncate_push( - &mut self, - height: Height, - height_state: &UnrealizedState, - ) -> Result<()> { - self.core.truncate_push(height, height_state)?; + #[inline(always)] + pub(crate) fn push_state(&mut self, state: &UnrealizedState) { + self.core.push_state(state); - self.invested_capital_in_profit_raw.truncate_push( - height, - CentsSats::new(height_state.invested_capital_in_profit_raw), - )?; - self.invested_capital_in_loss_raw.truncate_push( - height, - CentsSats::new(height_state.invested_capital_in_loss_raw), - )?; - self.investor_cap_in_profit_raw.truncate_push( - height, - CentsSquaredSats::new(height_state.investor_cap_in_profit_raw), - )?; - self.investor_cap_in_loss_raw.truncate_push( - height, - CentsSquaredSats::new(height_state.investor_cap_in_loss_raw), - )?; - - Ok(()) + self.invested_capital_in_profit_raw + .push(CentsSats::new(state.invested_capital_in_profit_raw)); + self.invested_capital_in_loss_raw + .push(CentsSats::new(state.invested_capital_in_loss_raw)); + self.investor_cap_in_profit_raw + .push(CentsSquaredSats::new(state.investor_cap_in_profit_raw)); + self.investor_cap_in_loss_raw + .push(CentsSquaredSats::new(state.investor_cap_in_loss_raw)); } pub(crate) fn collect_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> { @@ -134,8 +121,16 @@ impl UnrealizedBase { .map(|o| o.investor_cap_in_loss_raw.collect_range_at(start, end)) .collect(); + self.invested_capital_in_profit_raw + .truncate_if_needed_at(start)?; + self.invested_capital_in_loss_raw + .truncate_if_needed_at(start)?; + self.investor_cap_in_profit_raw + .truncate_if_needed_at(start)?; + self.investor_cap_in_loss_raw + .truncate_if_needed_at(start)?; + for i in start..end { - let height = Height::from(i); let local_i = i - start; let mut sum_invested_profit = CentsSats::ZERO; @@ -151,13 +146,13 @@ impl UnrealizedBase { } self.invested_capital_in_profit_raw - .truncate_push(height, sum_invested_profit)?; + .push(sum_invested_profit); self.invested_capital_in_loss_raw - .truncate_push(height, sum_invested_loss)?; + .push(sum_invested_loss); self.investor_cap_in_profit_raw - .truncate_push(height, sum_investor_profit)?; + .push(sum_investor_profit); self.investor_cap_in_loss_raw - .truncate_push(height, sum_investor_loss)?; + .push(sum_investor_loss); } Ok(()) diff --git a/crates/brk_computer/src/distribution/metrics/unrealized/basic.rs b/crates/brk_computer/src/distribution/metrics/unrealized/basic.rs index 37cf5b893..1059433be 100644 --- a/crates/brk_computer/src/distribution/metrics/unrealized/basic.rs +++ b/crates/brk_computer/src/distribution/metrics/unrealized/basic.rs @@ -53,18 +53,18 @@ impl UnrealizedBasic { .min(self.loss.base.cents.height.len()) } - pub(crate) fn truncate_push(&mut self, height: Height, state: &UnrealizedState) -> Result<()> { + #[inline(always)] + pub(crate) fn push_state(&mut self, state: &UnrealizedState) { self.profit .base .cents .height - .truncate_push(height, state.unrealized_profit)?; + .push(state.unrealized_profit); self.loss .base .cents .height - .truncate_push(height, state.unrealized_loss)?; - Ok(()) + .push(state.unrealized_loss); } pub(crate) fn collect_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> { diff --git a/crates/brk_computer/src/distribution/metrics/unrealized/core.rs b/crates/brk_computer/src/distribution/metrics/unrealized/core.rs index b827fb9ff..9d230a672 100644 --- a/crates/brk_computer/src/distribution/metrics/unrealized/core.rs +++ b/crates/brk_computer/src/distribution/metrics/unrealized/core.rs @@ -1,6 +1,6 @@ use brk_error::Result; use brk_traversable::Traversable; -use brk_types::{Cents, CentsSigned, Height, Indexes, Version}; +use brk_types::{Cents, CentsSigned, Indexes, Version}; use derive_more::{Deref, DerefMut}; use vecdb::{AnyStoredVec, Exit, Rw, StorageMode}; @@ -39,13 +39,9 @@ impl UnrealizedCore { self.basic.min_stateful_len() } - pub(crate) fn truncate_push( - &mut self, - height: Height, - height_state: &UnrealizedState, - ) -> Result<()> { - self.basic.truncate_push(height, height_state)?; - Ok(()) + #[inline(always)] + pub(crate) fn push_state(&mut self, state: &UnrealizedState) { + self.basic.push_state(state); } pub(crate) fn collect_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> { diff --git a/crates/brk_computer/src/distribution/metrics/unrealized/full.rs b/crates/brk_computer/src/distribution/metrics/unrealized/full.rs index 7cc2ba484..88d4c56f3 100644 --- a/crates/brk_computer/src/distribution/metrics/unrealized/full.rs +++ b/crates/brk_computer/src/distribution/metrics/unrealized/full.rs @@ -1,6 +1,6 @@ use brk_error::Result; use brk_traversable::Traversable; -use brk_types::{Cents, CentsSats, CentsSigned, Height, Indexes, Version}; +use brk_types::{Cents, CentsSats, CentsSigned, Indexes, Version}; use derive_more::{Deref, DerefMut}; use vecdb::{AnyStoredVec, Exit, Rw, StorageMode, WritableVec}; @@ -58,21 +58,17 @@ impl UnrealizedFull { }) } - pub(crate) fn truncate_push_all( - &mut self, - height: Height, - state: &UnrealizedState, - ) -> Result<()> { - self.inner.truncate_push(height, state)?; + #[inline(always)] + pub(crate) fn push_state_all(&mut self, state: &UnrealizedState) { + self.inner.push_state(state); self.invested_capital_in_profit .cents .height - .truncate_push(height, state.invested_capital_in_profit)?; + .push(state.invested_capital_in_profit); self.invested_capital_in_loss .cents .height - .truncate_push(height, state.invested_capital_in_loss)?; - Ok(()) + .push(state.invested_capital_in_loss); } pub(crate) fn collect_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> { diff --git a/crates/brk_computer/src/distribution/metrics/unrealized/mod.rs b/crates/brk_computer/src/distribution/metrics/unrealized/mod.rs index 840ef5ed1..2f5b2be46 100644 --- a/crates/brk_computer/src/distribution/metrics/unrealized/mod.rs +++ b/crates/brk_computer/src/distribution/metrics/unrealized/mod.rs @@ -11,7 +11,7 @@ pub use full::UnrealizedFull; pub use minimal::UnrealizedMinimal; use brk_error::Result; -use brk_types::{Height, Indexes}; +use brk_types::Indexes; use vecdb::Exit; use crate::{distribution::state::UnrealizedState, prices}; @@ -20,7 +20,7 @@ pub trait UnrealizedLike: Send + Sync { fn as_base(&self) -> &UnrealizedBase; fn as_base_mut(&mut self) -> &mut UnrealizedBase; fn min_stateful_len(&self) -> usize; - fn truncate_push(&mut self, height: Height, state: &UnrealizedState) -> Result<()>; + fn push_state(&mut self, state: &UnrealizedState); fn compute_rest( &mut self, prices: &prices::Vecs, @@ -44,8 +44,9 @@ impl UnrealizedLike for UnrealizedBase { fn min_stateful_len(&self) -> usize { self.min_stateful_len() } - fn truncate_push(&mut self, height: Height, state: &UnrealizedState) -> Result<()> { - self.truncate_push(height, state) + #[inline(always)] + fn push_state(&mut self, state: &UnrealizedState) { + self.push_state(state); } fn compute_rest( &mut self, @@ -74,8 +75,9 @@ impl UnrealizedLike for UnrealizedFull { fn min_stateful_len(&self) -> usize { self.inner.min_stateful_len() } - fn truncate_push(&mut self, height: Height, state: &UnrealizedState) -> Result<()> { - self.truncate_push_all(height, state) + #[inline(always)] + fn push_state(&mut self, state: &UnrealizedState) { + self.push_state_all(state); } fn compute_rest( &mut self, diff --git a/crates/brk_computer/src/internal/algo/aggregation.rs b/crates/brk_computer/src/internal/algo/aggregation.rs index 5ba0beb6b..953680477 100644 --- a/crates/brk_computer/src/internal/algo/aggregation.rs +++ b/crates/brk_computer/src/internal/algo/aggregation.rs @@ -89,6 +89,17 @@ where }); let start = index.to_usize(); + + // Truncate all vecs to start once, so the loop only pushes + macro_rules! truncate_vec { + ($($vec:ident),*) => { + $(if let Some(ref mut v) = $vec { + v.truncate_if_needed_at(start)?; + })* + }; + } + truncate_vec!(first, last, min, max, average, sum, cumulative, median, pct10, pct25, pct75, pct90); + let fi_len = first_indexes.len(); let first_indexes_batch: Vec = first_indexes.collect_range_at(start, fi_len); let count_indexes_batch: Vec = count_indexes.collect_range_at(start, fi_len); @@ -97,8 +108,7 @@ where .into_iter() .zip(count_indexes_batch) .enumerate() - .try_for_each(|(j, (first_index, count_index))| -> Result<()> { - let idx = start + j; + .try_for_each(|(_, (first_index, count_index))| -> Result<()> { let count = u64::from(count_index) as usize; // Effective count after skipping (e.g., skip coinbase for fee calculations) @@ -113,17 +123,15 @@ where } else { T::from(0_usize) }; - first_vec.truncate_push_at(idx, f)?; + first_vec.push(f); } if let Some(ref mut last_vec) = last { if effective_count == 0 { - // If all items skipped, use zero - last_vec.truncate_push_at(idx, T::from(0_usize))?; + last_vec.push(T::from(0_usize)); } else { let last_index = first_index + (count - 1); - let v = source.collect_one_at(last_index.to_usize()).unwrap(); - last_vec.truncate_push_at(idx, v)?; + last_vec.push(source.collect_one_at(last_index.to_usize()).unwrap()); } } @@ -143,12 +151,10 @@ where }); if let Some(ref mut min_vec) = min { - let v = min_val.or(max_val).unwrap_or_else(|| T::from(0_usize)); - min_vec.truncate_push_at(idx, v)?; + min_vec.push(min_val.or(max_val).unwrap_or_else(|| T::from(0_usize))); } if let Some(ref mut max_vec) = max { - let v = max_val.or(min_val).unwrap_or_else(|| T::from(0_usize)); - max_vec.truncate_push_at(idx, v)?; + max_vec.push(max_val.or(min_val).unwrap_or_else(|| T::from(0_usize))); } } else if needs_percentiles || needs_minmax { let mut values: Vec = source.collect_range_at( @@ -157,21 +163,18 @@ where ); if values.is_empty() { - // Handle edge case where all items were skipped macro_rules! push_zero { ($($vec:ident),*) => { $(if let Some(ref mut v) = $vec { - v.truncate_push_at(idx, T::from(0_usize))?; + v.push(T::from(0_usize)); })* }; } push_zero!(max, pct90, pct75, median, pct25, pct10, min, average, sum); if let Some(ref mut cumulative_vec) = cumulative { - let t = cumulative_val.unwrap(); - cumulative_vec.truncate_push_at(idx, t)?; + cumulative_vec.push(cumulative_val.unwrap()); } } else if needs_percentiles { - // Compute aggregates from unsorted values first to avoid clone let aggregate_result = if needs_aggregates { let len = values.len(); let sum_val = values.iter().copied().fold(T::from(0), |a, b| a + b); @@ -180,53 +183,52 @@ where None }; - // Sort in-place — no clone needed values.sort_unstable(); if let Some(ref mut max_vec) = max { - max_vec.truncate_push_at(idx, *values.last().unwrap())?; + max_vec.push(*values.last().unwrap()); } if let Some(ref mut pct90_vec) = pct90 { - pct90_vec.truncate_push_at(idx, get_percentile(&values, 0.90))?; + pct90_vec.push(get_percentile(&values, 0.90)); } if let Some(ref mut pct75_vec) = pct75 { - pct75_vec.truncate_push_at(idx, get_percentile(&values, 0.75))?; + pct75_vec.push(get_percentile(&values, 0.75)); } if let Some(ref mut median_vec) = median { - median_vec.truncate_push_at(idx, get_percentile(&values, 0.50))?; + median_vec.push(get_percentile(&values, 0.50)); } if let Some(ref mut pct25_vec) = pct25 { - pct25_vec.truncate_push_at(idx, get_percentile(&values, 0.25))?; + pct25_vec.push(get_percentile(&values, 0.25)); } if let Some(ref mut pct10_vec) = pct10 { - pct10_vec.truncate_push_at(idx, get_percentile(&values, 0.10))?; + pct10_vec.push(get_percentile(&values, 0.10)); } if let Some(ref mut min_vec) = min { - min_vec.truncate_push_at(idx, *values.first().unwrap())?; + min_vec.push(*values.first().unwrap()); } if let Some((len, sum_val)) = aggregate_result { if let Some(ref mut average_vec) = average { - average_vec.truncate_push_at(idx, sum_val / len)?; + average_vec.push(sum_val / len); } if needs_sum_or_cumulative { if let Some(ref mut sum_vec) = sum { - sum_vec.truncate_push_at(idx, sum_val)?; + sum_vec.push(sum_val); } if let Some(ref mut cumulative_vec) = cumulative { let t = cumulative_val.unwrap() + sum_val; cumulative_val.replace(t); - cumulative_vec.truncate_push_at(idx, t)?; + cumulative_vec.push(t); } } } } else if needs_minmax { if let Some(ref mut min_vec) = min { - min_vec.truncate_push_at(idx, *values.iter().min().unwrap())?; + min_vec.push(*values.iter().min().unwrap()); } if let Some(ref mut max_vec) = max { - max_vec.truncate_push_at(idx, *values.iter().max().unwrap())?; + max_vec.push(*values.iter().max().unwrap()); } if needs_aggregates { @@ -234,23 +236,22 @@ where let sum_val = values.into_iter().fold(T::from(0), |a, b| a + b); if let Some(ref mut average_vec) = average { - average_vec.truncate_push_at(idx, sum_val / len)?; + average_vec.push(sum_val / len); } if needs_sum_or_cumulative { if let Some(ref mut sum_vec) = sum { - sum_vec.truncate_push_at(idx, sum_val)?; + sum_vec.push(sum_val); } if let Some(ref mut cumulative_vec) = cumulative { let t = cumulative_val.unwrap() + sum_val; cumulative_val.replace(t); - cumulative_vec.truncate_push_at(idx, t)?; + cumulative_vec.push(t); } } } } } else if needs_aggregates { - // Aggregates only (sum/average/cumulative) — no Vec allocation needed let efi = effective_first_index.to_usize(); let (sum_val, len) = source.fold_range_at( efi, @@ -265,17 +266,17 @@ where } else { T::from(0_usize) }; - average_vec.truncate_push_at(idx, avg)?; + average_vec.push(avg); } if needs_sum_or_cumulative { if let Some(ref mut sum_vec) = sum { - sum_vec.truncate_push_at(idx, sum_val)?; + sum_vec.push(sum_val); } if let Some(ref mut cumulative_vec) = cumulative { let t = cumulative_val.unwrap() + sum_val; cumulative_val.replace(t); - cumulative_vec.truncate_push_at(idx, t)?; + cumulative_vec.push(t); } } } @@ -348,6 +349,19 @@ where let zero = T::from(0_usize); let mut values: Vec = Vec::new(); + for vec in [ + &mut *min, + &mut *max, + &mut *average, + &mut *median, + &mut *pct10, + &mut *pct25, + &mut *pct75, + &mut *pct90, + ] { + vec.truncate_if_needed_at(start)?; + } + count_indexes_batch .iter() .enumerate() @@ -378,7 +392,7 @@ where &mut *pct75, &mut *pct90, ] { - vec.truncate_push_at(idx, zero)?; + vec.push(zero); } } else { source.collect_range_into_at(range_start_usize, range_end_usize, &mut values); @@ -390,14 +404,14 @@ where values.sort_unstable(); - max.truncate_push_at(idx, *values.last().unwrap())?; - pct90.truncate_push_at(idx, get_percentile(&values, 0.90))?; - pct75.truncate_push_at(idx, get_percentile(&values, 0.75))?; - median.truncate_push_at(idx, get_percentile(&values, 0.50))?; - pct25.truncate_push_at(idx, get_percentile(&values, 0.25))?; - pct10.truncate_push_at(idx, get_percentile(&values, 0.10))?; - min.truncate_push_at(idx, *values.first().unwrap())?; - average.truncate_push_at(idx, avg)?; + max.push(*values.last().unwrap()); + pct90.push(get_percentile(&values, 0.90)); + pct75.push(get_percentile(&values, 0.75)); + median.push(get_percentile(&values, 0.50)); + pct25.push(get_percentile(&values, 0.25)); + pct10.push(get_percentile(&values, 0.10)); + min.push(*values.first().unwrap()); + average.push(avg); } Ok(()) diff --git a/crates/brk_computer/src/internal/algo/sliding_distribution.rs b/crates/brk_computer/src/internal/algo/sliding_distribution.rs index 815c87dbd..cf7b3ad8a 100644 --- a/crates/brk_computer/src/internal/algo/sliding_distribution.rs +++ b/crates/brk_computer/src/internal/algo/sliding_distribution.rs @@ -107,9 +107,21 @@ where let starts_batch = window_starts.collect_range_at(skip, end); + for v in [ + &mut *average_out, + &mut *min_out, + &mut *max_out, + &mut *p10_out, + &mut *p25_out, + &mut *median_out, + &mut *p75_out, + &mut *p90_out, + ] { + v.truncate_if_needed_at(skip)?; + } + for (j, start) in starts_batch.into_iter().enumerate() { - let i = skip + j; - let v = partial_values[i - range_start]; + let v = partial_values[skip + j - range_start]; let start_usize = start.to_usize(); window.advance(v, start_usize, partial_values, range_start); @@ -125,19 +137,19 @@ where &mut *p75_out, &mut *p90_out, ] { - v.truncate_push_at(i, zero)?; + v.push(zero); } } else { - average_out.truncate_push_at(i, T::from(window.average()))?; - min_out.truncate_push_at(i, T::from(window.min()))?; - max_out.truncate_push_at(i, T::from(window.max()))?; + average_out.push(T::from(window.average())); + min_out.push(T::from(window.min())); + max_out.push(T::from(window.max())); let [p10, p25, p50, p75, p90] = window.percentiles(&[0.10, 0.25, 0.50, 0.75, 0.90]); - p10_out.truncate_push_at(i, T::from(p10))?; - p25_out.truncate_push_at(i, T::from(p25))?; - median_out.truncate_push_at(i, T::from(p50))?; - p75_out.truncate_push_at(i, T::from(p75))?; - p90_out.truncate_push_at(i, T::from(p90))?; + p10_out.push(T::from(p10)); + p25_out.push(T::from(p25)); + median_out.push(T::from(p50)); + p75_out.push(T::from(p75)); + p90_out.push(T::from(p90)); } if average_out.batch_limit_reached() { diff --git a/crates/brk_computer/src/internal/per_block/amount/rolling_distribution.rs b/crates/brk_computer/src/internal/per_block/amount/rolling_distribution.rs index 105d38def..0bab318cb 100644 --- a/crates/brk_computer/src/internal/per_block/amount/rolling_distribution.rs +++ b/crates/brk_computer/src/internal/per_block/amount/rolling_distribution.rs @@ -35,6 +35,7 @@ impl RollingDistributionSlot { }) } + #[allow(clippy::too_many_arguments)] pub(crate) fn compute( &mut self, max_from: Height, diff --git a/crates/brk_computer/src/internal/per_block/percentiles.rs b/crates/brk_computer/src/internal/per_block/percentiles.rs index 1db4b0ac9..047b3feca 100644 --- a/crates/brk_computer/src/internal/per_block/percentiles.rs +++ b/crates/brk_computer/src/internal/per_block/percentiles.rs @@ -1,6 +1,6 @@ use brk_error::Result; use brk_traversable::{Traversable, TreeNode}; -use brk_types::{Cents, Height, Version}; +use brk_types::{Cents, Version}; use vecdb::{AnyExportableVec, Database, ReadOnlyClone, Ro, Rw, StorageMode, WritableVec}; use crate::indexes; @@ -38,16 +38,12 @@ impl PercentilesVecs { Ok(Self { vecs }) } - /// Push percentile prices at this height (in cents). - pub(crate) fn truncate_push( - &mut self, - height: Height, - percentile_prices: &[Cents; PERCENTILES_LEN], - ) -> Result<()> { + /// Push percentile prices (in cents). + #[inline(always)] + pub(crate) fn push(&mut self, percentile_prices: &[Cents; PERCENTILES_LEN]) { for (i, v) in self.vecs.iter_mut().enumerate() { - v.cents.height.truncate_push(height, percentile_prices[i])?; + v.cents.height.push(percentile_prices[i]); } - Ok(()) } /// Validate computed versions or reset if mismatched. diff --git a/crates/brk_computer/src/internal/per_block/ratio/percentiles.rs b/crates/brk_computer/src/internal/per_block/ratio/percentiles.rs index 35a84ecbb..30dc1a62d 100644 --- a/crates/brk_computer/src/internal/per_block/ratio/percentiles.rs +++ b/crates/brk_computer/src/internal/per_block/ratio/percentiles.rs @@ -46,12 +46,7 @@ impl RatioPerBlockPercentiles { macro_rules! import_ratio { ($suffix:expr) => { - RatioPerBlock::forced_import_raw( - db, - &format!("{name}_{}", $suffix), - v, - indexes, - )? + RatioPerBlock::forced_import_raw(db, &format!("{name}_{}", $suffix), v, indexes)? }; } @@ -126,12 +121,15 @@ impl RatioPerBlockPercentiles { const PCTS: [f64; 6] = [0.01, 0.02, 0.05, 0.95, 0.98, 0.99]; let mut out = [0u32; 6]; - for (offset, &ratio) in new_ratios.iter().enumerate() { + for vec in pct_vecs.iter_mut() { + vec.truncate_if_needed_at(start)?; + } + + for &ratio in new_ratios.iter() { self.expanding_pct.add(*ratio); self.expanding_pct.quantiles(&PCTS, &mut out); - let idx = start + offset; for (vec, &val) in pct_vecs.iter_mut().zip(out.iter()) { - vec.truncate_push_at(idx, BasisPoints32::from(val))?; + vec.push(BasisPoints32::from(val)); } } } diff --git a/crates/brk_computer/src/internal/per_block/rolling/delta.rs b/crates/brk_computer/src/internal/per_block/rolling/delta.rs index 2cca11437..f5501aad2 100644 --- a/crates/brk_computer/src/internal/per_block/rolling/delta.rs +++ b/crates/brk_computer/src/internal/per_block/rolling/delta.rs @@ -79,9 +79,9 @@ where let cached = cached_start.clone(); let starts_version = cached.version(); - // Change: source[h] - source[ago] as C (via f64) + // Absolute change: source[h] - source[ago] as C (via f64) let change_vec = LazyDeltaVec::::new( - &format!("{full_name}_change"), + &full_name, version, src.clone(), starts_version, @@ -91,7 +91,7 @@ where }, ); let change_resolutions = Resolutions::forced_import( - &format!("{full_name}_change"), + &full_name, change_vec.read_only_boxed_clone(), version, indexes, @@ -102,15 +102,16 @@ where }; // Rate BPS: (source[h] - source[ago]) / source[ago] as B (via f64) + let rate_bps_name = format!("{full_name}_rate_bps"); let rate_vec = LazyDeltaVec::::new( - &format!("{full_name}_rate_bps"), + &rate_bps_name, version, src.clone(), starts_version, move || cached.get(), ); let rate_resolutions = Resolutions::forced_import( - &format!("{full_name}_rate_bps"), + &rate_bps_name, rate_vec.read_only_boxed_clone(), version, indexes, @@ -121,28 +122,30 @@ where }; // Ratio: bps / 10000 + let rate_ratio_name = format!("{full_name}_rate_ratio"); let ratio = LazyPerBlock { height: LazyVecFrom1::transformed::( - &format!("{full_name}_rate_ratio"), + &rate_ratio_name, version, bps.height.read_only_boxed_clone(), ), resolutions: Box::new(DerivedResolutions::from_derived_computed::( - &format!("{full_name}_rate_ratio"), + &rate_ratio_name, version, &bps.resolutions, )), }; // Percent: bps / 100 + let rate_name = format!("{full_name}_rate"); let percent = LazyPerBlock { height: LazyVecFrom1::transformed::( - &format!("{full_name}_rate"), + &rate_name, version, bps.height.read_only_boxed_clone(), ), resolutions: Box::new(DerivedResolutions::from_derived_computed::( - &format!("{full_name}_rate"), + &rate_name, version, &bps.resolutions, )), @@ -214,9 +217,10 @@ where let cached = cached_start.clone(); let starts_version = cached.version(); - // Change cents: source[h] - source[ago] as C (via f64) + // Absolute change (cents): source[h] - source[ago] as C (via f64) + let cents_name = format!("{full_name}_cents"); let change_vec = LazyDeltaVec::::new( - &format!("{full_name}_change"), + ¢s_name, version, src.clone(), starts_version, @@ -226,7 +230,7 @@ where }, ); let change_resolutions = Resolutions::forced_import( - &format!("{full_name}_change"), + ¢s_name, change_vec.read_only_boxed_clone(), version, indexes, @@ -236,15 +240,15 @@ where resolutions: Box::new(change_resolutions), }; - // Change USD: lazy from cents delta + // Absolute change (usd): lazy from cents delta let usd = LazyPerBlock { height: LazyVecFrom1::transformed::( - &format!("{full_name}_change_usd"), + &full_name, version, cents.height.read_only_boxed_clone(), ), resolutions: Box::new(DerivedResolutions::from_derived_computed::( - &format!("{full_name}_change_usd"), + &full_name, version, ¢s.resolutions, )), @@ -253,15 +257,16 @@ where let absolute = LazyDeltaFiatFromHeight { usd, cents }; // Rate BPS: (source[h] - source[ago]) / source[ago] as B (via f64) + let rate_bps_name = format!("{full_name}_rate_bps"); let rate_vec = LazyDeltaVec::::new( - &format!("{full_name}_rate_bps"), + &rate_bps_name, version, src.clone(), starts_version, move || cached.get(), ); let rate_resolutions = Resolutions::forced_import( - &format!("{full_name}_rate_bps"), + &rate_bps_name, rate_vec.read_only_boxed_clone(), version, indexes, @@ -271,27 +276,29 @@ where resolutions: Box::new(rate_resolutions), }; + let rate_ratio_name = format!("{full_name}_rate_ratio"); let ratio = LazyPerBlock { height: LazyVecFrom1::transformed::( - &format!("{full_name}_rate_ratio"), + &rate_ratio_name, version, bps.height.read_only_boxed_clone(), ), resolutions: Box::new(DerivedResolutions::from_derived_computed::( - &format!("{full_name}_rate_ratio"), + &rate_ratio_name, version, &bps.resolutions, )), }; + let rate_name = format!("{full_name}_rate"); let percent = LazyPerBlock { height: LazyVecFrom1::transformed::( - &format!("{full_name}_rate"), + &rate_name, version, bps.height.read_only_boxed_clone(), ), resolutions: Box::new(DerivedResolutions::from_derived_computed::( - &format!("{full_name}_rate"), + &rate_name, version, &bps.resolutions, )), diff --git a/crates/brk_computer/src/internal/per_block/stddev/extended.rs b/crates/brk_computer/src/internal/per_block/stddev/extended.rs index e8bfba4d8..21426dc4a 100644 --- a/crates/brk_computer/src/internal/per_block/stddev/extended.rs +++ b/crates/brk_computer/src/internal/per_block/stddev/extended.rs @@ -163,11 +163,11 @@ impl StdDevPerBlockExtended { 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, -0.5, -1.0, -1.5, -2.0, -2.5, -3.0, ]; for (vec, mult) in self.mut_band_height_vecs().zip(MULTIPLIERS) { + vec.truncate_if_needed_at(start)?; for (offset, _) in source_data.iter().enumerate() { - let index = start + offset; let average = sma_data[offset]; let sd = sd_data[offset]; - vec.truncate_push_at(index, average + StoredF32::from(mult * *sd))?; + vec.push(average + StoredF32::from(mult * *sd)); } } diff --git a/crates/brk_computer/src/pools/mod.rs b/crates/brk_computer/src/pools/mod.rs index bea06cc6a..a88f67364 100644 --- a/crates/brk_computer/src/pools/mod.rs +++ b/crates/brk_computer/src/pools/mod.rs @@ -144,12 +144,14 @@ impl Vecs { first_tx_index_cursor.advance(min); let mut output_count_cursor = indexes.tx_index.output_count.cursor(); + self.height_to_pool.truncate_if_needed_at(min)?; + indexer .stores .height_to_coinbase_tag .iter() .skip(min) - .try_for_each(|(height, coinbase_tag)| -> Result<()> { + .try_for_each(|(_, coinbase_tag)| -> Result<()> { let tx_index = first_tx_index_cursor.next().unwrap(); let out_start = first_txout_index.get(tx_index.to_usize()); @@ -179,7 +181,7 @@ impl Vecs { .or_else(|| self.pools.find_from_coinbase_tag(&coinbase_tag)) .unwrap_or(unknown); - self.height_to_pool.truncate_push(height, pool.slug)?; + self.height_to_pool.push(pool.slug); Ok(()) })?; diff --git a/crates/brk_computer/src/positions.rs b/crates/brk_computer/src/positions.rs index 858af70be..322270b0c 100644 --- a/crates/brk_computer/src/positions.rs +++ b/crates/brk_computer/src/positions.rs @@ -8,7 +8,7 @@ use brk_types::{BlkPosition, Height, Indexes, TxIndex, Version}; use tracing::info; use vecdb::{ AnyStoredVec, AnyVec, Database, Exit, ImportableVec, PcoVec, ReadableVec, Rw, StorageMode, - VecIndex, WritableVec, + WritableVec, }; use crate::internal::db_utils::{finalize_db, open_db}; @@ -102,10 +102,15 @@ impl Vecs { return Ok(()); }; - // Cursor avoids per-height PcoVec page decompression. - // Heights are sequential, so the cursor only advances forward. - let mut first_tx_index_cursor = indexer.vecs.transactions.first_tx_index.cursor(); - first_tx_index_cursor.advance(min_height.to_usize()); + let first_tx_at_min_height = indexer + .vecs + .transactions + .first_tx_index + .collect_one(min_height) + .unwrap(); + + self.block.truncate_if_needed(min_height)?; + self.tx.truncate_if_needed(first_tx_at_min_height)?; parser .read( @@ -114,23 +119,13 @@ impl Vecs { ) .iter() .try_for_each(|block| -> Result<()> { - let height = block.height(); + self.block.push(block.metadata().position()); - self.block - .truncate_push(height, block.metadata().position())?; + block.tx_metadata().iter().for_each(|metadata| { + self.tx.push(metadata.position()); + }); - let tx_index = first_tx_index_cursor.next().unwrap(); - - block.tx_metadata().iter().enumerate().try_for_each( - |(index, metadata)| -> Result<()> { - let tx_index = tx_index + index; - self.tx - .truncate_push(tx_index, metadata.position())?; - Ok(()) - }, - )?; - - if *height % 1_000 == 0 { + if *block.height() % 1_000 == 0 { let _lock = exit.lock(); self.block.flush()?; self.tx.flush()?; diff --git a/crates/brk_computer/src/scripts/value/compute.rs b/crates/brk_computer/src/scripts/value/compute.rs index 03e17ff2f..0f57f59b5 100644 --- a/crates/brk_computer/src/scripts/value/compute.rs +++ b/crates/brk_computer/src/scripts/value/compute.rs @@ -52,9 +52,10 @@ impl Vecs { let mut output_types_buf: Vec = Vec::new(); let mut values_buf: Vec = Vec::new(); + height_vec.truncate_if_needed(starting_height)?; + // Iterate blocks for h in starting_height.to_usize()..=target_height.to_usize() { - let height = Height::from(h); let local_idx = h - starting_height.to_usize(); // Get output range for this block @@ -88,7 +89,7 @@ impl Vecs { } } - height_vec.truncate_push(height, op_return_value)?; + height_vec.push(op_return_value); } height_vec.write()?; diff --git a/crates/brk_computer/src/supply/burned/compute.rs b/crates/brk_computer/src/supply/burned/compute.rs index f59e1dafd..cc841d49d 100644 --- a/crates/brk_computer/src/supply/burned/compute.rs +++ b/crates/brk_computer/src/supply/burned/compute.rs @@ -37,6 +37,7 @@ impl Vecs { let start = starting_height.to_usize(); let end = target_height.to_usize() + 1; let unclaimed_data = unclaimed_height.collect_range_at(start, end); + height_vec.truncate_if_needed(starting_height)?; op_return_height.fold_range_at(start, end, start, |idx, op_return| { let unclaimed = unclaimed_data[idx - start]; let genesis = if idx == 0 { @@ -45,9 +46,7 @@ impl Vecs { Sats::ZERO }; let unspendable = genesis + op_return + unclaimed; - height_vec - .truncate_push(Height::from(idx), unspendable) - .unwrap(); + height_vec.push(unspendable); idx + 1 }); } diff --git a/modules/brk-client/index.js b/modules/brk-client/index.js index 1db9eff35..8116fa2d8 100644 --- a/modules/brk-client/index.js +++ b/modules/brk-client/index.js @@ -2968,10 +2968,10 @@ function create_1m1w1y2wPattern(client, acc) { */ function create_1m1w1y24hPattern3(client, acc) { return { - _1m: createCentsUsdPattern(client, _m(acc, '1m_change')), - _1w: createCentsUsdPattern(client, _m(acc, '1w_change')), - _1y: createCentsUsdPattern(client, _m(acc, '1y_change')), - _24h: createCentsUsdPattern(client, _m(acc, '24h_change')), + _1m: createCentsUsdPattern(client, _m(acc, '1m')), + _1w: createCentsUsdPattern(client, _m(acc, '1w')), + _1y: createCentsUsdPattern(client, _m(acc, '1y')), + _24h: createCentsUsdPattern(client, _m(acc, '24h')), }; } @@ -3910,8 +3910,8 @@ function createCentsUsdPattern2(client, acc) { */ function createCentsUsdPattern(client, acc) { return { - cents: createMetricPattern1(client, acc), - usd: createMetricPattern1(client, _m(acc, 'usd')), + cents: createMetricPattern1(client, _m(acc, 'cents')), + usd: createMetricPattern1(client, acc), }; } diff --git a/packages/brk_client/brk_client/__init__.py b/packages/brk_client/brk_client/__init__.py index 601dd1cdb..e18cb1c51 100644 --- a/packages/brk_client/brk_client/__init__.py +++ b/packages/brk_client/brk_client/__init__.py @@ -2729,10 +2729,10 @@ class _1m1w1y24hPattern3: def __init__(self, client: BrkClientBase, acc: str): """Create pattern node with accumulated metric name.""" - self._1m: CentsUsdPattern = CentsUsdPattern(client, _m(acc, '1m_change')) - self._1w: CentsUsdPattern = CentsUsdPattern(client, _m(acc, '1w_change')) - self._1y: CentsUsdPattern = CentsUsdPattern(client, _m(acc, '1y_change')) - self._24h: CentsUsdPattern = CentsUsdPattern(client, _m(acc, '24h_change')) + self._1m: CentsUsdPattern = CentsUsdPattern(client, _m(acc, '1m')) + self._1w: CentsUsdPattern = CentsUsdPattern(client, _m(acc, '1w')) + self._1y: CentsUsdPattern = CentsUsdPattern(client, _m(acc, '1y')) + self._24h: CentsUsdPattern = CentsUsdPattern(client, _m(acc, '24h')) class _1m1w1y24hPattern4: """Pattern struct for repeated tree structure.""" @@ -3132,8 +3132,8 @@ class CentsUsdPattern: def __init__(self, client: BrkClientBase, acc: str): """Create pattern node with accumulated metric name.""" - self.cents: MetricPattern1[CentsSigned] = MetricPattern1(client, acc) - self.usd: MetricPattern1[Dollars] = MetricPattern1(client, _m(acc, 'usd')) + self.cents: MetricPattern1[CentsSigned] = MetricPattern1(client, _m(acc, 'cents')) + self.usd: MetricPattern1[Dollars] = MetricPattern1(client, acc) class CoindaysSentPattern: """Pattern struct for repeated tree structure.""" diff --git a/website/scripts/options/cointime.js b/website/scripts/options/cointime.js index 815018d61..3bdf31229 100644 --- a/website/scripts/options/cointime.js +++ b/website/scripts/options/cointime.js @@ -2,7 +2,7 @@ import { colors } from "../utils/colors.js"; import { brk } from "../client.js"; import { Unit } from "../utils/units.js"; import { dots, line, baseline, price, rollingWindowsTree, percentRatioDots } from "./series.js"; -import { satsBtcUsd } from "./shared.js"; +import { satsBtcUsd, priceRatioPercentilesTree } from "./shared.js"; /** * Create Cointime section @@ -173,57 +173,18 @@ export function createCointimeSection() { ), ], }, - ...prices.map(({ pattern, name, color }) => { - const p = pattern.percentiles; - const pctUsd = /** @type {const} */ ([ - { name: "pct95", prop: p.pct95.price, color: colors.ratioPct._95 }, - { name: "pct5", prop: p.pct5.price, color: colors.ratioPct._5 }, - { name: "pct98", prop: p.pct98.price, color: colors.ratioPct._98 }, - { name: "pct2", prop: p.pct2.price, color: colors.ratioPct._2 }, - { name: "pct99", prop: p.pct99.price, color: colors.ratioPct._99 }, - { name: "pct1", prop: p.pct1.price, color: colors.ratioPct._1 }, - ]); - const pctRatio = /** @type {const} */ ([ - { name: "pct95", prop: p.pct95.ratio, color: colors.ratioPct._95 }, - { name: "pct5", prop: p.pct5.ratio, color: colors.ratioPct._5 }, - { name: "pct98", prop: p.pct98.ratio, color: colors.ratioPct._98 }, - { name: "pct2", prop: p.pct2.ratio, color: colors.ratioPct._2 }, - { name: "pct99", prop: p.pct99.ratio, color: colors.ratioPct._99 }, - { name: "pct1", prop: p.pct1.ratio, color: colors.ratioPct._1 }, - ]); - return { - name, - tree: [ - { - name: "Price", - title: `${name} Price`, - top: [ - price({ metric: pattern, name, color }), - price({ - metric: all.realized.price, - name: "Realized", - color: colors.realized, - defaultActive: false, - }), - ...pctUsd.map(({ name: pName, prop, color: pColor }) => - price({ metric: prop, name: pName, color: pColor, defaultActive: false, options: { lineStyle: 1 } }), - ), - ], - }, - { - name: "Ratio", - title: `${name} Price Ratio`, - top: [price({ metric: pattern, name, color })], - bottom: [ - baseline({ metric: pattern.ratio, name: "Ratio", unit: Unit.ratio, base: 1 }), - ...pctRatio.map(({ name: pName, prop, color: pColor }) => - line({ metric: prop, name: pName, color: pColor, defaultActive: false, unit: Unit.ratio, options: { lineStyle: 1 } }), - ), - ], - }, + ...prices.map(({ pattern, name, color }) => ({ + name, + tree: priceRatioPercentilesTree({ + pattern, + title: `${name} Price`, + legend: name, + color, + priceReferences: [ + price({ metric: all.realized.price, name: "Realized", color: colors.realized, defaultActive: false }), ], - }; - }), + }), + })), ], }, diff --git a/website/scripts/options/distribution/data.js b/website/scripts/options/distribution/data.js index 1ebdcb1ae..b10cfa1d4 100644 --- a/website/scripts/options/distribution/data.js +++ b/website/scripts/options/distribution/data.js @@ -33,6 +33,9 @@ export function buildCohortData() { AMOUNT_RANGE_NAMES, SPENDABLE_TYPE_NAMES, CLASS_NAMES, + PROFITABILITY_RANGE_NAMES, + PROFIT_NAMES, + LOSS_NAMES, } = brk; const cohortAll = { @@ -191,6 +194,28 @@ export function buildCohortData() { tree: utxoCohorts.class[key], })); + const { range, profit, loss } = utxoCohorts.profitability; + + const profitabilityRange = entries(PROFITABILITY_RANGE_NAMES).map( + ([key, names], i, arr) => ({ + name: names.short, + color: colors.at(i, arr.length), + pattern: range[key], + }), + ); + + const profitabilityProfit = entries(PROFIT_NAMES).map(([key, names], i, arr) => ({ + name: names.short, + color: colors.at(i, arr.length), + pattern: profit[key], + })); + + const profitabilityLoss = entries(LOSS_NAMES).map(([key, names], i, arr) => ({ + name: names.short, + color: colors.at(i, arr.length), + pattern: loss[key], + })); + return { cohortAll, termShort, @@ -208,5 +233,8 @@ export function buildCohortData() { typeAddressable, typeOther, class: class_, + profitabilityRange, + profitabilityProfit, + profitabilityLoss, }; } diff --git a/website/scripts/options/distribution/index.js b/website/scripts/options/distribution/index.js index fa6e82846..8e5ffe841 100644 --- a/website/scripts/options/distribution/index.js +++ b/website/scripts/options/distribution/index.js @@ -10,7 +10,9 @@ * - activity.js: SOPR, Volume, Lifespan */ -import { formatCohortTitle, satsBtcUsd } from "../shared.js"; +import { formatCohortTitle, satsBtcUsd, satsBtcUsdFullTree, simplePriceRatioTree, groupedSimplePriceRatioTree } from "../shared.js"; +import { ROLLING_WINDOWS, line, baseline, percentRatio, rollingWindowsTree, rollingPercentRatioTree } from "../series.js"; +import { Unit } from "../../utils/units.js"; // Section builders import { @@ -205,8 +207,11 @@ export function createCohortFolderAgeRangeWithMatured(cohort) { const title = formatCohortTitle(cohort.name); folder.tree.push({ name: "Matured", - title: title("Matured Supply"), - bottom: satsBtcUsd({ pattern: cohort.matured, name: cohort.name }), + tree: satsBtcUsdFullTree({ + pattern: cohort.matured, + name: cohort.name, + title: title("Matured Supply"), + }), }); return folder; } @@ -452,7 +457,7 @@ export function createGroupedCohortFolderAgeRangeWithMatured({ name: "Matured", title: title("Matured Supply"), bottom: list.flatMap((cohort) => - satsBtcUsd({ pattern: cohort.matured, name: cohort.name, color: cohort.color }), + satsBtcUsd({ pattern: cohort.matured.base, name: cohort.name, color: cohort.color }), ), }); return folder; @@ -580,3 +585,212 @@ export function createGroupedAddressCohortFolder({ ], }; } + +// ============================================================================ +// UTXO Profitability Folder Builders +// ============================================================================ + +/** + * @param {{ name: string, color: Color, pattern: RealizedSupplyPattern }} bucket + * @returns {PartialOptionsGroup} + */ +function singleBucketFolder({ name, color, pattern }) { + return { + name, + tree: [ + { + name: "Supply", + tree: [ + { + name: "All", + title: `${name}: Supply`, + bottom: satsBtcUsd({ pattern: pattern.supply.all, name, color }), + }, + { + name: "STH", + title: `${name}: STH Supply`, + bottom: satsBtcUsd({ pattern: pattern.supply.sth, name, color }), + }, + { + name: "Change", + tree: [ + { ...rollingWindowsTree({ windows: pattern.supply.all.delta.absolute, title: `${name}: Supply Change`, unit: Unit.sats, series: baseline }), name: "Absolute" }, + { ...rollingPercentRatioTree({ windows: pattern.supply.all.delta.rate, title: `${name}: Supply Rate` }), name: "Rate" }, + ], + }, + ], + }, + { + name: "Realized Cap", + tree: [ + { + name: "All", + title: `${name}: Realized Cap`, + bottom: [line({ metric: pattern.realizedCap.all, name, color, unit: Unit.usd })], + }, + { + name: "STH", + title: `${name}: STH Realized Cap`, + bottom: [line({ metric: pattern.realizedCap.sth, name, color, unit: Unit.usd })], + }, + ], + }, + { + name: "Realized Price", + tree: simplePriceRatioTree({ + pattern: pattern.realizedPrice, + title: `${name}: Realized Price`, + legend: name, + color, + }), + }, + { + name: "NUPL", + title: `${name}: NUPL`, + bottom: [line({ metric: pattern.nupl.ratio, name, color, unit: Unit.ratio })], + }, + ], + }; +} + +/** + * @param {{ name: string, color: Color, pattern: RealizedSupplyPattern }[]} list + * @param {string} titlePrefix + * @returns {PartialOptionsTree} + */ +function groupedBucketCharts(list, titlePrefix) { + return [ + { + name: "Supply", + tree: [ + { + name: "All", + title: `${titlePrefix}: Supply`, + bottom: list.flatMap(({ name, color, pattern }) => + satsBtcUsd({ pattern: pattern.supply.all, name, color }), + ), + }, + { + name: "STH", + title: `${titlePrefix}: STH Supply`, + bottom: list.flatMap(({ name, color, pattern }) => + satsBtcUsd({ pattern: pattern.supply.sth, name, color }), + ), + }, + { + name: "Change", + tree: [ + { + name: "Absolute", + tree: [ + { + name: "Compare", + title: `${titlePrefix}: Supply Change`, + bottom: ROLLING_WINDOWS.flatMap((w) => + list.map(({ name, color, pattern }) => + baseline({ metric: pattern.supply.all.delta.absolute[w.key], name: `${name} ${w.name}`, color, unit: Unit.sats }), + ), + ), + }, + ...ROLLING_WINDOWS.map((w) => ({ + name: w.name, + title: `${titlePrefix}: Supply Change ${w.name}`, + bottom: list.map(({ name, color, pattern }) => + baseline({ metric: pattern.supply.all.delta.absolute[w.key], name, color, unit: Unit.sats }), + ), + })), + ], + }, + { + name: "Rate", + tree: [ + { + name: "Compare", + title: `${titlePrefix}: Supply Rate`, + bottom: ROLLING_WINDOWS.flatMap((w) => + list.flatMap(({ name, color, pattern }) => + percentRatio({ pattern: pattern.supply.all.delta.rate[w.key], name: `${name} ${w.name}`, color }), + ), + ), + }, + ...ROLLING_WINDOWS.map((w) => ({ + name: w.name, + title: `${titlePrefix}: Supply Rate ${w.name}`, + bottom: list.flatMap(({ name, color, pattern }) => + percentRatio({ pattern: pattern.supply.all.delta.rate[w.key], name, color }), + ), + })), + ], + }, + ], + }, + ], + }, + { + name: "Realized Cap", + tree: [ + { + name: "All", + title: `${titlePrefix}: Realized Cap`, + bottom: list.map(({ name, color, pattern }) => + line({ metric: pattern.realizedCap.all, name, color, unit: Unit.usd }), + ), + }, + { + name: "STH", + title: `${titlePrefix}: STH Realized Cap`, + bottom: list.map(({ name, color, pattern }) => + line({ metric: pattern.realizedCap.sth, name, color, unit: Unit.usd }), + ), + }, + ], + }, + { + name: "Realized Price", + tree: groupedSimplePriceRatioTree({ + list: list.map(({ name, color, pattern }) => ({ name, color, pattern: pattern.realizedPrice })), + title: `${titlePrefix}: Realized Price`, + }), + }, + { + name: "NUPL", + title: `${titlePrefix}: NUPL`, + bottom: list.map(({ name, color, pattern }) => + line({ metric: pattern.nupl.ratio, name, color, unit: Unit.ratio }), + ), + }, + ]; +} + +/** + * @param {{ range: { name: string, color: Color, pattern: RealizedSupplyPattern }[], profit: { name: string, color: Color, pattern: RealizedSupplyPattern }[], loss: { name: string, color: Color, pattern: RealizedSupplyPattern }[] }} args + * @returns {PartialOptionsGroup} + */ +export function createUtxoProfitabilitySection({ range, profit, loss }) { + return { + name: "UTXO Profitability", + tree: [ + { + name: "Range", + tree: [ + { name: "Compare", tree: groupedBucketCharts(range, "Profitability Range") }, + ...range.map(singleBucketFolder), + ], + }, + { + name: "In Profit", + tree: [ + { name: "Compare", tree: groupedBucketCharts(profit, "In Profit") }, + ...profit.map(singleBucketFolder), + ], + }, + { + name: "In Loss", + tree: [ + { name: "Compare", tree: groupedBucketCharts(loss, "In Loss") }, + ...loss.map(singleBucketFolder), + ], + }, + ], + }; +} diff --git a/website/scripts/options/distribution/prices.js b/website/scripts/options/distribution/prices.js index a4cc4666c..4ff1cb3e2 100644 --- a/website/scripts/options/distribution/prices.js +++ b/website/scripts/options/distribution/prices.js @@ -14,7 +14,7 @@ */ import { colors } from "../../utils/colors.js"; -import { createPriceRatioCharts, mapCohortsWithAll } from "../shared.js"; +import { createPriceRatioCharts, mapCohortsWithAll, priceRatioPercentilesTree } from "../shared.js"; import { baseline, price } from "../series.js"; import { Unit } from "../../utils/units.js"; @@ -53,26 +53,12 @@ export function createPricesSectionFull({ cohort, title }) { }, { name: "Investor", - tree: [ - { - name: "Price", - title: title("Investor Price"), - top: [price({ metric: tree.realized.investor.price, name: "Investor", color })], - }, - { - name: "Ratio", - title: title("Investor Price Ratio"), - top: [price({ metric: tree.realized.investor.price, name: "Investor", color })], - bottom: [ - baseline({ - metric: tree.realized.investor.price.ratio, - name: "Ratio", - unit: Unit.ratio, - base: 1, - }), - ], - }, - ], + tree: priceRatioPercentilesTree({ + pattern: tree.realized.investor.price, + title: title("Investor Price"), + legend: "Investor", + color, + }), }, ], }; diff --git a/website/scripts/options/distribution/utxo-profitability.js b/website/scripts/options/distribution/utxo-profitability.js deleted file mode 100644 index 180af46a9..000000000 --- a/website/scripts/options/distribution/utxo-profitability.js +++ /dev/null @@ -1,89 +0,0 @@ -/** UTXO Profitability section — range bands, cumulative profit/loss thresholds */ - -import { colors } from "../../utils/colors.js"; -import { entries } from "../../utils/array.js"; -import { Unit } from "../../utils/units.js"; -import { line, price } from "../series.js"; -import { brk } from "../../client.js"; -import { satsBtcUsd } from "../shared.js"; - -/** - * @param {{ name: string, color: Color, pattern: RealizedSupplyPattern }[]} list - * @param {string} titlePrefix - * @returns {PartialOptionsTree} - */ -function bucketCharts(list, titlePrefix) { - return [ - { - name: "Supply", - title: `${titlePrefix}: Supply`, - bottom: list.flatMap(({ name, color, pattern }) => - satsBtcUsd({ pattern: pattern.supply, name, color }), - ), - }, - { - name: "Realized Cap", - title: `${titlePrefix}: Realized Cap`, - bottom: list.map(({ name, color, pattern }) => - line({ metric: pattern.realizedCap, name, color, unit: Unit.usd }), - ), - }, - { - name: "Realized Price", - title: `${titlePrefix}: Realized Price`, - top: list.map(({ name, color, pattern }) => - price({ metric: pattern.realizedPrice, name, color }), - ), - }, - ]; -} - -/** - * @returns {PartialOptionsGroup} - */ -export function createUtxoProfitabilitySection() { - const { range, profit, loss } = brk.metrics.cohorts.utxo.profitability; - const { - PROFITABILITY_RANGE_NAMES, - PROFIT_NAMES, - LOSS_NAMES, - } = brk; - - const rangeList = entries(PROFITABILITY_RANGE_NAMES).map( - ([key, names], i, arr) => ({ - name: names.short, - color: colors.at(i, arr.length), - pattern: range[key], - }), - ); - - const profitList = entries(PROFIT_NAMES).map(([key, names], i, arr) => ({ - name: names.short, - color: colors.at(i, arr.length), - pattern: profit[key], - })); - - const lossList = entries(LOSS_NAMES).map(([key, names], i, arr) => ({ - name: names.short, - color: colors.at(i, arr.length), - pattern: loss[key], - })); - - return { - name: "UTXO Profitability", - tree: [ - { - name: "Range", - tree: bucketCharts(rangeList, "Profitability Range"), - }, - { - name: "In Profit", - tree: bucketCharts(profitList, "In Profit"), - }, - { - name: "In Loss", - tree: bucketCharts(lossList, "In Loss"), - }, - ], - }; -} diff --git a/website/scripts/options/market.js b/website/scripts/options/market.js index 28bb4da14..7f210d36d 100644 --- a/website/scripts/options/market.js +++ b/website/scripts/options/market.js @@ -14,6 +14,7 @@ import { percentRatioBaseline, ROLLING_WINDOWS, } from "./series.js"; +import { simplePriceRatioTree } from "./shared.js"; import { periodIdToName } from "./utils.js"; /** @@ -68,26 +69,12 @@ function createMaSubSection(label, averages) { /** @param {MaPeriod} a */ const toFolder = (a) => ({ name: periodIdToName(a.id, true), - tree: [ - { - name: "Price", - title: `${periodIdToName(a.id, true)} ${label}`, - top: [price({ metric: a.ratio, name: "average", color: a.color })], - }, - { - name: "Ratio", - title: `${periodIdToName(a.id, true)} ${label} Ratio`, - top: [price({ metric: a.ratio, name: "average", color: a.color })], - bottom: [ - baseline({ - metric: a.ratio.ratio, - name: "Ratio", - color: a.color, - unit: Unit.ratio, - }), - ], - }, - ], + tree: simplePriceRatioTree({ + pattern: a.ratio, + title: `${periodIdToName(a.id, true)} ${label}`, + legend: "average", + color: a.color, + }), }); return { diff --git a/website/scripts/options/partial.js b/website/scripts/options/partial.js index 3a6158634..0621aea80 100644 --- a/website/scripts/options/partial.js +++ b/website/scripts/options/partial.js @@ -19,8 +19,8 @@ import { createGroupedCohortFolderBasicWithoutMarketCap, createGroupedCohortFolderAddress, createGroupedAddressCohortFolder, + createUtxoProfitabilitySection, } from "./distribution/index.js"; -import { createUtxoProfitabilitySection } from "./distribution/utxo-profitability.js"; import { createMarketSection } from "./market.js"; import { createNetworkSection } from "./network.js"; import { createMiningSection } from "./mining.js"; @@ -53,6 +53,9 @@ export function createPartialOptions() { typeAddressable, typeOther, class: class_, + profitabilityRange, + profitabilityProfit, + profitabilityLoss, } = buildCohortData(); return [ @@ -92,7 +95,7 @@ export function createPartialOptions() { // Ages cohorts { - name: "UTXO Ages", + name: "UTXO Age", tree: [ // Younger Than (< X old) { @@ -138,7 +141,7 @@ export function createPartialOptions() { // Sizes cohorts (UTXO size) { - name: "UTXO Sizes", + name: "UTXO Size", tree: [ // Less Than (< X sats) { @@ -184,7 +187,7 @@ export function createPartialOptions() { // Balances cohorts (Address balance) { - name: "Address Balances", + name: "Address Balance", tree: [ // Less Than (< X sats) { @@ -230,11 +233,11 @@ export function createPartialOptions() { // Script Types - addressable types have addrCount, others don't { - name: "Script Types", + name: "Script Type", tree: [ createGroupedCohortFolderAddress({ name: "Compare", - title: "Script Types", + title: "Script Type", list: typeAddressable, all: cohortAll, }), @@ -245,11 +248,11 @@ export function createPartialOptions() { // Epochs { - name: "Epochs", + name: "Epoch", tree: [ createGroupedCohortFolderWithAdjusted({ name: "Compare", - title: "Epochs", + title: "Epoch", list: epoch, all: cohortAll, }), @@ -257,13 +260,13 @@ export function createPartialOptions() { ], }, - // Years + // Classes { - name: "Years", + name: "Class", tree: [ createGroupedCohortFolderWithAdjusted({ name: "Compare", - title: "Years", + title: "Class", list: class_, all: cohortAll, }), @@ -272,7 +275,11 @@ export function createPartialOptions() { }, // UTXO Profitability bands - createUtxoProfitabilitySection(), + createUtxoProfitabilitySection({ + range: profitabilityRange, + profit: profitabilityProfit, + loss: profitabilityLoss, + }), ], }, diff --git a/website/scripts/options/shared.js b/website/scripts/options/shared.js index 73bab42aa..ebb796ebf 100644 --- a/website/scripts/options/shared.js +++ b/website/scripts/options/shared.js @@ -1,7 +1,7 @@ /** Shared helpers for options */ import { Unit } from "../utils/units.js"; -import { line, baseline, price } from "./series.js"; +import { line, baseline, price, ROLLING_WINDOWS } from "./series.js"; import { priceLine, priceLines } from "./constants.js"; import { colors } from "../utils/colors.js"; @@ -234,6 +234,159 @@ export function satsBtcUsdRolling({ pattern, name, color, defaultActive }) { return satsBtcUsd({ pattern, name, color, defaultActive }); } +/** + * Build a full Sum / Rolling / Cumulative tree from a FullValuePattern + * @param {Object} args + * @param {FullValuePattern} args.pattern + * @param {string} args.name + * @param {string} args.title + * @param {Color} [args.color] + * @returns {PartialOptionsTree} + */ +export function satsBtcUsdFullTree({ pattern, name, title, color }) { + return [ + { + name: "Sum", + title, + bottom: satsBtcUsd({ pattern: pattern.base, name, color }), + }, + { + name: "Rolling", + tree: [ + { + name: "Compare", + title: `${title} Rolling Sum`, + bottom: ROLLING_WINDOWS.flatMap((w) => + satsBtcUsd({ pattern: pattern.sum[w.key], name: w.name, color: w.color }), + ), + }, + ...ROLLING_WINDOWS.map((w) => ({ + name: w.name, + title: `${title} ${w.name} Rolling Sum`, + bottom: satsBtcUsd({ pattern: pattern.sum[w.key], name: w.name, color: w.color }), + })), + ], + }, + { + name: "Cumulative", + title: `${title} (Total)`, + bottom: satsBtcUsd({ pattern: pattern.cumulative, name: "all-time", color }), + }, + ]; +} + +/** + * Create Price + Ratio charts from a simple price pattern (BpsCentsRatioSatsUsdPattern) + * @param {Object} args + * @param {AnyPricePattern & { ratio: AnyMetricPattern }} args.pattern + * @param {string} args.title + * @param {string} args.legend + * @param {Color} [args.color] + * @returns {PartialOptionsTree} + */ +export function simplePriceRatioTree({ pattern, title, legend, color }) { + return [ + { + name: "Price", + title, + top: [price({ metric: pattern, name: legend, color })], + }, + { + name: "Ratio", + title: `${title} Ratio`, + top: [price({ metric: pattern, name: legend, color })], + bottom: [ + baseline({ metric: pattern.ratio, name: "Ratio", unit: Unit.ratio, base: 1 }), + ], + }, + ]; +} + +/** + * Create Price + Ratio charts with percentile bands (no SMAs/z-scores) + * @param {Object} args + * @param {PriceRatioPercentilesPattern} args.pattern + * @param {string} args.title + * @param {string} args.legend + * @param {Color} [args.color] + * @param {FetchedPriceSeriesBlueprint[]} [args.priceReferences] + * @returns {PartialOptionsTree} + */ +export function priceRatioPercentilesTree({ pattern, title, legend, color, priceReferences }) { + const p = pattern.percentiles; + const pctUsd = [ + { name: "pct95", prop: p.pct95.price, color: colors.ratioPct._95 }, + { name: "pct5", prop: p.pct5.price, color: colors.ratioPct._5 }, + { name: "pct98", prop: p.pct98.price, color: colors.ratioPct._98 }, + { name: "pct2", prop: p.pct2.price, color: colors.ratioPct._2 }, + { name: "pct99", prop: p.pct99.price, color: colors.ratioPct._99 }, + { name: "pct1", prop: p.pct1.price, color: colors.ratioPct._1 }, + ]; + const pctRatio = [ + { name: "pct95", prop: p.pct95.ratio, color: colors.ratioPct._95 }, + { name: "pct5", prop: p.pct5.ratio, color: colors.ratioPct._5 }, + { name: "pct98", prop: p.pct98.ratio, color: colors.ratioPct._98 }, + { name: "pct2", prop: p.pct2.ratio, color: colors.ratioPct._2 }, + { name: "pct99", prop: p.pct99.ratio, color: colors.ratioPct._99 }, + { name: "pct1", prop: p.pct1.ratio, color: colors.ratioPct._1 }, + ]; + return [ + { + name: "Price", + title, + top: [ + price({ metric: pattern, name: legend, color }), + ...(priceReferences ?? []), + ...pctUsd.map(({ name, prop, color }) => + price({ metric: prop, name, color, defaultActive: false, options: { lineStyle: 1 } }), + ), + ], + }, + { + name: "Ratio", + title: `${title} Ratio`, + top: [ + price({ metric: pattern, name: legend, color }), + ...pctUsd.map(({ name, prop, color }) => + price({ metric: prop, name, color, defaultActive: false, options: { lineStyle: 1 } }), + ), + ], + bottom: [ + baseline({ metric: pattern.ratio, name: "Ratio", unit: Unit.ratio, base: 1 }), + ...pctRatio.map(({ name, prop, color }) => + line({ metric: prop, name, color, defaultActive: false, unit: Unit.ratio, options: { lineStyle: 1 } }), + ), + ], + }, + ]; +} + +/** + * Create grouped Price + Ratio charts overlaying multiple series + * @param {Object} args + * @param {{ name: string, color?: Color, pattern: AnyPricePattern & { ratio: AnyMetricPattern } }[]} args.list + * @param {string} args.title + * @returns {PartialOptionsTree} + */ +export function groupedSimplePriceRatioTree({ list, title }) { + return [ + { + name: "Price", + title, + top: list.map(({ name, color, pattern }) => + price({ metric: pattern, name, color }), + ), + }, + { + name: "Ratio", + title: `${title} Ratio`, + bottom: list.map(({ name, color, pattern }) => + baseline({ metric: pattern.ratio, name, color, unit: Unit.ratio, base: 1 }), + ), + }, + ]; +} + /** * Create coinbase/subsidy/fee rolling sum series from separate sources * @param {Object} args diff --git a/website/scripts/options/types.js b/website/scripts/options/types.js index b63e5071b..93c3a2c0c 100644 --- a/website/scripts/options/types.js +++ b/website/scripts/options/types.js @@ -234,7 +234,7 @@ * @property {AgeRangePattern} tree * * Age range cohort with matured supply - * @typedef {CohortAgeRange & { matured: AnyValuePattern }} CohortAgeRangeWithMatured + * @typedef {CohortAgeRange & { matured: FullValuePattern }} CohortAgeRangeWithMatured * * Basic cohort WITH RelToMarketCap (geAmount.*, ltAmount.*) * @typedef {Object} CohortBasicWithMarketCap diff --git a/website/scripts/options/unused.js b/website/scripts/options/unused.js index b1c66cc23..bbfd2d839 100644 --- a/website/scripts/options/unused.js +++ b/website/scripts/options/unused.js @@ -26,6 +26,14 @@ function walkMetrics(node, map, path) { for (const [key, value] of Object.entries(node)) { const kn = key.toLowerCase(); if ( + key === "lookback" || + key === "cumulativeMarketCap" || + key === "sd24h" || + key === "spot" || + key === "ohlc" || + key === "state" || + key === "emaSlow" || + key === "emaFast" || key.endsWith("Raw") || key.endsWith("Cents") || key.endsWith("State") || diff --git a/website/scripts/types.js b/website/scripts/types.js index 58c81d259..40c79c38c 100644 --- a/website/scripts/types.js +++ b/website/scripts/types.js @@ -56,6 +56,8 @@ * @typedef {Brk.BaseCumulativeSumPattern4} CoinbasePattern * ActivePriceRatioPattern: ratio pattern with price (extended) * @typedef {Brk.BpsPriceRatioPattern} ActivePriceRatioPattern + * PriceRatioPercentilesPattern: price pattern with ratio + percentiles (no SMAs/stdDev) + * @typedef {Brk.BpsCentsPercentilesRatioSatsUsdPattern} PriceRatioPercentilesPattern * AnyRatioPattern: full ratio pattern with percentiles, SMAs, and std dev bands * @typedef {Brk.BpsCentsPercentilesRatioSatsSmaStdUsdPattern} AnyRatioPattern * ValuePattern: patterns with base + cumulative (no rolling) @@ -83,7 +85,7 @@ * @typedef {Brk.GrossInvestedLossNetNuplProfitSentimentPattern2} UnrealizedPattern * * Profitability bucket pattern - * @typedef {Brk.RealizedSupplyPattern} RealizedSupplyPattern + * @typedef {Brk.MvrvNuplRealizedSupplyPattern} RealizedSupplyPattern * * Realized patterns * @typedef {Brk.CapGrossInvestorLossMvrvNetPeakPriceProfitSellSoprPattern} RealizedPattern