global: snapshot

This commit is contained in:
nym21
2026-03-15 11:25:21 +01:00
parent 9e36a4188a
commit 9626c7de32
54 changed files with 884 additions and 750 deletions

View File

@@ -2161,10 +2161,10 @@ impl _1m1w1y24hPattern3 {
/// Create a new pattern node with accumulated metric name. /// Create a new pattern node with accumulated metric name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self { pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self { Self {
_1m: CentsUsdPattern::new(client.clone(), _m(&acc, "1m_change")), _1m: CentsUsdPattern::new(client.clone(), _m(&acc, "1m")),
_1w: CentsUsdPattern::new(client.clone(), _m(&acc, "1w_change")), _1w: CentsUsdPattern::new(client.clone(), _m(&acc, "1w")),
_1y: CentsUsdPattern::new(client.clone(), _m(&acc, "1y_change")), _1y: CentsUsdPattern::new(client.clone(), _m(&acc, "1y")),
_24h: CentsUsdPattern::new(client.clone(), _m(&acc, "24h_change")), _24h: CentsUsdPattern::new(client.clone(), _m(&acc, "24h")),
} }
} }
} }
@@ -2965,8 +2965,8 @@ impl CentsUsdPattern {
/// Create a new pattern node with accumulated metric name. /// Create a new pattern node with accumulated metric name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self { pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self { Self {
cents: MetricPattern1::new(client.clone(), acc.clone()), cents: MetricPattern1::new(client.clone(), _m(&acc, "cents")),
usd: MetricPattern1::new(client.clone(), _m(&acc, "usd")), usd: MetricPattern1::new(client.clone(), acc.clone()),
} }
} }
} }

View File

@@ -140,22 +140,12 @@ impl ActivityCountVecs {
Ok(()) Ok(())
} }
pub(crate) fn truncate_push_height( #[inline(always)]
&mut self, pub(crate) fn push_height(&mut self, counts: &BlockActivityCounts) {
height: Height, self.reactivated.height.push(counts.reactivated.into());
counts: &BlockActivityCounts, self.sending.height.push(counts.sending.into());
) -> Result<()> { self.receiving.height.push(counts.receiving.into());
self.reactivated self.both.height.push(counts.both.into());
.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(())
} }
pub(crate) fn compute_rest( pub(crate) fn compute_rest(
@@ -242,15 +232,11 @@ impl AddressTypeToActivityCountVecs {
Ok(()) Ok(())
} }
pub(crate) fn truncate_push_height( #[inline(always)]
&mut self, pub(crate) fn push_height(&mut self, counts: &AddressTypeToActivityCounts) {
height: Height,
counts: &AddressTypeToActivityCounts,
) -> Result<()> {
for (vecs, c) in self.0.values_mut().zip(counts.0.values()) { 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(()) Ok(())
} }
pub(crate) fn truncate_push_height( #[inline(always)]
&mut self, pub(crate) fn push_height(&mut self, counts: &AddressTypeToActivityCounts) {
height: Height,
counts: &AddressTypeToActivityCounts,
) -> Result<()> {
let totals = counts.totals(); let totals = counts.totals();
self.all.truncate_push_height(height, &totals)?; self.all.push_height(&totals);
self.by_address_type.truncate_push_height(height, counts)?; self.by_address_type.push_height(counts);
Ok(())
} }
} }

View File

@@ -137,19 +137,14 @@ impl AddressTypeToAddressCountVecs {
.map(|v| &mut v.height as &mut dyn AnyStoredVec) .map(|v| &mut v.height as &mut dyn AnyStoredVec)
} }
pub(crate) fn truncate_push_height( #[inline(always)]
&mut self, pub(crate) fn push_height(&mut self, address_counts: &AddressTypeToAddressCount) {
height: Height,
address_counts: &AddressTypeToAddressCount,
) -> Result<()> {
for (vecs, &count) in self.0.values_mut().zip(address_counts.values()) { 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<()> { pub(crate) fn reset_height(&mut self) -> Result<()> {
use vecdb::WritableVec;
for v in self.0.values_mut() { for v in self.0.values_mut() {
v.height.reset()?; v.height.reset()?;
} }
@@ -198,16 +193,10 @@ impl AddressCountsVecs {
Ok(()) Ok(())
} }
pub(crate) fn truncate_push_height( #[inline(always)]
&mut self, pub(crate) fn push_height(&mut self, total: u64, address_counts: &AddressTypeToAddressCount) {
height: Height, self.all.height.push(total.into());
total: u64, self.by_address_type.push_height(address_counts);
address_counts: &AddressTypeToAddressCount,
) -> Result<()> {
self.all.height.truncate_push(height, total.into())?;
self.by_address_type
.truncate_push_height(height, address_counts)?;
Ok(())
} }
pub(crate) fn compute_rest( pub(crate) fn compute_rest(

View File

@@ -155,31 +155,22 @@ impl DynCohortVecs for AddressCohortVecs {
Ok(()) 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) { if self.starting_height.is_some_and(|h| h > height) {
return Ok(()); return;
} }
if let Some(state) = self.state.as_ref() { if let Some(state) = self.state.as_ref() {
self.address_count self.address_count
.height .height
.truncate_push(height, state.address_count.into())?; .push(state.address_count.into());
self.metrics.supply.truncate_push(height, &state.inner)?; self.metrics.supply.push_state(&state.inner);
self.metrics.outputs.truncate_push(height, &state.inner)?; self.metrics.outputs.push_state(&state.inner);
self.metrics.realized.truncate_push(height, &state.inner)?; self.metrics.realized.push_state(&state.inner);
}
} }
Ok(()) fn push_unrealized_state(&mut self, _height_price: Cents) {}
}
fn compute_then_truncate_push_unrealized_states(
&mut self,
_height: Height,
_height_price: Cents,
_is_day_boundary: bool,
) -> Result<()> {
Ok(())
}
fn compute_rest_part1( fn compute_rest_part1(
&mut self, &mut self,

View File

@@ -20,16 +20,12 @@ pub trait DynCohortVecs: Send + Sync {
/// Validate that computed vectors have correct versions. /// Validate that computed vectors have correct versions.
fn validate_computed_versions(&mut self, base_version: Version) -> Result<()>; fn validate_computed_versions(&mut self, base_version: Version) -> Result<()>;
/// Push state to height-indexed vectors (truncating if needed). /// Push state to height-indexed vectors.
fn truncate_push(&mut self, height: Height) -> Result<()>; /// 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. /// Compute and push unrealized profit/loss states.
fn compute_then_truncate_push_unrealized_states( fn push_unrealized_state(&mut self, height_price: Cents);
&mut self,
height: Height,
height_price: Cents,
is_day_boundary: bool,
) -> Result<()>;
/// First phase of post-processing computations. /// First phase of post-processing computations.
fn compute_rest_part1( fn compute_rest_part1(

View File

@@ -339,15 +339,11 @@ impl UTXOCohorts<Rw> {
} }
/// Push maturation sats to the matured vecs for the given height. /// Push maturation sats to the matured vecs for the given height.
pub(crate) fn push_maturation( #[inline(always)]
&mut self, pub(crate) fn push_maturation(&mut self, matured: &AgeRange<Sats>) {
height: Height,
matured: &AgeRange<Sats>,
) -> Result<()> {
for (v, &sats) in self.matured.iter_mut().zip(matured.iter()) { 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( pub(crate) fn par_iter_separate_mut(
@@ -795,8 +791,8 @@ impl UTXOCohorts<Rw> {
} }
/// Aggregate RealizedFull fields from age_range states and push to all/sth/lth. /// 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. /// Called during the block loop after separate cohorts' push_state but before reset.
pub(crate) fn push_overlapping_realized_full(&mut self, height: Height) -> Result<()> { pub(crate) fn push_overlapping_realized_full(&mut self) {
let Self { let Self {
all, all,
sth, sth,
@@ -823,11 +819,9 @@ impl UTXOCohorts<Rw> {
} }
} }
all.metrics.realized.push_from_accum(&all_acc, height)?; all.metrics.realized.push_accum(&all_acc);
sth.metrics.realized.push_from_accum(&sth_acc, height)?; sth.metrics.realized.push_accum(&sth_acc);
lth.metrics.realized.push_from_accum(&lth_acc, height)?; lth.metrics.realized.push_accum(&lth_acc);
Ok(())
} }
} }

View File

@@ -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_cohort::{Filtered, PROFITABILITY_RANGE_COUNT, PROFIT_COUNT, TERM_NAMES};
use brk_error::Result; 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}; use crate::distribution::metrics::{CostBasis, ProfitabilityMetrics};
@@ -16,15 +16,14 @@ impl UTXOCohorts {
/// ///
/// Percentiles and profitability are computed per-block from the Fenwick tree. /// Percentiles and profitability are computed per-block from the Fenwick tree.
/// Disk distributions are written only at day boundaries via K-way merge. /// 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, &mut self,
height: Height,
spot_price: Cents, spot_price: Cents,
date_opt: Option<Date>, date_opt: Option<Date>,
states_path: &Path, states_path: &Path,
) -> Result<()> { ) -> Result<()> {
if self.fenwick.is_initialized() { if self.fenwick.is_initialized() {
self.push_fenwick_results(height, spot_price)?; self.push_fenwick_results(spot_price);
} }
// Disk distributions only at day boundaries // Disk distributions only at day boundaries
@@ -36,20 +35,20 @@ impl UTXOCohorts {
} }
/// Push all Fenwick-derived per-block results: percentiles, density, profitability. /// 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_d, sth_d, lth_d) = self.fenwick.density(spot_price);
let all = self.fenwick.percentiles_all(); 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(); 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(); let lth = self.fenwick.percentiles_lth();
push_cost_basis(height, &lth, lth_d, &mut self.lth.metrics.cost_basis)?; push_cost_basis(&lth, lth_d, &mut self.lth.metrics.cost_basis);
let prof = self.fenwick.profitability(spot_price); 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. /// 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. /// Push percentiles + density to cost basis vecs.
#[inline(always)]
fn push_cost_basis( fn push_cost_basis(
height: Height,
percentiles: &PercentileResult, percentiles: &PercentileResult,
density_bps: u16, density_bps: u16,
cost_basis: &mut CostBasis, cost_basis: &mut CostBasis,
) -> Result<()> { ) {
cost_basis.truncate_push_minmax(height, percentiles.min_price, percentiles.max_price)?; cost_basis.push_minmax(percentiles.min_price, percentiles.max_price);
cost_basis.truncate_push_percentiles(height, &percentiles.sat_prices, &percentiles.usd_prices)?; cost_basis.push_percentiles(&percentiles.sat_prices, &percentiles.usd_prices);
cost_basis.truncate_push_density(height, BasisPoints16::from(density_bps)) cost_basis.push_density(BasisPoints16::from(density_bps));
} }
/// Convert raw (cents × sats) accumulator to Dollars (÷ 100 for cents→dollars, ÷ 1e8 for sats). /// 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. /// Push profitability range + profit/loss aggregate values to vecs.
fn push_profitability( fn push_profitability(
height: Height,
buckets: &[ProfitabilityRangeResult; PROFITABILITY_RANGE_COUNT], buckets: &[ProfitabilityRangeResult; PROFITABILITY_RANGE_COUNT],
metrics: &mut ProfitabilityMetrics, metrics: &mut ProfitabilityMetrics,
) -> Result<()> { ) {
// Truncate all buckets once upfront to avoid per-push checks
metrics.truncate(height)?;
// Push 25 range buckets // Push 25 range buckets
for (i, bucket) in metrics.range.as_array_mut().into_iter().enumerate() { for (i, bucket) in metrics.range.as_array_mut().into_iter().enumerate() {
let r = &buckets[i]; let r = &buckets[i];
@@ -170,8 +165,6 @@ fn push_profitability(
raw_usd_to_dollars(cum_sth_usd), raw_usd_to_dollars(cum_sth_usd),
); );
} }
Ok(())
} }
fn write_distribution( fn write_distribution(

View File

@@ -28,38 +28,30 @@ impl DynCohortVecs for UTXOCohortVecs<CoreCohortMetrics> {
self.metrics.validate_computed_versions(base_version) 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) { if self.state_starting_height.is_some_and(|h| h > height) {
return Ok(()); return;
} }
if let Some(state) = self.state.as_ref() { if let Some(state) = self.state.as_ref() {
self.metrics.supply.truncate_push(height, state)?; self.metrics.supply.push_state(state);
self.metrics.outputs.truncate_push(height, state)?; self.metrics.outputs.push_state(state);
self.metrics.activity.truncate_push(height, state)?; self.metrics.activity.push_state(state);
self.metrics.realized.truncate_push(height, state)?; self.metrics.realized.push_state(state);
}
} }
Ok(()) fn push_unrealized_state(&mut self, height_price: Cents) {
}
fn compute_then_truncate_push_unrealized_states(
&mut self,
height: Height,
height_price: Cents,
_is_day_boundary: bool,
) -> Result<()> {
if let Some(state) = self.state.as_mut() { if let Some(state) = self.state.as_mut() {
state.apply_pending(); state.apply_pending();
let unrealized_state = state.compute_unrealized_state(height_price); let unrealized_state = state.compute_unrealized_state(height_price);
self.metrics self.metrics
.unrealized .unrealized
.truncate_push(height, &unrealized_state)?; .push_state(&unrealized_state);
self.metrics self.metrics
.supply .supply
.truncate_push_profitability(height, &unrealized_state)?; .push_profitability(&unrealized_state);
} }
Ok(())
} }
fn compute_rest_part1( fn compute_rest_part1(

View File

@@ -31,28 +31,19 @@ impl DynCohortVecs for UTXOCohortVecs<MinimalCohortMetrics> {
Ok(()) 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) { if self.state_starting_height.is_some_and(|h| h > height) {
return Ok(()); return;
} }
if let Some(state) = self.state.as_ref() { if let Some(state) = self.state.as_ref() {
self.metrics.supply.truncate_push(height, state)?; self.metrics.supply.push_state(state);
self.metrics.outputs.truncate_push(height, state)?; self.metrics.outputs.push_state(state);
self.metrics.realized.truncate_push(height, state)?; self.metrics.realized.push_state(state);
}
} }
Ok(()) fn push_unrealized_state(&mut self, _height_price: Cents) {}
}
fn compute_then_truncate_push_unrealized_states(
&mut self,
_height: Height,
_height_price: Cents,
_is_day_boundary: bool,
) -> Result<()> {
Ok(())
}
fn compute_rest_part1( fn compute_rest_part1(
&mut self, &mut self,

View File

@@ -166,29 +166,21 @@ impl<M: CohortMetricsBase + Traversable> DynCohortVecs for UTXOCohortVecs<M> {
self.metrics.validate_computed_versions(base_version) 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) { if self.state_starting_height.is_some_and(|h| h > height) {
return Ok(()); return;
} }
if let Some(state) = self.state.as_ref() { if let Some(state) = self.state.as_ref() {
self.metrics.truncate_push(height, state)?; self.metrics.push_state(state);
}
} }
Ok(()) fn push_unrealized_state(&mut self, height_price: Cents) {
}
fn compute_then_truncate_push_unrealized_states(
&mut self,
height: Height,
height_price: Cents,
_is_day_boundary: bool,
) -> Result<()> {
if let Some(state) = self.state.as_mut() { if let Some(state) = self.state.as_mut() {
self.metrics self.metrics
.compute_and_push_unrealized(height, height_price, state)?; .compute_and_push_unrealized(height_price, state);
} }
Ok(())
} }
fn compute_rest_part1( fn compute_rest_part1(

View File

@@ -28,37 +28,29 @@ impl DynCohortVecs for UTXOCohortVecs<TypeCohortMetrics> {
Ok(()) 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) { if self.state_starting_height.is_some_and(|h| h > height) {
return Ok(()); return;
} }
if let Some(state) = self.state.as_ref() { if let Some(state) = self.state.as_ref() {
self.metrics.supply.truncate_push(height, state)?; self.metrics.supply.push_state(state);
self.metrics.outputs.truncate_push(height, state)?; self.metrics.outputs.push_state(state);
self.metrics.realized.truncate_push(height, state)?; self.metrics.realized.push_state(state);
}
} }
Ok(()) fn push_unrealized_state(&mut self, height_price: Cents) {
}
fn compute_then_truncate_push_unrealized_states(
&mut self,
height: Height,
height_price: Cents,
_is_day_boundary: bool,
) -> Result<()> {
if let Some(state) = self.state.as_mut() { if let Some(state) = self.state.as_mut() {
state.apply_pending(); state.apply_pending();
let unrealized_state = state.compute_unrealized_state(height_price); let unrealized_state = state.compute_unrealized_state(height_price);
self.metrics self.metrics
.unrealized .unrealized
.truncate_push(height, &unrealized_state)?; .push_state(&unrealized_state);
self.metrics self.metrics
.supply .supply
.truncate_push_profitability(height, &unrealized_state)?; .push_profitability(&unrealized_state);
} }
Ok(())
} }
fn compute_rest_part1( fn compute_rest_part1(

View File

@@ -7,7 +7,7 @@ use brk_types::{
use rayon::prelude::*; use rayon::prelude::*;
use rustc_hash::FxHashSet; use rustc_hash::FxHashSet;
use tracing::{debug, info}; use tracing::{debug, info};
use vecdb::{AnyVec, Exit, ReadableVec, VecIndex, WritableVec}; use vecdb::{AnyStoredVec, AnyVec, Exit, ReadableVec, VecIndex, WritableVec};
use crate::{ use crate::{
distribution::{ distribution::{
@@ -210,6 +210,22 @@ pub(crate) fn process_blocks(
// Initialize Fenwick tree from imported BTreeMap state (one-time) // Initialize Fenwick tree from imported BTreeMap state (one-time)
vecs.utxo_cohorts.init_fenwick_if_needed(); 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) // Reusable hashsets (avoid per-block allocation)
let mut received_addresses = ByAddressType::<FxHashSet<TypeIndex>>::default(); let mut received_addresses = ByAddressType::<FxHashSet<TypeIndex>>::default();
let mut seen_senders = ByAddressType::<FxHashSet<TypeIndex>>::default(); let mut seen_senders = ByAddressType::<FxHashSet<TypeIndex>>::default();
@@ -364,14 +380,13 @@ pub(crate) fn process_blocks(
blocks_old as u128 * u64::from(sent.spendable_supply.value) as u128 blocks_old as u128 * u64::from(sent.spendable_supply.value) as u128
}) })
.sum(); .sum();
vecs.coinblocks_destroyed.base.height.truncate_push( vecs.coinblocks_destroyed.base.height.push(
height,
StoredF64::from(total_satblocks as f64 / Sats::ONE_BTC_U128 as f64), StoredF64::from(total_satblocks as f64 / Sats::ONE_BTC_U128 as f64),
)?; );
} }
// Record maturation (sats crossing age boundaries) // 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) // Build set of addresses that received this block (for detecting "both" in sent)
// Reuse pre-allocated hashsets: clear preserves capacity, avoiding reallocation // Reuse pre-allocated hashsets: clear preserves capacity, avoiding reallocation
@@ -437,14 +452,10 @@ pub(crate) fn process_blocks(
// Push to height-indexed vectors // Push to height-indexed vectors
vecs.addresses.funded vecs.addresses.funded
.truncate_push_height(height, address_counts.sum(), &address_counts)?; .push_height(address_counts.sum(), &address_counts);
vecs.addresses.empty.truncate_push_height( vecs.addresses.empty
height, .push_height(empty_address_counts.sum(), &empty_address_counts);
empty_address_counts.sum(), vecs.addresses.activity.push_height(&activity_counts);
&empty_address_counts,
)?;
vecs.addresses.activity
.truncate_push_height(height, &activity_counts)?;
let is_last_of_day = is_last_of_day[offset]; let is_last_of_day = is_last_of_day[offset];
let date_opt = is_last_of_day.then(|| Date::from(timestamp)); let date_opt = is_last_of_day.then(|| Date::from(timestamp));
@@ -454,11 +465,9 @@ pub(crate) fn process_blocks(
&mut vecs.address_cohorts, &mut vecs.address_cohorts,
height, height,
block_price, block_price,
date_opt.is_some(), );
)?;
vecs.utxo_cohorts.truncate_push_aggregate_percentiles( vecs.utxo_cohorts.push_aggregate_percentiles(
height,
block_price, block_price,
date_opt, date_opt,
&vecs.states_path, &vecs.states_path,
@@ -521,42 +530,29 @@ fn push_cohort_states(
address_cohorts: &mut AddressCohorts, address_cohorts: &mut AddressCohorts,
height: Height, height: Height,
height_price: Cents, height_price: Cents,
is_day_boundary: bool, ) {
) -> Result<()> {
// Phase 1: push + unrealized (no reset yet — states still needed for aggregation) // Phase 1: push + unrealized (no reset yet — states still needed for aggregation)
let (r1, r2) = rayon::join( rayon::join(
|| { || {
utxo_cohorts utxo_cohorts
.par_iter_separate_mut() .par_iter_separate_mut()
.try_for_each(|v| -> Result<()> { .for_each(|v| {
v.truncate_push(height)?; v.push_state(height);
v.compute_then_truncate_push_unrealized_states( v.push_unrealized_state(height_price);
height,
height_price,
is_day_boundary,
)?;
Ok(())
}) })
}, },
|| { || {
address_cohorts address_cohorts
.par_iter_separate_mut() .par_iter_separate_mut()
.try_for_each(|v| -> Result<()> { .for_each(|v| {
v.truncate_push(height)?; v.push_state(height);
v.compute_then_truncate_push_unrealized_states( v.push_unrealized_state(height_price);
height,
height_price,
is_day_boundary,
)?;
Ok(())
}) })
}, },
); );
r1?;
r2?;
// Phase 2: aggregate age_range realized states → push to overlapping cohorts' RealizedFull // 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 // Phase 3: reset per-block values
utxo_cohorts utxo_cohorts
@@ -565,6 +561,4 @@ fn push_cohort_states(
address_cohorts address_cohorts
.iter_separate_mut() .iter_separate_mut()
.for_each(|v| v.reset_single_iteration_values()); .for_each(|v| v.reset_single_iteration_values());
Ok(())
} }

View File

@@ -1,6 +1,6 @@
use brk_error::Result; use brk_error::Result;
use brk_traversable::Traversable; 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 vecdb::{AnyStoredVec, AnyVec, Exit, Rw, StorageMode, WritableVec};
use crate::{ use crate::{
@@ -40,27 +40,25 @@ impl ActivityCore {
.min(self.sent_in_loss.base.sats.height.len()) .min(self.sent_in_loss.base.sats.height.len())
} }
pub(crate) fn truncate_push( #[inline(always)]
pub(crate) fn push_state(
&mut self, &mut self,
height: Height,
state: &CohortState<impl RealizedOps, impl CostBasisOps>, state: &CohortState<impl RealizedOps, impl CostBasisOps>,
) -> Result<()> { ) {
self.sent.base.height.truncate_push(height, state.sent)?; self.sent.base.height.push(state.sent);
self.coindays_destroyed.base.height.truncate_push( self.coindays_destroyed.base.height.push(
height,
StoredF64::from(Bitcoin::from(state.satdays_destroyed)), StoredF64::from(Bitcoin::from(state.satdays_destroyed)),
)?; );
self.sent_in_profit self.sent_in_profit
.base .base
.sats .sats
.height .height
.truncate_push(height, state.realized.sent_in_profit())?; .push(state.realized.sent_in_profit());
self.sent_in_loss self.sent_in_loss
.base .base
.sats .sats
.height .height
.truncate_push(height, state.realized.sent_in_loss())?; .push(state.realized.sent_in_loss());
Ok(())
} }
pub(crate) fn collect_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> { pub(crate) fn collect_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {

View File

@@ -1,6 +1,6 @@
use brk_error::Result; use brk_error::Result;
use brk_traversable::Traversable; 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 derive_more::{Deref, DerefMut};
use vecdb::{AnyStoredVec, Exit, ReadableCloneableVec, Rw, StorageMode}; use vecdb::{AnyStoredVec, Exit, ReadableCloneableVec, Rw, StorageMode};
@@ -45,12 +45,12 @@ impl ActivityFull {
self.inner.min_len() self.inner.min_len()
} }
pub(crate) fn full_truncate_push( #[inline(always)]
pub(crate) fn full_push_state(
&mut self, &mut self,
height: Height,
state: &CohortState<impl RealizedOps, impl CostBasisOps>, state: &CohortState<impl RealizedOps, impl CostBasisOps>,
) -> Result<()> { ) {
self.inner.truncate_push(height, state) self.inner.push_state(state);
} }
pub(crate) fn collect_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> { pub(crate) fn collect_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {

View File

@@ -5,7 +5,7 @@ pub use self::core::ActivityCore;
pub use full::ActivityFull; pub use full::ActivityFull;
use brk_error::Result; use brk_error::Result;
use brk_types::{Height, Indexes, Version}; use brk_types::{Indexes, Version};
use vecdb::Exit; use vecdb::Exit;
use crate::distribution::state::{CohortState, CostBasisOps, RealizedOps}; use crate::distribution::state::{CohortState, CostBasisOps, RealizedOps};
@@ -14,11 +14,10 @@ pub trait ActivityLike: Send + Sync {
fn as_core(&self) -> &ActivityCore; fn as_core(&self) -> &ActivityCore;
fn as_core_mut(&mut self) -> &mut ActivityCore; fn as_core_mut(&mut self) -> &mut ActivityCore;
fn min_len(&self) -> usize; fn min_len(&self) -> usize;
fn truncate_push<R: RealizedOps>( fn push_state<R: RealizedOps>(
&mut self, &mut self,
height: Height,
state: &CohortState<R, impl CostBasisOps>, state: &CohortState<R, impl CostBasisOps>,
) -> Result<()>; );
fn validate_computed_versions(&mut self, base_version: Version) -> Result<()>; fn validate_computed_versions(&mut self, base_version: Version) -> Result<()>;
fn compute_from_stateful( fn compute_from_stateful(
&mut self, &mut self,
@@ -37,8 +36,8 @@ impl ActivityLike for ActivityCore {
fn as_core(&self) -> &ActivityCore { self } fn as_core(&self) -> &ActivityCore { self }
fn as_core_mut(&mut self) -> &mut ActivityCore { self } fn as_core_mut(&mut self) -> &mut ActivityCore { self }
fn min_len(&self) -> usize { self.min_len() } fn min_len(&self) -> usize { self.min_len() }
fn truncate_push<R: RealizedOps>(&mut self, height: Height, state: &CohortState<R, impl CostBasisOps>) -> Result<()> { fn push_state<R: RealizedOps>(&mut self, state: &CohortState<R, impl CostBasisOps>) {
self.truncate_push(height, state) self.push_state(state);
} }
fn validate_computed_versions(&mut self, base_version: Version) -> Result<()> { fn validate_computed_versions(&mut self, base_version: Version) -> Result<()> {
self.validate_computed_versions(base_version) self.validate_computed_versions(base_version)
@@ -55,8 +54,8 @@ impl ActivityLike for ActivityFull {
fn as_core(&self) -> &ActivityCore { &self.inner } fn as_core(&self) -> &ActivityCore { &self.inner }
fn as_core_mut(&mut self) -> &mut ActivityCore { &mut self.inner } fn as_core_mut(&mut self) -> &mut ActivityCore { &mut self.inner }
fn min_len(&self) -> usize { self.full_min_len() } fn min_len(&self) -> usize { self.full_min_len() }
fn truncate_push<R: RealizedOps>(&mut self, height: Height, state: &CohortState<R, impl CostBasisOps>) -> Result<()> { fn push_state<R: RealizedOps>(&mut self, state: &CohortState<R, impl CostBasisOps>) {
self.full_truncate_push(height, state) self.full_push_state(state);
} }
fn validate_computed_versions(&mut self, base_version: Version) -> Result<()> { fn validate_computed_versions(&mut self, base_version: Version) -> Result<()> {
self.inner.validate_computed_versions(base_version) self.inner.validate_computed_versions(base_version)

View File

@@ -1,6 +1,6 @@
use brk_error::Result; use brk_error::Result;
use brk_traversable::Traversable; 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 vecdb::{AnyStoredVec, AnyVec, Rw, StorageMode, WritableVec};
use crate::internal::{PerBlock, PercentPerBlock, PercentilesVecs, Price, PERCENTILES_LEN}; use crate::internal::{PerBlock, PercentPerBlock, PercentilesVecs, Price, PERCENTILES_LEN};
@@ -53,34 +53,25 @@ impl CostBasis {
.min(self.supply_density.bps.height.len()) .min(self.supply_density.bps.height.len())
} }
pub(crate) fn truncate_push_minmax( #[inline(always)]
&mut self, pub(crate) fn push_minmax(&mut self, min_price: Cents, max_price: Cents) {
height: Height, self.min.cents.height.push(min_price);
min_price: Cents, self.max.cents.height.push(max_price);
max_price: Cents,
) -> Result<()> {
self.min.cents.height.truncate_push(height, min_price)?;
self.max.cents.height.truncate_push(height, max_price)?;
Ok(())
} }
pub(crate) fn truncate_push_percentiles( #[inline(always)]
pub(crate) fn push_percentiles(
&mut self, &mut self,
height: Height,
sat_prices: &[Cents; PERCENTILES_LEN], sat_prices: &[Cents; PERCENTILES_LEN],
usd_prices: &[Cents; PERCENTILES_LEN], usd_prices: &[Cents; PERCENTILES_LEN],
) -> Result<()> { ) {
self.percentiles.truncate_push(height, sat_prices)?; self.percentiles.push(sat_prices);
self.invested_capital.truncate_push(height, usd_prices)?; self.invested_capital.push(usd_prices);
Ok(())
} }
pub(crate) fn truncate_push_density( #[inline(always)]
&mut self, pub(crate) fn push_density(&mut self, density_bps: BasisPoints16) {
height: Height, self.supply_density.bps.height.push(density_bps);
density_bps: BasisPoints16,
) -> Result<()> {
Ok(self.supply_density.bps.height.truncate_push(height, density_bps)?)
} }
pub(crate) fn validate_computed_versions(&mut self, base_version: Version) -> Result<()> { pub(crate) fn validate_computed_versions(&mut self, base_version: Version) -> Result<()> {

View File

@@ -85,7 +85,7 @@ pub use unrealized::{
use brk_cohort::Filter; use brk_cohort::Filter;
use brk_error::Result; use brk_error::Result;
use brk_types::{Cents, Height, Indexes, Version}; use brk_types::{Cents, Indexes, Version};
use vecdb::{AnyStoredVec, Exit, StorageMode}; use vecdb::{AnyStoredVec, Exit, StorageMode};
use crate::{ use crate::{
@@ -183,17 +183,13 @@ pub trait CohortMetricsBase:
/// Apply pending state, compute and push unrealized state. /// Apply pending state, compute and push unrealized state.
fn compute_and_push_unrealized( fn compute_and_push_unrealized(
&mut self, &mut self,
height: Height,
height_price: Cents, height_price: Cents,
state: &mut CohortState<RealizedState, CostBasisData<WithCapital>>, state: &mut CohortState<RealizedState, CostBasisData<WithCapital>>,
) -> Result<()> { ) {
state.apply_pending(); state.apply_pending();
let unrealized_state = state.compute_unrealized_state(height_price); let unrealized_state = state.compute_unrealized_state(height_price);
self.unrealized_mut() self.unrealized_mut().push_state(&unrealized_state);
.truncate_push(height, &unrealized_state)?; self.supply_mut().push_profitability(&unrealized_state);
self.supply_mut()
.truncate_push_profitability(height, &unrealized_state)?;
Ok(())
} }
fn collect_all_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec>; fn collect_all_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec>;
@@ -207,16 +203,14 @@ pub trait CohortMetricsBase:
.min(self.unrealized().min_stateful_len()) .min(self.unrealized().min_stateful_len())
} }
fn truncate_push( fn push_state(
&mut self, &mut self,
height: Height,
state: &CohortState<RealizedState, CostBasisData<WithCapital>>, state: &CohortState<RealizedState, CostBasisData<WithCapital>>,
) -> Result<()> { ) {
self.supply_mut().truncate_push(height, state)?; self.supply_mut().push_state(state);
self.outputs_mut().truncate_push(height, state)?; self.outputs_mut().push_state(state);
self.activity_mut().truncate_push(height, state)?; self.activity_mut().push_state(state);
self.realized_mut().truncate_push(height, state)?; self.realized_mut().push_state(state);
Ok(())
} }
/// First phase of computed metrics (indexes from height). /// First phase of computed metrics (indexes from height).

View File

@@ -1,6 +1,6 @@
use brk_error::Result; use brk_error::Result;
use brk_traversable::Traversable; 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 vecdb::{AnyStoredVec, AnyVec, Exit, Rw, StorageMode, WritableVec};
use crate::{ use crate::{
@@ -35,11 +35,11 @@ impl OutputsBase {
self.unspent_count.height.len() self.unspent_count.height.len()
} }
pub(crate) fn truncate_push(&mut self, height: Height, state: &CohortState<impl RealizedOps, impl CostBasisOps>) -> Result<()> { #[inline(always)]
pub(crate) fn push_state(&mut self, state: &CohortState<impl RealizedOps, impl CostBasisOps>) {
self.unspent_count self.unspent_count
.height .height
.truncate_push(height, StoredU64::from(state.supply.utxo_count))?; .push(StoredU64::from(state.supply.utxo_count));
Ok(())
} }
pub(crate) fn collect_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> { pub(crate) fn collect_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {

View File

@@ -2,7 +2,7 @@ use brk_cohort::{Loss, Profit, ProfitabilityRange};
use brk_error::Result; use brk_error::Result;
use brk_traversable::Traversable; use brk_traversable::Traversable;
use brk_types::{ 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}; 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)] #[inline(always)]
pub(crate) fn push( pub(crate) fn push(
&mut self, &mut self,
@@ -223,10 +214,6 @@ impl<M: StorageMode> ProfitabilityMetrics<M> {
} }
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( pub(crate) fn forced_import(
db: &Database, db: &Database,
version: Version, version: Version,

View File

@@ -73,9 +73,9 @@ impl RealizedCore {
self.minimal.min_stateful_len() self.minimal.min_stateful_len()
} }
pub(crate) fn truncate_push(&mut self, height: Height, state: &CohortState<impl RealizedOps, impl CostBasisOps>) -> Result<()> { #[inline(always)]
self.minimal.truncate_push(height, state)?; pub(crate) fn push_state(&mut self, state: &CohortState<impl RealizedOps, impl CostBasisOps>) {
Ok(()) self.minimal.push_state(state);
} }
pub(crate) fn collect_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> { pub(crate) fn collect_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {

View File

@@ -234,49 +234,47 @@ impl RealizedFull {
.min(self.peak_regret.value.base.height.len()) .min(self.peak_regret.value.base.height.len())
} }
pub(crate) fn truncate_push( #[inline(always)]
pub(crate) fn push_state(
&mut self, &mut self,
height: Height,
state: &CohortState<RealizedState, CostBasisData<WithCapital>>, state: &CohortState<RealizedState, CostBasisData<WithCapital>>,
) -> Result<()> { ) {
self.core.truncate_push(height, state)?; self.core.push_state(state);
self.profit self.profit
.value_created .value_created
.base .base
.height .height
.truncate_push(height, state.realized.profit_value_created())?; .push(state.realized.profit_value_created());
self.profit self.profit
.value_destroyed .value_destroyed
.base .base
.height .height
.truncate_push(height, state.realized.profit_value_destroyed())?; .push(state.realized.profit_value_destroyed());
self.loss self.loss
.value_created .value_created
.base .base
.height .height
.truncate_push(height, state.realized.loss_value_created())?; .push(state.realized.loss_value_created());
self.loss self.loss
.value_destroyed .value_destroyed
.base .base
.height .height
.truncate_push(height, state.realized.loss_value_destroyed())?; .push(state.realized.loss_value_destroyed());
self.investor self.investor
.price .price
.cents .cents
.height .height
.truncate_push(height, state.realized.investor_price())?; .push(state.realized.investor_price());
self.cap_raw self.cap_raw
.truncate_push(height, state.realized.cap_raw())?; .push(state.realized.cap_raw());
self.investor self.investor
.cap_raw .cap_raw
.truncate_push(height, state.realized.investor_cap_raw())?; .push(state.realized.investor_cap_raw());
self.peak_regret self.peak_regret
.value .value
.base .base
.height .height
.truncate_push(height, state.realized.peak_regret())?; .push(state.realized.peak_regret());
Ok(())
} }
pub(crate) fn collect_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> { pub(crate) fn collect_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
@@ -304,36 +302,36 @@ impl RealizedFull {
Ok(()) Ok(())
} }
pub(crate) fn push_from_accum( #[inline(always)]
pub(crate) fn push_accum(
&mut self, &mut self,
accum: &RealizedFullAccum, accum: &RealizedFullAccum,
height: Height, ) {
) -> Result<()> {
self.profit self.profit
.value_created .value_created
.base .base
.height .height
.truncate_push(height, accum.profit_value_created())?; .push(accum.profit_value_created());
self.profit self.profit
.value_destroyed .value_destroyed
.base .base
.height .height
.truncate_push(height, accum.profit_value_destroyed())?; .push(accum.profit_value_destroyed());
self.loss self.loss
.value_created .value_created
.base .base
.height .height
.truncate_push(height, accum.loss_value_created())?; .push(accum.loss_value_created());
self.loss self.loss
.value_destroyed .value_destroyed
.base .base
.height .height
.truncate_push(height, accum.loss_value_destroyed())?; .push(accum.loss_value_destroyed());
self.cap_raw self.cap_raw
.truncate_push(height, accum.cap_raw)?; .push(accum.cap_raw);
self.investor self.investor
.cap_raw .cap_raw
.truncate_push(height, accum.investor_cap_raw)?; .push(accum.investor_cap_raw);
let investor_price = { let investor_price = {
let cap = accum.cap_raw.as_u128(); let cap = accum.cap_raw.as_u128();
@@ -347,15 +345,13 @@ impl RealizedFull {
.price .price
.cents .cents
.height .height
.truncate_push(height, investor_price)?; .push(investor_price);
self.peak_regret self.peak_regret
.value .value
.base .base
.height .height
.truncate_push(height, accum.peak_regret())?; .push(accum.peak_regret());
Ok(())
} }
pub(crate) fn compute_rest_part1( pub(crate) fn compute_rest_part1(

View File

@@ -80,21 +80,21 @@ impl RealizedMinimal {
.min(self.sopr.value_destroyed.base.height.len()) .min(self.sopr.value_destroyed.base.height.len())
} }
pub(crate) fn truncate_push(&mut self, height: Height, state: &CohortState<impl RealizedOps, impl CostBasisOps>) -> Result<()> { #[inline(always)]
self.cap.cents.height.truncate_push(height, state.realized.cap())?; pub(crate) fn push_state(&mut self, state: &CohortState<impl RealizedOps, impl CostBasisOps>) {
self.profit.base.cents.height.truncate_push(height, state.realized.profit())?; self.cap.cents.height.push(state.realized.cap());
self.loss.base.cents.height.truncate_push(height, state.realized.loss())?; self.profit.base.cents.height.push(state.realized.profit());
self.loss.base.cents.height.push(state.realized.loss());
self.sopr self.sopr
.value_created .value_created
.base .base
.height .height
.truncate_push(height, state.realized.value_created())?; .push(state.realized.value_created());
self.sopr self.sopr
.value_destroyed .value_destroyed
.base .base
.height .height
.truncate_push(height, state.realized.value_destroyed())?; .push(state.realized.value_destroyed());
Ok(())
} }
pub(crate) fn collect_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> { pub(crate) fn collect_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {

View File

@@ -9,7 +9,7 @@ pub use full::{RealizedFull, RealizedFullAccum};
pub use minimal::RealizedMinimal; pub use minimal::RealizedMinimal;
use brk_error::Result; use brk_error::Result;
use brk_types::{Height, Indexes}; use brk_types::Indexes;
use vecdb::Exit; use vecdb::Exit;
use crate::distribution::state::{WithCapital, CohortState, CostBasisData, RealizedState}; 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(&self) -> &RealizedCore;
fn as_core_mut(&mut self) -> &mut RealizedCore; fn as_core_mut(&mut self) -> &mut RealizedCore;
fn min_stateful_len(&self) -> usize; fn min_stateful_len(&self) -> usize;
fn truncate_push(&mut self, height: Height, state: &CohortState<RealizedState, CostBasisData<WithCapital>>) -> Result<()>; fn push_state(&mut self, state: &CohortState<RealizedState, CostBasisData<WithCapital>>);
fn compute_rest_part1(&mut self, starting_indexes: &Indexes, exit: &Exit) -> Result<()>; fn compute_rest_part1(&mut self, starting_indexes: &Indexes, exit: &Exit) -> Result<()>;
fn compute_from_stateful( fn compute_from_stateful(
&mut self, &mut self,
@@ -32,8 +32,9 @@ impl RealizedLike for RealizedCore {
fn as_core(&self) -> &RealizedCore { self } fn as_core(&self) -> &RealizedCore { self }
fn as_core_mut(&mut self) -> &mut RealizedCore { self } fn as_core_mut(&mut self) -> &mut RealizedCore { self }
fn min_stateful_len(&self) -> usize { self.min_stateful_len() } fn min_stateful_len(&self) -> usize { self.min_stateful_len() }
fn truncate_push(&mut self, height: Height, state: &CohortState<RealizedState, CostBasisData<WithCapital>>) -> Result<()> { #[inline(always)]
self.truncate_push(height, state) fn push_state(&mut self, state: &CohortState<RealizedState, CostBasisData<WithCapital>>) {
self.push_state(state)
} }
fn compute_rest_part1(&mut self, starting_indexes: &Indexes, exit: &Exit) -> Result<()> { fn compute_rest_part1(&mut self, starting_indexes: &Indexes, exit: &Exit) -> Result<()> {
self.compute_rest_part1(starting_indexes, exit) 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(&self) -> &RealizedCore { &self.core }
fn as_core_mut(&mut self) -> &mut RealizedCore { &mut self.core } fn as_core_mut(&mut self) -> &mut RealizedCore { &mut self.core }
fn min_stateful_len(&self) -> usize { self.min_stateful_len() } fn min_stateful_len(&self) -> usize { self.min_stateful_len() }
fn truncate_push(&mut self, height: Height, state: &CohortState<RealizedState, CostBasisData<WithCapital>>) -> Result<()> { #[inline(always)]
self.truncate_push(height, state) fn push_state(&mut self, state: &CohortState<RealizedState, CostBasisData<WithCapital>>) {
self.push_state(state)
} }
fn compute_rest_part1(&mut self, starting_indexes: &Indexes, exit: &Exit) -> Result<()> { fn compute_rest_part1(&mut self, starting_indexes: &Indexes, exit: &Exit) -> Result<()> {
self.compute_rest_part1(starting_indexes, exit) self.compute_rest_part1(starting_indexes, exit)

View File

@@ -50,9 +50,9 @@ impl SupplyBase {
self.total.sats.height.len() self.total.sats.height.len()
} }
pub(crate) fn truncate_push(&mut self, height: Height, state: &CohortState<impl RealizedOps, impl CostBasisOps>) -> Result<()> { #[inline(always)]
self.total.sats.height.truncate_push(height, state.supply.value)?; pub(crate) fn push_state(&mut self, state: &CohortState<impl RealizedOps, impl CostBasisOps>) {
Ok(()) self.total.sats.height.push(state.supply.value);
} }
pub(crate) fn collect_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> { pub(crate) fn collect_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {

View File

@@ -43,20 +43,10 @@ impl SupplyCore {
.min(self.in_loss.sats.height.len()) .min(self.in_loss.sats.height.len())
} }
pub(crate) fn truncate_push_profitability( #[inline(always)]
&mut self, pub(crate) fn push_profitability(&mut self, state: &UnrealizedState) {
height: Height, self.in_profit.sats.height.push(state.supply_in_profit);
state: &UnrealizedState, self.in_loss.sats.height.push(state.supply_in_loss);
) -> 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(())
} }
pub(crate) fn collect_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> { pub(crate) fn collect_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {

View File

@@ -55,31 +55,18 @@ impl UnrealizedBase {
.min(self.investor_cap_in_loss_raw.len()) .min(self.investor_cap_in_loss_raw.len())
} }
pub(crate) fn truncate_push( #[inline(always)]
&mut self, pub(crate) fn push_state(&mut self, state: &UnrealizedState) {
height: Height, self.core.push_state(state);
height_state: &UnrealizedState,
) -> Result<()> {
self.core.truncate_push(height, height_state)?;
self.invested_capital_in_profit_raw.truncate_push( self.invested_capital_in_profit_raw
height, .push(CentsSats::new(state.invested_capital_in_profit_raw));
CentsSats::new(height_state.invested_capital_in_profit_raw), self.invested_capital_in_loss_raw
)?; .push(CentsSats::new(state.invested_capital_in_loss_raw));
self.invested_capital_in_loss_raw.truncate_push( self.investor_cap_in_profit_raw
height, .push(CentsSquaredSats::new(state.investor_cap_in_profit_raw));
CentsSats::new(height_state.invested_capital_in_loss_raw), self.investor_cap_in_loss_raw
)?; .push(CentsSquaredSats::new(state.investor_cap_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(())
} }
pub(crate) fn collect_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> { 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)) .map(|o| o.investor_cap_in_loss_raw.collect_range_at(start, end))
.collect(); .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 { for i in start..end {
let height = Height::from(i);
let local_i = i - start; let local_i = i - start;
let mut sum_invested_profit = CentsSats::ZERO; let mut sum_invested_profit = CentsSats::ZERO;
@@ -151,13 +146,13 @@ impl UnrealizedBase {
} }
self.invested_capital_in_profit_raw self.invested_capital_in_profit_raw
.truncate_push(height, sum_invested_profit)?; .push(sum_invested_profit);
self.invested_capital_in_loss_raw self.invested_capital_in_loss_raw
.truncate_push(height, sum_invested_loss)?; .push(sum_invested_loss);
self.investor_cap_in_profit_raw self.investor_cap_in_profit_raw
.truncate_push(height, sum_investor_profit)?; .push(sum_investor_profit);
self.investor_cap_in_loss_raw self.investor_cap_in_loss_raw
.truncate_push(height, sum_investor_loss)?; .push(sum_investor_loss);
} }
Ok(()) Ok(())

View File

@@ -53,18 +53,18 @@ impl UnrealizedBasic {
.min(self.loss.base.cents.height.len()) .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 self.profit
.base .base
.cents .cents
.height .height
.truncate_push(height, state.unrealized_profit)?; .push(state.unrealized_profit);
self.loss self.loss
.base .base
.cents .cents
.height .height
.truncate_push(height, state.unrealized_loss)?; .push(state.unrealized_loss);
Ok(())
} }
pub(crate) fn collect_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> { pub(crate) fn collect_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {

View File

@@ -1,6 +1,6 @@
use brk_error::Result; use brk_error::Result;
use brk_traversable::Traversable; 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 derive_more::{Deref, DerefMut};
use vecdb::{AnyStoredVec, Exit, Rw, StorageMode}; use vecdb::{AnyStoredVec, Exit, Rw, StorageMode};
@@ -39,13 +39,9 @@ impl UnrealizedCore {
self.basic.min_stateful_len() self.basic.min_stateful_len()
} }
pub(crate) fn truncate_push( #[inline(always)]
&mut self, pub(crate) fn push_state(&mut self, state: &UnrealizedState) {
height: Height, self.basic.push_state(state);
height_state: &UnrealizedState,
) -> Result<()> {
self.basic.truncate_push(height, height_state)?;
Ok(())
} }
pub(crate) fn collect_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> { pub(crate) fn collect_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {

View File

@@ -1,6 +1,6 @@
use brk_error::Result; use brk_error::Result;
use brk_traversable::Traversable; 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 derive_more::{Deref, DerefMut};
use vecdb::{AnyStoredVec, Exit, Rw, StorageMode, WritableVec}; use vecdb::{AnyStoredVec, Exit, Rw, StorageMode, WritableVec};
@@ -58,21 +58,17 @@ impl UnrealizedFull {
}) })
} }
pub(crate) fn truncate_push_all( #[inline(always)]
&mut self, pub(crate) fn push_state_all(&mut self, state: &UnrealizedState) {
height: Height, self.inner.push_state(state);
state: &UnrealizedState,
) -> Result<()> {
self.inner.truncate_push(height, state)?;
self.invested_capital_in_profit self.invested_capital_in_profit
.cents .cents
.height .height
.truncate_push(height, state.invested_capital_in_profit)?; .push(state.invested_capital_in_profit);
self.invested_capital_in_loss self.invested_capital_in_loss
.cents .cents
.height .height
.truncate_push(height, state.invested_capital_in_loss)?; .push(state.invested_capital_in_loss);
Ok(())
} }
pub(crate) fn collect_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> { pub(crate) fn collect_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {

View File

@@ -11,7 +11,7 @@ pub use full::UnrealizedFull;
pub use minimal::UnrealizedMinimal; pub use minimal::UnrealizedMinimal;
use brk_error::Result; use brk_error::Result;
use brk_types::{Height, Indexes}; use brk_types::Indexes;
use vecdb::Exit; use vecdb::Exit;
use crate::{distribution::state::UnrealizedState, prices}; use crate::{distribution::state::UnrealizedState, prices};
@@ -20,7 +20,7 @@ pub trait UnrealizedLike: Send + Sync {
fn as_base(&self) -> &UnrealizedBase; fn as_base(&self) -> &UnrealizedBase;
fn as_base_mut(&mut self) -> &mut UnrealizedBase; fn as_base_mut(&mut self) -> &mut UnrealizedBase;
fn min_stateful_len(&self) -> usize; 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( fn compute_rest(
&mut self, &mut self,
prices: &prices::Vecs, prices: &prices::Vecs,
@@ -44,8 +44,9 @@ impl UnrealizedLike for UnrealizedBase {
fn min_stateful_len(&self) -> usize { fn min_stateful_len(&self) -> usize {
self.min_stateful_len() self.min_stateful_len()
} }
fn truncate_push(&mut self, height: Height, state: &UnrealizedState) -> Result<()> { #[inline(always)]
self.truncate_push(height, state) fn push_state(&mut self, state: &UnrealizedState) {
self.push_state(state);
} }
fn compute_rest( fn compute_rest(
&mut self, &mut self,
@@ -74,8 +75,9 @@ impl UnrealizedLike for UnrealizedFull {
fn min_stateful_len(&self) -> usize { fn min_stateful_len(&self) -> usize {
self.inner.min_stateful_len() self.inner.min_stateful_len()
} }
fn truncate_push(&mut self, height: Height, state: &UnrealizedState) -> Result<()> { #[inline(always)]
self.truncate_push_all(height, state) fn push_state(&mut self, state: &UnrealizedState) {
self.push_state_all(state);
} }
fn compute_rest( fn compute_rest(
&mut self, &mut self,

View File

@@ -89,6 +89,17 @@ where
}); });
let start = index.to_usize(); 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 fi_len = first_indexes.len();
let first_indexes_batch: Vec<A> = first_indexes.collect_range_at(start, fi_len); let first_indexes_batch: Vec<A> = first_indexes.collect_range_at(start, fi_len);
let count_indexes_batch: Vec<StoredU64> = count_indexes.collect_range_at(start, fi_len); let count_indexes_batch: Vec<StoredU64> = count_indexes.collect_range_at(start, fi_len);
@@ -97,8 +108,7 @@ where
.into_iter() .into_iter()
.zip(count_indexes_batch) .zip(count_indexes_batch)
.enumerate() .enumerate()
.try_for_each(|(j, (first_index, count_index))| -> Result<()> { .try_for_each(|(_, (first_index, count_index))| -> Result<()> {
let idx = start + j;
let count = u64::from(count_index) as usize; let count = u64::from(count_index) as usize;
// Effective count after skipping (e.g., skip coinbase for fee calculations) // Effective count after skipping (e.g., skip coinbase for fee calculations)
@@ -113,17 +123,15 @@ where
} else { } else {
T::from(0_usize) T::from(0_usize)
}; };
first_vec.truncate_push_at(idx, f)?; first_vec.push(f);
} }
if let Some(ref mut last_vec) = last { if let Some(ref mut last_vec) = last {
if effective_count == 0 { if effective_count == 0 {
// If all items skipped, use zero last_vec.push(T::from(0_usize));
last_vec.truncate_push_at(idx, T::from(0_usize))?;
} else { } else {
let last_index = first_index + (count - 1); let last_index = first_index + (count - 1);
let v = source.collect_one_at(last_index.to_usize()).unwrap(); last_vec.push(source.collect_one_at(last_index.to_usize()).unwrap());
last_vec.truncate_push_at(idx, v)?;
} }
} }
@@ -143,12 +151,10 @@ where
}); });
if let Some(ref mut min_vec) = min { if let Some(ref mut min_vec) = min {
let v = min_val.or(max_val).unwrap_or_else(|| T::from(0_usize)); min_vec.push(min_val.or(max_val).unwrap_or_else(|| T::from(0_usize)));
min_vec.truncate_push_at(idx, v)?;
} }
if let Some(ref mut max_vec) = max { if let Some(ref mut max_vec) = max {
let v = max_val.or(min_val).unwrap_or_else(|| T::from(0_usize)); max_vec.push(max_val.or(min_val).unwrap_or_else(|| T::from(0_usize)));
max_vec.truncate_push_at(idx, v)?;
} }
} else if needs_percentiles || needs_minmax { } else if needs_percentiles || needs_minmax {
let mut values: Vec<T> = source.collect_range_at( let mut values: Vec<T> = source.collect_range_at(
@@ -157,21 +163,18 @@ where
); );
if values.is_empty() { if values.is_empty() {
// Handle edge case where all items were skipped
macro_rules! push_zero { macro_rules! push_zero {
($($vec:ident),*) => { ($($vec:ident),*) => {
$(if let Some(ref mut v) = $vec { $(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); push_zero!(max, pct90, pct75, median, pct25, pct10, min, average, sum);
if let Some(ref mut cumulative_vec) = cumulative { if let Some(ref mut cumulative_vec) = cumulative {
let t = cumulative_val.unwrap(); cumulative_vec.push(cumulative_val.unwrap());
cumulative_vec.truncate_push_at(idx, t)?;
} }
} else if needs_percentiles { } else if needs_percentiles {
// Compute aggregates from unsorted values first to avoid clone
let aggregate_result = if needs_aggregates { let aggregate_result = if needs_aggregates {
let len = values.len(); let len = values.len();
let sum_val = values.iter().copied().fold(T::from(0), |a, b| a + b); let sum_val = values.iter().copied().fold(T::from(0), |a, b| a + b);
@@ -180,53 +183,52 @@ where
None None
}; };
// Sort in-place — no clone needed
values.sort_unstable(); values.sort_unstable();
if let Some(ref mut max_vec) = max { 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 { 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 { 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 { 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 { 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 { 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 { 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((len, sum_val)) = aggregate_result {
if let Some(ref mut average_vec) = average { 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 needs_sum_or_cumulative {
if let Some(ref mut sum_vec) = sum { 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 { if let Some(ref mut cumulative_vec) = cumulative {
let t = cumulative_val.unwrap() + sum_val; let t = cumulative_val.unwrap() + sum_val;
cumulative_val.replace(t); cumulative_val.replace(t);
cumulative_vec.truncate_push_at(idx, t)?; cumulative_vec.push(t);
} }
} }
} }
} else if needs_minmax { } else if needs_minmax {
if let Some(ref mut min_vec) = min { 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 { 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 { if needs_aggregates {
@@ -234,23 +236,22 @@ where
let sum_val = values.into_iter().fold(T::from(0), |a, b| a + b); let sum_val = values.into_iter().fold(T::from(0), |a, b| a + b);
if let Some(ref mut average_vec) = average { 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 needs_sum_or_cumulative {
if let Some(ref mut sum_vec) = sum { 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 { if let Some(ref mut cumulative_vec) = cumulative {
let t = cumulative_val.unwrap() + sum_val; let t = cumulative_val.unwrap() + sum_val;
cumulative_val.replace(t); cumulative_val.replace(t);
cumulative_vec.truncate_push_at(idx, t)?; cumulative_vec.push(t);
} }
} }
} }
} }
} else if needs_aggregates { } else if needs_aggregates {
// Aggregates only (sum/average/cumulative) — no Vec allocation needed
let efi = effective_first_index.to_usize(); let efi = effective_first_index.to_usize();
let (sum_val, len) = source.fold_range_at( let (sum_val, len) = source.fold_range_at(
efi, efi,
@@ -265,17 +266,17 @@ where
} else { } else {
T::from(0_usize) T::from(0_usize)
}; };
average_vec.truncate_push_at(idx, avg)?; average_vec.push(avg);
} }
if needs_sum_or_cumulative { if needs_sum_or_cumulative {
if let Some(ref mut sum_vec) = sum { 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 { if let Some(ref mut cumulative_vec) = cumulative {
let t = cumulative_val.unwrap() + sum_val; let t = cumulative_val.unwrap() + sum_val;
cumulative_val.replace(t); 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 zero = T::from(0_usize);
let mut values: Vec<T> = Vec::new(); let mut values: Vec<T> = 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 count_indexes_batch
.iter() .iter()
.enumerate() .enumerate()
@@ -378,7 +392,7 @@ where
&mut *pct75, &mut *pct75,
&mut *pct90, &mut *pct90,
] { ] {
vec.truncate_push_at(idx, zero)?; vec.push(zero);
} }
} else { } else {
source.collect_range_into_at(range_start_usize, range_end_usize, &mut values); source.collect_range_into_at(range_start_usize, range_end_usize, &mut values);
@@ -390,14 +404,14 @@ where
values.sort_unstable(); values.sort_unstable();
max.truncate_push_at(idx, *values.last().unwrap())?; max.push(*values.last().unwrap());
pct90.truncate_push_at(idx, get_percentile(&values, 0.90))?; pct90.push(get_percentile(&values, 0.90));
pct75.truncate_push_at(idx, get_percentile(&values, 0.75))?; pct75.push(get_percentile(&values, 0.75));
median.truncate_push_at(idx, get_percentile(&values, 0.50))?; median.push(get_percentile(&values, 0.50));
pct25.truncate_push_at(idx, get_percentile(&values, 0.25))?; pct25.push(get_percentile(&values, 0.25));
pct10.truncate_push_at(idx, get_percentile(&values, 0.10))?; pct10.push(get_percentile(&values, 0.10));
min.truncate_push_at(idx, *values.first().unwrap())?; min.push(*values.first().unwrap());
average.truncate_push_at(idx, avg)?; average.push(avg);
} }
Ok(()) Ok(())

View File

@@ -107,9 +107,21 @@ where
let starts_batch = window_starts.collect_range_at(skip, end); 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() { for (j, start) in starts_batch.into_iter().enumerate() {
let i = skip + j; let v = partial_values[skip + j - range_start];
let v = partial_values[i - range_start];
let start_usize = start.to_usize(); let start_usize = start.to_usize();
window.advance(v, start_usize, partial_values, range_start); window.advance(v, start_usize, partial_values, range_start);
@@ -125,19 +137,19 @@ where
&mut *p75_out, &mut *p75_out,
&mut *p90_out, &mut *p90_out,
] { ] {
v.truncate_push_at(i, zero)?; v.push(zero);
} }
} else { } else {
average_out.truncate_push_at(i, T::from(window.average()))?; average_out.push(T::from(window.average()));
min_out.truncate_push_at(i, T::from(window.min()))?; min_out.push(T::from(window.min()));
max_out.truncate_push_at(i, T::from(window.max()))?; max_out.push(T::from(window.max()));
let [p10, p25, p50, p75, p90] = let [p10, p25, p50, p75, p90] =
window.percentiles(&[0.10, 0.25, 0.50, 0.75, 0.90]); window.percentiles(&[0.10, 0.25, 0.50, 0.75, 0.90]);
p10_out.truncate_push_at(i, T::from(p10))?; p10_out.push(T::from(p10));
p25_out.truncate_push_at(i, T::from(p25))?; p25_out.push(T::from(p25));
median_out.truncate_push_at(i, T::from(p50))?; median_out.push(T::from(p50));
p75_out.truncate_push_at(i, T::from(p75))?; p75_out.push(T::from(p75));
p90_out.truncate_push_at(i, T::from(p90))?; p90_out.push(T::from(p90));
} }
if average_out.batch_limit_reached() { if average_out.batch_limit_reached() {

View File

@@ -35,6 +35,7 @@ impl RollingDistributionSlot {
}) })
} }
#[allow(clippy::too_many_arguments)]
pub(crate) fn compute( pub(crate) fn compute(
&mut self, &mut self,
max_from: Height, max_from: Height,

View File

@@ -1,6 +1,6 @@
use brk_error::Result; use brk_error::Result;
use brk_traversable::{Traversable, TreeNode}; 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 vecdb::{AnyExportableVec, Database, ReadOnlyClone, Ro, Rw, StorageMode, WritableVec};
use crate::indexes; use crate::indexes;
@@ -38,16 +38,12 @@ impl PercentilesVecs {
Ok(Self { vecs }) Ok(Self { vecs })
} }
/// Push percentile prices at this height (in cents). /// Push percentile prices (in cents).
pub(crate) fn truncate_push( #[inline(always)]
&mut self, pub(crate) fn push(&mut self, percentile_prices: &[Cents; PERCENTILES_LEN]) {
height: Height,
percentile_prices: &[Cents; PERCENTILES_LEN],
) -> Result<()> {
for (i, v) in self.vecs.iter_mut().enumerate() { 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. /// Validate computed versions or reset if mismatched.

View File

@@ -46,12 +46,7 @@ impl RatioPerBlockPercentiles {
macro_rules! import_ratio { macro_rules! import_ratio {
($suffix:expr) => { ($suffix:expr) => {
RatioPerBlock::forced_import_raw( RatioPerBlock::forced_import_raw(db, &format!("{name}_{}", $suffix), v, indexes)?
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]; const PCTS: [f64; 6] = [0.01, 0.02, 0.05, 0.95, 0.98, 0.99];
let mut out = [0u32; 6]; 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.add(*ratio);
self.expanding_pct.quantiles(&PCTS, &mut out); self.expanding_pct.quantiles(&PCTS, &mut out);
let idx = start + offset;
for (vec, &val) in pct_vecs.iter_mut().zip(out.iter()) { for (vec, &val) in pct_vecs.iter_mut().zip(out.iter()) {
vec.truncate_push_at(idx, BasisPoints32::from(val))?; vec.push(BasisPoints32::from(val));
} }
} }
} }

View File

@@ -79,9 +79,9 @@ where
let cached = cached_start.clone(); let cached = cached_start.clone();
let starts_version = cached.version(); 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::<Height, S, C, DeltaChange>::new( let change_vec = LazyDeltaVec::<Height, S, C, DeltaChange>::new(
&format!("{full_name}_change"), &full_name,
version, version,
src.clone(), src.clone(),
starts_version, starts_version,
@@ -91,7 +91,7 @@ where
}, },
); );
let change_resolutions = Resolutions::forced_import( let change_resolutions = Resolutions::forced_import(
&format!("{full_name}_change"), &full_name,
change_vec.read_only_boxed_clone(), change_vec.read_only_boxed_clone(),
version, version,
indexes, indexes,
@@ -102,15 +102,16 @@ where
}; };
// Rate BPS: (source[h] - source[ago]) / source[ago] as B (via f64) // 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::<Height, S, B, DeltaRate>::new( let rate_vec = LazyDeltaVec::<Height, S, B, DeltaRate>::new(
&format!("{full_name}_rate_bps"), &rate_bps_name,
version, version,
src.clone(), src.clone(),
starts_version, starts_version,
move || cached.get(), move || cached.get(),
); );
let rate_resolutions = Resolutions::forced_import( let rate_resolutions = Resolutions::forced_import(
&format!("{full_name}_rate_bps"), &rate_bps_name,
rate_vec.read_only_boxed_clone(), rate_vec.read_only_boxed_clone(),
version, version,
indexes, indexes,
@@ -121,28 +122,30 @@ where
}; };
// Ratio: bps / 10000 // Ratio: bps / 10000
let rate_ratio_name = format!("{full_name}_rate_ratio");
let ratio = LazyPerBlock { let ratio = LazyPerBlock {
height: LazyVecFrom1::transformed::<B::ToRatio>( height: LazyVecFrom1::transformed::<B::ToRatio>(
&format!("{full_name}_rate_ratio"), &rate_ratio_name,
version, version,
bps.height.read_only_boxed_clone(), bps.height.read_only_boxed_clone(),
), ),
resolutions: Box::new(DerivedResolutions::from_derived_computed::<B::ToRatio>( resolutions: Box::new(DerivedResolutions::from_derived_computed::<B::ToRatio>(
&format!("{full_name}_rate_ratio"), &rate_ratio_name,
version, version,
&bps.resolutions, &bps.resolutions,
)), )),
}; };
// Percent: bps / 100 // Percent: bps / 100
let rate_name = format!("{full_name}_rate");
let percent = LazyPerBlock { let percent = LazyPerBlock {
height: LazyVecFrom1::transformed::<B::ToPercent>( height: LazyVecFrom1::transformed::<B::ToPercent>(
&format!("{full_name}_rate"), &rate_name,
version, version,
bps.height.read_only_boxed_clone(), bps.height.read_only_boxed_clone(),
), ),
resolutions: Box::new(DerivedResolutions::from_derived_computed::<B::ToPercent>( resolutions: Box::new(DerivedResolutions::from_derived_computed::<B::ToPercent>(
&format!("{full_name}_rate"), &rate_name,
version, version,
&bps.resolutions, &bps.resolutions,
)), )),
@@ -214,9 +217,10 @@ where
let cached = cached_start.clone(); let cached = cached_start.clone();
let starts_version = cached.version(); 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::<Height, S, C, DeltaChange>::new( let change_vec = LazyDeltaVec::<Height, S, C, DeltaChange>::new(
&format!("{full_name}_change"), &cents_name,
version, version,
src.clone(), src.clone(),
starts_version, starts_version,
@@ -226,7 +230,7 @@ where
}, },
); );
let change_resolutions = Resolutions::forced_import( let change_resolutions = Resolutions::forced_import(
&format!("{full_name}_change"), &cents_name,
change_vec.read_only_boxed_clone(), change_vec.read_only_boxed_clone(),
version, version,
indexes, indexes,
@@ -236,15 +240,15 @@ where
resolutions: Box::new(change_resolutions), resolutions: Box::new(change_resolutions),
}; };
// Change USD: lazy from cents delta // Absolute change (usd): lazy from cents delta
let usd = LazyPerBlock { let usd = LazyPerBlock {
height: LazyVecFrom1::transformed::<C::ToDollars>( height: LazyVecFrom1::transformed::<C::ToDollars>(
&format!("{full_name}_change_usd"), &full_name,
version, version,
cents.height.read_only_boxed_clone(), cents.height.read_only_boxed_clone(),
), ),
resolutions: Box::new(DerivedResolutions::from_derived_computed::<C::ToDollars>( resolutions: Box::new(DerivedResolutions::from_derived_computed::<C::ToDollars>(
&format!("{full_name}_change_usd"), &full_name,
version, version,
&cents.resolutions, &cents.resolutions,
)), )),
@@ -253,15 +257,16 @@ where
let absolute = LazyDeltaFiatFromHeight { usd, cents }; let absolute = LazyDeltaFiatFromHeight { usd, cents };
// Rate BPS: (source[h] - source[ago]) / source[ago] as B (via f64) // 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::<Height, S, B, DeltaRate>::new( let rate_vec = LazyDeltaVec::<Height, S, B, DeltaRate>::new(
&format!("{full_name}_rate_bps"), &rate_bps_name,
version, version,
src.clone(), src.clone(),
starts_version, starts_version,
move || cached.get(), move || cached.get(),
); );
let rate_resolutions = Resolutions::forced_import( let rate_resolutions = Resolutions::forced_import(
&format!("{full_name}_rate_bps"), &rate_bps_name,
rate_vec.read_only_boxed_clone(), rate_vec.read_only_boxed_clone(),
version, version,
indexes, indexes,
@@ -271,27 +276,29 @@ where
resolutions: Box::new(rate_resolutions), resolutions: Box::new(rate_resolutions),
}; };
let rate_ratio_name = format!("{full_name}_rate_ratio");
let ratio = LazyPerBlock { let ratio = LazyPerBlock {
height: LazyVecFrom1::transformed::<B::ToRatio>( height: LazyVecFrom1::transformed::<B::ToRatio>(
&format!("{full_name}_rate_ratio"), &rate_ratio_name,
version, version,
bps.height.read_only_boxed_clone(), bps.height.read_only_boxed_clone(),
), ),
resolutions: Box::new(DerivedResolutions::from_derived_computed::<B::ToRatio>( resolutions: Box::new(DerivedResolutions::from_derived_computed::<B::ToRatio>(
&format!("{full_name}_rate_ratio"), &rate_ratio_name,
version, version,
&bps.resolutions, &bps.resolutions,
)), )),
}; };
let rate_name = format!("{full_name}_rate");
let percent = LazyPerBlock { let percent = LazyPerBlock {
height: LazyVecFrom1::transformed::<B::ToPercent>( height: LazyVecFrom1::transformed::<B::ToPercent>(
&format!("{full_name}_rate"), &rate_name,
version, version,
bps.height.read_only_boxed_clone(), bps.height.read_only_boxed_clone(),
), ),
resolutions: Box::new(DerivedResolutions::from_derived_computed::<B::ToPercent>( resolutions: Box::new(DerivedResolutions::from_derived_computed::<B::ToPercent>(
&format!("{full_name}_rate"), &rate_name,
version, version,
&bps.resolutions, &bps.resolutions,
)), )),

View File

@@ -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, 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) { for (vec, mult) in self.mut_band_height_vecs().zip(MULTIPLIERS) {
vec.truncate_if_needed_at(start)?;
for (offset, _) in source_data.iter().enumerate() { for (offset, _) in source_data.iter().enumerate() {
let index = start + offset;
let average = sma_data[offset]; let average = sma_data[offset];
let sd = sd_data[offset]; let sd = sd_data[offset];
vec.truncate_push_at(index, average + StoredF32::from(mult * *sd))?; vec.push(average + StoredF32::from(mult * *sd));
} }
} }

View File

@@ -144,12 +144,14 @@ impl Vecs {
first_tx_index_cursor.advance(min); first_tx_index_cursor.advance(min);
let mut output_count_cursor = indexes.tx_index.output_count.cursor(); let mut output_count_cursor = indexes.tx_index.output_count.cursor();
self.height_to_pool.truncate_if_needed_at(min)?;
indexer indexer
.stores .stores
.height_to_coinbase_tag .height_to_coinbase_tag
.iter() .iter()
.skip(min) .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 tx_index = first_tx_index_cursor.next().unwrap();
let out_start = first_txout_index.get(tx_index.to_usize()); 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)) .or_else(|| self.pools.find_from_coinbase_tag(&coinbase_tag))
.unwrap_or(unknown); .unwrap_or(unknown);
self.height_to_pool.truncate_push(height, pool.slug)?; self.height_to_pool.push(pool.slug);
Ok(()) Ok(())
})?; })?;

View File

@@ -8,7 +8,7 @@ use brk_types::{BlkPosition, Height, Indexes, TxIndex, Version};
use tracing::info; use tracing::info;
use vecdb::{ use vecdb::{
AnyStoredVec, AnyVec, Database, Exit, ImportableVec, PcoVec, ReadableVec, Rw, StorageMode, AnyStoredVec, AnyVec, Database, Exit, ImportableVec, PcoVec, ReadableVec, Rw, StorageMode,
VecIndex, WritableVec, WritableVec,
}; };
use crate::internal::db_utils::{finalize_db, open_db}; use crate::internal::db_utils::{finalize_db, open_db};
@@ -102,10 +102,15 @@ impl Vecs {
return Ok(()); return Ok(());
}; };
// Cursor avoids per-height PcoVec page decompression. let first_tx_at_min_height = indexer
// Heights are sequential, so the cursor only advances forward. .vecs
let mut first_tx_index_cursor = indexer.vecs.transactions.first_tx_index.cursor(); .transactions
first_tx_index_cursor.advance(min_height.to_usize()); .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 parser
.read( .read(
@@ -114,23 +119,13 @@ impl Vecs {
) )
.iter() .iter()
.try_for_each(|block| -> Result<()> { .try_for_each(|block| -> Result<()> {
let height = block.height(); self.block.push(block.metadata().position());
self.block block.tx_metadata().iter().for_each(|metadata| {
.truncate_push(height, block.metadata().position())?; self.tx.push(metadata.position());
});
let tx_index = first_tx_index_cursor.next().unwrap(); if *block.height() % 1_000 == 0 {
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 {
let _lock = exit.lock(); let _lock = exit.lock();
self.block.flush()?; self.block.flush()?;
self.tx.flush()?; self.tx.flush()?;

View File

@@ -52,9 +52,10 @@ impl Vecs {
let mut output_types_buf: Vec<OutputType> = Vec::new(); let mut output_types_buf: Vec<OutputType> = Vec::new();
let mut values_buf: Vec<Sats> = Vec::new(); let mut values_buf: Vec<Sats> = Vec::new();
height_vec.truncate_if_needed(starting_height)?;
// Iterate blocks // Iterate blocks
for h in starting_height.to_usize()..=target_height.to_usize() { for h in starting_height.to_usize()..=target_height.to_usize() {
let height = Height::from(h);
let local_idx = h - starting_height.to_usize(); let local_idx = h - starting_height.to_usize();
// Get output range for this block // 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()?; height_vec.write()?;

View File

@@ -37,6 +37,7 @@ impl Vecs {
let start = starting_height.to_usize(); let start = starting_height.to_usize();
let end = target_height.to_usize() + 1; let end = target_height.to_usize() + 1;
let unclaimed_data = unclaimed_height.collect_range_at(start, end); 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| { op_return_height.fold_range_at(start, end, start, |idx, op_return| {
let unclaimed = unclaimed_data[idx - start]; let unclaimed = unclaimed_data[idx - start];
let genesis = if idx == 0 { let genesis = if idx == 0 {
@@ -45,9 +46,7 @@ impl Vecs {
Sats::ZERO Sats::ZERO
}; };
let unspendable = genesis + op_return + unclaimed; let unspendable = genesis + op_return + unclaimed;
height_vec height_vec.push(unspendable);
.truncate_push(Height::from(idx), unspendable)
.unwrap();
idx + 1 idx + 1
}); });
} }

View File

@@ -2968,10 +2968,10 @@ function create_1m1w1y2wPattern(client, acc) {
*/ */
function create_1m1w1y24hPattern3(client, acc) { function create_1m1w1y24hPattern3(client, acc) {
return { return {
_1m: createCentsUsdPattern(client, _m(acc, '1m_change')), _1m: createCentsUsdPattern(client, _m(acc, '1m')),
_1w: createCentsUsdPattern(client, _m(acc, '1w_change')), _1w: createCentsUsdPattern(client, _m(acc, '1w')),
_1y: createCentsUsdPattern(client, _m(acc, '1y_change')), _1y: createCentsUsdPattern(client, _m(acc, '1y')),
_24h: createCentsUsdPattern(client, _m(acc, '24h_change')), _24h: createCentsUsdPattern(client, _m(acc, '24h')),
}; };
} }
@@ -3910,8 +3910,8 @@ function createCentsUsdPattern2(client, acc) {
*/ */
function createCentsUsdPattern(client, acc) { function createCentsUsdPattern(client, acc) {
return { return {
cents: createMetricPattern1(client, acc), cents: createMetricPattern1(client, _m(acc, 'cents')),
usd: createMetricPattern1(client, _m(acc, 'usd')), usd: createMetricPattern1(client, acc),
}; };
} }

View File

@@ -2729,10 +2729,10 @@ class _1m1w1y24hPattern3:
def __init__(self, client: BrkClientBase, acc: str): def __init__(self, client: BrkClientBase, acc: str):
"""Create pattern node with accumulated metric name.""" """Create pattern node with accumulated metric name."""
self._1m: CentsUsdPattern = CentsUsdPattern(client, _m(acc, '1m_change')) self._1m: CentsUsdPattern = CentsUsdPattern(client, _m(acc, '1m'))
self._1w: CentsUsdPattern = CentsUsdPattern(client, _m(acc, '1w_change')) self._1w: CentsUsdPattern = CentsUsdPattern(client, _m(acc, '1w'))
self._1y: CentsUsdPattern = CentsUsdPattern(client, _m(acc, '1y_change')) self._1y: CentsUsdPattern = CentsUsdPattern(client, _m(acc, '1y'))
self._24h: CentsUsdPattern = CentsUsdPattern(client, _m(acc, '24h_change')) self._24h: CentsUsdPattern = CentsUsdPattern(client, _m(acc, '24h'))
class _1m1w1y24hPattern4: class _1m1w1y24hPattern4:
"""Pattern struct for repeated tree structure.""" """Pattern struct for repeated tree structure."""
@@ -3132,8 +3132,8 @@ class CentsUsdPattern:
def __init__(self, client: BrkClientBase, acc: str): def __init__(self, client: BrkClientBase, acc: str):
"""Create pattern node with accumulated metric name.""" """Create pattern node with accumulated metric name."""
self.cents: MetricPattern1[CentsSigned] = MetricPattern1(client, acc) self.cents: MetricPattern1[CentsSigned] = MetricPattern1(client, _m(acc, 'cents'))
self.usd: MetricPattern1[Dollars] = MetricPattern1(client, _m(acc, 'usd')) self.usd: MetricPattern1[Dollars] = MetricPattern1(client, acc)
class CoindaysSentPattern: class CoindaysSentPattern:
"""Pattern struct for repeated tree structure.""" """Pattern struct for repeated tree structure."""

View File

@@ -2,7 +2,7 @@ import { colors } from "../utils/colors.js";
import { brk } from "../client.js"; import { brk } from "../client.js";
import { Unit } from "../utils/units.js"; import { Unit } from "../utils/units.js";
import { dots, line, baseline, price, rollingWindowsTree, percentRatioDots } from "./series.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 * Create Cointime section
@@ -173,57 +173,18 @@ export function createCointimeSection() {
), ),
], ],
}, },
...prices.map(({ pattern, name, color }) => { ...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, name,
tree: [ tree: priceRatioPercentilesTree({
{ pattern,
name: "Price",
title: `${name} Price`, title: `${name} Price`,
top: [ legend: name,
price({ metric: pattern, name, color }), color,
price({ priceReferences: [
metric: all.realized.price, price({ metric: all.realized.price, name: "Realized", color: colors.realized, defaultActive: false }),
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 } }),
),
],
},
],
};
}), }),
})),
], ],
}, },

View File

@@ -33,6 +33,9 @@ export function buildCohortData() {
AMOUNT_RANGE_NAMES, AMOUNT_RANGE_NAMES,
SPENDABLE_TYPE_NAMES, SPENDABLE_TYPE_NAMES,
CLASS_NAMES, CLASS_NAMES,
PROFITABILITY_RANGE_NAMES,
PROFIT_NAMES,
LOSS_NAMES,
} = brk; } = brk;
const cohortAll = { const cohortAll = {
@@ -191,6 +194,28 @@ export function buildCohortData() {
tree: utxoCohorts.class[key], 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 { return {
cohortAll, cohortAll,
termShort, termShort,
@@ -208,5 +233,8 @@ export function buildCohortData() {
typeAddressable, typeAddressable,
typeOther, typeOther,
class: class_, class: class_,
profitabilityRange,
profitabilityProfit,
profitabilityLoss,
}; };
} }

View File

@@ -10,7 +10,9 @@
* - activity.js: SOPR, Volume, Lifespan * - 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 // Section builders
import { import {
@@ -205,8 +207,11 @@ export function createCohortFolderAgeRangeWithMatured(cohort) {
const title = formatCohortTitle(cohort.name); const title = formatCohortTitle(cohort.name);
folder.tree.push({ folder.tree.push({
name: "Matured", name: "Matured",
tree: satsBtcUsdFullTree({
pattern: cohort.matured,
name: cohort.name,
title: title("Matured Supply"), title: title("Matured Supply"),
bottom: satsBtcUsd({ pattern: cohort.matured, name: cohort.name }), }),
}); });
return folder; return folder;
} }
@@ -452,7 +457,7 @@ export function createGroupedCohortFolderAgeRangeWithMatured({
name: "Matured", name: "Matured",
title: title("Matured Supply"), title: title("Matured Supply"),
bottom: list.flatMap((cohort) => 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; 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),
],
},
],
};
}

View File

@@ -14,7 +14,7 @@
*/ */
import { colors } from "../../utils/colors.js"; 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 { baseline, price } from "../series.js";
import { Unit } from "../../utils/units.js"; import { Unit } from "../../utils/units.js";
@@ -53,26 +53,12 @@ export function createPricesSectionFull({ cohort, title }) {
}, },
{ {
name: "Investor", name: "Investor",
tree: [ tree: priceRatioPercentilesTree({
{ pattern: tree.realized.investor.price,
name: "Price",
title: title("Investor Price"), title: title("Investor Price"),
top: [price({ metric: tree.realized.investor.price, name: "Investor", color })], legend: "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,
}), }),
],
},
],
}, },
], ],
}; };

View File

@@ -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"),
},
],
};
}

View File

@@ -14,6 +14,7 @@ import {
percentRatioBaseline, percentRatioBaseline,
ROLLING_WINDOWS, ROLLING_WINDOWS,
} from "./series.js"; } from "./series.js";
import { simplePriceRatioTree } from "./shared.js";
import { periodIdToName } from "./utils.js"; import { periodIdToName } from "./utils.js";
/** /**
@@ -68,26 +69,12 @@ function createMaSubSection(label, averages) {
/** @param {MaPeriod} a */ /** @param {MaPeriod} a */
const toFolder = (a) => ({ const toFolder = (a) => ({
name: periodIdToName(a.id, true), name: periodIdToName(a.id, true),
tree: [ tree: simplePriceRatioTree({
{ pattern: a.ratio,
name: "Price",
title: `${periodIdToName(a.id, true)} ${label}`, title: `${periodIdToName(a.id, true)} ${label}`,
top: [price({ metric: a.ratio, name: "average", color: a.color })], legend: "average",
},
{
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, color: a.color,
unit: Unit.ratio,
}), }),
],
},
],
}); });
return { return {

View File

@@ -19,8 +19,8 @@ import {
createGroupedCohortFolderBasicWithoutMarketCap, createGroupedCohortFolderBasicWithoutMarketCap,
createGroupedCohortFolderAddress, createGroupedCohortFolderAddress,
createGroupedAddressCohortFolder, createGroupedAddressCohortFolder,
createUtxoProfitabilitySection,
} from "./distribution/index.js"; } from "./distribution/index.js";
import { createUtxoProfitabilitySection } from "./distribution/utxo-profitability.js";
import { createMarketSection } from "./market.js"; import { createMarketSection } from "./market.js";
import { createNetworkSection } from "./network.js"; import { createNetworkSection } from "./network.js";
import { createMiningSection } from "./mining.js"; import { createMiningSection } from "./mining.js";
@@ -53,6 +53,9 @@ export function createPartialOptions() {
typeAddressable, typeAddressable,
typeOther, typeOther,
class: class_, class: class_,
profitabilityRange,
profitabilityProfit,
profitabilityLoss,
} = buildCohortData(); } = buildCohortData();
return [ return [
@@ -92,7 +95,7 @@ export function createPartialOptions() {
// Ages cohorts // Ages cohorts
{ {
name: "UTXO Ages", name: "UTXO Age",
tree: [ tree: [
// Younger Than (< X old) // Younger Than (< X old)
{ {
@@ -138,7 +141,7 @@ export function createPartialOptions() {
// Sizes cohorts (UTXO size) // Sizes cohorts (UTXO size)
{ {
name: "UTXO Sizes", name: "UTXO Size",
tree: [ tree: [
// Less Than (< X sats) // Less Than (< X sats)
{ {
@@ -184,7 +187,7 @@ export function createPartialOptions() {
// Balances cohorts (Address balance) // Balances cohorts (Address balance)
{ {
name: "Address Balances", name: "Address Balance",
tree: [ tree: [
// Less Than (< X sats) // Less Than (< X sats)
{ {
@@ -230,11 +233,11 @@ export function createPartialOptions() {
// Script Types - addressable types have addrCount, others don't // Script Types - addressable types have addrCount, others don't
{ {
name: "Script Types", name: "Script Type",
tree: [ tree: [
createGroupedCohortFolderAddress({ createGroupedCohortFolderAddress({
name: "Compare", name: "Compare",
title: "Script Types", title: "Script Type",
list: typeAddressable, list: typeAddressable,
all: cohortAll, all: cohortAll,
}), }),
@@ -245,11 +248,11 @@ export function createPartialOptions() {
// Epochs // Epochs
{ {
name: "Epochs", name: "Epoch",
tree: [ tree: [
createGroupedCohortFolderWithAdjusted({ createGroupedCohortFolderWithAdjusted({
name: "Compare", name: "Compare",
title: "Epochs", title: "Epoch",
list: epoch, list: epoch,
all: cohortAll, all: cohortAll,
}), }),
@@ -257,13 +260,13 @@ export function createPartialOptions() {
], ],
}, },
// Years // Classes
{ {
name: "Years", name: "Class",
tree: [ tree: [
createGroupedCohortFolderWithAdjusted({ createGroupedCohortFolderWithAdjusted({
name: "Compare", name: "Compare",
title: "Years", title: "Class",
list: class_, list: class_,
all: cohortAll, all: cohortAll,
}), }),
@@ -272,7 +275,11 @@ export function createPartialOptions() {
}, },
// UTXO Profitability bands // UTXO Profitability bands
createUtxoProfitabilitySection(), createUtxoProfitabilitySection({
range: profitabilityRange,
profit: profitabilityProfit,
loss: profitabilityLoss,
}),
], ],
}, },

View File

@@ -1,7 +1,7 @@
/** Shared helpers for options */ /** Shared helpers for options */
import { Unit } from "../utils/units.js"; 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 { priceLine, priceLines } from "./constants.js";
import { colors } from "../utils/colors.js"; import { colors } from "../utils/colors.js";
@@ -234,6 +234,159 @@ export function satsBtcUsdRolling({ pattern, name, color, defaultActive }) {
return satsBtcUsd({ 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 * Create coinbase/subsidy/fee rolling sum series from separate sources
* @param {Object} args * @param {Object} args

View File

@@ -234,7 +234,7 @@
* @property {AgeRangePattern} tree * @property {AgeRangePattern} tree
* *
* Age range cohort with matured supply * Age range cohort with matured supply
* @typedef {CohortAgeRange & { matured: AnyValuePattern }} CohortAgeRangeWithMatured * @typedef {CohortAgeRange & { matured: FullValuePattern }} CohortAgeRangeWithMatured
* *
* Basic cohort WITH RelToMarketCap (geAmount.*, ltAmount.*) * Basic cohort WITH RelToMarketCap (geAmount.*, ltAmount.*)
* @typedef {Object} CohortBasicWithMarketCap * @typedef {Object} CohortBasicWithMarketCap

View File

@@ -26,6 +26,14 @@ function walkMetrics(node, map, path) {
for (const [key, value] of Object.entries(node)) { for (const [key, value] of Object.entries(node)) {
const kn = key.toLowerCase(); const kn = key.toLowerCase();
if ( if (
key === "lookback" ||
key === "cumulativeMarketCap" ||
key === "sd24h" ||
key === "spot" ||
key === "ohlc" ||
key === "state" ||
key === "emaSlow" ||
key === "emaFast" ||
key.endsWith("Raw") || key.endsWith("Raw") ||
key.endsWith("Cents") || key.endsWith("Cents") ||
key.endsWith("State") || key.endsWith("State") ||

View File

@@ -56,6 +56,8 @@
* @typedef {Brk.BaseCumulativeSumPattern4} CoinbasePattern * @typedef {Brk.BaseCumulativeSumPattern4} CoinbasePattern
* ActivePriceRatioPattern: ratio pattern with price (extended) * ActivePriceRatioPattern: ratio pattern with price (extended)
* @typedef {Brk.BpsPriceRatioPattern} ActivePriceRatioPattern * @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 * AnyRatioPattern: full ratio pattern with percentiles, SMAs, and std dev bands
* @typedef {Brk.BpsCentsPercentilesRatioSatsSmaStdUsdPattern} AnyRatioPattern * @typedef {Brk.BpsCentsPercentilesRatioSatsSmaStdUsdPattern} AnyRatioPattern
* ValuePattern: patterns with base + cumulative (no rolling) * ValuePattern: patterns with base + cumulative (no rolling)
@@ -83,7 +85,7 @@
* @typedef {Brk.GrossInvestedLossNetNuplProfitSentimentPattern2} UnrealizedPattern * @typedef {Brk.GrossInvestedLossNetNuplProfitSentimentPattern2} UnrealizedPattern
* *
* Profitability bucket pattern * Profitability bucket pattern
* @typedef {Brk.RealizedSupplyPattern} RealizedSupplyPattern * @typedef {Brk.MvrvNuplRealizedSupplyPattern} RealizedSupplyPattern
* *
* Realized patterns * Realized patterns
* @typedef {Brk.CapGrossInvestorLossMvrvNetPeakPriceProfitSellSoprPattern} RealizedPattern * @typedef {Brk.CapGrossInvestorLossMvrvNetPeakPriceProfitSellSoprPattern} RealizedPattern