mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-04-30 09:30:00 -07:00
global: snapshot
This commit is contained in:
@@ -1,10 +1,20 @@
|
||||
use std::path::Path;
|
||||
use std::{collections::BTreeMap, path::Path};
|
||||
|
||||
use brk_error::Result;
|
||||
use brk_types::{Age, Cents, CentsSats, CostBasisSnapshot, Height, Sats, SupplyState};
|
||||
use brk_types::{Age, Cents, CentsCompact, CentsSats, CentsSquaredSats, CostBasisSnapshot, Height, Sats, SupplyState};
|
||||
|
||||
use super::super::cost_basis::{CostBasisData, Percentiles, RealizedState, UnrealizedState};
|
||||
|
||||
pub struct SendPrecomputed {
|
||||
pub sats: Sats,
|
||||
pub prev_price: Cents,
|
||||
pub age: Age,
|
||||
pub current_ps: CentsSats,
|
||||
pub prev_ps: CentsSats,
|
||||
pub ath_ps: CentsSats,
|
||||
pub prev_investor_cap: CentsSquaredSats,
|
||||
}
|
||||
|
||||
pub struct CohortState {
|
||||
pub supply: SupplyState,
|
||||
pub realized: RealizedState,
|
||||
@@ -73,10 +83,6 @@ impl CohortState {
|
||||
self.realized.reset_single_iteration_values();
|
||||
}
|
||||
|
||||
pub(crate) fn increment(&mut self, supply: &SupplyState, price: Cents) {
|
||||
self.increment_snapshot(&CostBasisSnapshot::from_utxo(price, supply));
|
||||
}
|
||||
|
||||
pub(crate) fn increment_snapshot(&mut self, s: &CostBasisSnapshot) {
|
||||
self.supply += &s.supply_state;
|
||||
|
||||
@@ -92,10 +98,6 @@ impl CohortState {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn decrement(&mut self, supply: &SupplyState, price: Cents) {
|
||||
self.decrement_snapshot(&CostBasisSnapshot::from_utxo(price, supply));
|
||||
}
|
||||
|
||||
pub(crate) fn decrement_snapshot(&mut self, s: &CostBasisSnapshot) {
|
||||
self.supply -= &s.supply_state;
|
||||
|
||||
@@ -112,19 +114,27 @@ impl CohortState {
|
||||
}
|
||||
|
||||
pub(crate) fn receive_utxo(&mut self, supply: &SupplyState, price: Cents) {
|
||||
self.receive_utxo_snapshot(supply, &CostBasisSnapshot::from_utxo(price, supply));
|
||||
}
|
||||
|
||||
/// Like receive_utxo but takes a pre-computed snapshot to avoid redundant multiplication
|
||||
/// when the same supply/price is used across multiple cohorts.
|
||||
pub(crate) fn receive_utxo_snapshot(
|
||||
&mut self,
|
||||
supply: &SupplyState,
|
||||
snapshot: &CostBasisSnapshot,
|
||||
) {
|
||||
self.supply += supply;
|
||||
|
||||
if supply.value > Sats::ZERO {
|
||||
let sats = supply.value;
|
||||
self.realized.receive(snapshot.realized_price, supply.value);
|
||||
|
||||
// Compute once using typed values
|
||||
let price_sats = CentsSats::from_price_sats(price, sats);
|
||||
let investor_cap = price_sats.to_investor_cap(price);
|
||||
|
||||
self.realized.receive(price, sats);
|
||||
|
||||
self.cost_basis_data
|
||||
.increment(price, sats, price_sats, investor_cap);
|
||||
self.cost_basis_data.increment(
|
||||
snapshot.realized_price,
|
||||
supply.value,
|
||||
snapshot.price_sats,
|
||||
snapshot.investor_cap,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,6 +170,51 @@ impl CohortState {
|
||||
}
|
||||
}
|
||||
|
||||
/// Pre-computed values for send_utxo when the same supply/prices are shared
|
||||
/// across multiple cohorts (age_range, epoch, year).
|
||||
pub(crate) fn precompute_send(
|
||||
supply: &SupplyState,
|
||||
current_price: Cents,
|
||||
prev_price: Cents,
|
||||
ath: Cents,
|
||||
age: Age,
|
||||
) -> Option<SendPrecomputed> {
|
||||
if supply.utxo_count == 0 || supply.value == Sats::ZERO {
|
||||
return None;
|
||||
}
|
||||
let sats = supply.value;
|
||||
let current_ps = CentsSats::from_price_sats(current_price, sats);
|
||||
let prev_ps = CentsSats::from_price_sats(prev_price, sats);
|
||||
let ath_ps = CentsSats::from_price_sats(ath, sats);
|
||||
let prev_investor_cap = prev_ps.to_investor_cap(prev_price);
|
||||
Some(SendPrecomputed {
|
||||
sats,
|
||||
prev_price,
|
||||
age,
|
||||
current_ps,
|
||||
prev_ps,
|
||||
ath_ps,
|
||||
prev_investor_cap,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn send_utxo_precomputed(
|
||||
&mut self,
|
||||
supply: &SupplyState,
|
||||
pre: &SendPrecomputed,
|
||||
) {
|
||||
self.supply -= supply;
|
||||
self.sent += pre.sats;
|
||||
self.satblocks_destroyed += pre.age.satblocks_destroyed(pre.sats);
|
||||
self.satdays_destroyed += pre.age.satdays_destroyed(pre.sats);
|
||||
|
||||
self.realized
|
||||
.send(pre.sats, pre.current_ps, pre.prev_ps, pre.ath_ps, pre.prev_investor_cap);
|
||||
|
||||
self.cost_basis_data
|
||||
.decrement(pre.prev_price, pre.sats, pre.prev_ps, pre.prev_investor_cap);
|
||||
}
|
||||
|
||||
pub(crate) fn send_utxo(
|
||||
&mut self,
|
||||
supply: &SupplyState,
|
||||
@@ -168,33 +223,10 @@ impl CohortState {
|
||||
ath: Cents,
|
||||
age: Age,
|
||||
) {
|
||||
if supply.utxo_count == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
self.supply -= supply;
|
||||
|
||||
if supply.value > Sats::ZERO {
|
||||
self.sent += supply.value;
|
||||
self.satblocks_destroyed += age.satblocks_destroyed(supply.value);
|
||||
self.satdays_destroyed += age.satdays_destroyed(supply.value);
|
||||
|
||||
let cp = current_price;
|
||||
let pp = prev_price;
|
||||
let ath_price = ath;
|
||||
let sats = supply.value;
|
||||
|
||||
// Compute ONCE using typed values
|
||||
let current_ps = CentsSats::from_price_sats(cp, sats);
|
||||
let prev_ps = CentsSats::from_price_sats(pp, sats);
|
||||
let ath_ps = CentsSats::from_price_sats(ath_price, sats);
|
||||
let prev_investor_cap = prev_ps.to_investor_cap(pp);
|
||||
|
||||
self.realized
|
||||
.send(sats, current_ps, prev_ps, ath_ps, prev_investor_cap);
|
||||
|
||||
self.cost_basis_data
|
||||
.decrement(pp, sats, prev_ps, prev_investor_cap);
|
||||
if let Some(pre) = Self::precompute_send(supply, current_price, prev_price, ath, age) {
|
||||
self.send_utxo_precomputed(supply, &pre);
|
||||
} else if supply.utxo_count > 0 {
|
||||
self.supply -= supply;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,7 +295,7 @@ impl CohortState {
|
||||
self.cost_basis_data.write(height, cleanup)
|
||||
}
|
||||
|
||||
pub(crate) fn cost_basis_data_iter(&self) -> impl Iterator<Item = (Cents, &Sats)> {
|
||||
self.cost_basis_data.iter().map(|(k, v)| (k.into(), v))
|
||||
pub(crate) fn cost_basis_map(&self) -> &BTreeMap<CentsCompact, Sats> {
|
||||
self.cost_basis_data.map()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,8 @@ pub struct CostBasisData {
|
||||
percentiles_dirty: bool,
|
||||
cached_percentiles: Option<Percentiles>,
|
||||
rounding_digits: Option<i32>,
|
||||
/// Monotonically increasing counter, bumped on each apply_pending with actual changes.
|
||||
generation: u64,
|
||||
}
|
||||
|
||||
const STATE_TO_KEEP: usize = 10;
|
||||
@@ -49,6 +51,7 @@ impl CostBasisData {
|
||||
percentiles_dirty: true,
|
||||
cached_percentiles: None,
|
||||
rounding_digits: None,
|
||||
generation: 0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,15 +96,9 @@ impl CostBasisData {
|
||||
&& self.pending_raw.investor_cap_dec == CentsSquaredSats::ZERO
|
||||
}
|
||||
|
||||
pub(crate) fn iter(&self) -> impl Iterator<Item = (CentsCompact, &Sats)> {
|
||||
pub(crate) fn map(&self) -> &CostBasisMap {
|
||||
self.assert_pending_empty();
|
||||
self.state
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.base
|
||||
.map
|
||||
.iter()
|
||||
.map(|(&k, v)| (k, v))
|
||||
&self.state.as_ref().unwrap().base.map
|
||||
}
|
||||
|
||||
pub(crate) fn is_empty(&self) -> bool {
|
||||
@@ -183,18 +180,14 @@ impl CostBasisData {
|
||||
}
|
||||
|
||||
pub(crate) fn apply_pending(&mut self) {
|
||||
if !self.pending.is_empty() {
|
||||
self.percentiles_dirty = true;
|
||||
if self.pending.is_empty() && self.pending_raw_is_zero() {
|
||||
return;
|
||||
}
|
||||
self.generation = self.generation.wrapping_add(1);
|
||||
self.percentiles_dirty = true;
|
||||
let map = &mut self.state.as_mut().unwrap().base.map;
|
||||
for (cents, (inc, dec)) in self.pending.drain() {
|
||||
let entry = self
|
||||
.state
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.base
|
||||
.map
|
||||
.entry(cents)
|
||||
.or_default();
|
||||
let entry = map.entry(cents).or_default();
|
||||
*entry += inc;
|
||||
if *entry < dec {
|
||||
panic!(
|
||||
@@ -211,7 +204,7 @@ impl CostBasisData {
|
||||
}
|
||||
*entry -= dec;
|
||||
if *entry == Sats::ZERO {
|
||||
self.state.as_mut().unwrap().base.map.remove(¢s);
|
||||
map.remove(¢s);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -267,7 +260,8 @@ impl CostBasisData {
|
||||
if !self.percentiles_dirty {
|
||||
return self.cached_percentiles;
|
||||
}
|
||||
self.cached_percentiles = Percentiles::compute(self.iter().map(|(k, &v)| (k, v)));
|
||||
self.cached_percentiles =
|
||||
Percentiles::compute_from_map(&self.state.as_ref().unwrap().base.map);
|
||||
self.percentiles_dirty = false;
|
||||
self.cached_percentiles
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use brk_types::{Cents, CentsCompact, Sats};
|
||||
use brk_types::Cents;
|
||||
|
||||
use crate::internal::{PERCENTILES, PERCENTILES_LEN};
|
||||
|
||||
use super::CostBasisMap;
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Percentiles {
|
||||
/// Sat-weighted: percentiles by coin count
|
||||
@@ -11,19 +13,17 @@ pub struct Percentiles {
|
||||
}
|
||||
|
||||
impl Percentiles {
|
||||
/// Compute both sat-weighted and USD-weighted percentiles in a single pass.
|
||||
/// Takes an iterator over (price, sats) pairs, assumed sorted by price ascending.
|
||||
pub(crate) fn compute(iter: impl Iterator<Item = (CentsCompact, Sats)>) -> Option<Self> {
|
||||
// Collect to allow two passes: one for totals, one for percentiles
|
||||
let entries: Vec<_> = iter.collect();
|
||||
if entries.is_empty() {
|
||||
/// Compute both sat-weighted and USD-weighted percentiles in two passes over the BTreeMap.
|
||||
/// Avoids intermediate Vec allocation by iterating the map directly.
|
||||
pub(crate) fn compute_from_map(map: &CostBasisMap) -> Option<Self> {
|
||||
if map.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Compute totals
|
||||
// First pass: compute totals
|
||||
let mut total_sats: u64 = 0;
|
||||
let mut total_usd: u128 = 0;
|
||||
for &(cents, sats) in &entries {
|
||||
for (¢s, &sats) in map.iter() {
|
||||
total_sats += u64::from(sats);
|
||||
total_usd += cents.as_u128() * sats.as_u128();
|
||||
}
|
||||
@@ -32,6 +32,12 @@ impl Percentiles {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Precompute targets to avoid repeated multiplication in the inner loop
|
||||
let sat_targets: [u64; PERCENTILES_LEN] =
|
||||
PERCENTILES.map(|p| total_sats * u64::from(p) / 100);
|
||||
let usd_targets: [u128; PERCENTILES_LEN] =
|
||||
PERCENTILES.map(|p| total_usd * u128::from(p) / 100);
|
||||
|
||||
let mut sat_weighted = [Cents::ZERO; PERCENTILES_LEN];
|
||||
let mut usd_weighted = [Cents::ZERO; PERCENTILES_LEN];
|
||||
let mut cumsum_sats: u64 = 0;
|
||||
@@ -39,20 +45,17 @@ impl Percentiles {
|
||||
let mut sat_idx = 0;
|
||||
let mut usd_idx = 0;
|
||||
|
||||
for (cents, sats) in entries {
|
||||
// Second pass: compute percentiles
|
||||
for (¢s, &sats) in map.iter() {
|
||||
cumsum_sats += u64::from(sats);
|
||||
cumsum_usd += cents.as_u128() * sats.as_u128();
|
||||
|
||||
while sat_idx < PERCENTILES_LEN
|
||||
&& cumsum_sats >= total_sats * u64::from(PERCENTILES[sat_idx]) / 100
|
||||
{
|
||||
while sat_idx < PERCENTILES_LEN && cumsum_sats >= sat_targets[sat_idx] {
|
||||
sat_weighted[sat_idx] = cents.into();
|
||||
sat_idx += 1;
|
||||
}
|
||||
|
||||
while usd_idx < PERCENTILES_LEN
|
||||
&& cumsum_usd >= total_usd * u128::from(PERCENTILES[usd_idx]) / 100
|
||||
{
|
||||
while usd_idx < PERCENTILES_LEN && cumsum_usd >= usd_targets[usd_idx] {
|
||||
usd_weighted[usd_idx] = cents.into();
|
||||
usd_idx += 1;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user