mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-04-24 06:39:58 -07:00
global: snapshot
This commit is contained in:
@@ -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(<h, 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)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user