mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-05-02 02:20:00 -07:00
global: snapshot
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
use brk_cohort::{AmountBucket, ByAddressType};
|
||||
use brk_error::Result;
|
||||
use brk_types::{CheckedSub, Dollars, Height, Sats, Timestamp, TypeIndex};
|
||||
use vecdb::{VecIndex, unlikely};
|
||||
use brk_types::{Age, CheckedSub, Dollars, Height, Sats, Timestamp, TypeIndex};
|
||||
use vecdb::{unlikely, VecIndex};
|
||||
|
||||
use crate::distribution::{address::HeightToAddressTypeToVec, cohorts::AddressCohorts};
|
||||
|
||||
@@ -11,7 +11,7 @@ use super::super::cache::AddressLookup;
|
||||
///
|
||||
/// For each spent UTXO:
|
||||
/// 1. Look up address data
|
||||
/// 2. Calculate age metrics (blocks_old, days_old)
|
||||
/// 2. Calculate age metrics
|
||||
/// 3. Update address balance and cohort membership
|
||||
/// 4. Handle addresses becoming empty
|
||||
///
|
||||
@@ -33,13 +33,8 @@ pub fn process_sent(
|
||||
for (prev_height, by_type) in sent_data.into_iter() {
|
||||
let prev_price = height_to_price.map(|v| v[prev_height.to_usize()]);
|
||||
let prev_timestamp = height_to_timestamp[prev_height.to_usize()];
|
||||
|
||||
let blocks_old = current_height.to_usize() - prev_height.to_usize();
|
||||
let days_old = current_timestamp.difference_in_days_between_float(prev_timestamp);
|
||||
let older_than_hour = current_timestamp
|
||||
.checked_sub(prev_timestamp)
|
||||
.unwrap()
|
||||
.is_more_than_hour();
|
||||
let age = Age::new(current_timestamp, prev_timestamp, blocks_old);
|
||||
|
||||
for (output_type, vec) in by_type.unwrap().into_iter() {
|
||||
// Cache mutable refs for this address type
|
||||
@@ -123,15 +118,7 @@ pub fn process_sent(
|
||||
.state
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.send(
|
||||
addr_data,
|
||||
value,
|
||||
current_price,
|
||||
prev_price,
|
||||
blocks_old,
|
||||
days_old,
|
||||
older_than_hour,
|
||||
)?;
|
||||
.send(addr_data, value, current_price, prev_price, age)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,6 +72,7 @@ impl AddressCohortVecs {
|
||||
version,
|
||||
indexes,
|
||||
price,
|
||||
up_to_1h_realized: None,
|
||||
};
|
||||
|
||||
let height_to_addr_count =
|
||||
|
||||
@@ -38,7 +38,33 @@ impl UTXOCohorts {
|
||||
) -> Result<Self> {
|
||||
let v = version + VERSION;
|
||||
|
||||
// Create "all" cohort first - it doesn't need global sources (it IS the global source)
|
||||
// Phase 1: Import base cohorts that don't need adjusted (age_range, amount_range, etc.)
|
||||
// These are the source cohorts for overlapping computations.
|
||||
let base = |f: Filter, name: &'static str| {
|
||||
UTXOCohortVecs::forced_import(
|
||||
db,
|
||||
f,
|
||||
name,
|
||||
v,
|
||||
indexes,
|
||||
price,
|
||||
states_path,
|
||||
StateLevel::Full,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
};
|
||||
|
||||
let age_range = ByAgeRange::try_new(&base)?;
|
||||
let amount_range = ByAmountRange::try_new(&base)?;
|
||||
let epoch = ByEpoch::try_new(&base)?;
|
||||
let year = ByYear::try_new(&base)?;
|
||||
let type_ = BySpendableType::try_new(&base)?;
|
||||
|
||||
// Get up_to_1h realized for adjusted computation (cohort - up_to_1h)
|
||||
let up_to_1h_realized = age_range.up_to_1h.metrics.realized.as_ref();
|
||||
|
||||
// Phase 2: Import "all" cohort (needs up_to_1h for adjusted, is global supply source)
|
||||
let all = UTXOCohortVecs::forced_import(
|
||||
db,
|
||||
Filter::All,
|
||||
@@ -49,13 +75,13 @@ impl UTXOCohorts {
|
||||
states_path,
|
||||
StateLevel::PriceOnly,
|
||||
None,
|
||||
up_to_1h_realized,
|
||||
)?;
|
||||
|
||||
// Get reference to all's supply for other cohorts to use as global source
|
||||
let all_supply = Some(&all.metrics.supply);
|
||||
|
||||
// Create all cohorts first (while borrowing all_supply), then assemble struct
|
||||
let price_only = |f: Filter, name: &'static str| {
|
||||
// Phase 3: Import cohorts that need adjusted and/or all_supply
|
||||
let price_only_adjusted = |f: Filter, name: &'static str| {
|
||||
UTXOCohortVecs::forced_import(
|
||||
db,
|
||||
f,
|
||||
@@ -66,12 +92,13 @@ impl UTXOCohorts {
|
||||
states_path,
|
||||
StateLevel::PriceOnly,
|
||||
all_supply,
|
||||
up_to_1h_realized,
|
||||
)
|
||||
};
|
||||
|
||||
let term = ByTerm::try_new(&price_only)?;
|
||||
let term = ByTerm::try_new(&price_only_adjusted)?;
|
||||
|
||||
let full = |f: Filter, name: &'static str| {
|
||||
let none_adjusted = |f: Filter, name: &'static str| {
|
||||
UTXOCohortVecs::forced_import(
|
||||
db,
|
||||
f,
|
||||
@@ -80,10 +107,15 @@ impl UTXOCohorts {
|
||||
indexes,
|
||||
price,
|
||||
states_path,
|
||||
StateLevel::Full,
|
||||
StateLevel::None,
|
||||
all_supply,
|
||||
up_to_1h_realized,
|
||||
)
|
||||
};
|
||||
|
||||
let max_age = ByMaxAge::try_new(&none_adjusted)?;
|
||||
|
||||
// Phase 4: Import remaining cohorts (no adjusted needed)
|
||||
let none = |f: Filter, name: &'static str| {
|
||||
UTXOCohortVecs::forced_import(
|
||||
db,
|
||||
@@ -95,16 +127,11 @@ impl UTXOCohorts {
|
||||
states_path,
|
||||
StateLevel::None,
|
||||
all_supply,
|
||||
None,
|
||||
)
|
||||
};
|
||||
|
||||
let epoch = ByEpoch::try_new(&full)?;
|
||||
let year = ByYear::try_new(&full)?;
|
||||
let type_ = BySpendableType::try_new(&full)?;
|
||||
let max_age = ByMaxAge::try_new(&none)?;
|
||||
let min_age = ByMinAge::try_new(&none)?;
|
||||
let age_range = ByAgeRange::try_new(&full)?;
|
||||
let amount_range = ByAmountRange::try_new(&full)?;
|
||||
let lt_amount = ByLowerThanAmount::try_new(&none)?;
|
||||
let ge_amount = ByGreatEqualAmount::try_new(&none)?;
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ impl UTXOCohorts {
|
||||
/// Process received outputs for this block.
|
||||
///
|
||||
/// New UTXOs are added to:
|
||||
/// - The "up_to_1d" age cohort (all new UTXOs start at 0 days old)
|
||||
/// - The "up_to_1h" age cohort (all new UTXOs start at 0 hours old)
|
||||
/// - The appropriate epoch cohort based on block height
|
||||
/// - The appropriate year cohort based on block timestamp
|
||||
/// - The appropriate output type cohort (P2PKH, P2SH, etc.)
|
||||
@@ -22,9 +22,9 @@ impl UTXOCohorts {
|
||||
) {
|
||||
let supply_state = received.spendable_supply;
|
||||
|
||||
// New UTXOs go into up_to_1d, current epoch, and current year
|
||||
// New UTXOs go into up_to_1h, current epoch, and current year
|
||||
[
|
||||
&mut self.0.age_range.up_to_1d,
|
||||
&mut self.0.age_range.up_to_1h,
|
||||
self.0.epoch.mut_vec_from_height(height),
|
||||
self.0.year.mut_vec_from_timestamp(timestamp),
|
||||
]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use brk_types::{CheckedSub, Height};
|
||||
use brk_types::{Age, Height};
|
||||
use rustc_hash::FxHashMap;
|
||||
use vecdb::VecIndex;
|
||||
|
||||
@@ -35,37 +35,22 @@ impl UTXOCohorts {
|
||||
let block_state = &chain_state[height.to_usize()];
|
||||
let prev_price = block_state.price;
|
||||
let blocks_old = chain_len - 1 - height.to_usize();
|
||||
let days_old = last_timestamp.difference_in_days_between(block_state.timestamp);
|
||||
let days_old_float =
|
||||
last_timestamp.difference_in_days_between_float(block_state.timestamp);
|
||||
let older_than_hour = last_timestamp
|
||||
.checked_sub(block_state.timestamp)
|
||||
.unwrap()
|
||||
.is_more_than_hour();
|
||||
let age = Age::new(last_timestamp, block_state.timestamp, blocks_old);
|
||||
|
||||
// Update age range cohort (direct index lookup)
|
||||
self.0
|
||||
.age_range
|
||||
.get_mut_by_days_old(days_old)
|
||||
.state
|
||||
.um()
|
||||
.send(
|
||||
&sent.spendable_supply,
|
||||
current_price,
|
||||
prev_price,
|
||||
blocks_old,
|
||||
days_old_float,
|
||||
older_than_hour,
|
||||
);
|
||||
self.0.age_range.get_mut(age).state.um().send(
|
||||
&sent.spendable_supply,
|
||||
current_price,
|
||||
prev_price,
|
||||
age,
|
||||
);
|
||||
|
||||
// Update epoch cohort (direct lookup by height)
|
||||
self.0.epoch.mut_vec_from_height(height).state.um().send(
|
||||
&sent.spendable_supply,
|
||||
current_price,
|
||||
prev_price,
|
||||
blocks_old,
|
||||
days_old_float,
|
||||
older_than_hour,
|
||||
age,
|
||||
);
|
||||
|
||||
// Update year cohort (direct lookup by timestamp)
|
||||
@@ -74,42 +59,31 @@ impl UTXOCohorts {
|
||||
.mut_vec_from_timestamp(block_state.timestamp)
|
||||
.state
|
||||
.um()
|
||||
.send(
|
||||
&sent.spendable_supply,
|
||||
current_price,
|
||||
prev_price,
|
||||
blocks_old,
|
||||
days_old_float,
|
||||
older_than_hour,
|
||||
);
|
||||
.send(&sent.spendable_supply, current_price, prev_price, age);
|
||||
|
||||
// Update output type cohorts
|
||||
sent.by_type
|
||||
.spendable
|
||||
.iter_typed()
|
||||
.for_each(|(output_type, supply_state)| {
|
||||
self.0.type_.get_mut(output_type).state.um().send(
|
||||
supply_state,
|
||||
current_price,
|
||||
prev_price,
|
||||
blocks_old,
|
||||
days_old_float,
|
||||
older_than_hour,
|
||||
)
|
||||
self.0
|
||||
.type_
|
||||
.get_mut(output_type)
|
||||
.state
|
||||
.um()
|
||||
.send(supply_state, current_price, prev_price, age)
|
||||
});
|
||||
|
||||
// Update amount range cohorts
|
||||
sent.by_size_group
|
||||
.iter_typed()
|
||||
.for_each(|(group, supply_state)| {
|
||||
self.0.amount_range.get_mut(group).state.um().send(
|
||||
supply_state,
|
||||
current_price,
|
||||
prev_price,
|
||||
blocks_old,
|
||||
days_old_float,
|
||||
older_than_hour,
|
||||
);
|
||||
self.0
|
||||
.amount_range
|
||||
.get_mut(group)
|
||||
.state
|
||||
.um()
|
||||
.send(supply_state, current_price, prev_price, age);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use brk_cohort::AGE_BOUNDARIES;
|
||||
use brk_types::{ONE_DAY_IN_SEC, Timestamp};
|
||||
use brk_types::{ONE_HOUR_IN_SEC, Timestamp};
|
||||
|
||||
use crate::distribution::state::BlockState;
|
||||
|
||||
@@ -8,11 +8,11 @@ use super::groups::UTXOCohorts;
|
||||
impl UTXOCohorts {
|
||||
/// Handle age transitions when processing a new block.
|
||||
///
|
||||
/// UTXOs age with each block. When they cross day boundaries,
|
||||
/// they move between age-based cohorts (e.g., from "0-1d" to "1-7d").
|
||||
/// UTXOs age with each block. When they cross hour boundaries,
|
||||
/// they move between age-based cohorts (e.g., from "0-1h" to "1h-1d").
|
||||
///
|
||||
/// Complexity: O(k * (log n + m)) where:
|
||||
/// - k = 19 boundaries to check
|
||||
/// - k = 20 boundaries to check
|
||||
/// - n = total blocks in chain_state
|
||||
/// - m = blocks crossing each boundary (typically 0-2 per boundary per block)
|
||||
pub fn tick_tock_next_block(&mut self, chain_state: &[BlockState], timestamp: Timestamp) {
|
||||
@@ -28,19 +28,19 @@ impl UTXOCohorts {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get age_range cohort states (indexed 0..20)
|
||||
// Cohort i covers days [BOUNDARIES[i-1], BOUNDARIES[i])
|
||||
// Cohort 0 covers [0, 1) days
|
||||
// Cohort 19 covers [15*365, infinity) days
|
||||
// Get age_range cohort states (indexed 0..21)
|
||||
// Cohort i covers hours [BOUNDARIES[i-1], BOUNDARIES[i])
|
||||
// Cohort 0 covers [0, 1) hours
|
||||
// Cohort 20 covers [15*365*24, infinity) hours
|
||||
let mut age_cohorts: Vec<_> = self.0.age_range.iter_mut().map(|v| &mut v.state).collect();
|
||||
|
||||
// For each boundary, find blocks that just crossed it
|
||||
for (boundary_idx, &boundary_days) in AGE_BOUNDARIES.iter().enumerate() {
|
||||
let boundary_seconds = (boundary_days as u32) * ONE_DAY_IN_SEC;
|
||||
// For each boundary (in hours), find blocks that just crossed it
|
||||
for (boundary_idx, &boundary_hours) in AGE_BOUNDARIES.iter().enumerate() {
|
||||
let boundary_seconds = (boundary_hours as u32) * ONE_HOUR_IN_SEC;
|
||||
|
||||
// Blocks crossing boundary B have timestamps in (prev - B*DAY, curr - B*DAY]
|
||||
// prev_days < B and curr_days >= B
|
||||
// means: block was younger than B days, now is B days or older
|
||||
// Blocks crossing boundary B have timestamps in (prev - B*HOUR, curr - B*HOUR]
|
||||
// prev_hours < B and curr_hours >= B
|
||||
// means: block was younger than B hours, now is B hours or older
|
||||
let upper_timestamp = (*timestamp).saturating_sub(boundary_seconds);
|
||||
let lower_timestamp = (*prev_timestamp).saturating_sub(boundary_seconds);
|
||||
|
||||
@@ -53,23 +53,11 @@ impl UTXOCohorts {
|
||||
let start_idx = chain_state.partition_point(|b| *b.timestamp <= lower_timestamp);
|
||||
let end_idx = chain_state.partition_point(|b| *b.timestamp <= upper_timestamp);
|
||||
|
||||
// Process blocks that crossed this boundary
|
||||
// Move supply from younger cohort to older cohort
|
||||
for block_state in &chain_state[start_idx..end_idx] {
|
||||
// Double-check the day boundary was actually crossed
|
||||
// (handles edge cases with day boundaries)
|
||||
let prev_days = prev_timestamp.difference_in_days_between(block_state.timestamp);
|
||||
let curr_days = timestamp.difference_in_days_between(block_state.timestamp);
|
||||
|
||||
if prev_days >= boundary_days || curr_days < boundary_days {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Block crossed from cohort[boundary_idx] to cohort[boundary_idx + 1]
|
||||
// Decrement from the "younger" cohort
|
||||
if let Some(state) = age_cohorts[boundary_idx].as_mut() {
|
||||
state.decrement(&block_state.supply, block_state.price);
|
||||
}
|
||||
// Increment in the "older" cohort
|
||||
if let Some(state) = age_cohorts[boundary_idx + 1].as_mut() {
|
||||
state.increment(&block_state.supply, block_state.price);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ use vecdb::{AnyStoredVec, Database, Exit, IterableVec};
|
||||
|
||||
use crate::{ComputeIndexes, indexes, price, distribution::state::UTXOCohortState};
|
||||
|
||||
use crate::distribution::metrics::{CohortMetrics, ImportConfig, SupplyMetrics};
|
||||
use crate::distribution::metrics::{CohortMetrics, ImportConfig, RealizedMetrics, SupplyMetrics};
|
||||
|
||||
use super::super::traits::{CohortVecs, DynCohortVecs};
|
||||
|
||||
@@ -33,6 +33,9 @@ impl UTXOCohortVecs {
|
||||
///
|
||||
/// `all_supply` is the supply metrics from the "all" cohort, used as global
|
||||
/// sources for `*_rel_to_market_cap` ratios. Pass `None` for the "all" cohort itself.
|
||||
///
|
||||
/// `up_to_1h_realized` is used for cohorts where `compute_adjusted()` is true,
|
||||
/// to create lazy adjusted vecs: adjusted = cohort - up_to_1h.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn forced_import(
|
||||
db: &Database,
|
||||
@@ -44,6 +47,7 @@ impl UTXOCohortVecs {
|
||||
states_path: &Path,
|
||||
state_level: StateLevel,
|
||||
all_supply: Option<&SupplyMetrics>,
|
||||
up_to_1h_realized: Option<&RealizedMetrics>,
|
||||
) -> Result<Self> {
|
||||
let compute_dollars = price.is_some();
|
||||
let full_name = CohortContext::Utxo.full_name(&filter, name);
|
||||
@@ -56,6 +60,7 @@ impl UTXOCohortVecs {
|
||||
version,
|
||||
indexes,
|
||||
price,
|
||||
up_to_1h_realized,
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
|
||||
@@ -4,6 +4,8 @@ use vecdb::Database;
|
||||
|
||||
use crate::{indexes, price};
|
||||
|
||||
use super::RealizedMetrics;
|
||||
|
||||
/// Configuration for importing metrics.
|
||||
pub struct ImportConfig<'a> {
|
||||
pub db: &'a Database,
|
||||
@@ -13,6 +15,9 @@ pub struct ImportConfig<'a> {
|
||||
pub version: Version,
|
||||
pub indexes: &'a indexes::Vecs,
|
||||
pub price: Option<&'a price::Vecs>,
|
||||
/// Source for lazy adjusted computation: adjusted = cohort - up_to_1h.
|
||||
/// Required for cohorts where `compute_adjusted()` is true.
|
||||
pub up_to_1h_realized: Option<&'a RealizedMetrics>,
|
||||
}
|
||||
|
||||
impl<'a> ImportConfig<'a> {
|
||||
|
||||
@@ -13,8 +13,8 @@ use crate::{
|
||||
indexes,
|
||||
internal::{
|
||||
ComputedRatioVecsFromDateIndex, ComputedVecsFromDateIndex, ComputedVecsFromHeight,
|
||||
LazyVecsFrom2FromHeight, LazyVecsFromDateIndex, LazyVecsFromHeight, PercentageDollarsF32,
|
||||
Source, StoredF32Identity, VecBuilderOptions,
|
||||
DollarsMinus, LazyVecsFrom2FromHeight, LazyVecsFromDateIndex, LazyVecsFromHeight,
|
||||
PercentageDollarsF32, Source, StoredF32Identity, VecBuilderOptions,
|
||||
},
|
||||
price,
|
||||
utils::OptionExt,
|
||||
@@ -64,11 +64,11 @@ pub struct RealizedMetrics {
|
||||
pub height_to_value_destroyed: EagerVec<PcoVec<Height, Dollars>>,
|
||||
pub indexes_to_value_destroyed: ComputedVecsFromHeight<Dollars>,
|
||||
|
||||
// === Adjusted Value (optional) ===
|
||||
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>>,
|
||||
// === Adjusted Value (lazy: cohort - up_to_1h) ===
|
||||
pub indexes_to_adjusted_value_created:
|
||||
Option<LazyVecsFrom2FromHeight<Dollars, Dollars, Dollars>>,
|
||||
pub indexes_to_adjusted_value_destroyed:
|
||||
Option<LazyVecsFrom2FromHeight<Dollars, Dollars, Dollars>>,
|
||||
|
||||
// === SOPR (Spent Output Profit Ratio) ===
|
||||
pub dateindex_to_sopr: EagerVec<PcoVec<DateIndex, StoredF64>>,
|
||||
@@ -226,16 +226,48 @@ impl RealizedMetrics {
|
||||
let height_to_value_destroyed =
|
||||
EagerVec::forced_import(cfg.db, &cfg.name("value_destroyed"), cfg.version)?;
|
||||
|
||||
let height_to_adjusted_value_created = compute_adjusted
|
||||
let indexes_to_value_created = ComputedVecsFromHeight::forced_import(
|
||||
cfg.db,
|
||||
&cfg.name("value_created"),
|
||||
Source::Vec(height_to_value_created.boxed_clone()),
|
||||
cfg.version,
|
||||
cfg.indexes,
|
||||
sum,
|
||||
)?;
|
||||
let indexes_to_value_destroyed = ComputedVecsFromHeight::forced_import(
|
||||
cfg.db,
|
||||
&cfg.name("value_destroyed"),
|
||||
Source::Vec(height_to_value_destroyed.boxed_clone()),
|
||||
cfg.version,
|
||||
cfg.indexes,
|
||||
sum,
|
||||
)?;
|
||||
|
||||
// Create lazy adjusted vecs if compute_adjusted and up_to_1h is available
|
||||
let indexes_to_adjusted_value_created = (compute_adjusted && cfg.up_to_1h_realized.is_some())
|
||||
.then(|| {
|
||||
EagerVec::forced_import(cfg.db, &cfg.name("adjusted_value_created"), cfg.version)
|
||||
})
|
||||
.transpose()?;
|
||||
let height_to_adjusted_value_destroyed = compute_adjusted
|
||||
.then(|| {
|
||||
EagerVec::forced_import(cfg.db, &cfg.name("adjusted_value_destroyed"), cfg.version)
|
||||
})
|
||||
.transpose()?;
|
||||
let up_to_1h = cfg.up_to_1h_realized.unwrap();
|
||||
LazyVecsFrom2FromHeight::from_computed::<DollarsMinus>(
|
||||
&cfg.name("adjusted_value_created"),
|
||||
cfg.version,
|
||||
height_to_value_created.boxed_clone(),
|
||||
up_to_1h.height_to_value_created.boxed_clone(),
|
||||
&indexes_to_value_created,
|
||||
&up_to_1h.indexes_to_value_created,
|
||||
)
|
||||
});
|
||||
let indexes_to_adjusted_value_destroyed =
|
||||
(compute_adjusted && cfg.up_to_1h_realized.is_some()).then(|| {
|
||||
let up_to_1h = cfg.up_to_1h_realized.unwrap();
|
||||
LazyVecsFrom2FromHeight::from_computed::<DollarsMinus>(
|
||||
&cfg.name("adjusted_value_destroyed"),
|
||||
cfg.version,
|
||||
height_to_value_destroyed.boxed_clone(),
|
||||
up_to_1h.height_to_value_destroyed.boxed_clone(),
|
||||
&indexes_to_value_destroyed,
|
||||
&up_to_1h.indexes_to_value_destroyed,
|
||||
)
|
||||
});
|
||||
|
||||
// Create realized_price_extra first so we can reference its ratio for MVRV proxy
|
||||
let indexes_to_realized_price_extra = ComputedRatioVecsFromDateIndex::forced_import(
|
||||
@@ -317,62 +349,14 @@ impl RealizedMetrics {
|
||||
.transpose()?,
|
||||
|
||||
// === Value Created/Destroyed ===
|
||||
indexes_to_value_created: ComputedVecsFromHeight::forced_import(
|
||||
cfg.db,
|
||||
&cfg.name("value_created"),
|
||||
Source::Vec(height_to_value_created.boxed_clone()),
|
||||
cfg.version,
|
||||
cfg.indexes,
|
||||
sum,
|
||||
)?,
|
||||
indexes_to_value_destroyed: ComputedVecsFromHeight::forced_import(
|
||||
cfg.db,
|
||||
&cfg.name("value_destroyed"),
|
||||
Source::Vec(height_to_value_destroyed.boxed_clone()),
|
||||
cfg.version,
|
||||
cfg.indexes,
|
||||
sum,
|
||||
)?,
|
||||
height_to_value_created,
|
||||
indexes_to_value_created,
|
||||
height_to_value_destroyed,
|
||||
indexes_to_value_destroyed,
|
||||
|
||||
// === Adjusted Value (optional) ===
|
||||
indexes_to_adjusted_value_created: compute_adjusted
|
||||
.then(|| {
|
||||
ComputedVecsFromHeight::forced_import(
|
||||
cfg.db,
|
||||
&cfg.name("adjusted_value_created"),
|
||||
Source::Vec(
|
||||
height_to_adjusted_value_created
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.boxed_clone(),
|
||||
),
|
||||
cfg.version,
|
||||
cfg.indexes,
|
||||
sum,
|
||||
)
|
||||
})
|
||||
.transpose()?,
|
||||
indexes_to_adjusted_value_destroyed: compute_adjusted
|
||||
.then(|| {
|
||||
ComputedVecsFromHeight::forced_import(
|
||||
cfg.db,
|
||||
&cfg.name("adjusted_value_destroyed"),
|
||||
Source::Vec(
|
||||
height_to_adjusted_value_destroyed
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.boxed_clone(),
|
||||
),
|
||||
cfg.version,
|
||||
cfg.indexes,
|
||||
sum,
|
||||
)
|
||||
})
|
||||
.transpose()?,
|
||||
height_to_adjusted_value_created,
|
||||
height_to_adjusted_value_destroyed,
|
||||
// === Adjusted Value (lazy: cohort - up_to_1h) ===
|
||||
indexes_to_adjusted_value_created,
|
||||
indexes_to_adjusted_value_destroyed,
|
||||
|
||||
// === SOPR ===
|
||||
dateindex_to_sopr: EagerVec::forced_import(
|
||||
@@ -464,22 +448,12 @@ impl RealizedMetrics {
|
||||
|
||||
/// Get minimum length across height-indexed vectors written in block loop.
|
||||
pub fn min_stateful_height_len(&self) -> usize {
|
||||
let mut min = self
|
||||
.height_to_realized_cap
|
||||
self.height_to_realized_cap
|
||||
.len()
|
||||
.min(self.height_to_realized_profit.len())
|
||||
.min(self.height_to_realized_loss.len())
|
||||
.min(self.height_to_value_created.len())
|
||||
.min(self.height_to_value_destroyed.len());
|
||||
|
||||
if let Some(v) = &self.height_to_adjusted_value_created {
|
||||
min = min.min(v.len());
|
||||
}
|
||||
if let Some(v) = &self.height_to_adjusted_value_destroyed {
|
||||
min = min.min(v.len());
|
||||
}
|
||||
|
||||
min
|
||||
.min(self.height_to_value_destroyed.len())
|
||||
}
|
||||
|
||||
/// Push realized state values to height-indexed vectors.
|
||||
@@ -495,13 +469,6 @@ impl RealizedMetrics {
|
||||
self.height_to_value_destroyed
|
||||
.truncate_push(height, state.value_destroyed)?;
|
||||
|
||||
if let Some(v) = self.height_to_adjusted_value_created.as_mut() {
|
||||
v.truncate_push(height, state.adj_value_created)?;
|
||||
}
|
||||
if let Some(v) = self.height_to_adjusted_value_destroyed.as_mut() {
|
||||
v.truncate_push(height, state.adj_value_destroyed)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -512,31 +479,19 @@ impl RealizedMetrics {
|
||||
self.height_to_realized_loss.write()?;
|
||||
self.height_to_value_created.write()?;
|
||||
self.height_to_value_destroyed.write()?;
|
||||
if let Some(v) = self.height_to_adjusted_value_created.as_mut() {
|
||||
v.write()?;
|
||||
}
|
||||
if let Some(v) = self.height_to_adjusted_value_destroyed.as_mut() {
|
||||
v.write()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns a parallel iterator over all vecs for parallel writing.
|
||||
pub fn par_iter_mut(&mut self) -> impl ParallelIterator<Item = &mut dyn AnyStoredVec> {
|
||||
let mut vecs: Vec<&mut dyn AnyStoredVec> = vec![
|
||||
&mut self.height_to_realized_cap,
|
||||
[
|
||||
&mut self.height_to_realized_cap as &mut dyn AnyStoredVec,
|
||||
&mut self.height_to_realized_profit,
|
||||
&mut self.height_to_realized_loss,
|
||||
&mut self.height_to_value_created,
|
||||
&mut self.height_to_value_destroyed,
|
||||
];
|
||||
if let Some(v) = self.height_to_adjusted_value_created.as_mut() {
|
||||
vecs.push(v);
|
||||
}
|
||||
if let Some(v) = self.height_to_adjusted_value_destroyed.as_mut() {
|
||||
vecs.push(v);
|
||||
}
|
||||
vecs.into_par_iter()
|
||||
]
|
||||
.into_par_iter()
|
||||
}
|
||||
|
||||
/// Validate computed versions against base version.
|
||||
@@ -593,37 +548,6 @@ impl RealizedMetrics {
|
||||
exit,
|
||||
)?;
|
||||
|
||||
if self.height_to_adjusted_value_created.is_some() {
|
||||
self.height_to_adjusted_value_created
|
||||
.um()
|
||||
.compute_sum_of_others(
|
||||
starting_indexes.height,
|
||||
&others
|
||||
.iter()
|
||||
.map(|v| {
|
||||
v.height_to_adjusted_value_created
|
||||
.as_ref()
|
||||
.unwrap_or(&v.height_to_value_created)
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
exit,
|
||||
)?;
|
||||
self.height_to_adjusted_value_destroyed
|
||||
.um()
|
||||
.compute_sum_of_others(
|
||||
starting_indexes.height,
|
||||
&others
|
||||
.iter()
|
||||
.map(|v| {
|
||||
v.height_to_adjusted_value_destroyed
|
||||
.as_ref()
|
||||
.unwrap_or(&v.height_to_value_destroyed)
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
exit,
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -695,25 +619,6 @@ impl RealizedMetrics {
|
||||
Some(&self.height_to_value_destroyed),
|
||||
)?;
|
||||
|
||||
// Optional: adjusted value
|
||||
if let Some(adjusted_value_created) = self.indexes_to_adjusted_value_created.as_mut() {
|
||||
adjusted_value_created.compute_rest(
|
||||
indexes,
|
||||
starting_indexes,
|
||||
exit,
|
||||
self.height_to_adjusted_value_created.as_ref(),
|
||||
)?;
|
||||
}
|
||||
|
||||
if let Some(adjusted_value_destroyed) = self.indexes_to_adjusted_value_destroyed.as_mut() {
|
||||
adjusted_value_destroyed.compute_rest(
|
||||
indexes,
|
||||
starting_indexes,
|
||||
exit,
|
||||
self.height_to_adjusted_value_destroyed.as_ref(),
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -784,16 +689,20 @@ impl RealizedMetrics {
|
||||
exit,
|
||||
)?;
|
||||
|
||||
// Optional: adjusted SOPR
|
||||
// Optional: adjusted SOPR (lazy: cohort - up_to_1h)
|
||||
if let (Some(adjusted_sopr), Some(adj_created), Some(adj_destroyed)) = (
|
||||
self.dateindex_to_adjusted_sopr.as_mut(),
|
||||
self.indexes_to_adjusted_value_created.as_ref(),
|
||||
self.indexes_to_adjusted_value_destroyed.as_ref(),
|
||||
self.indexes_to_adjusted_value_created
|
||||
.as_ref()
|
||||
.and_then(|v| v.dateindex.sum.as_ref()),
|
||||
self.indexes_to_adjusted_value_destroyed
|
||||
.as_ref()
|
||||
.and_then(|v| v.dateindex.sum.as_ref()),
|
||||
) {
|
||||
adjusted_sopr.compute_divide(
|
||||
starting_indexes.dateindex,
|
||||
adj_created.dateindex.unwrap_sum(),
|
||||
adj_destroyed.dateindex.unwrap_sum(),
|
||||
adj_created.as_ref(),
|
||||
adj_destroyed.as_ref(),
|
||||
exit,
|
||||
)?;
|
||||
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
use std::path::Path;
|
||||
|
||||
use brk_error::Result;
|
||||
use brk_types::{Dollars, Height, LoadedAddressData, Sats, SupplyState};
|
||||
use brk_types::{Age, Dollars, Height, LoadedAddressData, Sats, SupplyState};
|
||||
use vecdb::unlikely;
|
||||
|
||||
use super::{
|
||||
super::cost_basis::RealizedState,
|
||||
base::CohortState,
|
||||
};
|
||||
use super::{super::cost_basis::RealizedState, base::CohortState};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AddressCohortState {
|
||||
@@ -43,16 +40,13 @@ impl AddressCohortState {
|
||||
self.inner.reset_single_iteration_values();
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn send(
|
||||
&mut self,
|
||||
addressdata: &mut LoadedAddressData,
|
||||
value: Sats,
|
||||
current_price: Option<Dollars>,
|
||||
prev_price: Option<Dollars>,
|
||||
blocks_old: usize,
|
||||
days_old: f64,
|
||||
older_than_hour: bool,
|
||||
age: Age,
|
||||
) -> Result<()> {
|
||||
let compute_price = current_price.is_some();
|
||||
|
||||
@@ -76,9 +70,7 @@ impl AddressCohortState {
|
||||
},
|
||||
current_price,
|
||||
prev_price,
|
||||
blocks_old,
|
||||
days_old,
|
||||
older_than_hour,
|
||||
age,
|
||||
compute_price.then(|| (addressdata.realized_price(), &supply_state)),
|
||||
prev_realized_price.map(|prev_price| (prev_price, &prev_supply_state)),
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::path::Path;
|
||||
|
||||
use brk_error::Result;
|
||||
use brk_types::{Dollars, Height, Sats, SupplyState};
|
||||
use brk_types::{Age, Dollars, Height, Sats, SupplyState};
|
||||
|
||||
use crate::internal::PERCENTILES_LEN;
|
||||
|
||||
@@ -246,17 +246,13 @@ impl CohortState {
|
||||
supply: &SupplyState,
|
||||
current_price: Option<Dollars>,
|
||||
prev_price: Option<Dollars>,
|
||||
blocks_old: usize,
|
||||
days_old: f64,
|
||||
older_than_hour: bool,
|
||||
age: Age,
|
||||
) {
|
||||
self.send_(
|
||||
supply,
|
||||
current_price,
|
||||
prev_price,
|
||||
blocks_old,
|
||||
days_old,
|
||||
older_than_hour,
|
||||
age,
|
||||
None,
|
||||
prev_price.map(|prev_price| (prev_price, supply)),
|
||||
);
|
||||
@@ -269,9 +265,7 @@ impl CohortState {
|
||||
supply: &SupplyState,
|
||||
current_price: Option<Dollars>,
|
||||
prev_price: Option<Dollars>,
|
||||
blocks_old: usize,
|
||||
days_old: f64,
|
||||
older_than_hour: bool,
|
||||
age: Age,
|
||||
price_to_amount_increment: Option<(Dollars, &SupplyState)>,
|
||||
price_to_amount_decrement: Option<(Dollars, &SupplyState)>,
|
||||
) {
|
||||
@@ -283,14 +277,13 @@ impl CohortState {
|
||||
|
||||
if supply.value > Sats::ZERO {
|
||||
self.sent += supply.value;
|
||||
self.satblocks_destroyed += supply.value * blocks_old;
|
||||
self.satdays_destroyed +=
|
||||
Sats::from((u64::from(supply.value) as f64 * days_old).floor() as u64);
|
||||
self.satblocks_destroyed += age.satblocks_destroyed(supply.value);
|
||||
self.satdays_destroyed += age.satdays_destroyed(supply.value);
|
||||
|
||||
if let Some(realized) = self.realized.as_mut() {
|
||||
let current_price = current_price.unwrap();
|
||||
let prev_price = prev_price.unwrap();
|
||||
realized.send(supply, current_price, prev_price, older_than_hour);
|
||||
realized.send(supply, current_price, prev_price);
|
||||
|
||||
if let Some((price, supply)) = price_to_amount_increment
|
||||
&& supply.value.is_not_zero()
|
||||
|
||||
@@ -8,9 +8,7 @@ pub struct RealizedState {
|
||||
pub profit: Dollars,
|
||||
pub loss: Dollars,
|
||||
pub value_created: Dollars,
|
||||
pub adj_value_created: Dollars,
|
||||
pub value_destroyed: Dollars,
|
||||
pub adj_value_destroyed: Dollars,
|
||||
}
|
||||
|
||||
impl RealizedState {
|
||||
@@ -19,9 +17,7 @@ impl RealizedState {
|
||||
profit: Dollars::NAN,
|
||||
loss: Dollars::NAN,
|
||||
value_created: Dollars::NAN,
|
||||
adj_value_created: Dollars::NAN,
|
||||
value_destroyed: Dollars::NAN,
|
||||
adj_value_destroyed: Dollars::NAN,
|
||||
};
|
||||
|
||||
pub fn reset_single_iteration_values(&mut self) {
|
||||
@@ -29,9 +25,7 @@ impl RealizedState {
|
||||
self.profit = Dollars::ZERO;
|
||||
self.loss = Dollars::ZERO;
|
||||
self.value_created = Dollars::ZERO;
|
||||
self.adj_value_created = Dollars::ZERO;
|
||||
self.value_destroyed = Dollars::ZERO;
|
||||
self.adj_value_destroyed = Dollars::ZERO;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,9 +43,7 @@ impl RealizedState {
|
||||
self.profit = Dollars::ZERO;
|
||||
self.loss = Dollars::ZERO;
|
||||
self.value_created = Dollars::ZERO;
|
||||
self.adj_value_created = Dollars::ZERO;
|
||||
self.value_destroyed = Dollars::ZERO;
|
||||
self.adj_value_destroyed = Dollars::ZERO;
|
||||
}
|
||||
|
||||
self.cap += realized_cap;
|
||||
@@ -74,7 +66,6 @@ impl RealizedState {
|
||||
supply_state: &SupplyState,
|
||||
current_price: Dollars,
|
||||
prev_price: Dollars,
|
||||
older_than_hour: bool,
|
||||
) {
|
||||
let current_value = current_price * supply_state.value;
|
||||
let prev_value = prev_price * supply_state.value;
|
||||
@@ -82,11 +73,6 @@ impl RealizedState {
|
||||
self.value_created += current_value;
|
||||
self.value_destroyed += prev_value;
|
||||
|
||||
if older_than_hour {
|
||||
self.adj_value_created += current_value;
|
||||
self.adj_value_destroyed += prev_value;
|
||||
}
|
||||
|
||||
match current_price.cmp(&prev_price) {
|
||||
Ordering::Greater => {
|
||||
self.profit += current_value.checked_sub(prev_value).unwrap();
|
||||
|
||||
Reference in New Issue
Block a user