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
+4 -4
View File
@@ -4470,8 +4470,8 @@ impl SeriesTree_Cointime_Adjusted {
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
Self {
inflation_rate: BpsPercentRatioPattern::new(client.clone(), "cointime_adj_inflation_rate".to_string()),
tx_velocity_native: SeriesPattern1::new(client.clone(), "cointime_adj_tx_velocity".to_string()),
tx_velocity_fiat: SeriesPattern1::new(client.clone(), "cointime_adj_tx_velocity_fiat".to_string()),
tx_velocity_native: SeriesPattern1::new(client.clone(), "cointime_adj_tx_velocity_btc".to_string()),
tx_velocity_fiat: SeriesPattern1::new(client.clone(), "cointime_adj_tx_velocity_usd".to_string()),
}
}
}
@@ -6345,8 +6345,8 @@ pub struct SeriesTree_Supply_Velocity {
impl SeriesTree_Supply_Velocity {
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
Self {
native: SeriesPattern1::new(client.clone(), "velocity".to_string()),
fiat: SeriesPattern1::new(client.clone(), "velocity_fiat".to_string()),
native: SeriesPattern1::new(client.clone(), "velocity_btc".to_string()),
fiat: SeriesPattern1::new(client.clone(), "velocity_usd".to_string()),
}
}
}
+14
View File
@@ -141,4 +141,18 @@ impl<T> Loss<T> {
&mut self._80pct,
]
}
/// Iterate from narrowest (_80pct) to broadest (all), yielding each threshold
/// with a growing suffix slice of `ranges` (1 range, 2 ranges, ..., LOSS_COUNT).
pub fn iter_mut_with_growing_suffix<'a, R>(
&'a mut self,
ranges: &'a [R],
) -> impl Iterator<Item = (&'a mut T, &'a [R])> {
let len = ranges.len();
self.as_array_mut()
.into_iter()
.rev()
.enumerate()
.map(move |(n, threshold)| (threshold, &ranges[len - 1 - n..]))
}
}
+13
View File
@@ -181,4 +181,17 @@ impl<T> Profit<T> {
&mut self._500pct,
]
}
/// Iterate from narrowest (_500pct) to broadest (all), yielding each threshold
/// with a growing prefix slice of `ranges` (1 range, 2 ranges, ..., PROFIT_COUNT).
pub fn iter_mut_with_growing_prefix<'a, R>(
&'a mut self,
ranges: &'a [R],
) -> impl Iterator<Item = (&'a mut T, &'a [R])> {
self.as_array_mut()
.into_iter()
.rev()
.enumerate()
.map(move |(n, threshold)| (threshold, &ranges[..n + 1]))
}
}
+30 -25
View File
@@ -253,32 +253,37 @@ impl<T> ProfitabilityRange<T> {
}
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut T> {
self.iter_mut_with_is_profit().map(|(_, v)| v)
}
/// Iterate mutably, yielding `(is_profit, &mut T)` for each range.
pub fn iter_mut_with_is_profit(&mut self) -> impl Iterator<Item = (bool, &mut T)> {
[
&mut self.over_1000pct_in_profit,
&mut self._500pct_to_1000pct_in_profit,
&mut self._300pct_to_500pct_in_profit,
&mut self._200pct_to_300pct_in_profit,
&mut self._100pct_to_200pct_in_profit,
&mut self._90pct_to_100pct_in_profit,
&mut self._80pct_to_90pct_in_profit,
&mut self._70pct_to_80pct_in_profit,
&mut self._60pct_to_70pct_in_profit,
&mut self._50pct_to_60pct_in_profit,
&mut self._40pct_to_50pct_in_profit,
&mut self._30pct_to_40pct_in_profit,
&mut self._20pct_to_30pct_in_profit,
&mut self._10pct_to_20pct_in_profit,
&mut self._0pct_to_10pct_in_profit,
&mut self._0pct_to_10pct_in_loss,
&mut self._10pct_to_20pct_in_loss,
&mut self._20pct_to_30pct_in_loss,
&mut self._30pct_to_40pct_in_loss,
&mut self._40pct_to_50pct_in_loss,
&mut self._50pct_to_60pct_in_loss,
&mut self._60pct_to_70pct_in_loss,
&mut self._70pct_to_80pct_in_loss,
&mut self._80pct_to_90pct_in_loss,
&mut self._90pct_to_100pct_in_loss,
(true, &mut self.over_1000pct_in_profit),
(true, &mut self._500pct_to_1000pct_in_profit),
(true, &mut self._300pct_to_500pct_in_profit),
(true, &mut self._200pct_to_300pct_in_profit),
(true, &mut self._100pct_to_200pct_in_profit),
(true, &mut self._90pct_to_100pct_in_profit),
(true, &mut self._80pct_to_90pct_in_profit),
(true, &mut self._70pct_to_80pct_in_profit),
(true, &mut self._60pct_to_70pct_in_profit),
(true, &mut self._50pct_to_60pct_in_profit),
(true, &mut self._40pct_to_50pct_in_profit),
(true, &mut self._30pct_to_40pct_in_profit),
(true, &mut self._20pct_to_30pct_in_profit),
(true, &mut self._10pct_to_20pct_in_profit),
(true, &mut self._0pct_to_10pct_in_profit),
(false, &mut self._0pct_to_10pct_in_loss),
(false, &mut self._10pct_to_20pct_in_loss),
(false, &mut self._20pct_to_30pct_in_loss),
(false, &mut self._30pct_to_40pct_in_loss),
(false, &mut self._40pct_to_50pct_in_loss),
(false, &mut self._50pct_to_60pct_in_loss),
(false, &mut self._60pct_to_70pct_in_loss),
(false, &mut self._70pct_to_80pct_in_loss),
(false, &mut self._80pct_to_90pct_in_loss),
(false, &mut self._90pct_to_100pct_in_loss),
]
.into_iter()
}
@@ -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,
)?,
@@ -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),
);
}
}
@@ -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> {
@@ -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)?,
})
}
}
+1 -1
View File
@@ -25,7 +25,7 @@ derive_more = { workspace = true }
jiff = { workspace = true }
parking_lot = { workspace = true }
# quickmatch = { path = "../../../quickmatch" }
quickmatch = "0.3.2"
quickmatch = "0.4.0"
tokio = { workspace = true, optional = true }
serde_json = { workspace = true }
vecdb = { workspace = true }