computer: stateful: split common into multiple impl files

This commit is contained in:
nym21
2025-12-05 19:36:40 +01:00
parent cfc5f7633b
commit 554c0e565d
11 changed files with 2695 additions and 2379 deletions

View File

@@ -3,7 +3,7 @@ use brk_traversable::{Traversable, TreeNode};
use brk_types::{Dollars, Height, Version};
use vecdb::{AnyExportableVec, AnyStoredVec, Database, EagerVec, Exit, GenericStoredVec, PcoVec};
use crate::{Indexes, indexes};
use crate::{Indexes, indexes, stateful::Flushable};
use super::{ComputedVecsFromHeight, Source, VecBuilderOptions};
@@ -84,7 +84,10 @@ impl PricePercentiles {
.and_then(|i| self.vecs[i].as_ref())
}
pub fn safe_flush(&mut self, exit: &Exit) -> Result<()> {
}
impl Flushable for PricePercentiles {
fn safe_flush(&mut self, exit: &Exit) -> Result<()> {
for vec in self.vecs.iter_mut().flatten() {
if let Some(height_vec) = vec.height.as_mut() {
height_vec.safe_flush(exit)?;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,914 @@
//! Import and validation methods for Vecs.
//!
//! This module contains methods for:
//! - `forced_import`: Creating a new Vecs instance from database
//! - `import_state`: Importing state when resuming from checkpoint
//! - `validate_computed_versions`: Version validation
//! - `min_height_vecs_len`: Finding minimum vector length
use brk_error::{Error, Result};
use brk_grouper::{CohortContext, Filter};
use brk_types::{DateIndex, Dollars, Height, Sats, StoredF32, StoredF64, Version};
use vecdb::{
AnyVec, Database, EagerVec, GenericStoredVec, ImportableVec, IterableCloneableVec, PcoVec,
StoredVec, TypedVecIterator,
};
use crate::{
grouped::{
ComputedHeightValueVecs, ComputedRatioVecsFromDateIndex, ComputedValueVecsFromDateIndex,
ComputedValueVecsFromHeight, ComputedVecsFromDateIndex, ComputedVecsFromHeight,
PricePercentiles, Source, VecBuilderOptions,
},
indexes, price,
states::CohortState,
utils::OptionExt,
};
use super::Vecs;
impl Vecs {
#[allow(clippy::too_many_arguments)]
pub fn forced_import(
db: &Database,
filter: Filter,
context: CohortContext,
parent_version: Version,
indexes: &indexes::Vecs,
price: Option<&price::Vecs>,
) -> Result<Self> {
let compute_dollars = price.is_some();
let extended = filter.is_extended(context);
let compute_rel_to_all = filter.compute_rel_to_all();
let compute_adjusted = filter.compute_adjusted(context);
let version = parent_version + Version::ZERO;
let name_prefix = filter.to_full_name(context);
let suffix = |s: &str| {
if name_prefix.is_empty() {
s.to_string()
} else {
format!("{name_prefix}_{s}")
}
};
// Helper macros for imports
macro_rules! eager {
($idx:ty, $val:ty, $name:expr, $v:expr) => {
EagerVec::<PcoVec<$idx, $val>>::forced_import(db, &suffix($name), version + $v)
.unwrap()
};
}
macro_rules! computed_h {
($name:expr, $source:expr, $v:expr, $opts:expr $(,)?) => {
ComputedVecsFromHeight::forced_import(
db,
&suffix($name),
$source,
version + $v,
indexes,
$opts,
)
.unwrap()
};
}
macro_rules! computed_di {
($name:expr, $source:expr, $v:expr, $opts:expr $(,)?) => {
ComputedVecsFromDateIndex::forced_import(
db,
&suffix($name),
$source,
version + $v,
indexes,
$opts,
)
.unwrap()
};
}
// Common version patterns
let v0 = Version::ZERO;
let v1 = Version::ONE;
let v2 = Version::TWO;
let v3 = Version::new(3);
let last = || VecBuilderOptions::default().add_last();
let sum = || VecBuilderOptions::default().add_sum();
let sum_cum = || VecBuilderOptions::default().add_sum().add_cumulative();
// Pre-create dateindex vecs that are used in computed vecs
let dateindex_to_supply_in_profit =
compute_dollars.then(|| eager!(DateIndex, Sats, "supply_in_profit", v0));
let dateindex_to_supply_in_loss =
compute_dollars.then(|| eager!(DateIndex, Sats, "supply_in_loss", v0));
let dateindex_to_unrealized_profit =
compute_dollars.then(|| eager!(DateIndex, Dollars, "unrealized_profit", v0));
let dateindex_to_unrealized_loss =
compute_dollars.then(|| eager!(DateIndex, Dollars, "unrealized_loss", v0));
Ok(Self {
filter,
// ==================== SUPPLY & UTXO COUNT ====================
height_to_supply: EagerVec::forced_import(db, &suffix("supply"), version + v0)?,
height_to_supply_value: ComputedHeightValueVecs::forced_import(
db,
&suffix("supply"),
Source::None,
version + v0,
compute_dollars,
)?,
indexes_to_supply: ComputedValueVecsFromDateIndex::forced_import(
db,
&suffix("supply"),
Source::Compute,
version + v1,
last(),
compute_dollars,
indexes,
)?,
height_to_utxo_count: EagerVec::forced_import(db, &suffix("utxo_count"), version + v0)?,
indexes_to_utxo_count: computed_h!("utxo_count", Source::None, v0, last()),
height_to_supply_half_value: ComputedHeightValueVecs::forced_import(
db,
&suffix("supply_half"),
Source::Compute,
version + v0,
compute_dollars,
)?,
indexes_to_supply_half: ComputedValueVecsFromDateIndex::forced_import(
db,
&suffix("supply_half"),
Source::Compute,
version + v0,
last(),
compute_dollars,
indexes,
)?,
// ==================== ACTIVITY ====================
height_to_sent: EagerVec::forced_import(db, &suffix("sent"), version + v0)?,
indexes_to_sent: ComputedValueVecsFromHeight::forced_import(
db,
&suffix("sent"),
Source::Compute,
version + v0,
sum(),
compute_dollars,
indexes,
)?,
height_to_satblocks_destroyed: EagerVec::forced_import(
db,
&suffix("satblocks_destroyed"),
version + v0,
)?,
height_to_satdays_destroyed: EagerVec::forced_import(
db,
&suffix("satdays_destroyed"),
version + v0,
)?,
indexes_to_coinblocks_destroyed: computed_h!(
"coinblocks_destroyed",
Source::Compute,
v2,
sum_cum(),
),
indexes_to_coindays_destroyed: computed_h!(
"coindays_destroyed",
Source::Compute,
v2,
sum_cum(),
),
// ==================== REALIZED CAP & PRICE ====================
height_to_realized_cap: compute_dollars
.then(|| eager!(Height, Dollars, "realized_cap", v0)),
indexes_to_realized_cap: compute_dollars
.then(|| computed_h!("realized_cap", Source::None, v0, last())),
indexes_to_realized_price: compute_dollars
.then(|| computed_h!("realized_price", Source::Compute, v0, last())),
indexes_to_realized_price_extra: compute_dollars.then(|| {
ComputedRatioVecsFromDateIndex::forced_import(
db,
&suffix("realized_price"),
Source::None,
version + v0,
indexes,
extended,
)
.unwrap()
}),
indexes_to_realized_cap_rel_to_own_market_cap: (compute_dollars && extended).then(
|| {
computed_h!(
"realized_cap_rel_to_own_market_cap",
Source::Compute,
v0,
last()
)
},
),
indexes_to_realized_cap_30d_delta: compute_dollars
.then(|| computed_di!("realized_cap_30d_delta", Source::Compute, v0, last())),
// ==================== REALIZED PROFIT & LOSS ====================
height_to_realized_profit: compute_dollars
.then(|| eager!(Height, Dollars, "realized_profit", v0)),
indexes_to_realized_profit: compute_dollars
.then(|| computed_h!("realized_profit", Source::None, v0, sum_cum())),
height_to_realized_loss: compute_dollars
.then(|| eager!(Height, Dollars, "realized_loss", v0)),
indexes_to_realized_loss: compute_dollars
.then(|| computed_h!("realized_loss", Source::None, v0, sum_cum())),
indexes_to_neg_realized_loss: compute_dollars
.then(|| computed_h!("neg_realized_loss", Source::Compute, v1, sum_cum())),
indexes_to_net_realized_pnl: compute_dollars
.then(|| computed_h!("net_realized_pnl", Source::Compute, v0, sum_cum())),
indexes_to_realized_value: compute_dollars
.then(|| computed_h!("realized_value", Source::Compute, v0, sum())),
indexes_to_realized_profit_rel_to_realized_cap: compute_dollars.then(|| {
computed_h!(
"realized_profit_rel_to_realized_cap",
Source::Compute,
v0,
sum()
)
}),
indexes_to_realized_loss_rel_to_realized_cap: compute_dollars.then(|| {
computed_h!(
"realized_loss_rel_to_realized_cap",
Source::Compute,
v0,
sum()
)
}),
indexes_to_net_realized_pnl_rel_to_realized_cap: compute_dollars.then(|| {
computed_h!(
"net_realized_pnl_rel_to_realized_cap",
Source::Compute,
v1,
sum()
)
}),
height_to_total_realized_pnl: compute_dollars
.then(|| eager!(Height, Dollars, "total_realized_pnl", v0)),
indexes_to_total_realized_pnl: compute_dollars
.then(|| computed_di!("total_realized_pnl", Source::Compute, v1, sum())),
dateindex_to_realized_profit_to_loss_ratio: (compute_dollars && extended)
.then(|| eager!(DateIndex, StoredF64, "realized_profit_to_loss_ratio", v1)),
// ==================== VALUE CREATED & DESTROYED ====================
height_to_value_created: compute_dollars
.then(|| eager!(Height, Dollars, "value_created", v0)),
indexes_to_value_created: compute_dollars
.then(|| computed_h!("value_created", Source::None, v0, sum())),
height_to_value_destroyed: compute_dollars
.then(|| eager!(Height, Dollars, "value_destroyed", v0)),
indexes_to_value_destroyed: compute_dollars
.then(|| computed_h!("value_destroyed", Source::None, v0, sum())),
height_to_adjusted_value_created: (compute_dollars && compute_adjusted)
.then(|| eager!(Height, Dollars, "adjusted_value_created", v0)),
indexes_to_adjusted_value_created: (compute_dollars && compute_adjusted)
.then(|| computed_h!("adjusted_value_created", Source::None, v0, sum())),
height_to_adjusted_value_destroyed: (compute_dollars && compute_adjusted)
.then(|| eager!(Height, Dollars, "adjusted_value_destroyed", v0)),
indexes_to_adjusted_value_destroyed: (compute_dollars && compute_adjusted)
.then(|| computed_h!("adjusted_value_destroyed", Source::None, v0, sum())),
// ==================== SOPR ====================
dateindex_to_sopr: compute_dollars.then(|| eager!(DateIndex, StoredF64, "sopr", v1)),
dateindex_to_sopr_7d_ema: compute_dollars
.then(|| eager!(DateIndex, StoredF64, "sopr_7d_ema", v1)),
dateindex_to_sopr_30d_ema: compute_dollars
.then(|| eager!(DateIndex, StoredF64, "sopr_30d_ema", v1)),
dateindex_to_adjusted_sopr: (compute_dollars && compute_adjusted)
.then(|| eager!(DateIndex, StoredF64, "adjusted_sopr", v1)),
dateindex_to_adjusted_sopr_7d_ema: (compute_dollars && compute_adjusted)
.then(|| eager!(DateIndex, StoredF64, "adjusted_sopr_7d_ema", v1)),
dateindex_to_adjusted_sopr_30d_ema: (compute_dollars && compute_adjusted)
.then(|| eager!(DateIndex, StoredF64, "adjusted_sopr_30d_ema", v1)),
// ==================== SELL SIDE RISK ====================
dateindex_to_sell_side_risk_ratio: compute_dollars
.then(|| eager!(DateIndex, StoredF32, "sell_side_risk_ratio", v1)),
dateindex_to_sell_side_risk_ratio_7d_ema: compute_dollars
.then(|| eager!(DateIndex, StoredF32, "sell_side_risk_ratio_7d_ema", v1)),
dateindex_to_sell_side_risk_ratio_30d_ema: compute_dollars
.then(|| eager!(DateIndex, StoredF32, "sell_side_risk_ratio_30d_ema", v1)),
// ==================== SUPPLY IN PROFIT/LOSS ====================
height_to_supply_in_profit: compute_dollars
.then(|| eager!(Height, Sats, "supply_in_profit", v0)),
indexes_to_supply_in_profit: compute_dollars.then(|| {
ComputedValueVecsFromDateIndex::forced_import(
db,
&suffix("supply_in_profit"),
dateindex_to_supply_in_profit
.as_ref()
.map(|v| v.boxed_clone())
.into(),
version + v0,
last(),
compute_dollars,
indexes,
)
.unwrap()
}),
height_to_supply_in_loss: compute_dollars
.then(|| eager!(Height, Sats, "supply_in_loss", v0)),
indexes_to_supply_in_loss: compute_dollars.then(|| {
ComputedValueVecsFromDateIndex::forced_import(
db,
&suffix("supply_in_loss"),
dateindex_to_supply_in_loss
.as_ref()
.map(|v| v.boxed_clone())
.into(),
version + v0,
last(),
compute_dollars,
indexes,
)
.unwrap()
}),
dateindex_to_supply_in_profit,
dateindex_to_supply_in_loss,
height_to_supply_in_profit_value: compute_dollars.then(|| {
ComputedHeightValueVecs::forced_import(
db,
&suffix("supply_in_profit"),
Source::None,
version + v0,
compute_dollars,
)
.unwrap()
}),
height_to_supply_in_loss_value: compute_dollars.then(|| {
ComputedHeightValueVecs::forced_import(
db,
&suffix("supply_in_loss"),
Source::None,
version + v0,
compute_dollars,
)
.unwrap()
}),
// ==================== UNREALIZED PROFIT & LOSS ====================
height_to_unrealized_profit: compute_dollars
.then(|| eager!(Height, Dollars, "unrealized_profit", v0)),
indexes_to_unrealized_profit: compute_dollars.then(|| {
ComputedVecsFromDateIndex::forced_import(
db,
&suffix("unrealized_profit"),
dateindex_to_unrealized_profit
.as_ref()
.map(|v| v.boxed_clone())
.into(),
version + v0,
indexes,
last(),
)
.unwrap()
}),
height_to_unrealized_loss: compute_dollars
.then(|| eager!(Height, Dollars, "unrealized_loss", v0)),
indexes_to_unrealized_loss: compute_dollars.then(|| {
ComputedVecsFromDateIndex::forced_import(
db,
&suffix("unrealized_loss"),
dateindex_to_unrealized_loss
.as_ref()
.map(|v| v.boxed_clone())
.into(),
version + v0,
indexes,
last(),
)
.unwrap()
}),
dateindex_to_unrealized_profit,
dateindex_to_unrealized_loss,
height_to_neg_unrealized_loss: compute_dollars
.then(|| eager!(Height, Dollars, "neg_unrealized_loss", v0)),
indexes_to_neg_unrealized_loss: compute_dollars
.then(|| computed_di!("neg_unrealized_loss", Source::Compute, v0, last())),
height_to_net_unrealized_pnl: compute_dollars
.then(|| eager!(Height, Dollars, "net_unrealized_pnl", v0)),
indexes_to_net_unrealized_pnl: compute_dollars
.then(|| computed_di!("net_unrealized_pnl", Source::Compute, v0, last())),
height_to_total_unrealized_pnl: compute_dollars
.then(|| eager!(Height, Dollars, "total_unrealized_pnl", v0)),
indexes_to_total_unrealized_pnl: compute_dollars
.then(|| computed_di!("total_unrealized_pnl", Source::Compute, v0, last())),
// ==================== PRICE PAID ====================
height_to_min_price_paid: compute_dollars
.then(|| eager!(Height, Dollars, "min_price_paid", v0)),
indexes_to_min_price_paid: compute_dollars
.then(|| computed_h!("min_price_paid", Source::None, v0, last())),
height_to_max_price_paid: compute_dollars
.then(|| eager!(Height, Dollars, "max_price_paid", v0)),
indexes_to_max_price_paid: compute_dollars
.then(|| computed_h!("max_price_paid", Source::None, v0, last())),
price_percentiles: (compute_dollars && extended).then(|| {
PricePercentiles::forced_import(db, &suffix(""), version + v0, indexes, true)
.unwrap()
}),
// ==================== RELATIVE METRICS: UNREALIZED vs MARKET CAP ====================
height_to_unrealized_profit_rel_to_market_cap: compute_dollars
.then(|| eager!(Height, StoredF32, "unrealized_profit_rel_to_market_cap", v0)),
height_to_unrealized_loss_rel_to_market_cap: compute_dollars
.then(|| eager!(Height, StoredF32, "unrealized_loss_rel_to_market_cap", v0)),
height_to_neg_unrealized_loss_rel_to_market_cap: compute_dollars.then(|| {
eager!(
Height,
StoredF32,
"neg_unrealized_loss_rel_to_market_cap",
v0
)
}),
height_to_net_unrealized_pnl_rel_to_market_cap: compute_dollars.then(|| {
eager!(
Height,
StoredF32,
"net_unrealized_pnl_rel_to_market_cap",
v1
)
}),
indexes_to_unrealized_profit_rel_to_market_cap: compute_dollars.then(|| {
computed_di!(
"unrealized_profit_rel_to_market_cap",
Source::Compute,
v1,
last()
)
}),
indexes_to_unrealized_loss_rel_to_market_cap: compute_dollars.then(|| {
computed_di!(
"unrealized_loss_rel_to_market_cap",
Source::Compute,
v1,
last()
)
}),
indexes_to_neg_unrealized_loss_rel_to_market_cap: compute_dollars.then(|| {
computed_di!(
"neg_unrealized_loss_rel_to_market_cap",
Source::Compute,
v1,
last()
)
}),
indexes_to_net_unrealized_pnl_rel_to_market_cap: compute_dollars.then(|| {
computed_di!(
"net_unrealized_pnl_rel_to_market_cap",
Source::Compute,
v1,
last()
)
}),
// ==================== RELATIVE METRICS: UNREALIZED vs OWN MARKET CAP ====================
height_to_unrealized_profit_rel_to_own_market_cap: (compute_dollars
&& extended
&& compute_rel_to_all)
.then(|| {
eager!(
Height,
StoredF32,
"unrealized_profit_rel_to_own_market_cap",
v1
)
}),
height_to_unrealized_loss_rel_to_own_market_cap: (compute_dollars
&& extended
&& compute_rel_to_all)
.then(|| {
eager!(
Height,
StoredF32,
"unrealized_loss_rel_to_own_market_cap",
v1
)
}),
height_to_neg_unrealized_loss_rel_to_own_market_cap: (compute_dollars
&& extended
&& compute_rel_to_all)
.then(|| {
eager!(
Height,
StoredF32,
"neg_unrealized_loss_rel_to_own_market_cap",
v1
)
}),
height_to_net_unrealized_pnl_rel_to_own_market_cap: (compute_dollars
&& extended
&& compute_rel_to_all)
.then(|| {
eager!(
Height,
StoredF32,
"net_unrealized_pnl_rel_to_own_market_cap",
v2
)
}),
indexes_to_unrealized_profit_rel_to_own_market_cap: (compute_dollars
&& extended
&& compute_rel_to_all)
.then(|| {
computed_di!(
"unrealized_profit_rel_to_own_market_cap",
Source::Compute,
v2,
last()
)
}),
indexes_to_unrealized_loss_rel_to_own_market_cap: (compute_dollars
&& extended
&& compute_rel_to_all)
.then(|| {
computed_di!(
"unrealized_loss_rel_to_own_market_cap",
Source::Compute,
v2,
last()
)
}),
indexes_to_neg_unrealized_loss_rel_to_own_market_cap: (compute_dollars
&& extended
&& compute_rel_to_all)
.then(|| {
computed_di!(
"neg_unrealized_loss_rel_to_own_market_cap",
Source::Compute,
v2,
last()
)
}),
indexes_to_net_unrealized_pnl_rel_to_own_market_cap: (compute_dollars
&& extended
&& compute_rel_to_all)
.then(|| {
computed_di!(
"net_unrealized_pnl_rel_to_own_market_cap",
Source::Compute,
v2,
last()
)
}),
// ==================== RELATIVE METRICS: UNREALIZED vs OWN TOTAL UNREALIZED ====================
height_to_unrealized_profit_rel_to_own_total_unrealized_pnl: (compute_dollars
&& extended)
.then(|| {
eager!(
Height,
StoredF32,
"unrealized_profit_rel_to_own_total_unrealized_pnl",
v0
)
}),
height_to_unrealized_loss_rel_to_own_total_unrealized_pnl: (compute_dollars
&& extended)
.then(|| {
eager!(
Height,
StoredF32,
"unrealized_loss_rel_to_own_total_unrealized_pnl",
v0
)
}),
height_to_neg_unrealized_loss_rel_to_own_total_unrealized_pnl: (compute_dollars
&& extended)
.then(|| {
eager!(
Height,
StoredF32,
"neg_unrealized_loss_rel_to_own_total_unrealized_pnl",
v0
)
}),
height_to_net_unrealized_pnl_rel_to_own_total_unrealized_pnl: (compute_dollars
&& extended)
.then(|| {
eager!(
Height,
StoredF32,
"net_unrealized_pnl_rel_to_own_total_unrealized_pnl",
v1
)
}),
indexes_to_unrealized_profit_rel_to_own_total_unrealized_pnl: (compute_dollars
&& extended)
.then(|| {
computed_di!(
"unrealized_profit_rel_to_own_total_unrealized_pnl",
Source::Compute,
v1,
last()
)
}),
indexes_to_unrealized_loss_rel_to_own_total_unrealized_pnl: (compute_dollars
&& extended)
.then(|| {
computed_di!(
"unrealized_loss_rel_to_own_total_unrealized_pnl",
Source::Compute,
v1,
last()
)
}),
indexes_to_neg_unrealized_loss_rel_to_own_total_unrealized_pnl: (compute_dollars
&& extended)
.then(|| {
computed_di!(
"neg_unrealized_loss_rel_to_own_total_unrealized_pnl",
Source::Compute,
v1,
last()
)
}),
indexes_to_net_unrealized_pnl_rel_to_own_total_unrealized_pnl: (compute_dollars
&& extended)
.then(|| {
computed_di!(
"net_unrealized_pnl_rel_to_own_total_unrealized_pnl",
Source::Compute,
v1,
last()
)
}),
// ==================== RELATIVE METRICS: SUPPLY vs CIRCULATING/OWN ====================
indexes_to_supply_rel_to_circulating_supply: compute_rel_to_all.then(|| {
computed_h!(
"supply_rel_to_circulating_supply",
Source::Compute,
v1,
last()
)
}),
height_to_supply_in_profit_rel_to_own_supply: compute_dollars
.then(|| eager!(Height, StoredF64, "supply_in_profit_rel_to_own_supply", v1)),
height_to_supply_in_loss_rel_to_own_supply: compute_dollars
.then(|| eager!(Height, StoredF64, "supply_in_loss_rel_to_own_supply", v1)),
indexes_to_supply_in_profit_rel_to_own_supply: compute_dollars.then(|| {
computed_di!(
"supply_in_profit_rel_to_own_supply",
Source::Compute,
v1,
last()
)
}),
indexes_to_supply_in_loss_rel_to_own_supply: compute_dollars.then(|| {
computed_di!(
"supply_in_loss_rel_to_own_supply",
Source::Compute,
v1,
last()
)
}),
height_to_supply_in_profit_rel_to_circulating_supply: (compute_rel_to_all
&& compute_dollars)
.then(|| {
eager!(
Height,
StoredF64,
"supply_in_profit_rel_to_circulating_supply",
v1
)
}),
height_to_supply_in_loss_rel_to_circulating_supply: (compute_rel_to_all
&& compute_dollars)
.then(|| {
eager!(
Height,
StoredF64,
"supply_in_loss_rel_to_circulating_supply",
v1
)
}),
indexes_to_supply_in_profit_rel_to_circulating_supply: (compute_rel_to_all
&& compute_dollars)
.then(|| {
computed_di!(
"supply_in_profit_rel_to_circulating_supply",
Source::Compute,
v1,
last()
)
}),
indexes_to_supply_in_loss_rel_to_circulating_supply: (compute_rel_to_all
&& compute_dollars)
.then(|| {
computed_di!(
"supply_in_loss_rel_to_circulating_supply",
Source::Compute,
v1,
last()
)
}),
// ==================== NET REALIZED PNL DELTAS ====================
indexes_to_net_realized_pnl_cumulative_30d_delta: compute_dollars.then(|| {
computed_di!(
"net_realized_pnl_cumulative_30d_delta",
Source::Compute,
v3,
last()
)
}),
indexes_to_net_realized_pnl_cumulative_30d_delta_rel_to_realized_cap: compute_dollars
.then(|| {
computed_di!(
"net_realized_pnl_cumulative_30d_delta_rel_to_realized_cap",
Source::Compute,
v3,
last()
)
}),
indexes_to_net_realized_pnl_cumulative_30d_delta_rel_to_market_cap: compute_dollars
.then(|| {
computed_di!(
"net_realized_pnl_cumulative_30d_delta_rel_to_market_cap",
Source::Compute,
v3,
last()
)
}),
})
}
/// Returns the minimum length of all height-indexed vectors.
/// Used to determine the starting point for processing.
pub fn min_height_vecs_len(&self) -> usize {
[
self.height_to_supply.len(),
self.height_to_utxo_count.len(),
self.height_to_realized_cap
.as_ref()
.map_or(usize::MAX, |v| v.len()),
self.height_to_realized_profit
.as_ref()
.map_or(usize::MAX, |v| v.len()),
self.height_to_realized_loss
.as_ref()
.map_or(usize::MAX, |v| v.len()),
self.height_to_value_created
.as_ref()
.map_or(usize::MAX, |v| v.len()),
self.height_to_adjusted_value_created
.as_ref()
.map_or(usize::MAX, |v| v.len()),
self.height_to_value_destroyed
.as_ref()
.map_or(usize::MAX, |v| v.len()),
self.height_to_adjusted_value_destroyed
.as_ref()
.map_or(usize::MAX, |v| v.len()),
self.height_to_supply_in_profit
.as_ref()
.map_or(usize::MAX, |v| v.len()),
self.height_to_supply_in_loss
.as_ref()
.map_or(usize::MAX, |v| v.len()),
self.height_to_unrealized_profit
.as_ref()
.map_or(usize::MAX, |v| v.len()),
self.height_to_unrealized_loss
.as_ref()
.map_or(usize::MAX, |v| v.len()),
self.height_to_min_price_paid
.as_ref()
.map_or(usize::MAX, |v| v.len()),
self.height_to_max_price_paid
.as_ref()
.map_or(usize::MAX, |v| v.len()),
self.height_to_sent.len(),
self.height_to_satdays_destroyed.len(),
self.height_to_satblocks_destroyed.len(),
]
.into_iter()
.min()
.unwrap()
}
/// Import state from a checkpoint when resuming processing.
/// Returns the next height to process from.
pub fn import_state(
&mut self,
starting_height: Height,
state: &mut CohortState,
) -> Result<Height> {
if let Some(mut prev_height) = starting_height.decremented() {
if self.height_to_realized_cap.as_mut().is_some() {
prev_height = state.import_at_or_before(prev_height)?;
}
state.supply.value = self.height_to_supply.into_iter().get_unwrap(prev_height);
state.supply.utxo_count = *self
.height_to_utxo_count
.into_iter()
.get_unwrap(prev_height);
if let Some(height_to_realized_cap) = self.height_to_realized_cap.as_mut() {
state.realized.um().cap =
height_to_realized_cap.into_iter().get_unwrap(prev_height);
}
Ok(prev_height.incremented())
} else {
Err(Error::Str("Unset"))
}
}
/// Validate that all computed versions match expected values, resetting if needed.
pub fn validate_computed_versions(&mut self, base_version: Version) -> Result<()> {
// Always-present vecs
self.height_to_supply.validate_computed_version_or_reset(
base_version + self.height_to_supply.inner_version(),
)?;
self.height_to_utxo_count
.validate_computed_version_or_reset(
base_version + self.height_to_utxo_count.inner_version(),
)?;
self.height_to_sent.validate_computed_version_or_reset(
base_version + self.height_to_sent.inner_version(),
)?;
self.height_to_satblocks_destroyed
.validate_computed_version_or_reset(
base_version + self.height_to_satblocks_destroyed.inner_version(),
)?;
self.height_to_satdays_destroyed
.validate_computed_version_or_reset(
base_version + self.height_to_satdays_destroyed.inner_version(),
)?;
// Dollar-dependent vecs
if let Some(height_to_realized_cap) = self.height_to_realized_cap.as_mut().as_mut() {
height_to_realized_cap.validate_computed_version_or_reset(
base_version + height_to_realized_cap.inner_version(),
)?;
Self::validate_optional_vec_version(&mut self.height_to_realized_profit, base_version)?;
Self::validate_optional_vec_version(&mut self.height_to_realized_loss, base_version)?;
Self::validate_optional_vec_version(&mut self.height_to_value_created, base_version)?;
Self::validate_optional_vec_version(&mut self.height_to_value_destroyed, base_version)?;
Self::validate_optional_vec_version(
&mut self.height_to_supply_in_profit,
base_version,
)?;
Self::validate_optional_vec_version(&mut self.height_to_supply_in_loss, base_version)?;
Self::validate_optional_vec_version(
&mut self.height_to_unrealized_profit,
base_version,
)?;
Self::validate_optional_vec_version(&mut self.height_to_unrealized_loss, base_version)?;
Self::validate_optional_vec_version(
&mut self.dateindex_to_supply_in_profit,
base_version,
)?;
Self::validate_optional_vec_version(
&mut self.dateindex_to_supply_in_loss,
base_version,
)?;
Self::validate_optional_vec_version(
&mut self.dateindex_to_unrealized_profit,
base_version,
)?;
Self::validate_optional_vec_version(
&mut self.dateindex_to_unrealized_loss,
base_version,
)?;
Self::validate_optional_vec_version(&mut self.height_to_min_price_paid, base_version)?;
Self::validate_optional_vec_version(&mut self.height_to_max_price_paid, base_version)?;
if self.height_to_adjusted_value_created.is_some() {
Self::validate_optional_vec_version(
&mut self.height_to_adjusted_value_created,
base_version,
)?;
Self::validate_optional_vec_version(
&mut self.height_to_adjusted_value_destroyed,
base_version,
)?;
}
}
Ok(())
}
/// Helper to validate an optional vec's version.
fn validate_optional_vec_version<V: StoredVec>(
vec: &mut Option<EagerVec<V>>,
base_version: Version,
) -> Result<()> {
if let Some(v) = vec.as_mut() {
v.validate_computed_version_or_reset(base_version + v.inner_version())?;
}
Ok(())
}
}

View File

@@ -0,0 +1,19 @@
//! Common vector structs and logic shared between UTXO and Address cohorts.
//!
//! This module contains the `Vecs` struct which holds all the computed vectors
//! for a single cohort, along with methods for importing, flushing, and computing.
//!
//! ## Module Organization
//!
//! The implementation is split across multiple files for maintainability:
//! - `vecs.rs`: Struct definition with field documentation
//! - `import.rs`: Import, validation, and initialization methods
//! - `push.rs`: Per-block push and flush methods
//! - `compute.rs`: Post-processing computation methods
mod compute;
mod import;
mod push;
mod vecs;
pub use vecs::Vecs;

View File

@@ -0,0 +1,178 @@
//! Push and flush methods for Vecs.
//!
//! This module contains methods for:
//! - `truncate_push`: Push state values to height-indexed vectors
//! - `compute_then_truncate_push_unrealized_states`: Compute and push unrealized states
//! - `safe_flush_stateful_vecs`: Safely flush all stateful vectors
use brk_error::Result;
use brk_types::{DateIndex, Dollars, Height, StoredU64};
use vecdb::{AnyStoredVec, Exit, GenericStoredVec};
use crate::{stateful::Flushable, states::CohortState, utils::OptionExt};
use super::Vecs;
impl Vecs {
pub fn truncate_push(&mut self, height: Height, state: &CohortState) -> Result<()> {
self.height_to_supply
.truncate_push(height, state.supply.value)?;
self.height_to_utxo_count
.truncate_push(height, StoredU64::from(state.supply.utxo_count))?;
self.height_to_sent.truncate_push(height, state.sent)?;
self.height_to_satblocks_destroyed
.truncate_push(height, state.satblocks_destroyed)?;
self.height_to_satdays_destroyed
.truncate_push(height, state.satdays_destroyed)?;
if let Some(height_to_realized_cap) = self.height_to_realized_cap.as_mut() {
let realized = state.realized.as_ref().unwrap_or_else(|| {
dbg!((&state.realized, &state.supply));
panic!();
});
height_to_realized_cap.truncate_push(height, realized.cap)?;
self.height_to_realized_profit
.um()
.truncate_push(height, realized.profit)?;
self.height_to_realized_loss
.um()
.truncate_push(height, realized.loss)?;
self.height_to_value_created
.um()
.truncate_push(height, realized.value_created)?;
self.height_to_value_destroyed
.um()
.truncate_push(height, realized.value_destroyed)?;
if self.height_to_adjusted_value_created.is_some() {
self.height_to_adjusted_value_created
.um()
.truncate_push(height, realized.adj_value_created)?;
self.height_to_adjusted_value_destroyed
.um()
.truncate_push(height, realized.adj_value_destroyed)?;
}
}
Ok(())
}
pub fn compute_then_truncate_push_unrealized_states(
&mut self,
height: Height,
height_price: Option<Dollars>,
dateindex: Option<DateIndex>,
date_price: Option<Option<Dollars>>,
state: &CohortState,
) -> Result<()> {
if let Some(height_price) = height_price {
self.height_to_min_price_paid.um().truncate_push(
height,
state
.price_to_amount_first_key_value()
.map(|(&dollars, _)| dollars)
.unwrap_or(Dollars::NAN),
)?;
self.height_to_max_price_paid.um().truncate_push(
height,
state
.price_to_amount_last_key_value()
.map(|(&dollars, _)| dollars)
.unwrap_or(Dollars::NAN),
)?;
let (height_unrealized_state, date_unrealized_state) =
state.compute_unrealized_states(height_price, date_price.unwrap());
self.height_to_supply_in_profit
.um()
.truncate_push(height, height_unrealized_state.supply_in_profit)?;
self.height_to_supply_in_loss
.um()
.truncate_push(height, height_unrealized_state.supply_in_loss)?;
self.height_to_unrealized_profit
.um()
.truncate_push(height, height_unrealized_state.unrealized_profit)?;
self.height_to_unrealized_loss
.um()
.truncate_push(height, height_unrealized_state.unrealized_loss)?;
if let Some(date_unrealized_state) = date_unrealized_state {
let dateindex = dateindex.unwrap();
self.dateindex_to_supply_in_profit
.um()
.truncate_push(dateindex, date_unrealized_state.supply_in_profit)?;
self.dateindex_to_supply_in_loss
.um()
.truncate_push(dateindex, date_unrealized_state.supply_in_loss)?;
self.dateindex_to_unrealized_profit
.um()
.truncate_push(dateindex, date_unrealized_state.unrealized_profit)?;
self.dateindex_to_unrealized_loss
.um()
.truncate_push(dateindex, date_unrealized_state.unrealized_loss)?;
}
// Compute and push price percentiles
if let Some(price_percentiles) = self.price_percentiles.as_mut() {
let percentile_prices = state.compute_percentile_prices();
price_percentiles.truncate_push(height, &percentile_prices)?;
}
}
Ok(())
}
pub fn safe_flush_stateful_vecs(
&mut self,
height: Height,
exit: &Exit,
state: &mut CohortState,
) -> Result<()> {
self.height_to_supply.safe_flush(exit)?;
self.height_to_utxo_count.safe_flush(exit)?;
self.height_to_sent.safe_flush(exit)?;
self.height_to_satdays_destroyed.safe_flush(exit)?;
self.height_to_satblocks_destroyed.safe_flush(exit)?;
if let Some(height_to_realized_cap) = self.height_to_realized_cap.as_mut() {
height_to_realized_cap.safe_flush(exit)?;
self.height_to_realized_profit.um().safe_flush(exit)?;
self.height_to_realized_loss.um().safe_flush(exit)?;
self.height_to_value_created.um().safe_flush(exit)?;
self.height_to_value_destroyed.um().safe_flush(exit)?;
self.height_to_supply_in_profit.um().safe_flush(exit)?;
self.height_to_supply_in_loss.um().safe_flush(exit)?;
self.height_to_unrealized_profit.um().safe_flush(exit)?;
self.height_to_unrealized_loss.um().safe_flush(exit)?;
self.dateindex_to_supply_in_profit.um().safe_flush(exit)?;
self.dateindex_to_supply_in_loss.um().safe_flush(exit)?;
self.dateindex_to_unrealized_profit.um().safe_flush(exit)?;
self.dateindex_to_unrealized_loss.um().safe_flush(exit)?;
self.height_to_min_price_paid.um().safe_flush(exit)?;
self.height_to_max_price_paid.um().safe_flush(exit)?;
if self.height_to_adjusted_value_created.is_some() {
self.height_to_adjusted_value_created
.um()
.safe_flush(exit)?;
self.height_to_adjusted_value_destroyed
.um()
.safe_flush(exit)?;
}
// Uses Flushable trait - Option<T> impl handles None case
self.price_percentiles.safe_flush(exit)?;
}
state.commit(height)?;
Ok(())
}
}

View File

@@ -0,0 +1,210 @@
use brk_grouper::Filter;
use brk_traversable::Traversable;
use brk_types::{DateIndex, Dollars, Height, Sats, StoredF32, StoredF64, StoredU64};
use vecdb::{EagerVec, PcoVec};
use crate::grouped::{
ComputedHeightValueVecs, ComputedRatioVecsFromDateIndex, ComputedValueVecsFromDateIndex,
ComputedValueVecsFromHeight, ComputedVecsFromDateIndex, ComputedVecsFromHeight,
PricePercentiles,
};
/// Common vectors shared between UTXO and Address cohorts.
///
/// This struct contains all the computed vectors for a single cohort. The fields are
/// organized into logical groups matching the initialization order in `forced_import`.
///
/// ## Field Groups
/// - **Supply & UTXO count**: Basic supply metrics (always computed)
/// - **Activity**: Sent amounts, satblocks/satdays destroyed
/// - **Realized**: Realized cap, profit/loss, value created/destroyed, SOPR
/// - **Unrealized**: Unrealized profit/loss, supply in profit/loss
/// - **Price**: Min/max price paid, price percentiles
/// - **Relative metrics**: Ratios relative to market cap, realized cap, etc.
#[derive(Clone, Traversable)]
pub struct Vecs {
#[traversable(skip)]
pub filter: Filter,
// ==================== SUPPLY & UTXO COUNT ====================
// Always computed - core supply metrics
pub height_to_supply: EagerVec<PcoVec<Height, Sats>>,
pub height_to_supply_value: ComputedHeightValueVecs,
pub indexes_to_supply: ComputedValueVecsFromDateIndex,
pub height_to_utxo_count: EagerVec<PcoVec<Height, StoredU64>>,
pub indexes_to_utxo_count: ComputedVecsFromHeight<StoredU64>,
pub height_to_supply_half_value: ComputedHeightValueVecs,
pub indexes_to_supply_half: ComputedValueVecsFromDateIndex,
// ==================== ACTIVITY ====================
// Always computed - transaction activity metrics
pub height_to_sent: EagerVec<PcoVec<Height, Sats>>,
pub indexes_to_sent: ComputedValueVecsFromHeight,
pub height_to_satblocks_destroyed: EagerVec<PcoVec<Height, Sats>>,
pub height_to_satdays_destroyed: EagerVec<PcoVec<Height, Sats>>,
pub indexes_to_coinblocks_destroyed: ComputedVecsFromHeight<StoredF64>,
pub indexes_to_coindays_destroyed: ComputedVecsFromHeight<StoredF64>,
// ==================== REALIZED CAP & PRICE ====================
// Conditional on compute_dollars
pub height_to_realized_cap: Option<EagerVec<PcoVec<Height, Dollars>>>,
pub indexes_to_realized_cap: Option<ComputedVecsFromHeight<Dollars>>,
pub indexes_to_realized_price: Option<ComputedVecsFromHeight<Dollars>>,
pub indexes_to_realized_price_extra: Option<ComputedRatioVecsFromDateIndex>,
pub indexes_to_realized_cap_rel_to_own_market_cap: Option<ComputedVecsFromHeight<StoredF32>>,
pub indexes_to_realized_cap_30d_delta: Option<ComputedVecsFromDateIndex<Dollars>>,
// ==================== REALIZED PROFIT & LOSS ====================
// Conditional on compute_dollars
pub height_to_realized_profit: Option<EagerVec<PcoVec<Height, Dollars>>>,
pub indexes_to_realized_profit: Option<ComputedVecsFromHeight<Dollars>>,
pub height_to_realized_loss: Option<EagerVec<PcoVec<Height, Dollars>>>,
pub indexes_to_realized_loss: Option<ComputedVecsFromHeight<Dollars>>,
pub indexes_to_neg_realized_loss: Option<ComputedVecsFromHeight<Dollars>>,
pub indexes_to_net_realized_pnl: Option<ComputedVecsFromHeight<Dollars>>,
pub indexes_to_realized_value: Option<ComputedVecsFromHeight<Dollars>>,
pub indexes_to_realized_profit_rel_to_realized_cap: Option<ComputedVecsFromHeight<StoredF32>>,
pub indexes_to_realized_loss_rel_to_realized_cap: Option<ComputedVecsFromHeight<StoredF32>>,
pub indexes_to_net_realized_pnl_rel_to_realized_cap: Option<ComputedVecsFromHeight<StoredF32>>,
pub height_to_total_realized_pnl: Option<EagerVec<PcoVec<Height, Dollars>>>,
pub indexes_to_total_realized_pnl: Option<ComputedVecsFromDateIndex<Dollars>>,
pub dateindex_to_realized_profit_to_loss_ratio: Option<EagerVec<PcoVec<DateIndex, StoredF64>>>,
// ==================== VALUE CREATED & DESTROYED ====================
// Conditional on compute_dollars
pub height_to_value_created: Option<EagerVec<PcoVec<Height, Dollars>>>,
pub indexes_to_value_created: Option<ComputedVecsFromHeight<Dollars>>,
pub height_to_value_destroyed: Option<EagerVec<PcoVec<Height, Dollars>>>,
pub indexes_to_value_destroyed: Option<ComputedVecsFromHeight<Dollars>>,
pub height_to_adjusted_value_created: Option<EagerVec<PcoVec<Height, Dollars>>>,
pub indexes_to_adjusted_value_created: Option<ComputedVecsFromHeight<Dollars>>,
pub height_to_adjusted_value_destroyed: Option<EagerVec<PcoVec<Height, Dollars>>>,
pub indexes_to_adjusted_value_destroyed: Option<ComputedVecsFromHeight<Dollars>>,
// ==================== SOPR ====================
// Spent Output Profit Ratio - conditional on compute_dollars
pub dateindex_to_sopr: Option<EagerVec<PcoVec<DateIndex, StoredF64>>>,
pub dateindex_to_sopr_7d_ema: Option<EagerVec<PcoVec<DateIndex, StoredF64>>>,
pub dateindex_to_sopr_30d_ema: Option<EagerVec<PcoVec<DateIndex, StoredF64>>>,
pub dateindex_to_adjusted_sopr: Option<EagerVec<PcoVec<DateIndex, StoredF64>>>,
pub dateindex_to_adjusted_sopr_7d_ema: Option<EagerVec<PcoVec<DateIndex, StoredF64>>>,
pub dateindex_to_adjusted_sopr_30d_ema: Option<EagerVec<PcoVec<DateIndex, StoredF64>>>,
// ==================== SELL SIDE RISK ====================
// Conditional on compute_dollars
pub dateindex_to_sell_side_risk_ratio: Option<EagerVec<PcoVec<DateIndex, StoredF32>>>,
pub dateindex_to_sell_side_risk_ratio_7d_ema: Option<EagerVec<PcoVec<DateIndex, StoredF32>>>,
pub dateindex_to_sell_side_risk_ratio_30d_ema: Option<EagerVec<PcoVec<DateIndex, StoredF32>>>,
// ==================== SUPPLY IN PROFIT/LOSS ====================
// Conditional on compute_dollars
pub height_to_supply_in_profit: Option<EagerVec<PcoVec<Height, Sats>>>,
pub indexes_to_supply_in_profit: Option<ComputedValueVecsFromDateIndex>,
pub height_to_supply_in_loss: Option<EagerVec<PcoVec<Height, Sats>>>,
pub indexes_to_supply_in_loss: Option<ComputedValueVecsFromDateIndex>,
pub dateindex_to_supply_in_profit: Option<EagerVec<PcoVec<DateIndex, Sats>>>,
pub dateindex_to_supply_in_loss: Option<EagerVec<PcoVec<DateIndex, Sats>>>,
pub height_to_supply_in_profit_value: Option<ComputedHeightValueVecs>,
pub height_to_supply_in_loss_value: Option<ComputedHeightValueVecs>,
// ==================== UNREALIZED PROFIT & LOSS ====================
// Conditional on compute_dollars
pub height_to_unrealized_profit: Option<EagerVec<PcoVec<Height, Dollars>>>,
pub indexes_to_unrealized_profit: Option<ComputedVecsFromDateIndex<Dollars>>,
pub height_to_unrealized_loss: Option<EagerVec<PcoVec<Height, Dollars>>>,
pub indexes_to_unrealized_loss: Option<ComputedVecsFromDateIndex<Dollars>>,
pub dateindex_to_unrealized_profit: Option<EagerVec<PcoVec<DateIndex, Dollars>>>,
pub dateindex_to_unrealized_loss: Option<EagerVec<PcoVec<DateIndex, Dollars>>>,
pub height_to_neg_unrealized_loss: Option<EagerVec<PcoVec<Height, Dollars>>>,
pub indexes_to_neg_unrealized_loss: Option<ComputedVecsFromDateIndex<Dollars>>,
pub height_to_net_unrealized_pnl: Option<EagerVec<PcoVec<Height, Dollars>>>,
pub indexes_to_net_unrealized_pnl: Option<ComputedVecsFromDateIndex<Dollars>>,
pub height_to_total_unrealized_pnl: Option<EagerVec<PcoVec<Height, Dollars>>>,
pub indexes_to_total_unrealized_pnl: Option<ComputedVecsFromDateIndex<Dollars>>,
// ==================== PRICE PAID ====================
// Conditional on compute_dollars
pub height_to_min_price_paid: Option<EagerVec<PcoVec<Height, Dollars>>>,
pub indexes_to_min_price_paid: Option<ComputedVecsFromHeight<Dollars>>,
pub height_to_max_price_paid: Option<EagerVec<PcoVec<Height, Dollars>>>,
pub indexes_to_max_price_paid: Option<ComputedVecsFromHeight<Dollars>>,
pub price_percentiles: Option<PricePercentiles>,
// ==================== RELATIVE METRICS: UNREALIZED vs MARKET CAP ====================
// Conditional on compute_dollars
pub height_to_unrealized_profit_rel_to_market_cap: Option<EagerVec<PcoVec<Height, StoredF32>>>,
pub height_to_unrealized_loss_rel_to_market_cap: Option<EagerVec<PcoVec<Height, StoredF32>>>,
pub height_to_neg_unrealized_loss_rel_to_market_cap:
Option<EagerVec<PcoVec<Height, StoredF32>>>,
pub height_to_net_unrealized_pnl_rel_to_market_cap: Option<EagerVec<PcoVec<Height, StoredF32>>>,
pub indexes_to_unrealized_profit_rel_to_market_cap:
Option<ComputedVecsFromDateIndex<StoredF32>>,
pub indexes_to_unrealized_loss_rel_to_market_cap: Option<ComputedVecsFromDateIndex<StoredF32>>,
pub indexes_to_neg_unrealized_loss_rel_to_market_cap:
Option<ComputedVecsFromDateIndex<StoredF32>>,
pub indexes_to_net_unrealized_pnl_rel_to_market_cap:
Option<ComputedVecsFromDateIndex<StoredF32>>,
// ==================== RELATIVE METRICS: UNREALIZED vs OWN MARKET CAP ====================
// Conditional on compute_dollars && extended && compute_rel_to_all
pub height_to_unrealized_profit_rel_to_own_market_cap:
Option<EagerVec<PcoVec<Height, StoredF32>>>,
pub height_to_unrealized_loss_rel_to_own_market_cap:
Option<EagerVec<PcoVec<Height, StoredF32>>>,
pub height_to_neg_unrealized_loss_rel_to_own_market_cap:
Option<EagerVec<PcoVec<Height, StoredF32>>>,
pub height_to_net_unrealized_pnl_rel_to_own_market_cap:
Option<EagerVec<PcoVec<Height, StoredF32>>>,
pub indexes_to_unrealized_profit_rel_to_own_market_cap:
Option<ComputedVecsFromDateIndex<StoredF32>>,
pub indexes_to_unrealized_loss_rel_to_own_market_cap:
Option<ComputedVecsFromDateIndex<StoredF32>>,
pub indexes_to_neg_unrealized_loss_rel_to_own_market_cap:
Option<ComputedVecsFromDateIndex<StoredF32>>,
pub indexes_to_net_unrealized_pnl_rel_to_own_market_cap:
Option<ComputedVecsFromDateIndex<StoredF32>>,
// ==================== RELATIVE METRICS: UNREALIZED vs OWN TOTAL UNREALIZED ====================
// Conditional on compute_dollars && extended
pub height_to_unrealized_profit_rel_to_own_total_unrealized_pnl:
Option<EagerVec<PcoVec<Height, StoredF32>>>,
pub height_to_unrealized_loss_rel_to_own_total_unrealized_pnl:
Option<EagerVec<PcoVec<Height, StoredF32>>>,
pub height_to_neg_unrealized_loss_rel_to_own_total_unrealized_pnl:
Option<EagerVec<PcoVec<Height, StoredF32>>>,
pub height_to_net_unrealized_pnl_rel_to_own_total_unrealized_pnl:
Option<EagerVec<PcoVec<Height, StoredF32>>>,
pub indexes_to_unrealized_profit_rel_to_own_total_unrealized_pnl:
Option<ComputedVecsFromDateIndex<StoredF32>>,
pub indexes_to_unrealized_loss_rel_to_own_total_unrealized_pnl:
Option<ComputedVecsFromDateIndex<StoredF32>>,
pub indexes_to_neg_unrealized_loss_rel_to_own_total_unrealized_pnl:
Option<ComputedVecsFromDateIndex<StoredF32>>,
pub indexes_to_net_unrealized_pnl_rel_to_own_total_unrealized_pnl:
Option<ComputedVecsFromDateIndex<StoredF32>>,
// ==================== RELATIVE METRICS: SUPPLY vs CIRCULATING/OWN ====================
// Conditional on compute_dollars
pub indexes_to_supply_rel_to_circulating_supply: Option<ComputedVecsFromHeight<StoredF64>>,
pub height_to_supply_in_profit_rel_to_own_supply: Option<EagerVec<PcoVec<Height, StoredF64>>>,
pub height_to_supply_in_loss_rel_to_own_supply: Option<EagerVec<PcoVec<Height, StoredF64>>>,
pub indexes_to_supply_in_profit_rel_to_own_supply: Option<ComputedVecsFromDateIndex<StoredF64>>,
pub indexes_to_supply_in_loss_rel_to_own_supply: Option<ComputedVecsFromDateIndex<StoredF64>>,
pub height_to_supply_in_profit_rel_to_circulating_supply:
Option<EagerVec<PcoVec<Height, StoredF64>>>,
pub height_to_supply_in_loss_rel_to_circulating_supply:
Option<EagerVec<PcoVec<Height, StoredF64>>>,
pub indexes_to_supply_in_profit_rel_to_circulating_supply:
Option<ComputedVecsFromDateIndex<StoredF64>>,
pub indexes_to_supply_in_loss_rel_to_circulating_supply:
Option<ComputedVecsFromDateIndex<StoredF64>>,
// ==================== NET REALIZED PNL DELTAS ====================
// Conditional on compute_dollars
pub indexes_to_net_realized_pnl_cumulative_30d_delta:
Option<ComputedVecsFromDateIndex<Dollars>>,
pub indexes_to_net_realized_pnl_cumulative_30d_delta_rel_to_realized_cap:
Option<ComputedVecsFromDateIndex<StoredF32>>,
pub indexes_to_net_realized_pnl_cumulative_30d_delta_rel_to_market_cap:
Option<ComputedVecsFromDateIndex<StoredF32>>,
}

View File

@@ -0,0 +1,70 @@
//! Traits for consistent state flushing and importing.
//!
//! These traits ensure all stateful components follow the same patterns
//! for checkpoint/resume operations, preventing bugs where new fields
//! are forgotten during flush operations.
use brk_error::Result;
use brk_types::Height;
use vecdb::Exit;
/// Trait for components that can be flushed to disk.
///
/// This is for simple flush operations that don't require height tracking.
pub trait Flushable {
/// Safely flush data to disk.
fn safe_flush(&mut self, exit: &Exit) -> Result<()>;
}
/// Trait for stateful components that track data indexed by height.
///
/// This ensures consistent patterns for:
/// - Flushing state at checkpoints
/// - Importing state when resuming from a checkpoint
/// - Resetting state when starting from scratch
pub trait HeightFlushable {
/// Flush state to disk at the given height checkpoint.
fn flush_at_height(&mut self, height: Height, exit: &Exit) -> Result<()>;
/// Import state from the most recent checkpoint at or before the given height.
/// Returns the actual height that was imported.
fn import_at_or_before(&mut self, height: Height) -> Result<Height>;
/// Reset state for starting from scratch.
fn reset(&mut self) -> Result<()>;
}
/// Blanket implementation for Option<T> where T: Flushable
impl<T: Flushable> Flushable for Option<T> {
fn safe_flush(&mut self, exit: &Exit) -> Result<()> {
if let Some(inner) = self.as_mut() {
inner.safe_flush(exit)?;
}
Ok(())
}
}
/// Blanket implementation for Option<T> where T: HeightFlushable
impl<T: HeightFlushable> HeightFlushable for Option<T> {
fn flush_at_height(&mut self, height: Height, exit: &Exit) -> Result<()> {
if let Some(inner) = self.as_mut() {
inner.flush_at_height(height, exit)?;
}
Ok(())
}
fn import_at_or_before(&mut self, height: Height) -> Result<Height> {
if let Some(inner) = self.as_mut() {
inner.import_at_or_before(height)
} else {
Ok(height)
}
}
fn reset(&mut self) -> Result<()> {
if let Some(inner) = self.as_mut() {
inner.reset()?;
}
Ok(())
}
}

View File

@@ -1,3 +1,33 @@
//! Stateful computation module for Bitcoin UTXO and address cohort analysis.
//!
//! This module contains the main computation loop that processes blocks and computes
//! various metrics for UTXO cohorts (grouped by age, amount, etc.) and address cohorts.
//!
//! ## Architecture
//!
//! The module is organized as follows:
//!
//! - **`Vecs`**: Main struct holding all computed vectors and state
//! - **Cohort Types**:
//! - **Separate cohorts**: Have full state tracking (e.g., UTXOs 1-2 years old)
//! - **Aggregate cohorts**: Computed from separate cohorts (e.g., all, sth, lth)
//!
//! ## Checkpoint/Resume
//!
//! The computation supports checkpointing via `flush_states()` which saves:
//! - All separate cohorts' state (via `safe_flush_stateful_vecs`)
//! - Aggregate cohorts' `price_to_amount` (via `HeightFlushable` trait)
//! - Aggregate cohorts' `price_percentiles` (via `Flushable` trait)
//!
//! Resume is handled by:
//! - `import_state()` for separate cohorts
//! - `import_aggregate_price_to_amount()` for aggregate cohorts
//!
//! ## Key Traits
//!
//! - `Flushable`: Simple flush operations (no height tracking)
//! - `HeightFlushable`: Height-indexed state (flush, import, reset)
use std::{cmp::Ordering, collections::BTreeSet, mem, path::Path, thread};
use brk_error::Result;
@@ -34,6 +64,7 @@ mod address_cohorts;
mod address_indexes;
mod addresstype;
mod common;
mod flushable;
mod range_map;
mod readers;
mod r#trait;
@@ -42,6 +73,8 @@ mod utxo_cohort;
mod utxo_cohorts;
mod withaddressdatasource;
pub use flushable::{Flushable, HeightFlushable};
use address_indexes::{AddressesDataVecs, AnyAddressIndexesVecs};
use addresstype::*;
use range_map::*;

View File

@@ -18,7 +18,7 @@ use vecdb::{Database, Exit, IterableVec, VecIndex};
use crate::{
Indexes, indexes, price,
stateful::r#trait::DynCohortVecs,
stateful::{Flushable, HeightFlushable, r#trait::DynCohortVecs},
states::{BlockState, Transacted},
utils::OptionExt,
};
@@ -614,13 +614,10 @@ impl Vecs {
.try_for_each(|v| v.safe_flush_stateful_vecs(height, exit))?;
// Flush aggregate cohorts' price_to_amount and price_percentiles
// Using traits ensures we can't forget to flush any field
self.0.par_iter_aggregate_mut().try_for_each(|v| {
if let Some(p2a) = v.price_to_amount.as_mut() {
p2a.flush(height)?;
}
if let Some(pp) = v.inner.price_percentiles.as_mut() {
pp.safe_flush(exit)?;
}
v.price_to_amount.flush_at_height(height, exit)?;
v.inner.price_percentiles.safe_flush(exit)?;
Ok(())
})
}
@@ -628,11 +625,7 @@ impl Vecs {
/// Reset aggregate cohorts' price_to_amount when starting from scratch
pub fn reset_aggregate_price_to_amount(&mut self) -> Result<()> {
self.0.iter_aggregate_mut().try_for_each(|v| {
if let Some(p2a) = v.price_to_amount.as_mut() {
p2a.clean()?;
p2a.init();
}
Ok(())
v.price_to_amount.reset()
})
}
@@ -651,10 +644,8 @@ impl Vecs {
};
for v in self.0.iter_aggregate_mut() {
if let Some(p2a) = v.price_to_amount.as_mut() {
// Match separate vecs: update prev_height to the checkpoint found
prev_height = prev_height.min(p2a.import_at_or_before(prev_height)?);
}
// Using HeightFlushable trait - if price_to_amount is None, returns height unchanged
prev_height = prev_height.min(v.price_to_amount.import_at_or_before(prev_height)?);
}
// Return prev_height + 1, matching separate vecs behavior
Ok(prev_height.incremented())

View File

@@ -9,9 +9,9 @@ use brk_types::{Dollars, Height, Sats};
use derive_deref::{Deref, DerefMut};
use pco::standalone::{simple_decompress, simpler_compress};
use serde::{Deserialize, Serialize};
use vecdb::Bytes;
use vecdb::{Bytes, Exit};
use crate::{states::SupplyState, utils::OptionExt};
use crate::{stateful::HeightFlushable, states::SupplyState, utils::OptionExt};
#[derive(Clone, Debug)]
pub struct PriceToAmount {
@@ -128,6 +128,22 @@ impl PriceToAmount {
}
}
impl HeightFlushable for PriceToAmount {
fn flush_at_height(&mut self, height: Height, _exit: &Exit) -> Result<()> {
self.flush(height)
}
fn import_at_or_before(&mut self, height: Height) -> Result<Height> {
PriceToAmount::import_at_or_before(self, height)
}
fn reset(&mut self) -> Result<()> {
self.clean()?;
self.init();
Ok(())
}
}
#[derive(Clone, Default, Debug, Deref, DerefMut, Serialize, Deserialize)]
struct State(BTreeMap<Dollars, Sats>);