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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,7 +2,7 @@ use brk_cohort::{Loss, Profit, ProfitabilityRange};
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{
BasisPoints32, BasisPointsSigned32, Cents, Dollars, Height, Indexes, Sats, StoredF32, Version,
BasisPoints32, BasisPointsSigned32, Cents, Dollars, Indexes, Sats, StoredF32, Version,
};
use vecdb::{AnyStoredVec, AnyVec, Database, Exit, Rw, StorageMode, WritableVec};
@@ -103,15 +103,6 @@ impl ProfitabilityBucket {
})
}
#[inline(always)]
pub(crate) fn truncate(&mut self, height: Height) -> Result<()> {
self.supply.all.sats.height.truncate_if_needed(height)?;
self.supply.sth.sats.height.truncate_if_needed(height)?;
self.realized_cap.all.height.truncate_if_needed(height)?;
self.realized_cap.sth.height.truncate_if_needed(height)?;
Ok(())
}
#[inline(always)]
pub(crate) fn push(
&mut self,
@@ -223,10 +214,6 @@ impl<M: StorageMode> ProfitabilityMetrics<M> {
}
impl ProfitabilityMetrics {
pub(crate) fn truncate(&mut self, height: Height) -> Result<()> {
self.iter_mut().try_for_each(|b| b.truncate(height))
}
pub(crate) fn forced_import(
db: &Database,
version: Version,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -53,18 +53,18 @@ impl UnrealizedBasic {
.min(self.loss.base.cents.height.len())
}
pub(crate) fn truncate_push(&mut self, height: Height, state: &UnrealizedState) -> Result<()> {
#[inline(always)]
pub(crate) fn push_state(&mut self, state: &UnrealizedState) {
self.profit
.base
.cents
.height
.truncate_push(height, state.unrealized_profit)?;
.push(state.unrealized_profit);
self.loss
.base
.cents
.height
.truncate_push(height, state.unrealized_loss)?;
Ok(())
.push(state.unrealized_loss);
}
pub(crate) fn collect_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,7 +2,7 @@ import { colors } from "../utils/colors.js";
import { brk } from "../client.js";
import { Unit } from "../utils/units.js";
import { dots, line, baseline, price, rollingWindowsTree, percentRatioDots } from "./series.js";
import { satsBtcUsd } from "./shared.js";
import { satsBtcUsd, priceRatioPercentilesTree } from "./shared.js";
/**
* Create Cointime section
@@ -173,57 +173,18 @@ export function createCointimeSection() {
),
],
},
...prices.map(({ pattern, name, color }) => {
const p = pattern.percentiles;
const pctUsd = /** @type {const} */ ([
{ name: "pct95", prop: p.pct95.price, color: colors.ratioPct._95 },
{ name: "pct5", prop: p.pct5.price, color: colors.ratioPct._5 },
{ name: "pct98", prop: p.pct98.price, color: colors.ratioPct._98 },
{ name: "pct2", prop: p.pct2.price, color: colors.ratioPct._2 },
{ name: "pct99", prop: p.pct99.price, color: colors.ratioPct._99 },
{ name: "pct1", prop: p.pct1.price, color: colors.ratioPct._1 },
]);
const pctRatio = /** @type {const} */ ([
{ name: "pct95", prop: p.pct95.ratio, color: colors.ratioPct._95 },
{ name: "pct5", prop: p.pct5.ratio, color: colors.ratioPct._5 },
{ name: "pct98", prop: p.pct98.ratio, color: colors.ratioPct._98 },
{ name: "pct2", prop: p.pct2.ratio, color: colors.ratioPct._2 },
{ name: "pct99", prop: p.pct99.ratio, color: colors.ratioPct._99 },
{ name: "pct1", prop: p.pct1.ratio, color: colors.ratioPct._1 },
]);
return {
...prices.map(({ pattern, name, color }) => ({
name,
tree: [
{
name: "Price",
tree: priceRatioPercentilesTree({
pattern,
title: `${name} Price`,
top: [
price({ metric: pattern, name, color }),
price({
metric: all.realized.price,
name: "Realized",
color: colors.realized,
defaultActive: false,
}),
...pctUsd.map(({ name: pName, prop, color: pColor }) =>
price({ metric: prop, name: pName, color: pColor, defaultActive: false, options: { lineStyle: 1 } }),
),
],
},
{
name: "Ratio",
title: `${name} Price Ratio`,
top: [price({ metric: pattern, name, color })],
bottom: [
baseline({ metric: pattern.ratio, name: "Ratio", unit: Unit.ratio, base: 1 }),
...pctRatio.map(({ name: pName, prop, color: pColor }) =>
line({ metric: prop, name: pName, color: pColor, defaultActive: false, unit: Unit.ratio, options: { lineStyle: 1 } }),
),
],
},
],
};
legend: name,
color,
priceReferences: [
price({ metric: all.realized.price, name: "Realized", color: colors.realized, defaultActive: false }),
],
}),
})),
],
},

View File

@@ -33,6 +33,9 @@ export function buildCohortData() {
AMOUNT_RANGE_NAMES,
SPENDABLE_TYPE_NAMES,
CLASS_NAMES,
PROFITABILITY_RANGE_NAMES,
PROFIT_NAMES,
LOSS_NAMES,
} = brk;
const cohortAll = {
@@ -191,6 +194,28 @@ export function buildCohortData() {
tree: utxoCohorts.class[key],
}));
const { range, profit, loss } = utxoCohorts.profitability;
const profitabilityRange = entries(PROFITABILITY_RANGE_NAMES).map(
([key, names], i, arr) => ({
name: names.short,
color: colors.at(i, arr.length),
pattern: range[key],
}),
);
const profitabilityProfit = entries(PROFIT_NAMES).map(([key, names], i, arr) => ({
name: names.short,
color: colors.at(i, arr.length),
pattern: profit[key],
}));
const profitabilityLoss = entries(LOSS_NAMES).map(([key, names], i, arr) => ({
name: names.short,
color: colors.at(i, arr.length),
pattern: loss[key],
}));
return {
cohortAll,
termShort,
@@ -208,5 +233,8 @@ export function buildCohortData() {
typeAddressable,
typeOther,
class: class_,
profitabilityRange,
profitabilityProfit,
profitabilityLoss,
};
}

View File

@@ -10,7 +10,9 @@
* - activity.js: SOPR, Volume, Lifespan
*/
import { formatCohortTitle, satsBtcUsd } from "../shared.js";
import { formatCohortTitle, satsBtcUsd, satsBtcUsdFullTree, simplePriceRatioTree, groupedSimplePriceRatioTree } from "../shared.js";
import { ROLLING_WINDOWS, line, baseline, percentRatio, rollingWindowsTree, rollingPercentRatioTree } from "../series.js";
import { Unit } from "../../utils/units.js";
// Section builders
import {
@@ -205,8 +207,11 @@ export function createCohortFolderAgeRangeWithMatured(cohort) {
const title = formatCohortTitle(cohort.name);
folder.tree.push({
name: "Matured",
tree: satsBtcUsdFullTree({
pattern: cohort.matured,
name: cohort.name,
title: title("Matured Supply"),
bottom: satsBtcUsd({ pattern: cohort.matured, name: cohort.name }),
}),
});
return folder;
}
@@ -452,7 +457,7 @@ export function createGroupedCohortFolderAgeRangeWithMatured({
name: "Matured",
title: title("Matured Supply"),
bottom: list.flatMap((cohort) =>
satsBtcUsd({ pattern: cohort.matured, name: cohort.name, color: cohort.color }),
satsBtcUsd({ pattern: cohort.matured.base, name: cohort.name, color: cohort.color }),
),
});
return folder;
@@ -580,3 +585,212 @@ export function createGroupedAddressCohortFolder({
],
};
}
// ============================================================================
// UTXO Profitability Folder Builders
// ============================================================================
/**
* @param {{ name: string, color: Color, pattern: RealizedSupplyPattern }} bucket
* @returns {PartialOptionsGroup}
*/
function singleBucketFolder({ name, color, pattern }) {
return {
name,
tree: [
{
name: "Supply",
tree: [
{
name: "All",
title: `${name}: Supply`,
bottom: satsBtcUsd({ pattern: pattern.supply.all, name, color }),
},
{
name: "STH",
title: `${name}: STH Supply`,
bottom: satsBtcUsd({ pattern: pattern.supply.sth, name, color }),
},
{
name: "Change",
tree: [
{ ...rollingWindowsTree({ windows: pattern.supply.all.delta.absolute, title: `${name}: Supply Change`, unit: Unit.sats, series: baseline }), name: "Absolute" },
{ ...rollingPercentRatioTree({ windows: pattern.supply.all.delta.rate, title: `${name}: Supply Rate` }), name: "Rate" },
],
},
],
},
{
name: "Realized Cap",
tree: [
{
name: "All",
title: `${name}: Realized Cap`,
bottom: [line({ metric: pattern.realizedCap.all, name, color, unit: Unit.usd })],
},
{
name: "STH",
title: `${name}: STH Realized Cap`,
bottom: [line({ metric: pattern.realizedCap.sth, name, color, unit: Unit.usd })],
},
],
},
{
name: "Realized Price",
tree: simplePriceRatioTree({
pattern: pattern.realizedPrice,
title: `${name}: Realized Price`,
legend: name,
color,
}),
},
{
name: "NUPL",
title: `${name}: NUPL`,
bottom: [line({ metric: pattern.nupl.ratio, name, color, unit: Unit.ratio })],
},
],
};
}
/**
* @param {{ name: string, color: Color, pattern: RealizedSupplyPattern }[]} list
* @param {string} titlePrefix
* @returns {PartialOptionsTree}
*/
function groupedBucketCharts(list, titlePrefix) {
return [
{
name: "Supply",
tree: [
{
name: "All",
title: `${titlePrefix}: Supply`,
bottom: list.flatMap(({ name, color, pattern }) =>
satsBtcUsd({ pattern: pattern.supply.all, name, color }),
),
},
{
name: "STH",
title: `${titlePrefix}: STH Supply`,
bottom: list.flatMap(({ name, color, pattern }) =>
satsBtcUsd({ pattern: pattern.supply.sth, name, color }),
),
},
{
name: "Change",
tree: [
{
name: "Absolute",
tree: [
{
name: "Compare",
title: `${titlePrefix}: Supply Change`,
bottom: ROLLING_WINDOWS.flatMap((w) =>
list.map(({ name, color, pattern }) =>
baseline({ metric: pattern.supply.all.delta.absolute[w.key], name: `${name} ${w.name}`, color, unit: Unit.sats }),
),
),
},
...ROLLING_WINDOWS.map((w) => ({
name: w.name,
title: `${titlePrefix}: Supply Change ${w.name}`,
bottom: list.map(({ name, color, pattern }) =>
baseline({ metric: pattern.supply.all.delta.absolute[w.key], name, color, unit: Unit.sats }),
),
})),
],
},
{
name: "Rate",
tree: [
{
name: "Compare",
title: `${titlePrefix}: Supply Rate`,
bottom: ROLLING_WINDOWS.flatMap((w) =>
list.flatMap(({ name, color, pattern }) =>
percentRatio({ pattern: pattern.supply.all.delta.rate[w.key], name: `${name} ${w.name}`, color }),
),
),
},
...ROLLING_WINDOWS.map((w) => ({
name: w.name,
title: `${titlePrefix}: Supply Rate ${w.name}`,
bottom: list.flatMap(({ name, color, pattern }) =>
percentRatio({ pattern: pattern.supply.all.delta.rate[w.key], name, color }),
),
})),
],
},
],
},
],
},
{
name: "Realized Cap",
tree: [
{
name: "All",
title: `${titlePrefix}: Realized Cap`,
bottom: list.map(({ name, color, pattern }) =>
line({ metric: pattern.realizedCap.all, name, color, unit: Unit.usd }),
),
},
{
name: "STH",
title: `${titlePrefix}: STH Realized Cap`,
bottom: list.map(({ name, color, pattern }) =>
line({ metric: pattern.realizedCap.sth, name, color, unit: Unit.usd }),
),
},
],
},
{
name: "Realized Price",
tree: groupedSimplePriceRatioTree({
list: list.map(({ name, color, pattern }) => ({ name, color, pattern: pattern.realizedPrice })),
title: `${titlePrefix}: Realized Price`,
}),
},
{
name: "NUPL",
title: `${titlePrefix}: NUPL`,
bottom: list.map(({ name, color, pattern }) =>
line({ metric: pattern.nupl.ratio, name, color, unit: Unit.ratio }),
),
},
];
}
/**
* @param {{ range: { name: string, color: Color, pattern: RealizedSupplyPattern }[], profit: { name: string, color: Color, pattern: RealizedSupplyPattern }[], loss: { name: string, color: Color, pattern: RealizedSupplyPattern }[] }} args
* @returns {PartialOptionsGroup}
*/
export function createUtxoProfitabilitySection({ range, profit, loss }) {
return {
name: "UTXO Profitability",
tree: [
{
name: "Range",
tree: [
{ name: "Compare", tree: groupedBucketCharts(range, "Profitability Range") },
...range.map(singleBucketFolder),
],
},
{
name: "In Profit",
tree: [
{ name: "Compare", tree: groupedBucketCharts(profit, "In Profit") },
...profit.map(singleBucketFolder),
],
},
{
name: "In Loss",
tree: [
{ name: "Compare", tree: groupedBucketCharts(loss, "In Loss") },
...loss.map(singleBucketFolder),
],
},
],
};
}

View File

@@ -14,7 +14,7 @@
*/
import { colors } from "../../utils/colors.js";
import { createPriceRatioCharts, mapCohortsWithAll } from "../shared.js";
import { createPriceRatioCharts, mapCohortsWithAll, priceRatioPercentilesTree } from "../shared.js";
import { baseline, price } from "../series.js";
import { Unit } from "../../utils/units.js";
@@ -53,26 +53,12 @@ export function createPricesSectionFull({ cohort, title }) {
},
{
name: "Investor",
tree: [
{
name: "Price",
tree: priceRatioPercentilesTree({
pattern: tree.realized.investor.price,
title: title("Investor Price"),
top: [price({ metric: tree.realized.investor.price, name: "Investor", color })],
},
{
name: "Ratio",
title: title("Investor Price Ratio"),
top: [price({ metric: tree.realized.investor.price, name: "Investor", color })],
bottom: [
baseline({
metric: tree.realized.investor.price.ratio,
name: "Ratio",
unit: Unit.ratio,
base: 1,
legend: "Investor",
color,
}),
],
},
],
},
],
};

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

View File

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

View File

@@ -1,7 +1,7 @@
/** Shared helpers for options */
import { Unit } from "../utils/units.js";
import { line, baseline, price } from "./series.js";
import { line, baseline, price, ROLLING_WINDOWS } from "./series.js";
import { priceLine, priceLines } from "./constants.js";
import { colors } from "../utils/colors.js";
@@ -234,6 +234,159 @@ export function satsBtcUsdRolling({ pattern, name, color, defaultActive }) {
return satsBtcUsd({ pattern, name, color, defaultActive });
}
/**
* Build a full Sum / Rolling / Cumulative tree from a FullValuePattern
* @param {Object} args
* @param {FullValuePattern} args.pattern
* @param {string} args.name
* @param {string} args.title
* @param {Color} [args.color]
* @returns {PartialOptionsTree}
*/
export function satsBtcUsdFullTree({ pattern, name, title, color }) {
return [
{
name: "Sum",
title,
bottom: satsBtcUsd({ pattern: pattern.base, name, color }),
},
{
name: "Rolling",
tree: [
{
name: "Compare",
title: `${title} Rolling Sum`,
bottom: ROLLING_WINDOWS.flatMap((w) =>
satsBtcUsd({ pattern: pattern.sum[w.key], name: w.name, color: w.color }),
),
},
...ROLLING_WINDOWS.map((w) => ({
name: w.name,
title: `${title} ${w.name} Rolling Sum`,
bottom: satsBtcUsd({ pattern: pattern.sum[w.key], name: w.name, color: w.color }),
})),
],
},
{
name: "Cumulative",
title: `${title} (Total)`,
bottom: satsBtcUsd({ pattern: pattern.cumulative, name: "all-time", color }),
},
];
}
/**
* Create Price + Ratio charts from a simple price pattern (BpsCentsRatioSatsUsdPattern)
* @param {Object} args
* @param {AnyPricePattern & { ratio: AnyMetricPattern }} args.pattern
* @param {string} args.title
* @param {string} args.legend
* @param {Color} [args.color]
* @returns {PartialOptionsTree}
*/
export function simplePriceRatioTree({ pattern, title, legend, color }) {
return [
{
name: "Price",
title,
top: [price({ metric: pattern, name: legend, color })],
},
{
name: "Ratio",
title: `${title} Ratio`,
top: [price({ metric: pattern, name: legend, color })],
bottom: [
baseline({ metric: pattern.ratio, name: "Ratio", unit: Unit.ratio, base: 1 }),
],
},
];
}
/**
* Create Price + Ratio charts with percentile bands (no SMAs/z-scores)
* @param {Object} args
* @param {PriceRatioPercentilesPattern} args.pattern
* @param {string} args.title
* @param {string} args.legend
* @param {Color} [args.color]
* @param {FetchedPriceSeriesBlueprint[]} [args.priceReferences]
* @returns {PartialOptionsTree}
*/
export function priceRatioPercentilesTree({ pattern, title, legend, color, priceReferences }) {
const p = pattern.percentiles;
const pctUsd = [
{ name: "pct95", prop: p.pct95.price, color: colors.ratioPct._95 },
{ name: "pct5", prop: p.pct5.price, color: colors.ratioPct._5 },
{ name: "pct98", prop: p.pct98.price, color: colors.ratioPct._98 },
{ name: "pct2", prop: p.pct2.price, color: colors.ratioPct._2 },
{ name: "pct99", prop: p.pct99.price, color: colors.ratioPct._99 },
{ name: "pct1", prop: p.pct1.price, color: colors.ratioPct._1 },
];
const pctRatio = [
{ name: "pct95", prop: p.pct95.ratio, color: colors.ratioPct._95 },
{ name: "pct5", prop: p.pct5.ratio, color: colors.ratioPct._5 },
{ name: "pct98", prop: p.pct98.ratio, color: colors.ratioPct._98 },
{ name: "pct2", prop: p.pct2.ratio, color: colors.ratioPct._2 },
{ name: "pct99", prop: p.pct99.ratio, color: colors.ratioPct._99 },
{ name: "pct1", prop: p.pct1.ratio, color: colors.ratioPct._1 },
];
return [
{
name: "Price",
title,
top: [
price({ metric: pattern, name: legend, color }),
...(priceReferences ?? []),
...pctUsd.map(({ name, prop, color }) =>
price({ metric: prop, name, color, defaultActive: false, options: { lineStyle: 1 } }),
),
],
},
{
name: "Ratio",
title: `${title} Ratio`,
top: [
price({ metric: pattern, name: legend, color }),
...pctUsd.map(({ name, prop, color }) =>
price({ metric: prop, name, color, defaultActive: false, options: { lineStyle: 1 } }),
),
],
bottom: [
baseline({ metric: pattern.ratio, name: "Ratio", unit: Unit.ratio, base: 1 }),
...pctRatio.map(({ name, prop, color }) =>
line({ metric: prop, name, color, defaultActive: false, unit: Unit.ratio, options: { lineStyle: 1 } }),
),
],
},
];
}
/**
* Create grouped Price + Ratio charts overlaying multiple series
* @param {Object} args
* @param {{ name: string, color?: Color, pattern: AnyPricePattern & { ratio: AnyMetricPattern } }[]} args.list
* @param {string} args.title
* @returns {PartialOptionsTree}
*/
export function groupedSimplePriceRatioTree({ list, title }) {
return [
{
name: "Price",
title,
top: list.map(({ name, color, pattern }) =>
price({ metric: pattern, name, color }),
),
},
{
name: "Ratio",
title: `${title} Ratio`,
bottom: list.map(({ name, color, pattern }) =>
baseline({ metric: pattern.ratio, name, color, unit: Unit.ratio, base: 1 }),
),
},
];
}
/**
* Create coinbase/subsidy/fee rolling sum series from separate sources
* @param {Object} args

View File

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

View File

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

View File

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