global: snapshot

This commit is contained in:
nym21
2026-03-22 12:19:06 +01:00
parent f731f0d9d0
commit 514fdc40ee
35 changed files with 732 additions and 745 deletions

View File

@@ -23,13 +23,13 @@ impl Vecs {
)?,
tx_velocity_native: PerBlock::forced_import(
db,
"cointime_adj_tx_velocity",
"cointime_adj_tx_velocity_btc",
version,
indexes,
)?,
tx_velocity_fiat: PerBlock::forced_import(
db,
"cointime_adj_tx_velocity_fiat",
"cointime_adj_tx_velocity_usd",
version,
indexes,
)?,

View File

@@ -1,6 +1,6 @@
use std::{cmp::Reverse, collections::BinaryHeap, fs, path::Path};
use brk_cohort::{Filtered, PROFIT_COUNT, PROFITABILITY_RANGE_COUNT, TERM_NAMES};
use brk_cohort::{Filtered, PROFITABILITY_RANGE_COUNT, TERM_NAMES};
use brk_error::Result;
use brk_types::{BasisPoints16, Cents, CentsCompact, CostBasisDistribution, Date, Dollars, Sats};
@@ -50,7 +50,7 @@ impl UTXOCohorts {
push_cost_basis(&lth, lth_d, &mut self.lth.metrics.cost_basis);
let prof = self.fenwick.profitability(spot_price);
push_profitability(&prof, spot_price, &mut self.profitability);
push_profitability(&prof, &mut self.profitability);
}
/// K-way merge only for writing daily cost basis distributions to disk.
@@ -100,93 +100,22 @@ fn push_cost_basis(percentiles: &PercentileResult, density_bps: u16, cost_basis:
cost_basis.push_density(BasisPoints16::from(density_bps));
}
/// Convert raw (cents × sats) accumulator to Dollars (÷ 100 for cents→dollars, ÷ 1e8 for sats).
#[inline(always)]
fn raw_usd_to_dollars(raw: u128) -> Dollars {
Dollars::from(raw as f64 / 1e10)
}
/// Number of profit ranges (0..=14 are profit, 15..=24 are loss).
const PROFIT_RANGE_COUNT: usize = 15;
/// Compute unrealized P&L from raw sats/usd for a given range.
/// Profit ranges: market_value - cost_basis. Loss ranges: cost_basis - market_value.
#[inline(always)]
fn compute_unrealized_pnl(spot_cents: u128, sats: u64, usd: u128, is_profit: bool) -> Dollars {
let market_value = spot_cents * sats as u128;
let raw = if is_profit {
market_value.saturating_sub(usd)
} else {
usd.saturating_sub(market_value)
};
raw_usd_to_dollars(raw)
}
/// Push profitability range + profit/loss aggregate values to vecs.
fn push_profitability(
buckets: &[ProfitabilityRangeResult; PROFITABILITY_RANGE_COUNT],
spot_price: Cents,
metrics: &mut ProfitabilityMetrics,
) {
let spot_cents = spot_price.as_u128();
// Push 25 range buckets
for (i, bucket) in metrics.range.as_array_mut().into_iter().enumerate() {
let r = &buckets[i];
let is_profit = i < PROFIT_RANGE_COUNT;
bucket.push(
Sats::from(r.all_sats),
Sats::from(r.sth_sats),
raw_usd_to_dollars(r.all_usd),
raw_usd_to_dollars(r.sth_usd),
compute_unrealized_pnl(spot_cents, r.all_sats, r.all_usd, is_profit),
compute_unrealized_pnl(spot_cents, r.sth_sats, r.sth_usd, is_profit),
);
}
// Profit: forward cumulative sum over ranges[0..15], pushed in reverse.
// profit[0] (breakeven) = sum(0..=13), ..., profit[13] (_500pct) = ranges[0]
let profit_arr = metrics.profit.as_array_mut();
let mut cum_sats = 0u64;
let mut cum_sth_sats = 0u64;
let mut cum_usd = 0u128;
let mut cum_sth_usd = 0u128;
for i in 0..PROFIT_COUNT {
cum_sats += buckets[i].all_sats;
cum_sth_sats += buckets[i].sth_sats;
cum_usd += buckets[i].all_usd;
cum_sth_usd += buckets[i].sth_usd;
profit_arr[PROFIT_COUNT - 1 - i].push(
Sats::from(cum_sats),
Sats::from(cum_sth_sats),
raw_usd_to_dollars(cum_usd),
raw_usd_to_dollars(cum_sth_usd),
compute_unrealized_pnl(spot_cents, cum_sats, cum_usd, true),
compute_unrealized_pnl(spot_cents, cum_sth_sats, cum_sth_usd, true),
);
}
// Loss: backward cumulative sum over ranges[15..25], pushed in reverse.
// loss[0] (breakeven) = sum(15..=24), ..., loss[8] (_80pct) = ranges[24]
let loss_arr = metrics.loss.as_array_mut();
let loss_count = loss_arr.len();
cum_sats = 0;
cum_sth_sats = 0;
cum_usd = 0;
cum_sth_usd = 0;
for i in 0..loss_count {
let r = &buckets[PROFITABILITY_RANGE_COUNT - 1 - i];
cum_sats += r.all_sats;
cum_sth_sats += r.sth_sats;
cum_usd += r.all_usd;
cum_sth_usd += r.sth_usd;
loss_arr[loss_count - 1 - i].push(
Sats::from(cum_sats),
Sats::from(cum_sth_sats),
raw_usd_to_dollars(cum_usd),
raw_usd_to_dollars(cum_sth_usd),
compute_unrealized_pnl(spot_cents, cum_sats, cum_usd, false),
compute_unrealized_pnl(spot_cents, cum_sth_sats, cum_sth_usd, false),
);
}
}

View File

@@ -1,7 +1,7 @@
use brk_cohort::{Loss, Profit, ProfitabilityRange};
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{BasisPointsSigned32, Cents, Dollars, Indexes, Sats, Version};
use brk_types::{BasisPointsSigned32, Bitcoin, Cents, Dollars, Indexes, Sats, Version};
use vecdb::{AnyStoredVec, AnyVec, Database, Exit, Rw, StorageMode, WritableVec};
use crate::{
@@ -32,7 +32,6 @@ impl<M: StorageMode> ProfitabilityBucket<M> {
.height
.len()
.min(self.realized_cap.all.height.len())
.min(self.unrealized_pnl.all.height.len())
}
}
@@ -104,21 +103,18 @@ impl ProfitabilityBucket {
sth_supply: Sats,
realized_cap: Dollars,
sth_realized_cap: Dollars,
unrealized_pnl: Dollars,
sth_unrealized_pnl: Dollars,
) {
self.supply.all.sats.height.push(supply);
self.supply.sth.sats.height.push(sth_supply);
self.realized_cap.all.height.push(realized_cap);
self.realized_cap.sth.height.push(sth_realized_cap);
self.unrealized_pnl.all.height.push(unrealized_pnl);
self.unrealized_pnl.sth.height.push(sth_unrealized_pnl);
}
pub(crate) fn compute(
&mut self,
prices: &prices::Vecs,
starting_indexes: &Indexes,
is_profit: bool,
exit: &Exit,
) -> Result<()> {
let max_from = starting_indexes.height;
@@ -126,8 +122,33 @@ impl ProfitabilityBucket {
self.supply.all.compute(prices, max_from, exit)?;
self.supply.sth.compute(prices, max_from, exit)?;
// NUPL = (spot - realized_price) / spot
// where realized_price = realized_cap_cents × ONE_BTC / supply_sats
self.unrealized_pnl.all.height.compute_transform3(
max_from,
&prices.spot.cents.height,
&self.realized_cap.all.height,
&self.supply.all.sats.height,
|(i, spot, cap, supply, ..)| {
let mv = f64::from(Dollars::from(spot)) * f64::from(Bitcoin::from(supply));
let rc = f64::from(cap);
let pnl = if is_profit { mv - rc } else { rc - mv }.max(0.0);
(i, Dollars::from(pnl))
},
exit,
)?;
self.unrealized_pnl.sth.height.compute_transform3(
max_from,
&prices.spot.cents.height,
&self.realized_cap.sth.height,
&self.supply.sth.sats.height,
|(i, spot, cap, supply, ..)| {
let mv = f64::from(Dollars::from(spot)) * f64::from(Bitcoin::from(supply));
let rc = f64::from(cap);
let pnl = if is_profit { mv - rc } else { rc - mv }.max(0.0);
(i, Dollars::from(pnl))
},
exit,
)?;
self.nupl.bps.height.compute_transform3(
max_from,
&prices.spot.cents.height,
@@ -150,6 +171,40 @@ impl ProfitabilityBucket {
Ok(())
}
pub(crate) fn compute_from_ranges(
&mut self,
prices: &prices::Vecs,
starting_indexes: &Indexes,
is_profit: bool,
sources: &[&ProfitabilityBucket],
exit: &Exit,
) -> Result<()> {
let max_from = starting_indexes.height;
self.supply.all.sats.height.compute_sum_of_others(
max_from,
&sources.iter().map(|s| &s.supply.all.sats.height).collect::<Vec<_>>(),
exit,
)?;
self.supply.sth.sats.height.compute_sum_of_others(
max_from,
&sources.iter().map(|s| &s.supply.sth.sats.height).collect::<Vec<_>>(),
exit,
)?;
self.realized_cap.all.height.compute_sum_of_others(
max_from,
&sources.iter().map(|s| &s.realized_cap.all.height).collect::<Vec<_>>(),
exit,
)?;
self.realized_cap.sth.height.compute_sum_of_others(
max_from,
&sources.iter().map(|s| &s.realized_cap.sth.height).collect::<Vec<_>>(),
exit,
)?;
self.compute(prices, starting_indexes, is_profit, exit)
}
pub(crate) fn collect_all_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
vec![
&mut self.supply.all.inner.sats.height as &mut dyn AnyStoredVec,
@@ -189,7 +244,7 @@ impl<M: StorageMode> ProfitabilityMetrics<M> {
}
pub(crate) fn min_stateful_len(&self) -> usize {
self.iter().map(|b| b.min_len()).min().unwrap_or(0)
self.range.iter().map(|b| b.min_len()).min().unwrap_or(0)
}
}
@@ -204,12 +259,14 @@ impl ProfitabilityMetrics {
ProfitabilityBucket::forced_import(db, name, version, indexes, cached_starts)
})?;
let aggregate_version = version + Version::ONE;
let profit = Profit::try_new(|name| {
ProfitabilityBucket::forced_import(db, name, version, indexes, cached_starts)
ProfitabilityBucket::forced_import(db, name, aggregate_version, indexes, cached_starts)
})?;
let loss = Loss::try_new(|name| {
ProfitabilityBucket::forced_import(db, name, version, indexes, cached_starts)
ProfitabilityBucket::forced_import(db, name, aggregate_version, indexes, cached_starts)
})?;
Ok(Self {
@@ -225,8 +282,20 @@ impl ProfitabilityMetrics {
starting_indexes: &Indexes,
exit: &Exit,
) -> Result<()> {
self.iter_mut()
.try_for_each(|b| b.compute(prices, starting_indexes, exit))
for (is_profit, bucket) in self.range.iter_mut_with_is_profit() {
bucket.compute(prices, starting_indexes, is_profit, exit)?;
}
let range_arr = self.range.as_array();
for (threshold, sources) in self.profit.iter_mut_with_growing_prefix(&range_arr) {
threshold.compute_from_ranges(prices, starting_indexes, true, sources, exit)?;
}
for (threshold, sources) in self.loss.iter_mut_with_growing_suffix(&range_arr) {
threshold.compute_from_ranges(prices, starting_indexes, false, sources, exit)?;
}
Ok(())
}
pub(crate) fn collect_all_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {

View File

@@ -12,8 +12,8 @@ impl Vecs {
indexes: &indexes::Vecs,
) -> Result<Self> {
Ok(Self {
native: PerBlock::forced_import(db, "velocity", version, indexes)?,
fiat: PerBlock::forced_import(db, "velocity_fiat", version, indexes)?,
native: PerBlock::forced_import(db, "velocity_btc", version, indexes)?,
fiat: PerBlock::forced_import(db, "velocity_usd", version, indexes)?,
})
}
}