use brk_error::Result; use brk_traversable::Traversable; use brk_types::{Bitcoin, DateIndex, Dollars, Height, Sats, StoredF32, StoredF64, Version}; use vecdb::{ EagerVec, Exit, ImportableVec, IterableCloneableVec, IterableVec, LazyVecFrom1, LazyVecFrom2, Negate, PcoVec, }; use crate::{ Indexes, grouped::{ ComputedVecsFromDateIndex, ComputedVecsFromHeight, LazyVecsFrom2FromDateIndex, LazyVecsFromDateIndex, NegPercentageDollarsF32, NegRatio32, PercentageDollarsF32, PercentageSatsF64, Ratio32, Source, VecBuilderOptions, }, indexes, }; use super::{ImportConfig, SupplyMetrics, UnrealizedMetrics}; /// Relative metrics comparing cohort values to global values. #[derive(Clone, Traversable)] pub struct RelativeMetrics { // === Supply Relative to Circulating Supply === pub indexes_to_supply_rel_to_circulating_supply: Option>, // === Supply in Profit/Loss Relative to Own Supply === pub height_to_supply_in_profit_rel_to_own_supply: EagerVec>, pub height_to_supply_in_loss_rel_to_own_supply: EagerVec>, pub indexes_to_supply_in_profit_rel_to_own_supply: LazyVecsFrom2FromDateIndex, pub indexes_to_supply_in_loss_rel_to_own_supply: LazyVecsFrom2FromDateIndex, // === Supply in Profit/Loss Relative to Circulating Supply === pub height_to_supply_in_profit_rel_to_circulating_supply: Option>>, pub height_to_supply_in_loss_rel_to_circulating_supply: Option>>, pub indexes_to_supply_in_profit_rel_to_circulating_supply: Option>, pub indexes_to_supply_in_loss_rel_to_circulating_supply: Option>, // === Unrealized vs Market Cap === pub height_to_unrealized_profit_rel_to_market_cap: EagerVec>, pub height_to_unrealized_loss_rel_to_market_cap: EagerVec>, pub height_to_neg_unrealized_loss_rel_to_market_cap: LazyVecFrom1, pub height_to_net_unrealized_pnl_rel_to_market_cap: EagerVec>, pub indexes_to_unrealized_profit_rel_to_market_cap: ComputedVecsFromDateIndex, pub indexes_to_unrealized_loss_rel_to_market_cap: ComputedVecsFromDateIndex, pub indexes_to_neg_unrealized_loss_rel_to_market_cap: LazyVecsFromDateIndex, pub indexes_to_net_unrealized_pnl_rel_to_market_cap: ComputedVecsFromDateIndex, // === Unrealized vs Own Market Cap (optional) === pub height_to_unrealized_profit_rel_to_own_market_cap: Option>>, pub height_to_unrealized_loss_rel_to_own_market_cap: Option>>, pub height_to_neg_unrealized_loss_rel_to_own_market_cap: Option>, pub height_to_net_unrealized_pnl_rel_to_own_market_cap: Option>>, pub indexes_to_unrealized_profit_rel_to_own_market_cap: Option>, pub indexes_to_unrealized_loss_rel_to_own_market_cap: Option>, pub indexes_to_neg_unrealized_loss_rel_to_own_market_cap: Option>, pub indexes_to_net_unrealized_pnl_rel_to_own_market_cap: Option>, // === Unrealized vs Own Total Unrealized PnL (optional) === pub height_to_unrealized_profit_rel_to_own_total_unrealized_pnl: Option>, pub height_to_unrealized_loss_rel_to_own_total_unrealized_pnl: Option>, pub height_to_neg_unrealized_loss_rel_to_own_total_unrealized_pnl: Option>, pub height_to_net_unrealized_pnl_rel_to_own_total_unrealized_pnl: Option>, pub indexes_to_unrealized_profit_rel_to_own_total_unrealized_pnl: Option>, pub indexes_to_unrealized_loss_rel_to_own_total_unrealized_pnl: Option>, pub indexes_to_neg_unrealized_loss_rel_to_own_total_unrealized_pnl: Option>, pub indexes_to_net_unrealized_pnl_rel_to_own_total_unrealized_pnl: Option>, } impl RelativeMetrics { /// Import relative metrics from database. pub fn forced_import( cfg: &ImportConfig, unrealized: &UnrealizedMetrics, supply: &SupplyMetrics, ) -> Result { let v0 = Version::ZERO; let v1 = Version::ONE; let v2 = Version::new(2); let extended = cfg.extended(); let compute_rel_to_all = cfg.compute_rel_to_all(); let last = VecBuilderOptions::default().add_last(); // Create sources for lazy neg vecs let height_to_unrealized_loss_rel_to_market_cap: EagerVec> = EagerVec::forced_import( cfg.db, &cfg.name("unrealized_loss_rel_to_market_cap"), cfg.version + v0, )?; let height_to_neg_unrealized_loss_rel_to_market_cap = LazyVecFrom1::transformed::( &cfg.name("neg_unrealized_loss_rel_to_market_cap"), cfg.version + v0, height_to_unrealized_loss_rel_to_market_cap.boxed_clone(), ); let indexes_to_unrealized_loss_rel_to_market_cap = ComputedVecsFromDateIndex::forced_import( cfg.db, &cfg.name("unrealized_loss_rel_to_market_cap"), Source::Compute, cfg.version + v1, cfg.indexes, last, )?; let indexes_to_neg_unrealized_loss_rel_to_market_cap = LazyVecsFromDateIndex::from_computed::( &cfg.name("neg_unrealized_loss_rel_to_market_cap"), cfg.version + v1, indexes_to_unrealized_loss_rel_to_market_cap .dateindex .as_ref() .map(|v| v.boxed_clone()), &indexes_to_unrealized_loss_rel_to_market_cap, ); // Optional: own market cap vecs let height_to_unrealized_loss_rel_to_own_market_cap: Option< EagerVec>, > = (extended && compute_rel_to_all) .then(|| { EagerVec::forced_import( cfg.db, &cfg.name("unrealized_loss_rel_to_own_market_cap"), cfg.version + v1, ) }) .transpose()?; let height_to_neg_unrealized_loss_rel_to_own_market_cap = height_to_unrealized_loss_rel_to_own_market_cap .as_ref() .map(|source| { LazyVecFrom1::transformed::( &cfg.name("neg_unrealized_loss_rel_to_own_market_cap"), cfg.version + v1, source.boxed_clone(), ) }); // Optional: own total unrealized pnl vecs (lazy from unrealized sources) let height_to_unrealized_profit_rel_to_own_total_unrealized_pnl = extended.then(|| { LazyVecFrom2::transformed::( &cfg.name("unrealized_profit_rel_to_own_total_unrealized_pnl"), cfg.version + v0, unrealized.height_to_unrealized_profit.boxed_clone(), unrealized.height_to_total_unrealized_pnl.boxed_clone(), ) }); let height_to_unrealized_loss_rel_to_own_total_unrealized_pnl = extended.then(|| { LazyVecFrom2::transformed::( &cfg.name("unrealized_loss_rel_to_own_total_unrealized_pnl"), cfg.version + v0, unrealized.height_to_unrealized_loss.boxed_clone(), unrealized.height_to_total_unrealized_pnl.boxed_clone(), ) }); let height_to_neg_unrealized_loss_rel_to_own_total_unrealized_pnl = extended.then(|| { LazyVecFrom2::transformed::( &cfg.name("neg_unrealized_loss_rel_to_own_total_unrealized_pnl"), cfg.version + v0, unrealized.height_to_unrealized_loss.boxed_clone(), unrealized.height_to_total_unrealized_pnl.boxed_clone(), ) }); let height_to_net_unrealized_pnl_rel_to_own_total_unrealized_pnl = extended.then(|| { LazyVecFrom2::transformed::( &cfg.name("net_unrealized_pnl_rel_to_own_total_unrealized_pnl"), cfg.version + v1, unrealized.height_to_net_unrealized_pnl.boxed_clone(), unrealized.height_to_total_unrealized_pnl.boxed_clone(), ) }); let indexes_to_unrealized_profit_rel_to_own_total_unrealized_pnl = extended.then(|| { LazyVecsFrom2FromDateIndex::from_computed::( &cfg.name("unrealized_profit_rel_to_own_total_unrealized_pnl"), cfg.version + v1, &unrealized.indexes_to_unrealized_profit, &unrealized.indexes_to_total_unrealized_pnl, ) }); let indexes_to_unrealized_loss_rel_to_own_total_unrealized_pnl = extended.then(|| { LazyVecsFrom2FromDateIndex::from_computed::( &cfg.name("unrealized_loss_rel_to_own_total_unrealized_pnl"), cfg.version + v1, &unrealized.indexes_to_unrealized_loss, &unrealized.indexes_to_total_unrealized_pnl, ) }); let indexes_to_neg_unrealized_loss_rel_to_own_total_unrealized_pnl = extended.then(|| { LazyVecsFrom2FromDateIndex::from_computed::( &cfg.name("neg_unrealized_loss_rel_to_own_total_unrealized_pnl"), cfg.version + v1, &unrealized.indexes_to_unrealized_loss, &unrealized.indexes_to_total_unrealized_pnl, ) }); let indexes_to_net_unrealized_pnl_rel_to_own_total_unrealized_pnl = extended.then(|| { LazyVecsFrom2FromDateIndex::from_computed::( &cfg.name("net_unrealized_pnl_rel_to_own_total_unrealized_pnl"), cfg.version + v1, &unrealized.indexes_to_net_unrealized_pnl, &unrealized.indexes_to_total_unrealized_pnl, ) }); Ok(Self { // === Supply Relative to Circulating Supply === indexes_to_supply_rel_to_circulating_supply: compute_rel_to_all .then(|| { ComputedVecsFromHeight::forced_import( cfg.db, &cfg.name("supply_rel_to_circulating_supply"), Source::Compute, cfg.version + v1, cfg.indexes, last, ) }) .transpose()?, // === Supply in Profit/Loss Relative to Own Supply === height_to_supply_in_profit_rel_to_own_supply: EagerVec::forced_import( cfg.db, &cfg.name("supply_in_profit_rel_to_own_supply"), cfg.version + v1, )?, height_to_supply_in_loss_rel_to_own_supply: EagerVec::forced_import( cfg.db, &cfg.name("supply_in_loss_rel_to_own_supply"), cfg.version + v1, )?, indexes_to_supply_in_profit_rel_to_own_supply: LazyVecsFrom2FromDateIndex::from_computed::( &cfg.name("supply_in_profit_rel_to_own_supply"), cfg.version + v1, &unrealized.indexes_to_supply_in_profit.sats, &supply.indexes_to_supply.sats, ), indexes_to_supply_in_loss_rel_to_own_supply: LazyVecsFrom2FromDateIndex::from_computed::< PercentageSatsF64, >( &cfg.name("supply_in_loss_rel_to_own_supply"), cfg.version + v1, &unrealized.indexes_to_supply_in_loss.sats, &supply.indexes_to_supply.sats, ), // === Supply in Profit/Loss Relative to Circulating Supply === height_to_supply_in_profit_rel_to_circulating_supply: compute_rel_to_all .then(|| { EagerVec::forced_import( cfg.db, &cfg.name("supply_in_profit_rel_to_circulating_supply"), cfg.version + v1, ) }) .transpose()?, height_to_supply_in_loss_rel_to_circulating_supply: compute_rel_to_all .then(|| { EagerVec::forced_import( cfg.db, &cfg.name("supply_in_loss_rel_to_circulating_supply"), cfg.version + v1, ) }) .transpose()?, indexes_to_supply_in_profit_rel_to_circulating_supply: compute_rel_to_all .then(|| { ComputedVecsFromDateIndex::forced_import( cfg.db, &cfg.name("supply_in_profit_rel_to_circulating_supply"), Source::Compute, cfg.version + v1, cfg.indexes, last, ) }) .transpose()?, indexes_to_supply_in_loss_rel_to_circulating_supply: compute_rel_to_all .then(|| { ComputedVecsFromDateIndex::forced_import( cfg.db, &cfg.name("supply_in_loss_rel_to_circulating_supply"), Source::Compute, cfg.version + v1, cfg.indexes, last, ) }) .transpose()?, // === Unrealized vs Market Cap === height_to_unrealized_profit_rel_to_market_cap: EagerVec::forced_import( cfg.db, &cfg.name("unrealized_profit_rel_to_market_cap"), cfg.version + v0, )?, height_to_unrealized_loss_rel_to_market_cap, height_to_neg_unrealized_loss_rel_to_market_cap, height_to_net_unrealized_pnl_rel_to_market_cap: EagerVec::forced_import( cfg.db, &cfg.name("net_unrealized_pnl_rel_to_market_cap"), cfg.version + v1, )?, indexes_to_unrealized_profit_rel_to_market_cap: ComputedVecsFromDateIndex::forced_import( cfg.db, &cfg.name("unrealized_profit_rel_to_market_cap"), Source::Compute, cfg.version + v1, cfg.indexes, last, )?, indexes_to_unrealized_loss_rel_to_market_cap, indexes_to_neg_unrealized_loss_rel_to_market_cap, indexes_to_net_unrealized_pnl_rel_to_market_cap: ComputedVecsFromDateIndex::forced_import( cfg.db, &cfg.name("net_unrealized_pnl_rel_to_market_cap"), Source::Compute, cfg.version + v1, cfg.indexes, last, )?, // === Unrealized vs Own Market Cap (optional) === height_to_unrealized_profit_rel_to_own_market_cap: (extended && compute_rel_to_all) .then(|| { EagerVec::forced_import( cfg.db, &cfg.name("unrealized_profit_rel_to_own_market_cap"), cfg.version + v1, ) }) .transpose()?, height_to_unrealized_loss_rel_to_own_market_cap, height_to_neg_unrealized_loss_rel_to_own_market_cap, height_to_net_unrealized_pnl_rel_to_own_market_cap: (extended && compute_rel_to_all) .then(|| { EagerVec::forced_import( cfg.db, &cfg.name("net_unrealized_pnl_rel_to_own_market_cap"), cfg.version + v2, ) }) .transpose()?, indexes_to_unrealized_profit_rel_to_own_market_cap: (extended && compute_rel_to_all) .then(|| { supply .indexes_to_supply .dollars .as_ref() .map(|supply_dollars| { LazyVecsFrom2FromDateIndex::from_computed::( &cfg.name("unrealized_profit_rel_to_own_market_cap"), cfg.version + v2, &unrealized.indexes_to_unrealized_profit, supply_dollars, ) }) }) .flatten(), indexes_to_unrealized_loss_rel_to_own_market_cap: (extended && compute_rel_to_all) .then(|| { supply .indexes_to_supply .dollars .as_ref() .map(|supply_dollars| { LazyVecsFrom2FromDateIndex::from_computed::( &cfg.name("unrealized_loss_rel_to_own_market_cap"), cfg.version + v2, &unrealized.indexes_to_unrealized_loss, supply_dollars, ) }) }) .flatten(), indexes_to_neg_unrealized_loss_rel_to_own_market_cap: (extended && compute_rel_to_all) .then(|| { supply .indexes_to_supply .dollars .as_ref() .map(|supply_dollars| { LazyVecsFrom2FromDateIndex::from_computed::( &cfg.name("neg_unrealized_loss_rel_to_own_market_cap"), cfg.version + v2, &unrealized.indexes_to_unrealized_loss, supply_dollars, ) }) }) .flatten(), indexes_to_net_unrealized_pnl_rel_to_own_market_cap: (extended && compute_rel_to_all) .then(|| { supply .indexes_to_supply .dollars .as_ref() .map(|supply_dollars| { LazyVecsFrom2FromDateIndex::from_computed::( &cfg.name("net_unrealized_pnl_rel_to_own_market_cap"), cfg.version + v2, &unrealized.indexes_to_net_unrealized_pnl, supply_dollars, ) }) }) .flatten(), // === Unrealized vs Own Total Unrealized PnL (optional) === height_to_unrealized_profit_rel_to_own_total_unrealized_pnl, height_to_unrealized_loss_rel_to_own_total_unrealized_pnl, height_to_neg_unrealized_loss_rel_to_own_total_unrealized_pnl, height_to_net_unrealized_pnl_rel_to_own_total_unrealized_pnl, indexes_to_unrealized_profit_rel_to_own_total_unrealized_pnl, indexes_to_unrealized_loss_rel_to_own_total_unrealized_pnl, indexes_to_neg_unrealized_loss_rel_to_own_total_unrealized_pnl, indexes_to_net_unrealized_pnl_rel_to_own_total_unrealized_pnl, }) } /// Second phase of computed metrics (ratios, relative values). /// /// This computes percentage ratios comparing cohort metrics to global metrics: /// - Supply relative to circulating supply /// - Supply in profit/loss relative to own supply and circulating supply /// - Unrealized profit/loss relative to market cap, total unrealized /// /// See `stateful/common/compute.rs` lines 800-1200 for the full original implementation. #[allow(clippy::too_many_arguments)] pub fn compute_rest_part2( &mut self, indexes: &indexes::Vecs, starting_indexes: &Indexes, height_to_supply: &impl IterableVec, dateindex_to_supply: &impl IterableVec, height_to_market_cap: Option<&impl IterableVec>, dateindex_to_market_cap: Option<&impl IterableVec>, supply: &SupplyMetrics, unrealized: Option<&super::UnrealizedMetrics>, exit: &Exit, ) -> Result<()> { // === Supply Relative to Circulating Supply === if let Some(v) = self.indexes_to_supply_rel_to_circulating_supply.as_mut() { v.compute_all(indexes, starting_indexes, exit, |v| { v.compute_percentage( starting_indexes.height, &supply.height_to_supply_value.bitcoin, height_to_supply, exit, )?; Ok(()) })?; } // === Supply in Profit/Loss Relative to Own Supply === // Note: indexes_to_* versions are now lazy (LazyVecsFrom2FromDateIndex) if let Some(unrealized) = unrealized { self.height_to_supply_in_profit_rel_to_own_supply .compute_percentage( starting_indexes.height, &unrealized.height_to_supply_in_profit_value.bitcoin, &supply.height_to_supply_value.bitcoin, exit, )?; self.height_to_supply_in_loss_rel_to_own_supply .compute_percentage( starting_indexes.height, &unrealized.height_to_supply_in_loss_value.bitcoin, &supply.height_to_supply_value.bitcoin, exit, )?; } // === Supply in Profit/Loss Relative to Circulating Supply === if let (Some(unrealized), Some(v)) = ( unrealized, self.height_to_supply_in_profit_rel_to_circulating_supply .as_mut(), ) { v.compute_percentage( starting_indexes.height, &unrealized.height_to_supply_in_profit_value.bitcoin, height_to_supply, exit, )?; } if let (Some(unrealized), Some(v)) = ( unrealized, self.height_to_supply_in_loss_rel_to_circulating_supply .as_mut(), ) { v.compute_percentage( starting_indexes.height, &unrealized.height_to_supply_in_loss_value.bitcoin, height_to_supply, exit, )?; } // === Unrealized vs Market Cap === if let (Some(unrealized), Some(height_to_mc)) = (unrealized, height_to_market_cap) { self.height_to_unrealized_profit_rel_to_market_cap .compute_percentage( starting_indexes.height, &unrealized.height_to_unrealized_profit, height_to_mc, exit, )?; self.height_to_unrealized_loss_rel_to_market_cap .compute_percentage( starting_indexes.height, &unrealized.height_to_unrealized_loss, height_to_mc, exit, )?; self.height_to_net_unrealized_pnl_rel_to_market_cap .compute_percentage( starting_indexes.height, &unrealized.height_to_net_unrealized_pnl, height_to_mc, exit, )?; } if let Some(dateindex_to_mc) = dateindex_to_market_cap && let Some(unrealized) = unrealized { self.indexes_to_unrealized_profit_rel_to_market_cap .compute_all(starting_indexes, exit, |v| { v.compute_percentage( starting_indexes.dateindex, &unrealized.dateindex_to_unrealized_profit, dateindex_to_mc, exit, )?; Ok(()) })?; self.indexes_to_unrealized_loss_rel_to_market_cap .compute_all(starting_indexes, exit, |v| { v.compute_percentage( starting_indexes.dateindex, &unrealized.dateindex_to_unrealized_loss, dateindex_to_mc, exit, )?; Ok(()) })?; } if let Some(dateindex_to_mc) = dateindex_to_market_cap && let Some(unrealized) = unrealized && let Some(dateindex_vec) = unrealized.indexes_to_net_unrealized_pnl.dateindex.as_ref() { self.indexes_to_net_unrealized_pnl_rel_to_market_cap .compute_all(starting_indexes, exit, |v| { v.compute_percentage( starting_indexes.dateindex, dateindex_vec, dateindex_to_mc, exit, )?; Ok(()) })?; } // === Supply in Profit/Loss Relative to Circulating Supply (indexes) === if let Some(v) = self .indexes_to_supply_in_profit_rel_to_circulating_supply .as_mut() && let Some(unrealized) = unrealized && let Some(dateindex_vec) = unrealized .indexes_to_supply_in_profit .bitcoin .dateindex .as_ref() { v.compute_all(starting_indexes, exit, |vec| { vec.compute_percentage( starting_indexes.dateindex, dateindex_vec, dateindex_to_supply, exit, )?; Ok(()) })?; } if let Some(v) = self .indexes_to_supply_in_loss_rel_to_circulating_supply .as_mut() && let Some(unrealized) = unrealized && let Some(dateindex_vec) = unrealized .indexes_to_supply_in_loss .bitcoin .dateindex .as_ref() { v.compute_all(starting_indexes, exit, |vec| { vec.compute_percentage( starting_indexes.dateindex, dateindex_vec, dateindex_to_supply, exit, )?; Ok(()) })?; } // === Unrealized vs Own Market Cap === // own_market_cap = supply_value.dollars // Note: indexes_to_* versions are now lazy (LazyVecsFrom2FromDateIndex) if let Some(unrealized) = unrealized { if let Some(v) = self .height_to_unrealized_profit_rel_to_own_market_cap .as_mut() && let Some(supply_dollars) = supply.height_to_supply_value.dollars.as_ref() { v.compute_percentage( starting_indexes.height, &unrealized.height_to_unrealized_profit, supply_dollars, exit, )?; } if let Some(v) = self .height_to_unrealized_loss_rel_to_own_market_cap .as_mut() && let Some(supply_dollars) = supply.height_to_supply_value.dollars.as_ref() { v.compute_percentage( starting_indexes.height, &unrealized.height_to_unrealized_loss, supply_dollars, exit, )?; } if let Some(v) = self .height_to_net_unrealized_pnl_rel_to_own_market_cap .as_mut() && let Some(supply_dollars) = supply.height_to_supply_value.dollars.as_ref() { v.compute_percentage( starting_indexes.height, &unrealized.height_to_net_unrealized_pnl, supply_dollars, exit, )?; } } Ok(()) } }