mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-07-01 22:39:03 -07:00
global: add cohorts by entry
This commit is contained in:
@@ -30,18 +30,34 @@ const TREE_SIZE: usize = TIER0_COUNT + TIER1_COUNT + OVERFLOW; // 190,001
|
||||
pub(super) struct CostBasisNode {
|
||||
all_sats: i64,
|
||||
sth_sats: i64,
|
||||
discount_sats: i64,
|
||||
all_usd: i128,
|
||||
sth_usd: i128,
|
||||
discount_usd: i128,
|
||||
}
|
||||
|
||||
impl CostBasisNode {
|
||||
#[inline(always)]
|
||||
fn new(sats: i64, usd: i128, is_sth: bool) -> Self {
|
||||
fn new_supply(sats: i64, usd: i128, is_sth: bool) -> Self {
|
||||
Self {
|
||||
all_sats: sats,
|
||||
sth_sats: if is_sth { sats } else { 0 },
|
||||
discount_sats: 0,
|
||||
all_usd: usd,
|
||||
sth_usd: if is_sth { usd } else { 0 },
|
||||
discount_usd: 0,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn new_discount(sats: i64, usd: i128) -> Self {
|
||||
Self {
|
||||
all_sats: 0,
|
||||
sth_sats: 0,
|
||||
discount_sats: sats,
|
||||
all_usd: 0,
|
||||
sth_usd: 0,
|
||||
discount_usd: usd,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -51,8 +67,10 @@ impl FenwickNode for CostBasisNode {
|
||||
fn add_assign(&mut self, other: &Self) {
|
||||
self.all_sats += other.all_sats;
|
||||
self.sth_sats += other.sth_sats;
|
||||
self.discount_sats += other.discount_sats;
|
||||
self.all_usd += other.all_usd;
|
||||
self.sth_usd += other.sth_usd;
|
||||
self.discount_usd += other.discount_usd;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,16 +169,34 @@ impl CostBasisFenwick {
|
||||
}
|
||||
let bucket = price_to_bucket(price);
|
||||
let delta =
|
||||
CostBasisNode::new(net_sats, price.as_u128() as i128 * net_sats as i128, is_sth);
|
||||
CostBasisNode::new_supply(net_sats, price.as_u128() as i128 * net_sats as i128, is_sth);
|
||||
self.tree.add(bucket, &delta);
|
||||
self.totals.add_assign(&delta);
|
||||
}
|
||||
|
||||
/// Bulk-initialize from BTreeMaps (one per age-range cohort).
|
||||
/// Call after state import when all pending maps have been drained.
|
||||
pub(super) fn bulk_init<'a>(
|
||||
/// Apply a net delta from the discount-entry cohort.
|
||||
///
|
||||
/// Supply totals are maintained from the age-range cohorts; this updates
|
||||
/// only the discount-entry partition so premium can be derived as all - discount.
|
||||
pub(super) fn apply_discount_delta(&mut self, price: CentsCompact, pending: &PendingDelta) {
|
||||
let net_sats = u64::from(pending.inc) as i64 - u64::from(pending.dec) as i64;
|
||||
if net_sats == 0 {
|
||||
return;
|
||||
}
|
||||
let bucket = price_to_bucket(price);
|
||||
let delta =
|
||||
CostBasisNode::new_discount(net_sats, price.as_u128() as i128 * net_sats as i128);
|
||||
self.tree.add(bucket, &delta);
|
||||
self.totals.add_assign(&delta);
|
||||
}
|
||||
|
||||
/// Bulk-initialize from age-range maps plus the discount-entry map.
|
||||
/// Age-range maps maintain all/STH/LTH totals; the discount-entry map
|
||||
/// maintains only the discount partition used to derive premium.
|
||||
pub(super) fn bulk_init_with_discount<'a>(
|
||||
&mut self,
|
||||
maps: impl Iterator<Item = (&'a std::collections::BTreeMap<CentsCompact, Sats>, bool)>,
|
||||
discount_maps: impl Iterator<Item = &'a std::collections::BTreeMap<CentsCompact, Sats>>,
|
||||
) {
|
||||
self.tree.reset();
|
||||
self.totals = CostBasisNode::default();
|
||||
@@ -169,7 +205,18 @@ impl CostBasisFenwick {
|
||||
for (&price, &sats) in map.iter() {
|
||||
let bucket = price_to_bucket(price);
|
||||
let s = u64::from(sats) as i64;
|
||||
let node = CostBasisNode::new(s, price.as_u128() as i128 * s as i128, is_sth);
|
||||
let node =
|
||||
CostBasisNode::new_supply(s, price.as_u128() as i128 * s as i128, is_sth);
|
||||
self.tree.add_raw(bucket, &node);
|
||||
self.totals.add_assign(&node);
|
||||
}
|
||||
}
|
||||
|
||||
for map in discount_maps {
|
||||
for (&price, &sats) in map.iter() {
|
||||
let bucket = price_to_bucket(price);
|
||||
let s = u64::from(sats) as i64;
|
||||
let node = CostBasisNode::new_discount(s, price.as_u128() as i128 * s as i128);
|
||||
self.tree.add_raw(bucket, &node);
|
||||
self.totals.add_assign(&node);
|
||||
}
|
||||
@@ -212,6 +259,26 @@ impl CostBasisFenwick {
|
||||
)
|
||||
}
|
||||
|
||||
/// Compute percentile prices for discount-entry cohort.
|
||||
pub(super) fn percentiles_discount_entry(&self) -> PercentileResult {
|
||||
self.compute_percentiles(
|
||||
self.totals.discount_sats,
|
||||
self.totals.discount_usd,
|
||||
|n| n.discount_sats,
|
||||
|n| n.discount_usd,
|
||||
)
|
||||
}
|
||||
|
||||
/// Compute percentile prices for premium-entry cohort (all - discount).
|
||||
pub(super) fn percentiles_premium_entry(&self) -> PercentileResult {
|
||||
self.compute_percentiles(
|
||||
self.totals.all_sats - self.totals.discount_sats,
|
||||
self.totals.all_usd - self.totals.discount_usd,
|
||||
|n| n.all_sats - n.discount_sats,
|
||||
|n| n.all_usd - n.discount_usd,
|
||||
)
|
||||
}
|
||||
|
||||
fn compute_percentiles(
|
||||
&self,
|
||||
total_sats: i64,
|
||||
@@ -271,6 +338,37 @@ impl CostBasisFenwick {
|
||||
return (0, 0, 0);
|
||||
}
|
||||
|
||||
let range = self.density_range(spot_price);
|
||||
let all_range = range.all_sats.max(0);
|
||||
let sth_range = range.sth_sats.max(0);
|
||||
let lth_range = all_range - sth_range;
|
||||
|
||||
let lth_total = self.totals.all_sats - self.totals.sth_sats;
|
||||
(
|
||||
Self::to_bps(all_range, self.totals.all_sats),
|
||||
Self::to_bps(sth_range, self.totals.sth_sats),
|
||||
Self::to_bps(lth_range, lth_total),
|
||||
)
|
||||
}
|
||||
|
||||
/// Compute supply density for entry cohorts: (discount_bps, premium_bps).
|
||||
pub(super) fn entry_density(&self, spot_price: Cents) -> (u16, u16) {
|
||||
if self.totals.all_sats <= 0 {
|
||||
return (0, 0);
|
||||
}
|
||||
|
||||
let range = self.density_range(spot_price);
|
||||
let discount_range = range.discount_sats.max(0);
|
||||
let premium_range = range.all_sats.max(0) - discount_range;
|
||||
let premium_total = self.totals.all_sats - self.totals.discount_sats;
|
||||
|
||||
(
|
||||
Self::to_bps(discount_range, self.totals.discount_sats),
|
||||
Self::to_bps(premium_range, premium_total),
|
||||
)
|
||||
}
|
||||
|
||||
fn density_range(&self, spot_price: Cents) -> CostBasisNode {
|
||||
let spot_f64 = u64::from(spot_price) as f64;
|
||||
let low = Cents::from((spot_f64 * 0.95) as u64);
|
||||
let high = Cents::from((spot_f64 * 1.05) as u64);
|
||||
@@ -285,24 +383,23 @@ impl CostBasisFenwick {
|
||||
CostBasisNode::default()
|
||||
};
|
||||
|
||||
let all_range = (cum_high.all_sats - cum_low.all_sats).max(0);
|
||||
let sth_range = (cum_high.sth_sats - cum_low.sth_sats).max(0);
|
||||
let lth_range = all_range - sth_range;
|
||||
CostBasisNode {
|
||||
all_sats: cum_high.all_sats - cum_low.all_sats,
|
||||
sth_sats: cum_high.sth_sats - cum_low.sth_sats,
|
||||
discount_sats: cum_high.discount_sats - cum_low.discount_sats,
|
||||
all_usd: cum_high.all_usd - cum_low.all_usd,
|
||||
sth_usd: cum_high.sth_usd - cum_low.sth_usd,
|
||||
discount_usd: cum_high.discount_usd - cum_low.discount_usd,
|
||||
}
|
||||
}
|
||||
|
||||
let to_bps = |range: i64, total: i64| -> u16 {
|
||||
if total <= 0 {
|
||||
0
|
||||
} else {
|
||||
(range as f64 / total as f64 * 10000.0).round() as u16
|
||||
}
|
||||
};
|
||||
|
||||
let lth_total = self.totals.all_sats - self.totals.sth_sats;
|
||||
(
|
||||
to_bps(all_range, self.totals.all_sats),
|
||||
to_bps(sth_range, self.totals.sth_sats),
|
||||
to_bps(lth_range, lth_total),
|
||||
)
|
||||
#[inline(always)]
|
||||
fn to_bps(range: i64, total: i64) -> u16 {
|
||||
if total <= 0 {
|
||||
0
|
||||
} else {
|
||||
(range as f64 / total as f64 * 10000.0).round() as u16
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use std::path::Path;
|
||||
|
||||
use brk_cohort::{
|
||||
AgeRange, AmountRange, ByEpoch, Class, CohortContext, Filter, Filtered, OverAge, OverAmount,
|
||||
SpendableType, Term, UnderAge, UnderAmount,
|
||||
AgeRange, AmountRange, ByEntry, ByEpoch, Class, CohortContext, Filter, Filtered, OverAge,
|
||||
OverAmount, SpendableType, Term, UnderAge, UnderAmount,
|
||||
};
|
||||
use brk_error::Result;
|
||||
use brk_indexer::Lengths;
|
||||
@@ -16,7 +16,6 @@ use vecdb::{
|
||||
use crate::{
|
||||
blocks,
|
||||
distribution::{
|
||||
DynCohortVecs,
|
||||
metrics::{
|
||||
AllCohortMetrics, BasicCohortMetrics, CohortMetricsBase, CoreCohortMetrics,
|
||||
ExtendedAdjustedCohortMetrics, ExtendedCohortMetrics, ImportConfig,
|
||||
@@ -24,6 +23,7 @@ use crate::{
|
||||
TypeCohortMetrics,
|
||||
},
|
||||
state::UTXOCohortState,
|
||||
DynCohortVecs,
|
||||
},
|
||||
indexes,
|
||||
internal::{ValuePerBlockCumulativeRolling, WindowStartVec, Windows},
|
||||
@@ -45,6 +45,7 @@ pub struct UTXOCohorts<M: StorageMode = Rw> {
|
||||
pub over_age: OverAge<UTXOCohortVecs<CoreCohortMetrics<M>>>,
|
||||
pub epoch: ByEpoch<UTXOCohortVecs<CoreCohortMetrics<M>>>,
|
||||
pub class: Class<UTXOCohortVecs<CoreCohortMetrics<M>>>,
|
||||
pub entry: ByEntry<UTXOCohortVecs<ExtendedCohortMetrics<M>>>,
|
||||
pub over_amount: OverAmount<UTXOCohortVecs<MinimalCohortMetrics<M>>>,
|
||||
pub amount_range: AmountRange<UTXOCohortVecs<MinimalCohortMetrics<M>>>,
|
||||
pub under_amount: UnderAmount<UTXOCohortVecs<MinimalCohortMetrics<M>>>,
|
||||
@@ -67,8 +68,10 @@ pub(crate) struct UTXOCohortsTransientState {
|
||||
}
|
||||
|
||||
impl UTXOCohorts<Rw> {
|
||||
/// ~71 separate cohorts (21 age + 5 epoch + 18 class + 15 amount + 12 type)
|
||||
const SEPARATE_COHORT_CAPACITY: usize = 80;
|
||||
/// Separate cohorts currently total 72:
|
||||
/// 21 age + 5 epoch + 18 class + 2 entry + 15 amount + 11 spendable type.
|
||||
/// Keep small headroom because this is only Vec allocation capacity.
|
||||
const SEPARATE_COHORT_CAPACITY: usize = 82;
|
||||
|
||||
/// Import all UTXO cohorts from database.
|
||||
pub(crate) fn forced_import(
|
||||
@@ -136,6 +139,26 @@ impl UTXOCohorts<Rw> {
|
||||
let epoch = ByEpoch::try_new(&core_separate)?;
|
||||
let class = Class::try_new(&core_separate)?;
|
||||
|
||||
let extended_separate =
|
||||
|f: Filter, name: &'static str| -> Result<UTXOCohortVecs<ExtendedCohortMetrics>> {
|
||||
let full_name = CohortContext::Utxo.full_name(&f, name);
|
||||
let cfg = ImportConfig {
|
||||
db,
|
||||
filter: &f,
|
||||
full_name: &full_name,
|
||||
version: v,
|
||||
indexes,
|
||||
cached_starts,
|
||||
};
|
||||
let state = Some(Box::new(UTXOCohortState::new(states_path, &full_name)));
|
||||
Ok(UTXOCohortVecs::new(
|
||||
state,
|
||||
ExtendedCohortMetrics::forced_import(&cfg)?,
|
||||
))
|
||||
};
|
||||
|
||||
let entry = ByEntry::try_new(&extended_separate)?;
|
||||
|
||||
// Helper for separate cohorts with MinimalCohortMetrics + MinimalRealizedState
|
||||
let minimal_separate =
|
||||
|f: Filter, name: &'static str| -> Result<UTXOCohortVecs<MinimalCohortMetrics>> {
|
||||
@@ -281,6 +304,7 @@ impl UTXOCohorts<Rw> {
|
||||
lth,
|
||||
epoch,
|
||||
class,
|
||||
entry,
|
||||
type_,
|
||||
under_age,
|
||||
over_age,
|
||||
@@ -309,6 +333,7 @@ impl UTXOCohorts<Rw> {
|
||||
sth,
|
||||
caches,
|
||||
age_range,
|
||||
entry,
|
||||
..
|
||||
} = self;
|
||||
caches
|
||||
@@ -327,7 +352,15 @@ impl UTXOCohorts<Rw> {
|
||||
Some((map, caches.fenwick.is_sth_at(i)))
|
||||
})
|
||||
.collect();
|
||||
caches.fenwick.bulk_init(maps.into_iter());
|
||||
let discount_maps = entry
|
||||
.discount
|
||||
.state
|
||||
.as_ref()
|
||||
.map(|state| state.cost_basis_map())
|
||||
.into_iter();
|
||||
caches
|
||||
.fenwick
|
||||
.bulk_init_with_discount(maps.into_iter(), discount_maps);
|
||||
}
|
||||
|
||||
/// Apply pending deltas from all age-range cohorts to the Fenwick tree.
|
||||
@@ -338,7 +371,10 @@ impl UTXOCohorts<Rw> {
|
||||
}
|
||||
// Destructure to get separate borrows on caches and age_range
|
||||
let Self {
|
||||
caches, age_range, ..
|
||||
caches,
|
||||
age_range,
|
||||
entry,
|
||||
..
|
||||
} = self;
|
||||
for (i, sub) in age_range.iter().enumerate() {
|
||||
if let Some(state) = sub.state.as_ref() {
|
||||
@@ -348,6 +384,11 @@ impl UTXOCohorts<Rw> {
|
||||
});
|
||||
}
|
||||
}
|
||||
if let Some(state) = entry.discount.state.as_ref() {
|
||||
state.for_each_cost_basis_pending(|&price, delta| {
|
||||
caches.fenwick.apply_discount_delta(price, delta);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Push maturation sats to the matured vecs for the given height.
|
||||
@@ -365,6 +406,7 @@ impl UTXOCohorts<Rw> {
|
||||
age_range,
|
||||
epoch,
|
||||
class,
|
||||
entry,
|
||||
amount_range,
|
||||
type_,
|
||||
..
|
||||
@@ -374,6 +416,7 @@ impl UTXOCohorts<Rw> {
|
||||
.map(|x| x as &mut dyn DynCohortVecs)
|
||||
.chain(epoch.par_iter_mut().map(|x| x as &mut dyn DynCohortVecs))
|
||||
.chain(class.par_iter_mut().map(|x| x as &mut dyn DynCohortVecs))
|
||||
.chain(entry.par_iter_mut().map(|x| x as &mut dyn DynCohortVecs))
|
||||
.chain(
|
||||
amount_range
|
||||
.par_iter_mut()
|
||||
@@ -389,6 +432,7 @@ impl UTXOCohorts<Rw> {
|
||||
age_range,
|
||||
epoch,
|
||||
class,
|
||||
entry,
|
||||
amount_range,
|
||||
type_,
|
||||
..
|
||||
@@ -398,6 +442,7 @@ impl UTXOCohorts<Rw> {
|
||||
.map(|x| x as &mut dyn DynCohortVecs)
|
||||
.chain(epoch.iter_mut().map(|x| x as &mut dyn DynCohortVecs))
|
||||
.chain(class.iter_mut().map(|x| x as &mut dyn DynCohortVecs))
|
||||
.chain(entry.iter_mut().map(|x| x as &mut dyn DynCohortVecs))
|
||||
.chain(amount_range.iter_mut().map(|x| x as &mut dyn DynCohortVecs))
|
||||
.chain(type_.iter_mut().map(|x| x as &mut dyn DynCohortVecs))
|
||||
}
|
||||
@@ -409,6 +454,7 @@ impl UTXOCohorts<Rw> {
|
||||
.map(|x| x as &dyn DynCohortVecs)
|
||||
.chain(self.epoch.iter().map(|x| x as &dyn DynCohortVecs))
|
||||
.chain(self.class.iter().map(|x| x as &dyn DynCohortVecs))
|
||||
.chain(self.entry.iter().map(|x| x as &dyn DynCohortVecs))
|
||||
.chain(self.amount_range.iter().map(|x| x as &dyn DynCohortVecs))
|
||||
.chain(self.type_.iter().map(|x| x as &dyn DynCohortVecs))
|
||||
}
|
||||
@@ -516,6 +562,7 @@ impl UTXOCohorts<Rw> {
|
||||
);
|
||||
all.extend(self.epoch.iter_mut().map(|x| x as &mut dyn DynCohortVecs));
|
||||
all.extend(self.class.iter_mut().map(|x| x as &mut dyn DynCohortVecs));
|
||||
all.extend(self.entry.iter_mut().map(|x| x as &mut dyn DynCohortVecs));
|
||||
all.extend(
|
||||
self.amount_range
|
||||
.iter_mut()
|
||||
@@ -604,6 +651,7 @@ impl UTXOCohorts<Rw> {
|
||||
under_amount,
|
||||
epoch,
|
||||
class,
|
||||
entry,
|
||||
type_,
|
||||
..
|
||||
} = self;
|
||||
@@ -676,6 +724,19 @@ impl UTXOCohorts<Rw> {
|
||||
.compute_rest_part2(prices, starting_lengths, ss, au, exit)
|
||||
})
|
||||
}),
|
||||
Box::new(|| {
|
||||
entry.par_iter_mut().try_for_each(|v| {
|
||||
v.metrics.compute_rest_part2(
|
||||
blocks,
|
||||
prices,
|
||||
starting_lengths,
|
||||
height_to_market_cap,
|
||||
ss,
|
||||
au,
|
||||
exit,
|
||||
)
|
||||
})
|
||||
}),
|
||||
Box::new(|| {
|
||||
amount_range.par_iter_mut().try_for_each(|v| {
|
||||
v.metrics
|
||||
@@ -730,6 +791,9 @@ impl UTXOCohorts<Rw> {
|
||||
for v in self.class.iter_mut() {
|
||||
vecs.extend(v.metrics.collect_all_vecs_mut());
|
||||
}
|
||||
for v in self.entry.iter_mut() {
|
||||
vecs.extend(v.metrics.collect_all_vecs_mut());
|
||||
}
|
||||
for v in self.amount_range.iter_mut() {
|
||||
vecs.extend(v.metrics.collect_all_vecs_mut());
|
||||
}
|
||||
@@ -813,7 +877,7 @@ impl UTXOCohorts<Rw> {
|
||||
|
||||
/// Aggregate RealizedFull fields from age_range states and push to all/sth/lth.
|
||||
/// Called during the block loop after separate cohorts' push_state but before reset.
|
||||
pub(crate) fn push_overlapping(&mut self, height_price: Cents) {
|
||||
pub(crate) fn push_overlapping(&mut self, height_price: Cents) -> Cents {
|
||||
let Self {
|
||||
all,
|
||||
sth,
|
||||
@@ -852,7 +916,7 @@ impl UTXOCohorts<Rw> {
|
||||
}
|
||||
}
|
||||
|
||||
all.metrics.realized.push_accum(&all_acc);
|
||||
let all_capitalized_price = all.metrics.realized.push_accum(&all_acc);
|
||||
sth.metrics.realized.push_accum(&sth_acc);
|
||||
lth.metrics.realized.push_accum(<h_acc);
|
||||
|
||||
@@ -880,6 +944,8 @@ impl UTXOCohorts<Rw> {
|
||||
.unrealized
|
||||
.capitalized_cap_in_loss_raw
|
||||
.push(CentsSquaredSats::new(lth_ccap.1));
|
||||
|
||||
all_capitalized_price
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -50,6 +50,22 @@ impl UTXOCohorts {
|
||||
let lth = self.caches.fenwick.percentiles_lth();
|
||||
push_cost_basis(<h, lth_d, &mut self.lth.metrics.cost_basis);
|
||||
|
||||
let (discount_d, premium_d) = self.caches.fenwick.entry_density(spot_price);
|
||||
|
||||
let discount = self.caches.fenwick.percentiles_discount_entry();
|
||||
push_cost_basis(
|
||||
&discount,
|
||||
discount_d,
|
||||
&mut self.entry.discount.metrics.cost_basis,
|
||||
);
|
||||
|
||||
let premium = self.caches.fenwick.percentiles_premium_entry();
|
||||
push_cost_basis(
|
||||
&premium,
|
||||
premium_d,
|
||||
&mut self.entry.premium.metrics.cost_basis,
|
||||
);
|
||||
|
||||
let prof = self.caches.fenwick.profitability(spot_price);
|
||||
push_profitability(&prof, &mut self.profitability);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use brk_cohort::EntryPrice;
|
||||
use brk_types::{Cents, CostBasisSnapshot, Height, Timestamp};
|
||||
use vecdb::Rw;
|
||||
|
||||
@@ -12,6 +13,7 @@ impl UTXOCohorts<Rw> {
|
||||
/// - The "under_1h" age cohort (all new UTXOs start at 0 hours old)
|
||||
/// - The appropriate epoch cohort based on block height
|
||||
/// - The appropriate class cohort based on block timestamp
|
||||
/// - The immutable entry valuation cohort based on creation price versus anchor
|
||||
/// - The appropriate output type cohort (P2PKH, P2SH, etc.)
|
||||
/// - The appropriate amount range cohort based on value
|
||||
pub(crate) fn receive(
|
||||
@@ -20,13 +22,14 @@ impl UTXOCohorts<Rw> {
|
||||
height: Height,
|
||||
timestamp: Timestamp,
|
||||
price: Cents,
|
||||
entry: EntryPrice,
|
||||
) {
|
||||
let supply_state = received.spendable_supply;
|
||||
|
||||
// Pre-compute snapshot once for the 3 cohorts sharing the same supply_state
|
||||
// Pre-compute snapshot once for cohorts sharing the block-level supply_state
|
||||
let snapshot = CostBasisSnapshot::from_utxo(price, &supply_state);
|
||||
|
||||
// New UTXOs go into under_1h, current epoch, and current class
|
||||
// New UTXOs go into under_1h plus immutable creation cohorts
|
||||
self.age_range
|
||||
.under_1h
|
||||
.state
|
||||
@@ -45,6 +48,12 @@ impl UTXOCohorts<Rw> {
|
||||
.unwrap()
|
||||
.receive_utxo_snapshot(&supply_state, &snapshot);
|
||||
}
|
||||
self.entry
|
||||
.get_mut(entry)
|
||||
.state
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.receive_utxo_snapshot(&supply_state, &snapshot);
|
||||
|
||||
// Update output type cohorts (skip types with no outputs this block)
|
||||
self.type_.iter_typed_mut().for_each(|(output_type, vecs)| {
|
||||
|
||||
@@ -49,7 +49,7 @@ impl UTXOCohorts<Rw> {
|
||||
// This is the max price between receive and send heights
|
||||
let peak_price = price_range_max.max_between(receive_height, send_height);
|
||||
|
||||
// Pre-compute once for age_range, epoch, year (all share sent.spendable_supply)
|
||||
// Pre-compute once for cohorts sharing the sent supply.
|
||||
if let Some(pre) = SendPrecomputed::new(
|
||||
&sent.spendable_supply,
|
||||
current_price,
|
||||
@@ -75,6 +75,12 @@ impl UTXOCohorts<Rw> {
|
||||
.unwrap()
|
||||
.send_utxo_precomputed(&sent.spendable_supply, &pre);
|
||||
}
|
||||
self.entry
|
||||
.get_mut(block_state.entry)
|
||||
.state
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.send_utxo_precomputed(&sent.spendable_supply, &pre);
|
||||
} else if sent.spendable_supply.utxo_count > 0 {
|
||||
// Zero-value UTXOs: just subtract supply
|
||||
self.age_range.get_mut(age).state.as_mut().unwrap().supply -=
|
||||
@@ -85,6 +91,12 @@ impl UTXOCohorts<Rw> {
|
||||
if let Some(v) = self.class.mut_vec_from_timestamp(block_state.timestamp) {
|
||||
v.state.as_mut().unwrap().supply -= &sent.spendable_supply;
|
||||
}
|
||||
self.entry
|
||||
.get_mut(block_state.entry)
|
||||
.state
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.supply -= &sent.spendable_supply;
|
||||
}
|
||||
|
||||
// Update output type cohorts (skip zero-supply entries)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use brk_cohort::ByAddrType;
|
||||
use brk_cohort::{ByAddrType, EntryPrice};
|
||||
use brk_error::Result;
|
||||
use brk_indexer::Indexer;
|
||||
use brk_types::{
|
||||
@@ -46,6 +46,7 @@ pub(crate) fn process_blocks(
|
||||
last_height: Height,
|
||||
chain_state: &mut Vec<BlockState>,
|
||||
tx_index_to_height: &mut RangeMap<TxIndex, Height>,
|
||||
mut entry_anchor: Cents,
|
||||
cached_prices: &[Cents],
|
||||
cached_timestamps: &[Timestamp],
|
||||
cached_price_range_max: &PriceRangeMax,
|
||||
@@ -370,9 +371,14 @@ pub(crate) fn process_blocks(
|
||||
.iterate(Sats::FIFTY_BTC, OutputType::P2PK65);
|
||||
}
|
||||
|
||||
let entry = EntryPrice::from_is_discount(
|
||||
entry_anchor == Cents::ZERO || block_price <= entry_anchor,
|
||||
);
|
||||
|
||||
// Push current block state before processing cohort updates
|
||||
chain_state.push(BlockState {
|
||||
supply: transacted.spendable_supply,
|
||||
entry,
|
||||
price: block_price,
|
||||
timestamp,
|
||||
});
|
||||
@@ -411,7 +417,7 @@ pub(crate) fn process_blocks(
|
||||
|| {
|
||||
// UTXO cohorts receive/send
|
||||
vecs.utxo_cohorts
|
||||
.receive(transacted, height, timestamp, block_price);
|
||||
.receive(transacted, height, timestamp, block_price, entry);
|
||||
if let Some(min_h) =
|
||||
vecs.utxo_cohorts
|
||||
.send(height_to_sent, chain_state, ctx.price_range_max)
|
||||
@@ -460,7 +466,7 @@ pub(crate) fn process_blocks(
|
||||
let is_last_of_day = is_last_of_day[offset];
|
||||
let date_opt = is_last_of_day.then(|| Date::from(timestamp));
|
||||
|
||||
push_cohort_states(
|
||||
entry_anchor = push_cohort_states(
|
||||
&mut vecs.utxo_cohorts,
|
||||
&mut vecs.addr_cohorts,
|
||||
height,
|
||||
@@ -527,7 +533,7 @@ fn push_cohort_states(
|
||||
addr_cohorts: &mut AddrCohorts,
|
||||
height: Height,
|
||||
height_price: Cents,
|
||||
) {
|
||||
) -> Cents {
|
||||
// Phase 1: push + unrealized (no reset yet, states still needed for aggregation)
|
||||
rayon::join(
|
||||
|| {
|
||||
@@ -545,7 +551,7 @@ fn push_cohort_states(
|
||||
);
|
||||
|
||||
// Phase 2: aggregate age_range states → push to overlapping cohorts
|
||||
utxo_cohorts.push_overlapping(height_price);
|
||||
let all_capitalized_price = utxo_cohorts.push_overlapping(height_price);
|
||||
|
||||
// Phase 3: reset per-block values
|
||||
utxo_cohorts
|
||||
@@ -554,4 +560,6 @@ fn push_cohort_states(
|
||||
addr_cohorts
|
||||
.iter_separate_mut()
|
||||
.for_each(|v| v.reset_single_iteration_values());
|
||||
|
||||
all_capitalized_price
|
||||
}
|
||||
|
||||
@@ -206,7 +206,7 @@ impl RealizedFull {
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub(crate) fn push_accum(&mut self, accum: &RealizedFullAccum) {
|
||||
pub(crate) fn push_accum(&mut self, accum: &RealizedFullAccum) -> Cents {
|
||||
self.cap_raw.push(accum.cap_raw);
|
||||
self.capitalized.cap_raw.push(accum.capitalized_cap_raw);
|
||||
|
||||
@@ -221,6 +221,8 @@ impl RealizedFull {
|
||||
self.capitalized.price.cents.height.push(capitalized_price);
|
||||
|
||||
self.peak_regret.value.block.cents.push(accum.peak_regret());
|
||||
|
||||
capitalized_price
|
||||
}
|
||||
|
||||
pub(crate) fn compute_rest_part1(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use std::ops::{Add, AddAssign, SubAssign};
|
||||
|
||||
use brk_cohort::EntryPrice;
|
||||
use brk_types::{Cents, SupplyState, Timestamp};
|
||||
use serde::Serialize;
|
||||
|
||||
@@ -8,6 +9,8 @@ pub struct BlockState {
|
||||
#[serde(flatten)]
|
||||
pub supply: SupplyState,
|
||||
#[serde(skip)]
|
||||
pub entry: EntryPrice,
|
||||
#[serde(skip)]
|
||||
pub price: Cents,
|
||||
#[serde(skip)]
|
||||
pub timestamp: Timestamp,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use brk_cohort::{ByAddrType, Filter};
|
||||
use brk_cohort::{ByAddrType, EntryPrice, Filter};
|
||||
use brk_error::Result;
|
||||
use brk_indexer::Indexer;
|
||||
use brk_traversable::Traversable;
|
||||
@@ -436,13 +436,34 @@ impl Vecs {
|
||||
let end = usize::from(recovered_height);
|
||||
debug!("building supply_state vec for {} heights", recovered_height);
|
||||
let supply_state_data: Vec<_> = self.supply_state.collect_range_at(0, end);
|
||||
let capitalized_price_data: Vec<_> = self
|
||||
.utxo_cohorts
|
||||
.all
|
||||
.metrics
|
||||
.realized
|
||||
.capitalized
|
||||
.price
|
||||
.cents
|
||||
.height
|
||||
.collect_range_at(0, end);
|
||||
|
||||
let mut entry_anchor = Cents::ZERO;
|
||||
chain_state = supply_state_data
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(h, supply)| BlockState {
|
||||
supply,
|
||||
price: self.caches.prices[h],
|
||||
timestamp: self.caches.timestamps[h],
|
||||
.map(|(h, supply)| {
|
||||
let price = self.caches.prices[h];
|
||||
let entry = EntryPrice::from_is_discount(
|
||||
entry_anchor == Cents::ZERO || price <= entry_anchor,
|
||||
);
|
||||
entry_anchor = capitalized_price_data[h];
|
||||
|
||||
BlockState {
|
||||
supply,
|
||||
entry,
|
||||
price,
|
||||
timestamp: self.caches.timestamps[h],
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
debug!("chain_state rebuilt");
|
||||
@@ -474,6 +495,20 @@ impl Vecs {
|
||||
let prices = std::mem::take(&mut self.caches.prices);
|
||||
let timestamps = std::mem::take(&mut self.caches.timestamps);
|
||||
let price_range_max = std::mem::take(&mut self.caches.price_range_max);
|
||||
let entry_anchor = starting_height
|
||||
.decremented()
|
||||
.and_then(|height| {
|
||||
self.utxo_cohorts
|
||||
.all
|
||||
.metrics
|
||||
.realized
|
||||
.capitalized
|
||||
.price
|
||||
.cents
|
||||
.height
|
||||
.collect_one(height)
|
||||
})
|
||||
.unwrap_or(Cents::ZERO);
|
||||
|
||||
process_blocks(
|
||||
self,
|
||||
@@ -486,6 +521,7 @@ impl Vecs {
|
||||
last_height,
|
||||
&mut chain_state,
|
||||
&mut tx_index_to_height,
|
||||
entry_anchor,
|
||||
&prices,
|
||||
×tamps,
|
||||
&price_range_max,
|
||||
|
||||
Reference in New Issue
Block a user