mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-07-16 13:38:12 -07:00
global: final snapshot and fixes before release
This commit is contained in:
@@ -3,7 +3,10 @@ use brk_types::{Cents, CentsCompact, Sats};
|
||||
|
||||
use crate::{
|
||||
distribution::state::PendingDelta,
|
||||
internal::{PERCENTILES, PERCENTILES_LEN, algo::{FenwickNode, FenwickTree}},
|
||||
internal::{
|
||||
PERCENTILES, PERCENTILES_LEN,
|
||||
algo::{FenwickNode, FenwickTree},
|
||||
},
|
||||
};
|
||||
|
||||
use super::COST_BASIS_PRICE_DIGITS;
|
||||
@@ -70,7 +73,6 @@ pub(super) struct CostBasisFenwick {
|
||||
// to a flat bucket index across two tiers.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Map rounded dollars to a flat bucket index.
|
||||
/// Prices >= $1M are clamped to the last bucket.
|
||||
#[inline]
|
||||
fn dollars_to_bucket(dollars: u64) -> usize {
|
||||
@@ -83,7 +85,6 @@ fn dollars_to_bucket(dollars: u64) -> usize {
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a bucket index back to a price in Cents.
|
||||
#[inline]
|
||||
fn bucket_to_cents(bucket: usize) -> Cents {
|
||||
let dollars: u64 = if bucket < TIER1_START {
|
||||
@@ -96,24 +97,18 @@ fn bucket_to_cents(bucket: usize) -> Cents {
|
||||
Cents::from(dollars * 100)
|
||||
}
|
||||
|
||||
/// Map a CentsCompact price to a bucket index.
|
||||
#[inline]
|
||||
fn price_to_bucket(price: CentsCompact) -> usize {
|
||||
cents_to_bucket(price.into())
|
||||
}
|
||||
|
||||
/// Map a Cents price to a bucket index.
|
||||
#[inline]
|
||||
fn cents_to_bucket(price: Cents) -> usize {
|
||||
dollars_to_bucket(u64::from(price.round_to_dollar(COST_BASIS_PRICE_DIGITS)) / 100)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CostBasisFenwick implementation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
impl CostBasisFenwick {
|
||||
pub(super) fn new() -> Self {
|
||||
impl Default for CostBasisFenwick {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
tree: FenwickTree::new(TREE_SIZE),
|
||||
totals: CostBasisNode::default(),
|
||||
@@ -121,7 +116,9 @@ impl CostBasisFenwick {
|
||||
initialized: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CostBasisFenwick {
|
||||
pub(super) fn is_initialized(&self) -> bool {
|
||||
self.initialized
|
||||
}
|
||||
@@ -153,7 +150,8 @@ impl CostBasisFenwick {
|
||||
return;
|
||||
}
|
||||
let bucket = price_to_bucket(price);
|
||||
let delta = CostBasisNode::new(net_sats, price.as_u128() as i128 * net_sats as i128, is_sth);
|
||||
let delta =
|
||||
CostBasisNode::new(net_sats, price.as_u128() as i128 * net_sats as i128, is_sth);
|
||||
self.tree.add(bucket, &delta);
|
||||
self.totals.add_assign(&delta);
|
||||
}
|
||||
@@ -236,8 +234,7 @@ impl CostBasisFenwick {
|
||||
sat_targets[PERCENTILES_LEN + 1] = total_sats - 1; // max
|
||||
|
||||
let mut sat_buckets = [0usize; PERCENTILES_LEN + 2];
|
||||
self.tree
|
||||
.kth(&sat_targets, &sat_field, &mut sat_buckets);
|
||||
self.tree.kth(&sat_targets, &sat_field, &mut sat_buckets);
|
||||
|
||||
result.min_price = bucket_to_cents(sat_buckets[0]);
|
||||
(0..PERCENTILES_LEN).for_each(|i| {
|
||||
@@ -253,8 +250,7 @@ impl CostBasisFenwick {
|
||||
}
|
||||
|
||||
let mut usd_buckets = [0usize; PERCENTILES_LEN];
|
||||
self.tree
|
||||
.kth(&usd_targets, &usd_field, &mut usd_buckets);
|
||||
self.tree.kth(&usd_targets, &usd_field, &mut usd_buckets);
|
||||
|
||||
(0..PERCENTILES_LEN).for_each(|i| {
|
||||
result.usd_prices[i] = bucket_to_cents(usd_buckets[i]);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use std::path::Path;
|
||||
|
||||
use brk_cohort::{
|
||||
AgeRange, AmountRange, Class, ByEpoch, OverAmount, UnderAmount, UnderAge,
|
||||
OverAge, SpendableType, CohortContext, Filter, Filtered, Term,
|
||||
AgeRange, AmountRange, ByEpoch, Class, CohortContext, Filter, Filtered, OverAge, OverAmount,
|
||||
SpendableType, Term, UnderAge, UnderAmount,
|
||||
};
|
||||
use brk_error::Result;
|
||||
use brk_traversable::Traversable;
|
||||
@@ -17,8 +17,8 @@ use crate::{
|
||||
distribution::{
|
||||
DynCohortVecs,
|
||||
metrics::{
|
||||
AllCohortMetrics, BasicCohortMetrics, CohortMetricsBase,
|
||||
CoreCohortMetrics, ExtendedAdjustedCohortMetrics, ExtendedCohortMetrics, ImportConfig,
|
||||
AllCohortMetrics, BasicCohortMetrics, CohortMetricsBase, CoreCohortMetrics,
|
||||
ExtendedAdjustedCohortMetrics, ExtendedCohortMetrics, ImportConfig,
|
||||
MinimalCohortMetrics, ProfitabilityMetrics, RealizedFullAccum, SupplyCore,
|
||||
TypeCohortMetrics,
|
||||
},
|
||||
@@ -52,11 +52,16 @@ pub struct UTXOCohorts<M: StorageMode = Rw> {
|
||||
pub profitability: ProfitabilityMetrics<M>,
|
||||
pub matured: AgeRange<AmountPerBlockCumulativeRolling<M>>,
|
||||
#[traversable(skip)]
|
||||
pub(super) caches: UTXOCohortsTransientState,
|
||||
}
|
||||
|
||||
/// In-memory state that does NOT survive rollback.
|
||||
#[derive(Clone, Default)]
|
||||
pub(crate) struct UTXOCohortsTransientState {
|
||||
pub(super) fenwick: CostBasisFenwick,
|
||||
/// Cached partition_point positions for tick_tock boundary searches.
|
||||
/// Avoids O(log n) binary search per boundary per block; scans forward
|
||||
/// from last known position (typically O(1) per boundary).
|
||||
#[traversable(skip)]
|
||||
pub(super) tick_tock_cached_positions: [usize; 20],
|
||||
}
|
||||
|
||||
@@ -258,7 +263,7 @@ impl UTXOCohorts<Rw> {
|
||||
|
||||
let prefix = CohortContext::Utxo.prefix();
|
||||
let matured = AgeRange::try_new(&|_f: Filter,
|
||||
name: &'static str|
|
||||
name: &'static str|
|
||||
-> Result<AmountPerBlockCumulativeRolling> {
|
||||
AmountPerBlockCumulativeRolling::forced_import(
|
||||
db,
|
||||
@@ -284,24 +289,30 @@ impl UTXOCohorts<Rw> {
|
||||
over_amount,
|
||||
profitability,
|
||||
matured,
|
||||
fenwick: CostBasisFenwick::new(),
|
||||
tick_tock_cached_positions: [0; 20],
|
||||
caches: UTXOCohortsTransientState::default(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Reset in-memory caches that become stale after rollback.
|
||||
pub(crate) fn reset_caches(&mut self) {
|
||||
self.caches = UTXOCohortsTransientState::default();
|
||||
}
|
||||
|
||||
/// Initialize the Fenwick tree from all age-range BTreeMaps.
|
||||
/// Call after state import when all pending maps have been drained.
|
||||
pub(crate) fn init_fenwick_if_needed(&mut self) {
|
||||
if self.fenwick.is_initialized() {
|
||||
if self.caches.fenwick.is_initialized() {
|
||||
return;
|
||||
}
|
||||
let Self {
|
||||
sth,
|
||||
fenwick,
|
||||
caches,
|
||||
age_range,
|
||||
..
|
||||
} = self;
|
||||
fenwick.compute_is_sth(&sth.metrics.filter, age_range.iter().map(|v| v.filter()));
|
||||
caches
|
||||
.fenwick
|
||||
.compute_is_sth(&sth.metrics.filter, age_range.iter().map(|v| v.filter()));
|
||||
|
||||
let maps: Vec<_> = age_range
|
||||
.iter()
|
||||
@@ -312,27 +323,27 @@ impl UTXOCohorts<Rw> {
|
||||
if map.is_empty() {
|
||||
return None;
|
||||
}
|
||||
Some((map, fenwick.is_sth_at(i)))
|
||||
Some((map, caches.fenwick.is_sth_at(i)))
|
||||
})
|
||||
.collect();
|
||||
fenwick.bulk_init(maps.into_iter());
|
||||
caches.fenwick.bulk_init(maps.into_iter());
|
||||
}
|
||||
|
||||
/// Apply pending deltas from all age-range cohorts to the Fenwick tree.
|
||||
/// Call after receive/send, before push_cohort_states.
|
||||
pub(crate) fn update_fenwick_from_pending(&mut self) {
|
||||
if !self.fenwick.is_initialized() {
|
||||
if !self.caches.fenwick.is_initialized() {
|
||||
return;
|
||||
}
|
||||
// Destructure to get separate borrows on fenwick and age_range
|
||||
// Destructure to get separate borrows on caches and age_range
|
||||
let Self {
|
||||
fenwick, age_range, ..
|
||||
caches, age_range, ..
|
||||
} = self;
|
||||
for (i, sub) in age_range.iter().enumerate() {
|
||||
if let Some(state) = sub.state.as_ref() {
|
||||
let is_sth = fenwick.is_sth_at(i);
|
||||
let is_sth = caches.fenwick.is_sth_at(i);
|
||||
state.for_each_cost_basis_pending(|&price, delta| {
|
||||
fenwick.apply_delta(price, delta, is_sth);
|
||||
caches.fenwick.apply_delta(price, delta, is_sth);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -455,8 +466,7 @@ impl UTXOCohorts<Rw> {
|
||||
.try_for_each(|vecs| {
|
||||
let sources =
|
||||
filter_minimal_sources_from(amr.iter(), Some(&vecs.metrics.filter));
|
||||
vecs.metrics
|
||||
.compute_from_sources(si, &sources, exit)
|
||||
vecs.metrics.compute_from_sources(si, &sources, exit)
|
||||
})
|
||||
}),
|
||||
];
|
||||
@@ -483,8 +493,16 @@ impl UTXOCohorts<Rw> {
|
||||
all.push(&mut self.all);
|
||||
all.push(&mut self.sth);
|
||||
all.push(&mut self.lth);
|
||||
all.extend(self.under_age.iter_mut().map(|x| x as &mut dyn DynCohortVecs));
|
||||
all.extend(self.over_age.iter_mut().map(|x| x as &mut dyn DynCohortVecs));
|
||||
all.extend(
|
||||
self.under_age
|
||||
.iter_mut()
|
||||
.map(|x| x as &mut dyn DynCohortVecs),
|
||||
);
|
||||
all.extend(
|
||||
self.over_age
|
||||
.iter_mut()
|
||||
.map(|x| x as &mut dyn DynCohortVecs),
|
||||
);
|
||||
all.extend(
|
||||
self.over_amount
|
||||
.iter_mut()
|
||||
@@ -542,7 +560,8 @@ impl UTXOCohorts<Rw> {
|
||||
.metrics
|
||||
.activity
|
||||
.transfer_volume
|
||||
.block.cents
|
||||
.block
|
||||
.cents
|
||||
.read_only_clone();
|
||||
let under_1h_value_destroyed = self
|
||||
.age_range
|
||||
@@ -567,7 +586,13 @@ impl UTXOCohorts<Rw> {
|
||||
|
||||
// Clone all_supply_sats and all_utxo_count for non-all cohorts.
|
||||
let all_supply_sats = self.all.metrics.supply.total.sats.height.read_only_clone();
|
||||
let all_utxo_count = self.all.metrics.outputs.unspent_count.height.read_only_clone();
|
||||
let all_utxo_count = self
|
||||
.all
|
||||
.metrics
|
||||
.outputs
|
||||
.unspent_count
|
||||
.height
|
||||
.read_only_clone();
|
||||
|
||||
// Destructure to allow parallel mutable access to independent fields.
|
||||
let Self {
|
||||
@@ -636,9 +661,10 @@ impl UTXOCohorts<Rw> {
|
||||
})
|
||||
}),
|
||||
Box::new(|| {
|
||||
over_amount
|
||||
.par_iter_mut()
|
||||
.try_for_each(|v| v.metrics.compute_rest_part2(prices, starting_indexes, au, exit))
|
||||
over_amount.par_iter_mut().try_for_each(|v| {
|
||||
v.metrics
|
||||
.compute_rest_part2(prices, starting_indexes, au, exit)
|
||||
})
|
||||
}),
|
||||
Box::new(|| {
|
||||
epoch.par_iter_mut().try_for_each(|v| {
|
||||
@@ -653,19 +679,22 @@ impl UTXOCohorts<Rw> {
|
||||
})
|
||||
}),
|
||||
Box::new(|| {
|
||||
amount_range
|
||||
.par_iter_mut()
|
||||
.try_for_each(|v| v.metrics.compute_rest_part2(prices, starting_indexes, au, exit))
|
||||
amount_range.par_iter_mut().try_for_each(|v| {
|
||||
v.metrics
|
||||
.compute_rest_part2(prices, starting_indexes, au, exit)
|
||||
})
|
||||
}),
|
||||
Box::new(|| {
|
||||
under_amount
|
||||
.par_iter_mut()
|
||||
.try_for_each(|v| v.metrics.compute_rest_part2(prices, starting_indexes, au, exit))
|
||||
under_amount.par_iter_mut().try_for_each(|v| {
|
||||
v.metrics
|
||||
.compute_rest_part2(prices, starting_indexes, au, exit)
|
||||
})
|
||||
}),
|
||||
Box::new(|| {
|
||||
type_
|
||||
.par_iter_mut()
|
||||
.try_for_each(|v| v.metrics.compute_rest_part2(prices, starting_indexes, au, exit))
|
||||
type_.par_iter_mut().try_for_each(|v| {
|
||||
v.metrics
|
||||
.compute_rest_part2(prices, starting_indexes, au, exit)
|
||||
})
|
||||
}),
|
||||
];
|
||||
|
||||
@@ -829,12 +858,30 @@ impl UTXOCohorts<Rw> {
|
||||
sth.metrics.realized.push_accum(&sth_acc);
|
||||
lth.metrics.realized.push_accum(<h_acc);
|
||||
|
||||
all.metrics.unrealized.investor_cap_in_profit_raw.push(CentsSquaredSats::new(all_icap.0));
|
||||
all.metrics.unrealized.investor_cap_in_loss_raw.push(CentsSquaredSats::new(all_icap.1));
|
||||
sth.metrics.unrealized.investor_cap_in_profit_raw.push(CentsSquaredSats::new(sth_icap.0));
|
||||
sth.metrics.unrealized.investor_cap_in_loss_raw.push(CentsSquaredSats::new(sth_icap.1));
|
||||
lth.metrics.unrealized.investor_cap_in_profit_raw.push(CentsSquaredSats::new(lth_icap.0));
|
||||
lth.metrics.unrealized.investor_cap_in_loss_raw.push(CentsSquaredSats::new(lth_icap.1));
|
||||
all.metrics
|
||||
.unrealized
|
||||
.investor_cap_in_profit_raw
|
||||
.push(CentsSquaredSats::new(all_icap.0));
|
||||
all.metrics
|
||||
.unrealized
|
||||
.investor_cap_in_loss_raw
|
||||
.push(CentsSquaredSats::new(all_icap.1));
|
||||
sth.metrics
|
||||
.unrealized
|
||||
.investor_cap_in_profit_raw
|
||||
.push(CentsSquaredSats::new(sth_icap.0));
|
||||
sth.metrics
|
||||
.unrealized
|
||||
.investor_cap_in_loss_raw
|
||||
.push(CentsSquaredSats::new(sth_icap.1));
|
||||
lth.metrics
|
||||
.unrealized
|
||||
.investor_cap_in_profit_raw
|
||||
.push(CentsSquaredSats::new(lth_icap.0));
|
||||
lth.metrics
|
||||
.unrealized
|
||||
.investor_cap_in_loss_raw
|
||||
.push(CentsSquaredSats::new(lth_icap.1));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ impl UTXOCohorts {
|
||||
date_opt: Option<Date>,
|
||||
states_path: &Path,
|
||||
) -> Result<()> {
|
||||
if self.fenwick.is_initialized() {
|
||||
if self.caches.fenwick.is_initialized() {
|
||||
self.push_fenwick_results(spot_price);
|
||||
}
|
||||
|
||||
@@ -38,18 +38,18 @@ impl UTXOCohorts {
|
||||
|
||||
/// Push all Fenwick-derived per-block results: percentiles, density, profitability.
|
||||
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.caches.fenwick.density(spot_price);
|
||||
|
||||
let all = self.fenwick.percentiles_all();
|
||||
let all = self.caches.fenwick.percentiles_all();
|
||||
push_cost_basis(&all, all_d, &mut self.all.metrics.cost_basis);
|
||||
|
||||
let sth = self.fenwick.percentiles_sth();
|
||||
let sth = self.caches.fenwick.percentiles_sth();
|
||||
push_cost_basis(&sth, sth_d, &mut self.sth.metrics.cost_basis);
|
||||
|
||||
let lth = self.fenwick.percentiles_lth();
|
||||
let lth = self.caches.fenwick.percentiles_lth();
|
||||
push_cost_basis(<h, lth_d, &mut self.lth.metrics.cost_basis);
|
||||
|
||||
let prof = self.fenwick.profitability(spot_price);
|
||||
let prof = self.caches.fenwick.profitability(spot_price);
|
||||
push_profitability(&prof, &mut self.profitability);
|
||||
}
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ impl UTXOCohorts<Rw> {
|
||||
// Cohort 0 covers [0, 1) hours
|
||||
// Cohort 20 covers [15*365*24, infinity) hours
|
||||
let mut age_cohorts: Vec<_> = self.age_range.iter_mut().map(|v| &mut v.state).collect();
|
||||
let cached = &mut self.tick_tock_cached_positions;
|
||||
let cached = &mut self.caches.tick_tock_cached_positions;
|
||||
|
||||
// For each boundary (in hours), find blocks that just crossed it
|
||||
for (boundary_idx, &boundary_hours) in AGE_BOUNDARIES.iter().enumerate() {
|
||||
|
||||
Reference in New Issue
Block a user