From f494486e12a71bed673c06a5cc51a8e8861fdef9 Mon Sep 17 00:00:00 2001 From: Brandon Collins Date: Tue, 20 Jan 2026 10:53:20 -0500 Subject: [PATCH] feat(cointime): add Reserve Risk metric MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- crates/brk_computer/src/cointime/compute.rs | 10 ++- crates/brk_computer/src/cointime/import.rs | 6 +- crates/brk_computer/src/cointime/mod.rs | 3 + .../src/cointime/reserve_risk/compute.rs | 78 +++++++++++++++++++ .../src/cointime/reserve_risk/import.rs | 23 ++++++ .../src/cointime/reserve_risk/mod.rs | 5 ++ .../src/cointime/reserve_risk/vecs.rs | 26 +++++++ .../src/cointime/value/compute.rs | 20 +++++ .../brk_computer/src/cointime/value/import.rs | 6 ++ .../brk_computer/src/cointime/value/vecs.rs | 3 + 10 files changed, 178 insertions(+), 2 deletions(-) create mode 100644 crates/brk_computer/src/cointime/reserve_risk/compute.rs create mode 100644 crates/brk_computer/src/cointime/reserve_risk/import.rs create mode 100644 crates/brk_computer/src/cointime/reserve_risk/mod.rs create mode 100644 crates/brk_computer/src/cointime/reserve_risk/vecs.rs diff --git a/crates/brk_computer/src/cointime/compute.rs b/crates/brk_computer/src/cointime/compute.rs index 2e662cb2f..9176795ab 100644 --- a/crates/brk_computer/src/cointime/compute.rs +++ b/crates/brk_computer/src/cointime/compute.rs @@ -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(); diff --git a/crates/brk_computer/src/cointime/import.rs b/crates/brk_computer/src/cointime/import.rs index 5a9a76754..7030d025e 100644 --- a/crates/brk_computer/src/cointime/import.rs +++ b/crates/brk_computer/src/cointime/import.rs @@ -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( diff --git a/crates/brk_computer/src/cointime/mod.rs b/crates/brk_computer/src/cointime/mod.rs index 686d84930..6d3e97a3b 100644 --- a/crates/brk_computer/src/cointime/mod.rs +++ b/crates/brk_computer/src/cointime/mod.rs @@ -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, } diff --git a/crates/brk_computer/src/cointime/reserve_risk/compute.rs b/crates/brk_computer/src/cointime/reserve_risk/compute.rs new file mode 100644 index 000000000..a47273dbb --- /dev/null +++ b/crates/brk_computer/src/cointime/reserve_risk/compute.rs @@ -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(()) + } +} diff --git a/crates/brk_computer/src/cointime/reserve_risk/import.rs b/crates/brk_computer/src/cointime/reserve_risk/import.rs new file mode 100644 index 000000000..1909b700b --- /dev/null +++ b/crates/brk_computer/src/cointime/reserve_risk/import.rs @@ -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 { + 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()?, + }) + } +} diff --git a/crates/brk_computer/src/cointime/reserve_risk/mod.rs b/crates/brk_computer/src/cointime/reserve_risk/mod.rs new file mode 100644 index 000000000..1136f9ebd --- /dev/null +++ b/crates/brk_computer/src/cointime/reserve_risk/mod.rs @@ -0,0 +1,5 @@ +mod compute; +mod import; +mod vecs; + +pub use vecs::Vecs; diff --git a/crates/brk_computer/src/cointime/reserve_risk/vecs.rs b/crates/brk_computer/src/cointime/reserve_risk/vecs.rs new file mode 100644 index 000000000..9471b5ae4 --- /dev/null +++ b/crates/brk_computer/src/cointime/reserve_risk/vecs.rs @@ -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>, + + /// HODL Bank = cumulative sum of (price - vocdd_365d_sma) + /// Represents the opportunity cost of holding Bitcoin vs trading + pub hodl_bank: EagerVec>, + + /// Reserve Risk = price / hodl_bank + /// A timing indicator for long-term Bitcoin accumulation + pub reserve_risk: Option>, +} diff --git a/crates/brk_computer/src/cointime/value/compute.rs b/crates/brk_computer/src/cointime/value/compute.rs index 5aa45229a..f2e5b8706 100644 --- a/crates/brk_computer/src/cointime/value/compute.rs +++ b/crates/brk_computer/src/cointime/value/compute.rs @@ -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(()) } } diff --git a/crates/brk_computer/src/cointime/value/import.rs b/crates/brk_computer/src/cointime/value/import.rs index 62b76496b..54d6022e0 100644 --- a/crates/brk_computer/src/cointime/value/import.rs +++ b/crates/brk_computer/src/cointime/value/import.rs @@ -26,6 +26,12 @@ impl Vecs { version, indexes, )?, + vocdd: ComputedFromHeightSumCum::forced_import( + db, + "vocdd", + version, + indexes, + )?, }) } } diff --git a/crates/brk_computer/src/cointime/value/vecs.rs b/crates/brk_computer/src/cointime/value/vecs.rs index 6767b3066..3d30d3269 100644 --- a/crates/brk_computer/src/cointime/value/vecs.rs +++ b/crates/brk_computer/src/cointime/value/vecs.rs @@ -8,4 +8,7 @@ pub struct Vecs { pub cointime_value_destroyed: ComputedFromHeightSumCum, pub cointime_value_created: ComputedFromHeightSumCum, pub cointime_value_stored: ComputedFromHeightSumCum, + /// Value-weighted Coin Days Destroyed (CDD × price) + /// Used for Reserve Risk calculation: VOCDD = coindays_destroyed × price + pub vocdd: ComputedFromHeightSumCum, }