mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-06-14 08:43:35 -07:00
feat(cointime): add Reserve Risk metric
Implements Reserve Risk as a new market indicator for Bitcoin. ## Formula - Reserve Risk = Price / HODL Bank - HODL Bank = cumulative Σ(Price - avg_VOCDD) over time - VOCDD = CDD × Price (Value-weighted Coin Days Destroyed) ## Changes - Added `vocdd` (Value-weighted CDD) to `cointime/value` module - Created new `cointime/reserve_risk` module containing: - `vocdd_365d_sma`: 365-day moving average of VOCDD - `hodl_bank`: Cumulative opportunity cost of holding - `reserve_risk`: Final ratio metric for timing accumulation - Wired into cointime compute pipeline (price-dependent) ## Use Case Reserve Risk measures long-term holder confidence. Low values indicate high confidence and potential buying opportunity. High values suggest overheated market conditions. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -40,7 +40,7 @@ impl Vecs {
|
||||
|
||||
// Price-dependent metrics
|
||||
if let Some(price) = price {
|
||||
// Value computes (cointime value destroyed/created/stored)
|
||||
// Value computes (cointime value destroyed/created/stored, VOCDD)
|
||||
self.value.compute(
|
||||
indexes,
|
||||
starting_indexes,
|
||||
@@ -72,6 +72,14 @@ impl Vecs {
|
||||
&self.cap,
|
||||
exit,
|
||||
)?;
|
||||
|
||||
// Reserve Risk computes (depends on value.vocdd and price)
|
||||
self.reserve_risk.compute(
|
||||
starting_indexes,
|
||||
price,
|
||||
&self.value,
|
||||
exit,
|
||||
)?;
|
||||
}
|
||||
|
||||
let _lock = exit.lock();
|
||||
|
||||
@@ -6,7 +6,8 @@ use brk_types::Version;
|
||||
use vecdb::{Database, PAGE_SIZE};
|
||||
|
||||
use super::{
|
||||
ActivityVecs, AdjustedVecs, CapVecs, PricingVecs, SupplyVecs, ValueVecs, Vecs, DB_NAME, VERSION,
|
||||
ActivityVecs, AdjustedVecs, CapVecs, PricingVecs, ReserveRiskVecs, SupplyVecs, ValueVecs, Vecs,
|
||||
DB_NAME, VERSION,
|
||||
};
|
||||
use crate::{indexes, price};
|
||||
|
||||
@@ -22,6 +23,7 @@ impl Vecs {
|
||||
|
||||
let version = parent_version + VERSION;
|
||||
let v1 = version + Version::ONE;
|
||||
let compute_dollars = price.is_some();
|
||||
|
||||
let activity = ActivityVecs::forced_import(&db, version, indexes)?;
|
||||
let supply = SupplyVecs::forced_import(&db, v1, indexes, price)?;
|
||||
@@ -29,6 +31,7 @@ impl Vecs {
|
||||
let cap = CapVecs::forced_import(&db, v1, indexes)?;
|
||||
let pricing = PricingVecs::forced_import(&db, version, indexes, price)?;
|
||||
let adjusted = AdjustedVecs::forced_import(&db, version, indexes)?;
|
||||
let reserve_risk = ReserveRiskVecs::forced_import(&db, v1, indexes, compute_dollars)?;
|
||||
|
||||
let this = Self {
|
||||
db,
|
||||
@@ -38,6 +41,7 @@ impl Vecs {
|
||||
cap,
|
||||
pricing,
|
||||
adjusted,
|
||||
reserve_risk,
|
||||
};
|
||||
|
||||
this.db.retain_regions(
|
||||
|
||||
@@ -2,6 +2,7 @@ pub mod activity;
|
||||
pub mod adjusted;
|
||||
pub mod cap;
|
||||
pub mod pricing;
|
||||
pub mod reserve_risk;
|
||||
pub mod supply;
|
||||
pub mod value;
|
||||
|
||||
@@ -16,6 +17,7 @@ pub use activity::Vecs as ActivityVecs;
|
||||
pub use adjusted::Vecs as AdjustedVecs;
|
||||
pub use cap::Vecs as CapVecs;
|
||||
pub use pricing::Vecs as PricingVecs;
|
||||
pub use reserve_risk::Vecs as ReserveRiskVecs;
|
||||
pub use supply::Vecs as SupplyVecs;
|
||||
pub use value::Vecs as ValueVecs;
|
||||
|
||||
@@ -33,4 +35,5 @@ pub struct Vecs {
|
||||
pub cap: CapVecs,
|
||||
pub pricing: PricingVecs,
|
||||
pub adjusted: AdjustedVecs,
|
||||
pub reserve_risk: ReserveRiskVecs,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
use brk_error::Result;
|
||||
use brk_types::{DateIndex, StoredF64};
|
||||
use vecdb::{Exit, TypedVecIterator};
|
||||
|
||||
use super::{super::value, Vecs};
|
||||
use crate::{price, ComputeIndexes};
|
||||
|
||||
impl Vecs {
|
||||
pub fn compute(
|
||||
&mut self,
|
||||
starting_indexes: &ComputeIndexes,
|
||||
price: &price::Vecs,
|
||||
value: &value::Vecs,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
// Get VOCDD dateindex sum data (from cointime/value module)
|
||||
// The dateindex.sum.0 contains daily VOCDD values as EagerVec
|
||||
let vocdd_dateindex_sum = &value.vocdd.dateindex.sum.0;
|
||||
|
||||
// Compute 365-day SMA of VOCDD
|
||||
self.vocdd_365d_sma.compute_sma(
|
||||
starting_indexes.dateindex,
|
||||
vocdd_dateindex_sum,
|
||||
365,
|
||||
exit,
|
||||
)?;
|
||||
|
||||
let price_close = &price.usd.split.close.dateindex;
|
||||
|
||||
// Compute HODL Bank = cumulative sum of (price - vocdd_sma)
|
||||
// Start from where we left off and maintain cumulative state
|
||||
let starting_dateindex = starting_indexes.dateindex.to_usize().min(self.hodl_bank.len());
|
||||
let target_len = price_close.len().min(self.vocdd_365d_sma.len());
|
||||
|
||||
if target_len > starting_dateindex {
|
||||
let mut price_iter = price_close.into_iter();
|
||||
let mut vocdd_sma_iter = self.vocdd_365d_sma.into_iter();
|
||||
|
||||
// Get previous cumulative value, or start at 0
|
||||
let mut cumulative: f64 = if starting_dateindex > 0 {
|
||||
let prev_dateindex = DateIndex::from(starting_dateindex - 1);
|
||||
f64::from(*self.hodl_bank.into_iter().get_unwrap(prev_dateindex))
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
for i in starting_dateindex..target_len {
|
||||
let dateindex = DateIndex::from(i);
|
||||
let price_val = f64::from(*price_iter.get_unwrap(dateindex));
|
||||
let vocdd_sma = f64::from(*vocdd_sma_iter.get_unwrap(dateindex));
|
||||
|
||||
// HODL Bank contribution: price - smoothed VOCDD
|
||||
// Accumulate over time
|
||||
cumulative += price_val - vocdd_sma;
|
||||
self.hodl_bank
|
||||
.truncate_push(dateindex, StoredF64::from(cumulative))?;
|
||||
|
||||
exit.check()?;
|
||||
}
|
||||
self.hodl_bank.write()?;
|
||||
}
|
||||
|
||||
// Compute Reserve Risk = price / hodl_bank (if enabled)
|
||||
if let Some(reserve_risk) = self.reserve_risk.as_mut() {
|
||||
reserve_risk.compute_all(starting_indexes, exit, |v| {
|
||||
v.compute_divide(
|
||||
starting_indexes.dateindex,
|
||||
price_close,
|
||||
&self.hodl_bank,
|
||||
exit,
|
||||
)?;
|
||||
Ok(())
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
use brk_error::Result;
|
||||
use brk_types::Version;
|
||||
use vecdb::{Database, EagerVec, ImportableVec};
|
||||
|
||||
use super::Vecs;
|
||||
use crate::{indexes, internal::ComputedFromDateLast};
|
||||
|
||||
impl Vecs {
|
||||
pub fn forced_import(
|
||||
db: &Database,
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
compute_dollars: bool,
|
||||
) -> Result<Self> {
|
||||
Ok(Self {
|
||||
vocdd_365d_sma: EagerVec::forced_import(db, "vocdd_365d_sma", version)?,
|
||||
hodl_bank: EagerVec::forced_import(db, "hodl_bank", version)?,
|
||||
reserve_risk: compute_dollars
|
||||
.then(|| ComputedFromDateLast::forced_import(db, "reserve_risk", version, indexes))
|
||||
.transpose()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
mod compute;
|
||||
mod import;
|
||||
mod vecs;
|
||||
|
||||
pub use vecs::Vecs;
|
||||
@@ -0,0 +1,26 @@
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{DateIndex, StoredF64};
|
||||
use vecdb::{EagerVec, PcoVec};
|
||||
|
||||
use crate::internal::ComputedFromDateLast;
|
||||
|
||||
/// Reserve Risk metric components.
|
||||
///
|
||||
/// Reserve Risk = Price / HODL Bank
|
||||
/// Where HODL Bank = Σ(Price - avg_VOCDD) over time
|
||||
///
|
||||
/// Low Reserve Risk = high long-term holder confidence = good buying opportunity.
|
||||
#[derive(Clone, Traversable)]
|
||||
pub struct Vecs {
|
||||
/// Moving average of VOCDD (Value-weighted CDD) over 365 days
|
||||
/// Used to smooth the VOCDD signal for HODL Bank calculation
|
||||
pub vocdd_365d_sma: EagerVec<PcoVec<DateIndex, StoredF64>>,
|
||||
|
||||
/// HODL Bank = cumulative sum of (price - vocdd_365d_sma)
|
||||
/// Represents the opportunity cost of holding Bitcoin vs trading
|
||||
pub hodl_bank: EagerVec<PcoVec<DateIndex, StoredF64>>,
|
||||
|
||||
/// Reserve Risk = price / hodl_bank
|
||||
/// A timing indicator for long-term Bitcoin accumulation
|
||||
pub reserve_risk: Option<ComputedFromDateLast<StoredF64>>,
|
||||
}
|
||||
@@ -22,6 +22,13 @@ impl Vecs {
|
||||
.activity
|
||||
.coinblocks_destroyed;
|
||||
|
||||
let coindays_destroyed = &distribution
|
||||
.utxo_cohorts
|
||||
.all
|
||||
.metrics
|
||||
.activity
|
||||
.coindays_destroyed;
|
||||
|
||||
self.cointime_value_destroyed
|
||||
.compute_all(indexes, starting_indexes, exit, |vec| {
|
||||
vec.compute_multiply(
|
||||
@@ -55,6 +62,19 @@ impl Vecs {
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// VOCDD: Value-weighted Coin Days Destroyed = CDD × price
|
||||
// This is a key input for Reserve Risk calculation
|
||||
self.vocdd
|
||||
.compute_all(indexes, starting_indexes, exit, |vec| {
|
||||
vec.compute_multiply(
|
||||
starting_indexes.height,
|
||||
&price.usd.split.close.height,
|
||||
&coindays_destroyed.height,
|
||||
exit,
|
||||
)?;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,12 @@ impl Vecs {
|
||||
version,
|
||||
indexes,
|
||||
)?,
|
||||
vocdd: ComputedFromHeightSumCum::forced_import(
|
||||
db,
|
||||
"vocdd",
|
||||
version,
|
||||
indexes,
|
||||
)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,4 +8,7 @@ pub struct Vecs {
|
||||
pub cointime_value_destroyed: ComputedFromHeightSumCum<StoredF64>,
|
||||
pub cointime_value_created: ComputedFromHeightSumCum<StoredF64>,
|
||||
pub cointime_value_stored: ComputedFromHeightSumCum<StoredF64>,
|
||||
/// Value-weighted Coin Days Destroyed (CDD × price)
|
||||
/// Used for Reserve Risk calculation: VOCDD = coindays_destroyed × price
|
||||
pub vocdd: ComputedFromHeightSumCum<StoredF64>,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user