diff --git a/crates/brk_computer/src/cointime.rs b/crates/brk_computer/src/cointime.rs index bb8989e1e..56185972b 100644 --- a/crates/brk_computer/src/cointime.rs +++ b/crates/brk_computer/src/cointime.rs @@ -314,7 +314,7 @@ impl Vecs { stateful: &stateful::Vecs, exit: &Exit, ) -> Result<()> { - let circulating_supply = &stateful.utxo_cohorts.all.1.height_to_supply; + let circulating_supply = &stateful.utxo_cohorts.all.inner.height_to_supply; self.indexes_to_coinblocks_created .compute_all(indexes, starting_indexes, exit, |vec| { @@ -328,7 +328,7 @@ impl Vecs { })?; let indexes_to_coinblocks_destroyed = - &stateful.utxo_cohorts.all.1.indexes_to_coinblocks_destroyed; + &stateful.utxo_cohorts.all.inner.indexes_to_coinblocks_destroyed; self.indexes_to_coinblocks_stored .compute_all(indexes, starting_indexes, exit, |vec| { @@ -454,7 +454,7 @@ impl Vecs { let realized_cap = stateful .utxo_cohorts .all - .1 + .inner .height_to_realized_cap .as_ref() .unwrap(); @@ -462,7 +462,7 @@ impl Vecs { let realized_price = stateful .utxo_cohorts .all - .1 + .inner .indexes_to_realized_price .as_ref() .unwrap() diff --git a/crates/brk_computer/src/grouped/price_percentiles.rs b/crates/brk_computer/src/grouped/price_percentiles.rs index a0594f4d5..c9072c9bb 100644 --- a/crates/brk_computer/src/grouped/price_percentiles.rs +++ b/crates/brk_computer/src/grouped/price_percentiles.rs @@ -1,14 +1,14 @@ use brk_error::Result; use brk_traversable::{Traversable, TreeNode}; use brk_types::{Dollars, Height, Version}; -use vecdb::{AnyExportableVec, Database, EagerVec, Exit, PcoVec}; +use vecdb::{AnyExportableVec, Database, EagerVec, Exit, GenericStoredVec, PcoVec}; -use crate::{indexes, Indexes}; +use crate::{Indexes, indexes}; use super::{ComputedVecsFromHeight, Source, VecBuilderOptions}; -pub const PERCENTILES: [u8; 21] = [ - 0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95, 100, +pub const PERCENTILES: [u8; 19] = [ + 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95, ]; pub const PERCENTILES_LEN: usize = PERCENTILES.len(); @@ -51,7 +51,10 @@ impl PricePercentiles { ) -> Result<()> { for (i, vec) in self.vecs.iter_mut().enumerate() { if let Some(v) = vec { - v.height.as_mut().unwrap().truncate_push(height, percentile_prices[i])?; + v.height + .as_mut() + .unwrap() + .truncate_push(height, percentile_prices[i])?; } } Ok(()) @@ -64,7 +67,12 @@ impl PricePercentiles { exit: &Exit, ) -> Result<()> { for vec in self.vecs.iter_mut().flatten() { - vec.compute_rest(indexes, starting_indexes, exit, None::<&EagerVec>>)?; + vec.compute_rest( + indexes, + starting_indexes, + exit, + None::<&EagerVec>>, + )?; } Ok(()) } @@ -89,6 +97,9 @@ impl Traversable for PricePercentiles { } fn iter_any_exportable(&self) -> impl Iterator { - self.vecs.iter().flatten().flat_map(|p| p.iter_any_exportable()) + self.vecs + .iter() + .flatten() + .flat_map(|p| p.iter_any_exportable()) } } diff --git a/crates/brk_computer/src/lib.rs b/crates/brk_computer/src/lib.rs index c75632a8b..4afc9a791 100644 --- a/crates/brk_computer/src/lib.rs +++ b/crates/brk_computer/src/lib.rs @@ -239,8 +239,6 @@ impl Computer { exit, )?; - return Ok(()); - info!("Computing cointime..."); self.cointime.compute( &self.indexes, diff --git a/crates/brk_computer/src/stateful/address_cohort.rs b/crates/brk_computer/src/stateful/address_cohort.rs index ecf71308b..fa1bed7ca 100644 --- a/crates/brk_computer/src/stateful/address_cohort.rs +++ b/crates/brk_computer/src/stateful/address_cohort.rs @@ -1,6 +1,7 @@ use std::path::Path; use brk_error::Result; +use brk_grouper::{CohortContext, Filter, Filtered}; use brk_traversable::Traversable; use brk_types::{Bitcoin, DateIndex, Dollars, Height, StoredU64, Version}; use vecdb::{ @@ -39,7 +40,7 @@ impl Vecs { #[allow(clippy::too_many_arguments)] pub fn forced_import( db: &Database, - cohort_name: Option<&str>, + filter: Filter, version: Version, indexes: &indexes::Vecs, price: Option<&price::Vecs>, @@ -48,16 +49,19 @@ impl Vecs { ) -> Result { let compute_dollars = price.is_some(); - let suffix = |s: &str| cohort_name.map_or(s.to_string(), |name| format!("{name}_{s}")); + let full_name = filter.to_full_name(CohortContext::Address); + let suffix = |s: &str| { + if full_name.is_empty() { + s.to_string() + } else { + format!("{full_name}_{s}") + } + }; Ok(Self { starting_height: None, state: states_path.map(|states_path| { - AddressCohortState::new( - states_path, - cohort_name.unwrap_or_default(), - compute_dollars, - ) + AddressCohortState::new(states_path, &full_name, compute_dollars) }), height_to_addr_count: EagerVec::forced_import( db, @@ -74,7 +78,8 @@ impl Vecs { )?, inner: common::Vecs::forced_import( db, - cohort_name, + filter, + CohortContext::Address, version, indexes, price, @@ -230,3 +235,9 @@ impl CohortVecs for Vecs { ) } } + +impl Filtered for Vecs { + fn filter(&self) -> &Filter { + &self.inner.filter + } +} diff --git a/crates/brk_computer/src/stateful/address_cohorts.rs b/crates/brk_computer/src/stateful/address_cohorts.rs index b05cd9250..6635d372a 100644 --- a/crates/brk_computer/src/stateful/address_cohorts.rs +++ b/crates/brk_computer/src/stateful/address_cohorts.rs @@ -1,7 +1,7 @@ use std::path::Path; use brk_error::Result; -use brk_grouper::{AddressGroups, ByAmountRange, ByGreatEqualAmount, ByLowerThanAmount, Filtered}; +use brk_grouper::{AddressGroups, AmountFilter, Filter, Filtered}; use brk_traversable::Traversable; use brk_types::{Bitcoin, DateIndex, Dollars, Height, Version}; use derive_deref::{Deref, DerefMut}; @@ -18,7 +18,7 @@ use crate::{ const VERSION: Version = Version::new(0); #[derive(Clone, Deref, DerefMut, Traversable)] -pub struct Vecs(AddressGroups>); +pub struct Vecs(AddressGroups); impl Vecs { pub fn forced_import( @@ -28,386 +28,23 @@ impl Vecs { price: Option<&price::Vecs>, states_path: &Path, ) -> Result { - Ok(Self( - AddressGroups { - amount_range: ByAmountRange { - _0sats: address_cohort::Vecs::forced_import( - db, - Some("addrs_with_0sats"), - version + VERSION + Version::ZERO, - indexes, - price, - Some(states_path), - true, - )?, - _1sat_to_10sats: address_cohort::Vecs::forced_import( - db, - Some("addrs_above_1sat_under_10sats"), - version + VERSION + Version::ZERO, - indexes, - price, - Some(states_path), - true, - )?, - _10sats_to_100sats: address_cohort::Vecs::forced_import( - db, - Some("addrs_above_10sats_under_100sats"), - version + VERSION + Version::ZERO, - indexes, - price, - Some(states_path), - true, - )?, - _100sats_to_1k_sats: address_cohort::Vecs::forced_import( - db, - Some("addrs_above_100sats_under_1k_sats"), - version + VERSION + Version::ZERO, - indexes, - price, - Some(states_path), - true, - )?, - _1k_sats_to_10k_sats: address_cohort::Vecs::forced_import( - db, - Some("addrs_above_1k_sats_under_10k_sats"), - version + VERSION + Version::ZERO, - indexes, - price, - Some(states_path), - true, - )?, - _10k_sats_to_100k_sats: address_cohort::Vecs::forced_import( - db, - Some("addrs_above_10k_sats_under_100k_sats"), - version + VERSION + Version::ZERO, - indexes, - price, - Some(states_path), - true, - )?, - _100k_sats_to_1m_sats: address_cohort::Vecs::forced_import( - db, - Some("addrs_above_100k_sats_under_1m_sats"), - version + VERSION + Version::ZERO, - indexes, - price, - Some(states_path), - true, - )?, - _1m_sats_to_10m_sats: address_cohort::Vecs::forced_import( - db, - Some("addrs_above_1m_sats_under_10m_sats"), - version + VERSION + Version::ZERO, - indexes, - price, - Some(states_path), - true, - )?, - _10m_sats_to_1btc: address_cohort::Vecs::forced_import( - db, - Some("addrs_above_10m_sats_under_1btc"), - version + VERSION + Version::ZERO, - indexes, - price, - Some(states_path), - true, - )?, - _1btc_to_10btc: address_cohort::Vecs::forced_import( - db, - Some("addrs_above_1btc_under_10btc"), - version + VERSION + Version::ZERO, - indexes, - price, - Some(states_path), - true, - )?, - _10btc_to_100btc: address_cohort::Vecs::forced_import( - db, - Some("addrs_above_10btc_under_100btc"), - version + VERSION + Version::ZERO, - indexes, - price, - Some(states_path), - true, - )?, - _100btc_to_1k_btc: address_cohort::Vecs::forced_import( - db, - Some("addrs_above_100btc_under_1k_btc"), - version + VERSION + Version::ZERO, - indexes, - price, - Some(states_path), - true, - )?, - _1k_btc_to_10k_btc: address_cohort::Vecs::forced_import( - db, - Some("addrs_above_1k_btc_under_10k_btc"), - version + VERSION + Version::ZERO, - indexes, - price, - Some(states_path), - true, - )?, - _10k_btc_to_100k_btc: address_cohort::Vecs::forced_import( - db, - Some("addrs_above_10k_btc_under_100k_btc"), - version + VERSION + Version::ZERO, - indexes, - price, - Some(states_path), - true, - )?, - _100k_btc_or_more: address_cohort::Vecs::forced_import( - db, - Some("addrs_above_100k_btc"), - version + VERSION + Version::ZERO, - indexes, - price, - Some(states_path), - true, - )?, - }, - lt_amount: ByLowerThanAmount { - _10sats: address_cohort::Vecs::forced_import( - db, - Some("addrs_under_10sats"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - true, - )?, - _100sats: address_cohort::Vecs::forced_import( - db, - Some("addrs_under_100sats"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - true, - )?, - _1k_sats: address_cohort::Vecs::forced_import( - db, - Some("addrs_under_1k_sats"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - true, - )?, - _10k_sats: address_cohort::Vecs::forced_import( - db, - Some("addrs_under_10k_sats"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - true, - )?, - _100k_sats: address_cohort::Vecs::forced_import( - db, - Some("addrs_under_100k_sats"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - true, - )?, - _1m_sats: address_cohort::Vecs::forced_import( - db, - Some("addrs_under_1m_sats"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - true, - )?, - _10m_sats: address_cohort::Vecs::forced_import( - db, - Some("addrs_under_10m_sats"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - true, - )?, - _1btc: address_cohort::Vecs::forced_import( - db, - Some("addrs_under_1btc"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - true, - )?, - _10btc: address_cohort::Vecs::forced_import( - db, - Some("addrs_under_10btc"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - true, - )?, - _100btc: address_cohort::Vecs::forced_import( - db, - Some("addrs_under_100btc"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - true, - )?, - _1k_btc: address_cohort::Vecs::forced_import( - db, - Some("addrs_under_1k_btc"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - true, - )?, - _10k_btc: address_cohort::Vecs::forced_import( - db, - Some("addrs_under_10k_btc"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - true, - )?, - _100k_btc: address_cohort::Vecs::forced_import( - db, - Some("addrs_under_100k_btc"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - true, - )?, - }, - ge_amount: ByGreatEqualAmount { - _1sat: address_cohort::Vecs::forced_import( - db, - Some("addrs_above_1sat"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - true, - )?, - _10sats: address_cohort::Vecs::forced_import( - db, - Some("addrs_above_10sats"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - true, - )?, - _100sats: address_cohort::Vecs::forced_import( - db, - Some("addrs_above_100sats"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - true, - )?, - _1k_sats: address_cohort::Vecs::forced_import( - db, - Some("addrs_above_1k_sats"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - true, - )?, - _10k_sats: address_cohort::Vecs::forced_import( - db, - Some("addrs_above_10k_sats"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - true, - )?, - _100k_sats: address_cohort::Vecs::forced_import( - db, - Some("addrs_above_100k_sats"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - true, - )?, - _1m_sats: address_cohort::Vecs::forced_import( - db, - Some("addrs_above_1m_sats"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - true, - )?, - _10m_sats: address_cohort::Vecs::forced_import( - db, - Some("addrs_above_10m_sats"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - true, - )?, - _1btc: address_cohort::Vecs::forced_import( - db, - Some("addrs_above_1btc"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - true, - )?, - _10btc: address_cohort::Vecs::forced_import( - db, - Some("addrs_above_10btc"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - true, - )?, - _100btc: address_cohort::Vecs::forced_import( - db, - Some("addrs_above_100btc"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - true, - )?, - _1k_btc: address_cohort::Vecs::forced_import( - db, - Some("addrs_above_1k_btc"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - true, - )?, - _10k_btc: address_cohort::Vecs::forced_import( - db, - Some("addrs_above_10k_btc"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - true, - )?, - }, - } - .into(), - )) + Ok(Self(AddressGroups::new(|filter| { + let states_path = match &filter { + Filter::Amount(AmountFilter::Range(_)) => Some(states_path), + _ => None, + }; + + address_cohort::Vecs::forced_import( + db, + filter, + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + true, + ) + .unwrap() + }))) } pub fn compute_overlapping_vecs( @@ -421,13 +58,13 @@ impl Vecs { self.0 .ge_amount .iter_mut() - .map(|Filtered(filter, vecs)| { + .map(|vecs| { + let filter = vecs.filter().clone(); ( vecs, by_size_range .iter() - .filter(|Filtered(other, _)| filter.includes(other)) - .map(Filtered::t) + .filter(|other| filter.includes(other.filter())) .collect::>(), ) }) @@ -435,13 +72,13 @@ impl Vecs { self.0 .lt_amount .iter_mut() - .map(|Filtered(filter, vecs)| { + .map(|vecs| { + let filter = vecs.filter().clone(); ( vecs, by_size_range .iter() - .filter(|Filtered(other, _)| filter.includes(other)) - .map(Filtered::t) + .filter(|other| filter.includes(other.filter())) .collect::>(), ) }) @@ -462,7 +99,6 @@ impl Vecs { exit: &Exit, ) -> Result<()> { self.iter_mut() - .map(Filtered::mut_t) .try_for_each(|v| v.compute_rest_part1(indexes, price, starting_indexes, exit)) } @@ -480,7 +116,7 @@ impl Vecs { dateindex_to_realized_cap: Option<&impl IterableVec>, exit: &Exit, ) -> Result<()> { - self.0.iter_mut().map(Filtered::mut_t).try_for_each(|v| { + self.0.iter_mut().try_for_each(|v| { v.compute_rest_part2( indexes, price, @@ -498,7 +134,6 @@ impl Vecs { pub fn safe_flush_stateful_vecs(&mut self, height: Height, exit: &Exit) -> Result<()> { self.iter_separate_mut() - .map(Filtered::mut_t) .try_for_each(|v| v.safe_flush_stateful_vecs(height, exit)) } } diff --git a/crates/brk_computer/src/stateful/common.rs b/crates/brk_computer/src/stateful/common.rs index 818bcf8df..62f7586df 100644 --- a/crates/brk_computer/src/stateful/common.rs +++ b/crates/brk_computer/src/stateful/common.rs @@ -1,4 +1,5 @@ use brk_error::{Error, Result}; +use brk_grouper::{CohortContext, Filter}; use brk_traversable::Traversable; use brk_types::{ Bitcoin, DateIndex, Dollars, Height, Sats, StoredF32, StoredF64, StoredU64, Version, @@ -21,6 +22,9 @@ use crate::{ #[derive(Clone, Traversable)] pub struct Vecs { + #[traversable(skip)] + pub filter: Filter, + // Cumulative pub height_to_realized_cap: Option>>, pub height_to_supply: EagerVec>, @@ -167,7 +171,8 @@ impl Vecs { #[allow(clippy::too_many_arguments)] pub fn forced_import( db: &Database, - cohort_name: Option<&str>, + filter: Filter, + context: CohortContext, parent_version: Version, indexes: &indexes::Vecs, price: Option<&price::Vecs>, @@ -179,9 +184,14 @@ impl Vecs { let version = parent_version + Version::ZERO; - // let prefix = |s: &str| cohort_name.map_or(s.to_string(), |name| format!("{s}_{name}")); - - let suffix = |s: &str| cohort_name.map_or(s.to_string(), |name| format!("{name}_{s}")); + 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}") + } + }; let dateindex_to_supply_in_profit = compute_dollars.then(|| { EagerVec::forced_import(db, &suffix("supply_in_profit"), version + Version::ZERO) @@ -203,6 +213,8 @@ impl Vecs { }); Ok(Self { + filter, + height_to_supply_in_profit: compute_dollars.then(|| { EagerVec::forced_import(db, &suffix("supply_in_profit"), version + Version::ZERO) .unwrap() @@ -1592,6 +1604,12 @@ impl Vecs { .unwrap() .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(()) diff --git a/crates/brk_computer/src/stateful/mod.rs b/crates/brk_computer/src/stateful/mod.rs index 22571d401..bff27fe22 100644 --- a/crates/brk_computer/src/stateful/mod.rs +++ b/crates/brk_computer/src/stateful/mod.rs @@ -182,7 +182,7 @@ impl Vecs { version + VERSION + Version::ONE, utxo_cohorts .all - .1 + .inner .height_to_supply_value .dollars .as_ref() @@ -604,10 +604,10 @@ impl Vecs { separate_utxo_vecs .par_iter_mut() - .try_for_each(|Filtered(_, v)| v.validate_computed_versions(base_version))?; + .try_for_each(|v| v.validate_computed_versions(base_version))?; separate_address_vecs .par_iter_mut() - .try_for_each(|Filtered(_, v)| v.validate_computed_versions(base_version))?; + .try_for_each(|v| v.validate_computed_versions(base_version))?; self.height_to_unspendable_supply .validate_computed_version_or_reset( base_version + self.height_to_unspendable_supply.inner_version(), @@ -620,13 +620,13 @@ impl Vecs { let mut chain_state_starting_height = Height::from(self.chain_state.len()); let stateful_starting_height = match separate_utxo_vecs .par_iter_mut() - .map(|Filtered(_, v)| Height::from(v.min_height_vecs_len())) + .map(|v| Height::from(v.min_height_vecs_len())) .min() .unwrap_or_default() .min( separate_address_vecs .par_iter_mut() - .map(|Filtered(_, v)| Height::from(v.min_height_vecs_len())) + .map(|v| Height::from(v.min_height_vecs_len())) .min() .unwrap_or_default(), ) @@ -683,7 +683,7 @@ impl Vecs { let starting_height = if starting_height.is_not_zero() && separate_utxo_vecs .iter_mut() - .map(|Filtered(_, v)| v.import_state(starting_height).unwrap_or_default()) + .map(|v| v.import_state(starting_height).unwrap_or_default()) .all(|h| h == starting_height) { starting_height @@ -695,7 +695,7 @@ impl Vecs { let starting_height = if starting_height.is_not_zero() && separate_address_vecs .iter_mut() - .map(|Filtered(_, v)| v.import_state(starting_height).unwrap_or_default()) + .map(|v| v.import_state(starting_height).unwrap_or_default()) .all(|h| h == starting_height) { starting_height @@ -703,6 +703,7 @@ impl Vecs { Height::ZERO }; + // info!("starting_height = {starting_height}"); let mut chain_state: Vec; @@ -737,14 +738,17 @@ impl Vecs { separate_utxo_vecs .par_iter_mut() - .try_for_each(|Filtered(_, v)| { + .try_for_each(|v| { v.reset_state_starting_height(); v.state.as_mut().unwrap().reset_price_to_amount_if_needed() })?; + // Reset aggregate cohorts' price_to_amount + self.utxo_cohorts.reset_aggregate_price_to_amount()?; + separate_address_vecs .par_iter_mut() - .try_for_each(|Filtered(_, v)| { + .try_for_each(|v| { v.reset_state_starting_height(); v.state.as_mut().unwrap().reset_price_to_amount_if_needed() })?; @@ -839,13 +843,13 @@ impl Vecs { self.utxo_cohorts .iter_separate_mut() - .for_each(|Filtered(_, v)| { + .for_each(|v| { v.state.as_mut().unwrap().reset_single_iteration_values() }); self.address_cohorts .iter_separate_mut() - .for_each(|Filtered(_, v)| { + .for_each(|v| { v.state.as_mut().unwrap().reset_single_iteration_values() }); @@ -1328,11 +1332,11 @@ impl Vecs { self.utxo_cohorts .par_iter_separate_mut() - .map(|Filtered(_, v)| v as &mut dyn DynCohortVecs) + .map(|v| v as &mut dyn DynCohortVecs) .chain( self.address_cohorts .par_iter_separate_mut() - .map(|Filtered(_, v)| v as &mut dyn DynCohortVecs), + .map(|v| v as &mut dyn DynCohortVecs), ) .try_for_each(|v| { v.truncate_push(height)?; @@ -1341,6 +1345,10 @@ impl Vecs { ) })?; + // Compute and push percentiles for aggregate cohorts (all, sth, lth) + self.utxo_cohorts + .truncate_push_aggregate_percentiles(height)?; + if height != last_height && height != Height::ZERO && height.to_usize() % 10_000 == 0 @@ -1454,7 +1462,7 @@ impl Vecs { starting_indexes.dateindex, self.utxo_cohorts .all - .1 + .inner .indexes_to_supply .dollars .as_ref() @@ -1474,14 +1482,14 @@ impl Vecs { let height_to_supply = &self .utxo_cohorts .all - .1 + .inner .height_to_supply_value .bitcoin .clone(); let dateindex_to_supply = self .utxo_cohorts .all - .1 + .inner .indexes_to_supply .bitcoin .dateindex @@ -1491,11 +1499,11 @@ impl Vecs { .indexes_to_market_cap .as_ref() .map(|v| v.dateindex.as_ref().unwrap().clone()); - let height_to_realized_cap = self.utxo_cohorts.all.1.height_to_realized_cap.clone(); + let height_to_realized_cap = self.utxo_cohorts.all.inner.height_to_realized_cap.clone(); let dateindex_to_realized_cap = self .utxo_cohorts .all - .1 + .inner .indexes_to_realized_cap .as_ref() .map(|v| v.dateindex.unwrap_last().clone()); @@ -1615,10 +1623,10 @@ impl Vecs { self.utxo_cohorts .par_iter_separate_mut() - .try_for_each(|Filtered(_, v)| v.safe_flush_stateful_vecs(height, exit))?; + .try_for_each(|v| v.safe_flush_stateful_vecs(height, exit))?; self.address_cohorts .par_iter_separate_mut() - .try_for_each(|Filtered(_, v)| v.safe_flush_stateful_vecs(height, exit))?; + .try_for_each(|v| v.safe_flush_stateful_vecs(height, exit))?; self.height_to_unspendable_supply.safe_flush(exit)?; self.height_to_opreturn_supply.safe_flush(exit)?; self.addresstype_to_height_to_addr_count @@ -1816,13 +1824,12 @@ impl AddressTypeToVec<(TypeIndex, Sats)> { if is_new || from_any_empty - || vecs.amount_range.get_mut(amount).0.clone() - != vecs.amount_range.get_mut(prev_amount).0.clone() + || vecs.amount_range.get_mut(amount).filter().clone() + != vecs.amount_range.get_mut(prev_amount).filter().clone() { if !is_new && !from_any_empty { vecs.amount_range .get_mut(prev_amount) - .1 .state .as_mut() .unwrap() @@ -1833,7 +1840,6 @@ impl AddressTypeToVec<(TypeIndex, Sats)> { vecs.amount_range .get_mut(amount) - .1 .state .as_mut() .unwrap() @@ -1841,7 +1847,6 @@ impl AddressTypeToVec<(TypeIndex, Sats)> { } else { vecs.amount_range .get_mut(amount) - .1 .state .as_mut() .unwrap() @@ -1913,12 +1918,11 @@ impl HeightToAddressTypeToVec<(TypeIndex, Sats)> { let will_be_empty = addressdata.has_1_utxos(); if will_be_empty - || vecs.amount_range.get_mut(amount).0.clone() - != vecs.amount_range.get_mut(prev_amount).0.clone() + || vecs.amount_range.get_mut(amount).filter().clone() + != vecs.amount_range.get_mut(prev_amount).filter().clone() { vecs.amount_range .get_mut(prev_amount) - .1 .state .as_mut() .unwrap() @@ -1944,7 +1948,6 @@ impl HeightToAddressTypeToVec<(TypeIndex, Sats)> { } else { vecs.amount_range .get_mut(amount) - .1 .state .as_mut() .unwrap() @@ -1953,7 +1956,6 @@ impl HeightToAddressTypeToVec<(TypeIndex, Sats)> { } else { vecs.amount_range .get_mut(amount) - .1 .state .as_mut() .unwrap() diff --git a/crates/brk_computer/src/stateful/utxo_cohort.rs b/crates/brk_computer/src/stateful/utxo_cohort.rs index cb0938c3c..010995f3f 100644 --- a/crates/brk_computer/src/stateful/utxo_cohort.rs +++ b/crates/brk_computer/src/stateful/utxo_cohort.rs @@ -1,12 +1,15 @@ use std::{ops::Deref, path::Path}; use brk_error::Result; +use brk_grouper::{CohortContext, Filter, Filtered, StateLevel}; use brk_traversable::Traversable; -use brk_types::{Bitcoin, DateIndex, Dollars, Height, Version}; +use brk_types::{Bitcoin, DateIndex, Dollars, Height, Sats, Version}; use vecdb::{Database, Exit, IterableVec}; use crate::{ - Indexes, UTXOCohortState, indexes, price, + Indexes, PriceToAmount, UTXOCohortState, + grouped::PERCENTILES_LEN, + indexes, price, stateful::{ common, r#trait::{CohortVecs, DynCohortVecs}, @@ -20,6 +23,10 @@ pub struct Vecs { #[traversable(skip)] pub state: Option, + /// For aggregate cohorts (all, sth, lth) that only need price_to_amount for percentiles + #[traversable(skip)] + pub price_to_amount: Option, + #[traversable(flatten)] pub inner: common::Vecs, } @@ -28,31 +35,39 @@ impl Vecs { #[allow(clippy::too_many_arguments)] pub fn forced_import( db: &Database, - cohort_name: Option<&str>, + filter: Filter, version: Version, indexes: &indexes::Vecs, price: Option<&price::Vecs>, - states_path: Option<&Path>, + states_path: &Path, + state_level: StateLevel, extended: bool, compute_rel_to_all: bool, compute_adjusted: bool, ) -> Result { let compute_dollars = price.is_some(); + let full_name = filter.to_full_name(CohortContext::Utxo); + Ok(Self { state_starting_height: None, - state: states_path.map(|states_path| { - UTXOCohortState::new( - states_path, - cohort_name.unwrap_or_default(), - compute_dollars, - ) - }), + state: if state_level.is_full() { + Some(UTXOCohortState::new(states_path, &full_name, compute_dollars)) + } else { + None + }, + + price_to_amount: if state_level.is_price_only() && compute_dollars { + Some(PriceToAmount::create(states_path, &full_name)) + } else { + None + }, inner: common::Vecs::forced_import( db, - cohort_name, + filter, + CohortContext::Utxo, version, indexes, price, @@ -173,9 +188,55 @@ impl CohortVecs for Vecs { } } +impl Vecs { + /// Compute percentile prices for aggregate cohorts that have standalone price_to_amount. + /// Returns NaN array if price_to_amount is None or empty. + pub fn compute_percentile_prices_from_standalone(&self, supply: Sats) -> [Dollars; PERCENTILES_LEN] { + use crate::grouped::PERCENTILES; + + let mut result = [Dollars::NAN; PERCENTILES_LEN]; + + let price_to_amount = match self.price_to_amount.as_ref() { + Some(p) => p, + None => return result, + }; + + if price_to_amount.is_empty() || supply == Sats::ZERO { + return result; + } + + let total = u64::from(supply); + let targets = PERCENTILES.map(|p| total * u64::from(p) / 100); + + let mut accumulated = 0u64; + let mut pct_idx = 0; + + for (&price, &sats) in price_to_amount.iter() { + accumulated += u64::from(sats); + + while pct_idx < PERCENTILES_LEN && accumulated >= targets[pct_idx] { + result[pct_idx] = price; + pct_idx += 1; + } + + if pct_idx >= PERCENTILES_LEN { + break; + } + } + + result + } +} + impl Deref for Vecs { type Target = common::Vecs; fn deref(&self) -> &Self::Target { &self.inner } } + +impl Filtered for Vecs { + fn filter(&self) -> &Filter { + &self.inner.filter + } +} diff --git a/crates/brk_computer/src/stateful/utxo_cohorts.rs b/crates/brk_computer/src/stateful/utxo_cohorts.rs index 09210e054..664179f65 100644 --- a/crates/brk_computer/src/stateful/utxo_cohorts.rs +++ b/crates/brk_computer/src/stateful/utxo_cohorts.rs @@ -2,12 +2,14 @@ use std::{ops::ControlFlow, path::Path}; use brk_error::Result; use brk_grouper::{ - ByAgeRange, ByAmountRange, ByEpoch, ByGreatEqualAmount, ByLowerThanAmount, ByMaxAge, ByMinAge, - BySpendableType, ByTerm, Filter, Filtered, UTXOGroups, + AmountFilter, ByAgeRange, ByAmountRange, ByEpoch, ByGreatEqualAmount, ByLowerThanAmount, + ByMaxAge, ByMinAge, BySpendableType, ByTerm, Filter, Filtered, StateLevel, Term, TimeFilter, + UTXOGroups, }; use brk_traversable::Traversable; use brk_types::{ - Bitcoin, CheckedSub, DateIndex, Dollars, HalvingEpoch, Height, Timestamp, Version, + Bitcoin, CheckedSub, DateIndex, Dollars, HalvingEpoch, Height, OutputType, Sats, Timestamp, + Version, }; use derive_deref::{Deref, DerefMut}; use rustc_hash::FxHashMap; @@ -24,7 +26,7 @@ use super::{r#trait::CohortVecs, utxo_cohort}; const VERSION: Version = Version::new(0); #[derive(Clone, Deref, DerefMut, Traversable)] -pub struct Vecs(UTXOGroups>); +pub struct Vecs(UTXOGroups); impl Vecs { pub fn forced_import( @@ -34,1305 +36,1418 @@ impl Vecs { price: Option<&price::Vecs>, states_path: &Path, ) -> Result { - Ok(Self( - UTXOGroups { - all: utxo_cohort::Vecs::forced_import( + Ok(Self(UTXOGroups { + all: utxo_cohort::Vecs::forced_import( + db, + Filter::All, + version + VERSION + Version::ONE, + indexes, + price, + states_path, + StateLevel::PriceOnly, + true, + false, + true, + )?, + term: ByTerm { + short: utxo_cohort::Vecs::forced_import( db, - None, - version + VERSION + Version::ONE, + Filter::Term(Term::Sth), + version + VERSION + Version::ZERO, indexes, price, - None, + states_path, + StateLevel::PriceOnly, + true, true, - false, true, )?, - term: ByTerm { - short: utxo_cohort::Vecs::forced_import( - db, - Some("sth"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - true, - true, - true, - )?, - long: utxo_cohort::Vecs::forced_import( - db, - Some("lth"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - true, - true, - false, - )?, - }, - epoch: ByEpoch { - _0: utxo_cohort::Vecs::forced_import( - db, - Some("epoch_0"), - version + VERSION + Version::ZERO, - indexes, - price, - Some(states_path), - true, - true, - false, - )?, - _1: utxo_cohort::Vecs::forced_import( - db, - Some("epoch_1"), - version + VERSION + Version::ZERO, - indexes, - price, - Some(states_path), - true, - true, - false, - )?, - _2: utxo_cohort::Vecs::forced_import( - db, - Some("epoch_2"), - version + VERSION + Version::ZERO, - indexes, - price, - Some(states_path), - true, - true, - false, - )?, - _3: utxo_cohort::Vecs::forced_import( - db, - Some("epoch_3"), - version + VERSION + Version::ZERO, - indexes, - price, - Some(states_path), - true, - true, - false, - )?, - _4: utxo_cohort::Vecs::forced_import( - db, - Some("epoch_4"), - version + VERSION + Version::ZERO, - indexes, - price, - Some(states_path), - true, - true, - false, - )?, - }, - _type: BySpendableType { - p2pk65: utxo_cohort::Vecs::forced_import( - db, - Some("p2pk65"), - version + VERSION + Version::ZERO, - indexes, - price, - Some(states_path), - false, - true, - false, - )?, - p2pk33: utxo_cohort::Vecs::forced_import( - db, - Some("p2pk33"), - version + VERSION + Version::ZERO, - indexes, - price, - Some(states_path), - false, - true, - false, - )?, - p2pkh: utxo_cohort::Vecs::forced_import( - db, - Some("p2pkh"), - version + VERSION + Version::ZERO, - indexes, - price, - Some(states_path), - false, - true, - false, - )?, - p2sh: utxo_cohort::Vecs::forced_import( - db, - Some("p2sh"), - version + VERSION + Version::ZERO, - indexes, - price, - Some(states_path), - false, - true, - false, - )?, - p2wpkh: utxo_cohort::Vecs::forced_import( - db, - Some("p2wpkh"), - version + VERSION + Version::ZERO, - indexes, - price, - Some(states_path), - false, - true, - false, - )?, - p2wsh: utxo_cohort::Vecs::forced_import( - db, - Some("p2wsh"), - version + VERSION + Version::ZERO, - indexes, - price, - Some(states_path), - false, - true, - false, - )?, - p2tr: utxo_cohort::Vecs::forced_import( - db, - Some("p2tr"), - version + VERSION + Version::ZERO, - indexes, - price, - Some(states_path), - false, - true, - false, - )?, - p2a: utxo_cohort::Vecs::forced_import( - db, - Some("p2a"), - version + VERSION + Version::ZERO, - indexes, - price, - Some(states_path), - false, - true, - false, - )?, - p2ms: utxo_cohort::Vecs::forced_import( - db, - Some("p2ms_outputs"), - version + VERSION + Version::ZERO, - indexes, - price, - Some(states_path), - false, - true, - false, - )?, - empty: utxo_cohort::Vecs::forced_import( - db, - Some("empty_outputs"), - version + VERSION + Version::ZERO, - indexes, - price, - Some(states_path), - false, - true, - false, - )?, - unknown: utxo_cohort::Vecs::forced_import( - db, - Some("unknown_outputs"), - version + VERSION + Version::ZERO, - indexes, - price, - Some(states_path), - false, - true, - false, - )?, - }, - max_age: ByMaxAge { - _1w: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_up_to_1w_old"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - true, - true, - true, - )?, - _1m: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_up_to_1m_old"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - true, - true, - true, - )?, - _2m: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_up_to_2m_old"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - true, - true, - true, - )?, - _3m: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_up_to_3m_old"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - true, - true, - true, - )?, - _4m: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_up_to_4m_old"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - true, - true, - true, - )?, - _5m: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_up_to_5m_old"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - true, - true, - true, - )?, - _6m: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_up_to_6m_old"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - true, - true, - true, - )?, - _1y: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_up_to_1y_old"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - true, - true, - true, - )?, - _2y: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_up_to_2y_old"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - true, - true, - true, - )?, - _3y: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_up_to_3y_old"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - true, - true, - true, - )?, - _4y: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_up_to_4y_old"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - true, - true, - true, - )?, - _5y: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_up_to_5y_old"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - true, - true, - true, - )?, - _6y: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_up_to_6y_old"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - true, - true, - true, - )?, - _7y: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_up_to_7y_old"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - true, - true, - true, - )?, - _8y: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_up_to_8y_old"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - true, - true, - true, - )?, - _10y: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_up_to_10y_old"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - true, - true, - true, - )?, - _12y: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_up_to_12y_old"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - true, - true, - true, - )?, - _15y: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_up_to_15y_old"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - true, - true, - true, - )?, - }, - min_age: ByMinAge { - _1d: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_at_least_1d_old"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - true, - true, - false, - )?, - _1w: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_at_least_1w_old"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - true, - true, - false, - )?, - _1m: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_at_least_1m_old"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - true, - true, - false, - )?, - _2m: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_at_least_2m_old"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - true, - true, - false, - )?, - _3m: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_at_least_3m_old"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - true, - true, - false, - )?, - _4m: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_at_least_4m_old"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - true, - true, - false, - )?, - _5m: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_at_least_5m_old"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - true, - true, - false, - )?, - _6m: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_at_least_6m_old"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - true, - true, - false, - )?, - _1y: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_at_least_1y_old"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - true, - true, - false, - )?, - _2y: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_at_least_2y_old"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - true, - true, - false, - )?, - _3y: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_at_least_3y_old"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - true, - true, - false, - )?, - _4y: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_at_least_4y_old"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - true, - true, - false, - )?, - _5y: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_at_least_5y_old"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - true, - true, - false, - )?, - _6y: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_at_least_6y_old"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - true, - true, - false, - )?, - _7y: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_at_least_7y_old"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - true, - true, - false, - )?, - _8y: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_at_least_8y_old"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - true, - true, - false, - )?, - _10y: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_at_least_10y_old"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - true, - true, - false, - )?, - _12y: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_at_least_12y_old"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - true, - true, - false, - )?, - }, - age_range: ByAgeRange { - up_to_1d: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_up_to_1d_old"), - version + VERSION + Version::ZERO, - indexes, - price, - Some(states_path), - true, - true, - true, - )?, - _1d_to_1w: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_at_least_1d_up_to_1w_old"), - version + VERSION + Version::ZERO, - indexes, - price, - Some(states_path), - true, - true, - false, - )?, - _1w_to_1m: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_at_least_1w_up_to_1m_old"), - version + VERSION + Version::ZERO, - indexes, - price, - Some(states_path), - true, - true, - false, - )?, - _1m_to_2m: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_at_least_1m_up_to_2m_old"), - version + VERSION + Version::ZERO, - indexes, - price, - Some(states_path), - true, - true, - false, - )?, - _2m_to_3m: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_at_least_2m_up_to_3m_old"), - version + VERSION + Version::ZERO, - indexes, - price, - Some(states_path), - true, - true, - false, - )?, - _3m_to_4m: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_at_least_3m_up_to_4m_old"), - version + VERSION + Version::ZERO, - indexes, - price, - Some(states_path), - true, - true, - false, - )?, - _4m_to_5m: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_at_least_4m_up_to_5m_old"), - version + VERSION + Version::ZERO, - indexes, - price, - Some(states_path), - true, - true, - false, - )?, - _5m_to_6m: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_at_least_5m_up_to_6m_old"), - version + VERSION + Version::ZERO, - indexes, - price, - Some(states_path), - true, - true, - false, - )?, - _6m_to_1y: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_at_least_6m_up_to_1y_old"), - version + VERSION + Version::ZERO, - indexes, - price, - Some(states_path), - true, - true, - false, - )?, - _1y_to_2y: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_at_least_1y_up_to_2y_old"), - version + VERSION + Version::ZERO, - indexes, - price, - Some(states_path), - true, - true, - false, - )?, - _2y_to_3y: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_at_least_2y_up_to_3y_old"), - version + VERSION + Version::ZERO, - indexes, - price, - Some(states_path), - true, - true, - false, - )?, - _3y_to_4y: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_at_least_3y_up_to_4y_old"), - version + VERSION + Version::ZERO, - indexes, - price, - Some(states_path), - true, - true, - false, - )?, - _4y_to_5y: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_at_least_4y_up_to_5y_old"), - version + VERSION + Version::ZERO, - indexes, - price, - Some(states_path), - true, - true, - false, - )?, - _5y_to_6y: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_at_least_5y_up_to_6y_old"), - version + VERSION + Version::ZERO, - indexes, - price, - Some(states_path), - true, - true, - false, - )?, - _6y_to_7y: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_at_least_6y_up_to_7y_old"), - version + VERSION + Version::ZERO, - indexes, - price, - Some(states_path), - true, - true, - false, - )?, - _7y_to_8y: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_at_least_7y_up_to_8y_old"), - version + VERSION + Version::ZERO, - indexes, - price, - Some(states_path), - true, - true, - false, - )?, - _8y_to_10y: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_at_least_8y_up_to_10y_old"), - version + VERSION + Version::ZERO, - indexes, - price, - Some(states_path), - true, - true, - false, - )?, - _10y_to_12y: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_at_least_10y_up_to_12y_old"), - version + VERSION + Version::ZERO, - indexes, - price, - Some(states_path), - true, - true, - false, - )?, - _12y_to_15y: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_at_least_12y_up_to_15y_old"), - version + VERSION + Version::ZERO, - indexes, - price, - Some(states_path), - true, - true, - false, - )?, - from_15y: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_at_least_15y_old"), - version + VERSION + Version::ZERO, - indexes, - price, - Some(states_path), - true, - true, - false, - )?, - }, - amount_range: ByAmountRange { - _0sats: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_with_0sats"), - version + VERSION + Version::ZERO, - indexes, - price, - Some(states_path), - false, - true, - false, - )?, - _1sat_to_10sats: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_above_1sat_under_10sats"), - version + VERSION + Version::ZERO, - indexes, - price, - Some(states_path), - false, - true, - false, - )?, - _10sats_to_100sats: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_above_10sats_under_100sats"), - version + VERSION + Version::ZERO, - indexes, - price, - Some(states_path), - false, - true, - false, - )?, - _100sats_to_1k_sats: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_above_100sats_under_1k_sats"), - version + VERSION + Version::ZERO, - indexes, - price, - Some(states_path), - false, - true, - false, - )?, - _1k_sats_to_10k_sats: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_above_1k_sats_under_10k_sats"), - version + VERSION + Version::ZERO, - indexes, - price, - Some(states_path), - false, - true, - false, - )?, - _10k_sats_to_100k_sats: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_above_10k_sats_under_100k_sats"), - version + VERSION + Version::ZERO, - indexes, - price, - Some(states_path), - false, - true, - false, - )?, - _100k_sats_to_1m_sats: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_above_100k_sats_under_1m_sats"), - version + VERSION + Version::ZERO, - indexes, - price, - Some(states_path), - false, - true, - false, - )?, - _1m_sats_to_10m_sats: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_above_1m_sats_under_10m_sats"), - version + VERSION + Version::ZERO, - indexes, - price, - Some(states_path), - false, - true, - false, - )?, - _10m_sats_to_1btc: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_above_10m_sats_under_1btc"), - version + VERSION + Version::ZERO, - indexes, - price, - Some(states_path), - false, - true, - false, - )?, - _1btc_to_10btc: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_above_1btc_under_10btc"), - version + VERSION + Version::ZERO, - indexes, - price, - Some(states_path), - false, - true, - false, - )?, - _10btc_to_100btc: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_above_10btc_under_100btc"), - version + VERSION + Version::ZERO, - indexes, - price, - Some(states_path), - false, - true, - false, - )?, - _100btc_to_1k_btc: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_above_100btc_under_1k_btc"), - version + VERSION + Version::ZERO, - indexes, - price, - Some(states_path), - false, - true, - false, - )?, - _1k_btc_to_10k_btc: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_above_1k_btc_under_10k_btc"), - version + VERSION + Version::ZERO, - indexes, - price, - Some(states_path), - false, - true, - false, - )?, - _10k_btc_to_100k_btc: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_above_10k_btc_under_100k_btc"), - version + VERSION + Version::ZERO, - indexes, - price, - Some(states_path), - false, - true, - false, - )?, - _100k_btc_or_more: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_above_100k_btc"), - version + VERSION + Version::ZERO, - indexes, - price, - Some(states_path), - false, - true, - false, - )?, - }, - lt_amount: ByLowerThanAmount { - _10sats: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_under_10sats"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - false, - true, - false, - )?, - _100sats: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_under_100sats"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - false, - true, - false, - )?, - _1k_sats: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_under_1k_sats"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - false, - true, - false, - )?, - _10k_sats: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_under_10k_sats"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - false, - true, - false, - )?, - _100k_sats: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_under_100k_sats"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - false, - true, - false, - )?, - _1m_sats: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_under_1m_sats"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - false, - true, - false, - )?, - _10m_sats: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_under_10m_sats"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - false, - true, - false, - )?, - _1btc: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_under_1btc"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - false, - true, - false, - )?, - _10btc: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_under_10btc"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - false, - true, - false, - )?, - _100btc: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_under_100btc"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - false, - true, - false, - )?, - _1k_btc: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_under_1k_btc"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - false, - true, - false, - )?, - _10k_btc: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_under_10k_btc"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - false, - true, - false, - )?, - _100k_btc: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_under_100k_btc"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - false, - true, - false, - )?, - }, - ge_amount: ByGreatEqualAmount { - _1sat: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_above_1sat"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - false, - true, - false, - )?, - _10sats: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_above_10sats"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - false, - true, - false, - )?, - _100sats: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_above_100sats"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - false, - true, - false, - )?, - _1k_sats: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_above_1k_sats"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - false, - true, - false, - )?, - _10k_sats: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_above_10k_sats"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - false, - true, - false, - )?, - _100k_sats: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_above_100k_sats"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - false, - true, - false, - )?, - _1m_sats: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_above_1m_sats"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - false, - true, - false, - )?, - _10m_sats: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_above_10m_sats"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - false, - true, - false, - )?, - _1btc: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_above_1btc"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - false, - true, - false, - )?, - _10btc: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_above_10btc"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - false, - true, - false, - )?, - _100btc: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_above_100btc"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - false, - true, - false, - )?, - _1k_btc: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_above_1k_btc"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - false, - true, - false, - )?, - _10k_btc: utxo_cohort::Vecs::forced_import( - db, - Some("utxos_above_10k_btc"), - version + VERSION + Version::ZERO, - indexes, - price, - None, - false, - true, - false, - )?, - }, - } - .into(), - )) + long: utxo_cohort::Vecs::forced_import( + db, + Filter::Term(Term::Lth), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::PriceOnly, + true, + true, + false, + )?, + }, + epoch: ByEpoch { + _0: utxo_cohort::Vecs::forced_import( + db, + Filter::Epoch(HalvingEpoch::new(0)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::Full, + true, + true, + false, + )?, + _1: utxo_cohort::Vecs::forced_import( + db, + Filter::Epoch(HalvingEpoch::new(1)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::Full, + true, + true, + false, + )?, + _2: utxo_cohort::Vecs::forced_import( + db, + Filter::Epoch(HalvingEpoch::new(2)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::Full, + true, + true, + false, + )?, + _3: utxo_cohort::Vecs::forced_import( + db, + Filter::Epoch(HalvingEpoch::new(3)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::Full, + true, + true, + false, + )?, + _4: utxo_cohort::Vecs::forced_import( + db, + Filter::Epoch(HalvingEpoch::new(4)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::Full, + true, + true, + false, + )?, + }, + type_: BySpendableType { + p2pk65: utxo_cohort::Vecs::forced_import( + db, + Filter::Type(OutputType::P2PK65), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::Full, + false, + true, + false, + )?, + p2pk33: utxo_cohort::Vecs::forced_import( + db, + Filter::Type(OutputType::P2PK33), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::Full, + false, + true, + false, + )?, + p2pkh: utxo_cohort::Vecs::forced_import( + db, + Filter::Type(OutputType::P2PKH), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::Full, + false, + true, + false, + )?, + p2sh: utxo_cohort::Vecs::forced_import( + db, + Filter::Type(OutputType::P2SH), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::Full, + false, + true, + false, + )?, + p2wpkh: utxo_cohort::Vecs::forced_import( + db, + Filter::Type(OutputType::P2WPKH), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::Full, + false, + true, + false, + )?, + p2wsh: utxo_cohort::Vecs::forced_import( + db, + Filter::Type(OutputType::P2WSH), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::Full, + false, + true, + false, + )?, + p2tr: utxo_cohort::Vecs::forced_import( + db, + Filter::Type(OutputType::P2TR), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::Full, + false, + true, + false, + )?, + p2a: utxo_cohort::Vecs::forced_import( + db, + Filter::Type(OutputType::P2A), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::Full, + false, + true, + false, + )?, + p2ms: utxo_cohort::Vecs::forced_import( + db, + Filter::Type(OutputType::P2MS), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::Full, + false, + true, + false, + )?, + empty: utxo_cohort::Vecs::forced_import( + db, + Filter::Type(OutputType::Empty), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::Full, + false, + true, + false, + )?, + unknown: utxo_cohort::Vecs::forced_import( + db, + Filter::Type(OutputType::Unknown), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::Full, + false, + true, + false, + )?, + }, + max_age: ByMaxAge { + _1w: utxo_cohort::Vecs::forced_import( + db, + Filter::Time(TimeFilter::LowerThan(7)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::None, + true, + true, + true, + )?, + _1m: utxo_cohort::Vecs::forced_import( + db, + Filter::Time(TimeFilter::LowerThan(30)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::None, + true, + true, + true, + )?, + _2m: utxo_cohort::Vecs::forced_import( + db, + Filter::Time(TimeFilter::LowerThan(2 * 30)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::None, + true, + true, + true, + )?, + _3m: utxo_cohort::Vecs::forced_import( + db, + Filter::Time(TimeFilter::LowerThan(3 * 30)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::None, + true, + true, + true, + )?, + _4m: utxo_cohort::Vecs::forced_import( + db, + Filter::Time(TimeFilter::LowerThan(4 * 30)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::None, + true, + true, + true, + )?, + _5m: utxo_cohort::Vecs::forced_import( + db, + Filter::Time(TimeFilter::LowerThan(5 * 30)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::None, + true, + true, + true, + )?, + _6m: utxo_cohort::Vecs::forced_import( + db, + Filter::Time(TimeFilter::LowerThan(6 * 30)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::None, + true, + true, + true, + )?, + _1y: utxo_cohort::Vecs::forced_import( + db, + Filter::Time(TimeFilter::LowerThan(365)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::None, + true, + true, + true, + )?, + _2y: utxo_cohort::Vecs::forced_import( + db, + Filter::Time(TimeFilter::LowerThan(2 * 365)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::None, + true, + true, + true, + )?, + _3y: utxo_cohort::Vecs::forced_import( + db, + Filter::Time(TimeFilter::LowerThan(3 * 365)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::None, + true, + true, + true, + )?, + _4y: utxo_cohort::Vecs::forced_import( + db, + Filter::Time(TimeFilter::LowerThan(4 * 365)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::None, + true, + true, + true, + )?, + _5y: utxo_cohort::Vecs::forced_import( + db, + Filter::Time(TimeFilter::LowerThan(5 * 365)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::None, + true, + true, + true, + )?, + _6y: utxo_cohort::Vecs::forced_import( + db, + Filter::Time(TimeFilter::LowerThan(6 * 365)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::None, + true, + true, + true, + )?, + _7y: utxo_cohort::Vecs::forced_import( + db, + Filter::Time(TimeFilter::LowerThan(7 * 365)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::None, + true, + true, + true, + )?, + _8y: utxo_cohort::Vecs::forced_import( + db, + Filter::Time(TimeFilter::LowerThan(8 * 365)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::None, + true, + true, + true, + )?, + _10y: utxo_cohort::Vecs::forced_import( + db, + Filter::Time(TimeFilter::LowerThan(10 * 365)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::None, + true, + true, + true, + )?, + _12y: utxo_cohort::Vecs::forced_import( + db, + Filter::Time(TimeFilter::LowerThan(12 * 365)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::None, + true, + true, + true, + )?, + _15y: utxo_cohort::Vecs::forced_import( + db, + Filter::Time(TimeFilter::LowerThan(15 * 365)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::None, + true, + true, + true, + )?, + }, + min_age: ByMinAge { + _1d: utxo_cohort::Vecs::forced_import( + db, + Filter::Time(TimeFilter::GreaterOrEqual(1)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::None, + true, + true, + false, + )?, + _1w: utxo_cohort::Vecs::forced_import( + db, + Filter::Time(TimeFilter::GreaterOrEqual(7)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::None, + true, + true, + false, + )?, + _1m: utxo_cohort::Vecs::forced_import( + db, + Filter::Time(TimeFilter::GreaterOrEqual(30)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::None, + true, + true, + false, + )?, + _2m: utxo_cohort::Vecs::forced_import( + db, + Filter::Time(TimeFilter::GreaterOrEqual(2 * 30)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::None, + true, + true, + false, + )?, + _3m: utxo_cohort::Vecs::forced_import( + db, + Filter::Time(TimeFilter::GreaterOrEqual(3 * 30)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::None, + true, + true, + false, + )?, + _4m: utxo_cohort::Vecs::forced_import( + db, + Filter::Time(TimeFilter::GreaterOrEqual(4 * 30)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::None, + true, + true, + false, + )?, + _5m: utxo_cohort::Vecs::forced_import( + db, + Filter::Time(TimeFilter::GreaterOrEqual(5 * 30)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::None, + true, + true, + false, + )?, + _6m: utxo_cohort::Vecs::forced_import( + db, + Filter::Time(TimeFilter::GreaterOrEqual(6 * 30)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::None, + true, + true, + false, + )?, + _1y: utxo_cohort::Vecs::forced_import( + db, + Filter::Time(TimeFilter::GreaterOrEqual(365)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::None, + true, + true, + false, + )?, + _2y: utxo_cohort::Vecs::forced_import( + db, + Filter::Time(TimeFilter::GreaterOrEqual(2 * 365)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::None, + true, + true, + false, + )?, + _3y: utxo_cohort::Vecs::forced_import( + db, + Filter::Time(TimeFilter::GreaterOrEqual(3 * 365)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::None, + true, + true, + false, + )?, + _4y: utxo_cohort::Vecs::forced_import( + db, + Filter::Time(TimeFilter::GreaterOrEqual(4 * 365)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::None, + true, + true, + false, + )?, + _5y: utxo_cohort::Vecs::forced_import( + db, + Filter::Time(TimeFilter::GreaterOrEqual(5 * 365)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::None, + true, + true, + false, + )?, + _6y: utxo_cohort::Vecs::forced_import( + db, + Filter::Time(TimeFilter::GreaterOrEqual(6 * 365)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::None, + true, + true, + false, + )?, + _7y: utxo_cohort::Vecs::forced_import( + db, + Filter::Time(TimeFilter::GreaterOrEqual(7 * 365)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::None, + true, + true, + false, + )?, + _8y: utxo_cohort::Vecs::forced_import( + db, + Filter::Time(TimeFilter::GreaterOrEqual(8 * 365)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::None, + true, + true, + false, + )?, + _10y: utxo_cohort::Vecs::forced_import( + db, + Filter::Time(TimeFilter::GreaterOrEqual(10 * 365)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::None, + true, + true, + false, + )?, + _12y: utxo_cohort::Vecs::forced_import( + db, + Filter::Time(TimeFilter::GreaterOrEqual(12 * 365)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::None, + true, + true, + false, + )?, + }, + age_range: ByAgeRange { + up_to_1d: utxo_cohort::Vecs::forced_import( + db, + Filter::Time(TimeFilter::Range(0..1)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::Full, + true, + true, + true, + )?, + _1d_to_1w: utxo_cohort::Vecs::forced_import( + db, + Filter::Time(TimeFilter::Range(1..7)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::Full, + true, + true, + false, + )?, + _1w_to_1m: utxo_cohort::Vecs::forced_import( + db, + Filter::Time(TimeFilter::Range(7..30)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::Full, + true, + true, + false, + )?, + _1m_to_2m: utxo_cohort::Vecs::forced_import( + db, + Filter::Time(TimeFilter::Range(30..60)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::Full, + true, + true, + false, + )?, + _2m_to_3m: utxo_cohort::Vecs::forced_import( + db, + Filter::Time(TimeFilter::Range(60..90)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::Full, + true, + true, + false, + )?, + _3m_to_4m: utxo_cohort::Vecs::forced_import( + db, + Filter::Time(TimeFilter::Range(90..120)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::Full, + true, + true, + false, + )?, + _4m_to_5m: utxo_cohort::Vecs::forced_import( + db, + Filter::Time(TimeFilter::Range(120..150)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::Full, + true, + true, + false, + )?, + _5m_to_6m: utxo_cohort::Vecs::forced_import( + db, + Filter::Time(TimeFilter::Range(150..180)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::Full, + true, + true, + false, + )?, + _6m_to_1y: utxo_cohort::Vecs::forced_import( + db, + Filter::Time(TimeFilter::Range(180..365)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::Full, + true, + true, + false, + )?, + _1y_to_2y: utxo_cohort::Vecs::forced_import( + db, + Filter::Time(TimeFilter::Range(365..730)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::Full, + true, + true, + false, + )?, + _2y_to_3y: utxo_cohort::Vecs::forced_import( + db, + Filter::Time(TimeFilter::Range(730..1095)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::Full, + true, + true, + false, + )?, + _3y_to_4y: utxo_cohort::Vecs::forced_import( + db, + Filter::Time(TimeFilter::Range(1095..1460)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::Full, + true, + true, + false, + )?, + _4y_to_5y: utxo_cohort::Vecs::forced_import( + db, + Filter::Time(TimeFilter::Range(1460..1825)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::Full, + true, + true, + false, + )?, + _5y_to_6y: utxo_cohort::Vecs::forced_import( + db, + Filter::Time(TimeFilter::Range(1825..2190)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::Full, + true, + true, + false, + )?, + _6y_to_7y: utxo_cohort::Vecs::forced_import( + db, + Filter::Time(TimeFilter::Range(2190..2555)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::Full, + true, + true, + false, + )?, + _7y_to_8y: utxo_cohort::Vecs::forced_import( + db, + Filter::Time(TimeFilter::Range(2555..2920)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::Full, + true, + true, + false, + )?, + _8y_to_10y: utxo_cohort::Vecs::forced_import( + db, + Filter::Time(TimeFilter::Range(2920..3650)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::Full, + true, + true, + false, + )?, + _10y_to_12y: utxo_cohort::Vecs::forced_import( + db, + Filter::Time(TimeFilter::Range(3650..4380)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::Full, + true, + true, + false, + )?, + _12y_to_15y: utxo_cohort::Vecs::forced_import( + db, + Filter::Time(TimeFilter::Range(4380..5475)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::Full, + true, + true, + false, + )?, + from_15y: utxo_cohort::Vecs::forced_import( + db, + Filter::Time(TimeFilter::GreaterOrEqual(15 * 365)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::Full, + true, + true, + false, + )?, + }, + amount_range: ByAmountRange { + _0sats: utxo_cohort::Vecs::forced_import( + db, + Filter::Amount(AmountFilter::LowerThan(Sats::_1)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::Full, + false, + true, + false, + )?, + _1sat_to_10sats: utxo_cohort::Vecs::forced_import( + db, + Filter::Amount(AmountFilter::Range(Sats::_1..Sats::_10)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::Full, + false, + true, + false, + )?, + _10sats_to_100sats: utxo_cohort::Vecs::forced_import( + db, + Filter::Amount(AmountFilter::Range(Sats::_10..Sats::_100)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::Full, + false, + true, + false, + )?, + _100sats_to_1k_sats: utxo_cohort::Vecs::forced_import( + db, + Filter::Amount(AmountFilter::Range(Sats::_100..Sats::_1K)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::Full, + false, + true, + false, + )?, + _1k_sats_to_10k_sats: utxo_cohort::Vecs::forced_import( + db, + Filter::Amount(AmountFilter::Range(Sats::_1K..Sats::_10K)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::Full, + false, + true, + false, + )?, + _10k_sats_to_100k_sats: utxo_cohort::Vecs::forced_import( + db, + Filter::Amount(AmountFilter::Range(Sats::_10K..Sats::_100K)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::Full, + false, + true, + false, + )?, + _100k_sats_to_1m_sats: utxo_cohort::Vecs::forced_import( + db, + Filter::Amount(AmountFilter::Range(Sats::_100K..Sats::_1M)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::Full, + false, + true, + false, + )?, + _1m_sats_to_10m_sats: utxo_cohort::Vecs::forced_import( + db, + Filter::Amount(AmountFilter::Range(Sats::_1M..Sats::_10M)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::Full, + false, + true, + false, + )?, + _10m_sats_to_1btc: utxo_cohort::Vecs::forced_import( + db, + Filter::Amount(AmountFilter::Range(Sats::_10M..Sats::_1BTC)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::Full, + false, + true, + false, + )?, + _1btc_to_10btc: utxo_cohort::Vecs::forced_import( + db, + Filter::Amount(AmountFilter::Range(Sats::_1BTC..Sats::_10BTC)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::Full, + false, + true, + false, + )?, + _10btc_to_100btc: utxo_cohort::Vecs::forced_import( + db, + Filter::Amount(AmountFilter::Range(Sats::_10BTC..Sats::_100BTC)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::Full, + false, + true, + false, + )?, + _100btc_to_1k_btc: utxo_cohort::Vecs::forced_import( + db, + Filter::Amount(AmountFilter::Range(Sats::_100BTC..Sats::_1K_BTC)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::Full, + false, + true, + false, + )?, + _1k_btc_to_10k_btc: utxo_cohort::Vecs::forced_import( + db, + Filter::Amount(AmountFilter::Range(Sats::_1K_BTC..Sats::_10K_BTC)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::Full, + false, + true, + false, + )?, + _10k_btc_to_100k_btc: utxo_cohort::Vecs::forced_import( + db, + Filter::Amount(AmountFilter::Range(Sats::_10K_BTC..Sats::_100K_BTC)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::Full, + false, + true, + false, + )?, + _100k_btc_or_more: utxo_cohort::Vecs::forced_import( + db, + Filter::Amount(AmountFilter::GreaterOrEqual(Sats::_100K_BTC)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::Full, + false, + true, + false, + )?, + }, + lt_amount: ByLowerThanAmount { + _10sats: utxo_cohort::Vecs::forced_import( + db, + Filter::Amount(AmountFilter::LowerThan(Sats::_10)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::None, + false, + true, + false, + )?, + _100sats: utxo_cohort::Vecs::forced_import( + db, + Filter::Amount(AmountFilter::LowerThan(Sats::_100)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::None, + false, + true, + false, + )?, + _1k_sats: utxo_cohort::Vecs::forced_import( + db, + Filter::Amount(AmountFilter::LowerThan(Sats::_1K)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::None, + false, + true, + false, + )?, + _10k_sats: utxo_cohort::Vecs::forced_import( + db, + Filter::Amount(AmountFilter::LowerThan(Sats::_10K)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::None, + false, + true, + false, + )?, + _100k_sats: utxo_cohort::Vecs::forced_import( + db, + Filter::Amount(AmountFilter::LowerThan(Sats::_100K)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::None, + false, + true, + false, + )?, + _1m_sats: utxo_cohort::Vecs::forced_import( + db, + Filter::Amount(AmountFilter::LowerThan(Sats::_1M)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::None, + false, + true, + false, + )?, + _10m_sats: utxo_cohort::Vecs::forced_import( + db, + Filter::Amount(AmountFilter::LowerThan(Sats::_10M)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::None, + false, + true, + false, + )?, + _1btc: utxo_cohort::Vecs::forced_import( + db, + Filter::Amount(AmountFilter::LowerThan(Sats::_1BTC)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::None, + false, + true, + false, + )?, + _10btc: utxo_cohort::Vecs::forced_import( + db, + Filter::Amount(AmountFilter::LowerThan(Sats::_10BTC)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::None, + false, + true, + false, + )?, + _100btc: utxo_cohort::Vecs::forced_import( + db, + Filter::Amount(AmountFilter::LowerThan(Sats::_100BTC)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::None, + false, + true, + false, + )?, + _1k_btc: utxo_cohort::Vecs::forced_import( + db, + Filter::Amount(AmountFilter::LowerThan(Sats::_1K_BTC)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::None, + false, + true, + false, + )?, + _10k_btc: utxo_cohort::Vecs::forced_import( + db, + Filter::Amount(AmountFilter::LowerThan(Sats::_10K_BTC)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::None, + false, + true, + false, + )?, + _100k_btc: utxo_cohort::Vecs::forced_import( + db, + Filter::Amount(AmountFilter::LowerThan(Sats::_100K_BTC)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::None, + false, + true, + false, + )?, + }, + ge_amount: ByGreatEqualAmount { + _1sat: utxo_cohort::Vecs::forced_import( + db, + Filter::Amount(AmountFilter::GreaterOrEqual(Sats::_1)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::None, + false, + true, + false, + )?, + _10sats: utxo_cohort::Vecs::forced_import( + db, + Filter::Amount(AmountFilter::GreaterOrEqual(Sats::_10)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::None, + false, + true, + false, + )?, + _100sats: utxo_cohort::Vecs::forced_import( + db, + Filter::Amount(AmountFilter::GreaterOrEqual(Sats::_100)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::None, + false, + true, + false, + )?, + _1k_sats: utxo_cohort::Vecs::forced_import( + db, + Filter::Amount(AmountFilter::GreaterOrEqual(Sats::_1K)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::None, + false, + true, + false, + )?, + _10k_sats: utxo_cohort::Vecs::forced_import( + db, + Filter::Amount(AmountFilter::GreaterOrEqual(Sats::_10K)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::None, + false, + true, + false, + )?, + _100k_sats: utxo_cohort::Vecs::forced_import( + db, + Filter::Amount(AmountFilter::GreaterOrEqual(Sats::_100K)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::None, + false, + true, + false, + )?, + _1m_sats: utxo_cohort::Vecs::forced_import( + db, + Filter::Amount(AmountFilter::GreaterOrEqual(Sats::_1M)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::None, + false, + true, + false, + )?, + _10m_sats: utxo_cohort::Vecs::forced_import( + db, + Filter::Amount(AmountFilter::GreaterOrEqual(Sats::_10M)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::None, + false, + true, + false, + )?, + _1btc: utxo_cohort::Vecs::forced_import( + db, + Filter::Amount(AmountFilter::GreaterOrEqual(Sats::_1BTC)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::None, + false, + true, + false, + )?, + _10btc: utxo_cohort::Vecs::forced_import( + db, + Filter::Amount(AmountFilter::GreaterOrEqual(Sats::_10BTC)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::None, + false, + true, + false, + )?, + _100btc: utxo_cohort::Vecs::forced_import( + db, + Filter::Amount(AmountFilter::GreaterOrEqual(Sats::_100BTC)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::None, + false, + true, + false, + )?, + _1k_btc: utxo_cohort::Vecs::forced_import( + db, + Filter::Amount(AmountFilter::GreaterOrEqual(Sats::_1K_BTC)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::None, + false, + true, + false, + )?, + _10k_btc: utxo_cohort::Vecs::forced_import( + db, + Filter::Amount(AmountFilter::GreaterOrEqual(Sats::_10K_BTC)), + version + VERSION + Version::ZERO, + indexes, + price, + states_path, + StateLevel::None, + false, + true, + false, + )?, + }, + })) } pub fn tick_tock_next_block(&mut self, chain_state: &[BlockState], timestamp: Timestamp) { @@ -1345,7 +1460,7 @@ impl Vecs { let mut vecs = self .age_range .iter_mut() - .map(|Filtered(filter, v)| (filter, &mut v.state)) + .map(|v| (v.filter().clone(), &mut v.state)) .collect::>(); let _ = chain_state @@ -1360,8 +1475,8 @@ impl Vecs { } vecs.iter_mut().for_each(|(filter, state)| { - let is = filter.contains(days_old); - let was = filter.contains(prev_days_old); + let is = filter.contains_time(days_old); + let was = filter.contains_time(prev_days_old); if is && !was { state @@ -1417,14 +1532,14 @@ impl Vecs { time_based_vecs .iter_mut() - .filter(|Filtered(filter, _)| match filter { - Filter::GreaterOrEqual(from) => *from <= days_old, - Filter::LowerThan(to) => *to > days_old, - Filter::Range(range) => range.contains(&days_old), + .filter(|v| match v.filter() { + Filter::Time(TimeFilter::GreaterOrEqual(from)) => *from <= days_old, + Filter::Time(TimeFilter::LowerThan(to)) => *to > days_old, + Filter::Time(TimeFilter::Range(range)) => range.contains(&days_old), Filter::Epoch(epoch) => *epoch == HalvingEpoch::from(height), _ => unreachable!(), }) - .for_each(|Filtered(_, vecs)| { + .for_each(|vecs| { vecs.state.as_mut().unwrap().send( &sent.spendable_supply, current_price, @@ -1440,9 +1555,8 @@ impl Vecs { .iter_typed() .for_each(|(output_type, supply_state)| { self.0 - ._type + .type_ .get_mut(output_type) - .1 .state .as_mut() .unwrap() @@ -1462,7 +1576,6 @@ impl Vecs { self.0 .amount_range .get_mut(group) - .1 .state .as_mut() .unwrap() @@ -1475,6 +1588,26 @@ impl Vecs { older_than_hour, ); }); + + // Update aggregate cohorts' price_to_amount + // All sends decrement from "all", and either from "sth" or "lth" based on age + if let Some(prev_price) = prev_price { + let supply_state = &sent.spendable_supply; + if supply_state.value.is_not_zero() { + const STH_THRESHOLD: usize = Term::THRESHOLD_DAYS; + + if let Some(p2a) = self.0.all.price_to_amount.as_mut() { + p2a.decrement(prev_price, supply_state); + } + if days_old < STH_THRESHOLD { + if let Some(p2a) = self.0.term.short.price_to_amount.as_mut() { + p2a.decrement(prev_price, supply_state); + } + } else if let Some(p2a) = self.0.term.long.price_to_amount.as_mut() { + p2a.decrement(prev_price, supply_state); + } + } + } }); } @@ -1482,16 +1615,29 @@ impl Vecs { let supply_state = received.spendable_supply; [ - &mut self.0.age_range.up_to_1d.1, - &mut self.0.epoch.mut_vec_from_height(height).1, + &mut self.0.age_range.up_to_1d, + self.0.epoch.mut_vec_from_height(height), ] .into_iter() .for_each(|v| { v.state.as_mut().unwrap().receive(&supply_state, price); }); - self._type.iter_mut().for_each(|Filtered(filter, vecs)| { - let output_type = match filter { + // Update aggregate cohorts' price_to_amount + // New UTXOs are always part of "all" and are always < 150 days old (so part of "sth") + if let Some(price) = price + && supply_state.value.is_not_zero() + { + if let Some(p2a) = self.0.all.price_to_amount.as_mut() { + p2a.increment(price, &supply_state); + } + if let Some(p2a) = self.0.term.short.price_to_amount.as_mut() { + p2a.increment(price, &supply_state); + } + } + + self.type_.iter_mut().for_each(|vecs| { + let output_type = match vecs.filter() { Filter::Type(output_type) => *output_type, _ => unreachable!(), }; @@ -1507,7 +1653,6 @@ impl Vecs { .for_each(|(group, supply_state)| { self.amount_range .get_mut(group) - .1 .state .as_mut() .unwrap() @@ -1523,64 +1668,61 @@ impl Vecs { let by_date_range = &self.0.age_range; let by_size_range = &self.0.amount_range; - [( - &mut self.0.all.1, - by_date_range.iter().map(Filtered::t).collect::>(), - )] - .into_iter() - .chain(self.0.min_age.iter_mut().map(|Filtered(filter, vecs)| { - ( - vecs, - by_date_range - .iter() - .filter(|Filtered(other, _)| filter.includes(other)) - .map(Filtered::t) - .collect::>(), - ) - })) - .chain(self.0.max_age.iter_mut().map(|Filtered(filter, vecs)| { - ( - vecs, - by_date_range - .iter() - .filter(|Filtered(other, _)| filter.includes(other)) - .map(Filtered::t) - .collect::>(), - ) - })) - .chain(self.0.term.iter_mut().map(|Filtered(filter, vecs)| { - ( - vecs, - by_date_range - .iter() - .filter(|Filtered(other, _)| filter.includes(other)) - .map(Filtered::t) - .collect::>(), - ) - })) - .chain(self.0.ge_amount.iter_mut().map(|Filtered(filter, vecs)| { - ( - vecs, - by_size_range - .iter() - .filter(|Filtered(other, _)| filter.includes(other)) - .map(Filtered::t) - .collect::>(), - ) - })) - .chain(self.0.lt_amount.iter_mut().map(|Filtered(filter, vecs)| { - ( - vecs, - by_size_range - .iter() - .filter(|Filtered(other, _)| filter.includes(other)) - .map(Filtered::t) - .collect::>(), - ) - })) - .try_for_each(|(vecs, stateful)| { - vecs.compute_from_stateful(starting_indexes, &stateful, exit) - }) + [(&mut self.0.all, by_date_range.iter().collect::>())] + .into_iter() + .chain(self.0.min_age.iter_mut().map(|vecs| { + let filter = vecs.filter().clone(); + ( + vecs, + by_date_range + .iter() + .filter(|other| filter.includes(other.filter())) + .collect::>(), + ) + })) + .chain(self.0.max_age.iter_mut().map(|vecs| { + let filter = vecs.filter().clone(); + ( + vecs, + by_date_range + .iter() + .filter(|other| filter.includes(other.filter())) + .collect::>(), + ) + })) + .chain(self.0.term.iter_mut().map(|vecs| { + let filter = vecs.filter().clone(); + ( + vecs, + by_date_range + .iter() + .filter(|other| filter.includes(other.filter())) + .collect::>(), + ) + })) + .chain(self.0.ge_amount.iter_mut().map(|vecs| { + let filter = vecs.filter().clone(); + ( + vecs, + by_size_range + .iter() + .filter(|other| filter.includes(other.filter())) + .collect::>(), + ) + })) + .chain(self.0.lt_amount.iter_mut().map(|vecs| { + let filter = vecs.filter().clone(); + ( + vecs, + by_size_range + .iter() + .filter(|other| filter.includes(other.filter())) + .collect::>(), + ) + })) + .try_for_each(|(vecs, stateful)| { + vecs.compute_from_stateful(starting_indexes, &stateful, exit) + }) } pub fn compute_rest_part1( @@ -1591,7 +1733,6 @@ impl Vecs { exit: &Exit, ) -> Result<()> { self.iter_mut() - .map(Filtered::mut_t) .try_for_each(|v| v.compute_rest_part1(indexes, price, starting_indexes, exit)) } @@ -1609,7 +1750,7 @@ impl Vecs { dateindex_to_realized_cap: Option<&impl IterableVec>, exit: &Exit, ) -> Result<()> { - self.iter_mut().map(Filtered::mut_t).try_for_each(|v| { + self.iter_mut().try_for_each(|v| { v.compute_rest_part2( indexes, price, @@ -1626,8 +1767,89 @@ impl Vecs { } pub fn safe_flush_stateful_vecs(&mut self, height: Height, exit: &Exit) -> Result<()> { + // Flush stateful cohorts self.iter_separate_mut() - .map(Filtered::mut_t) - .try_for_each(|v| v.safe_flush_stateful_vecs(height, exit)) + .try_for_each(|v| v.safe_flush_stateful_vecs(height, exit))?; + + // Flush aggregate cohorts' price_to_amount + if let Some(p2a) = self.0.all.price_to_amount.as_mut() { + p2a.flush(height)?; + } + if let Some(p2a) = self.0.term.short.price_to_amount.as_mut() { + p2a.flush(height)?; + } + if let Some(p2a) = self.0.term.long.price_to_amount.as_mut() { + p2a.flush(height)?; + } + + Ok(()) + } + + /// Reset aggregate cohorts' price_to_amount when starting from scratch + pub fn reset_aggregate_price_to_amount(&mut self) -> Result<()> { + if let Some(p2a) = self.0.all.price_to_amount.as_mut() { + p2a.clean()?; + p2a.init(); + } + if let Some(p2a) = self.0.term.short.price_to_amount.as_mut() { + p2a.clean()?; + p2a.init(); + } + if let Some(p2a) = self.0.term.long.price_to_amount.as_mut() { + p2a.clean()?; + p2a.init(); + } + Ok(()) + } + + /// Compute and push percentiles for aggregate cohorts (all, sth, lth). + /// Must be called after receive()/send() when price_to_amount is up to date. + pub fn truncate_push_aggregate_percentiles(&mut self, height: Height) -> Result<()> { + // Helper to compute supply by summing age_range cohorts matching a filter + let compute_supply = |filter: &Filter| -> Sats { + self.0 + .age_range + .iter() + .filter(|v| filter.includes(v.filter())) + .map(|v| v.state.as_ref().unwrap().supply.value) + .fold(Sats::ZERO, |acc, v| acc + v) + }; + + // Compute and push percentiles for "all" + if self.0.all.price_to_amount.is_some() { + let supply = compute_supply(self.0.all.filter()); + let percentiles = self.0.all.compute_percentile_prices_from_standalone(supply); + if let Some(pp) = self.0.all.inner.price_percentiles.as_mut() { + pp.truncate_push(height, &percentiles)?; + } + } + + // Compute and push percentiles for "sth" + if self.0.term.short.price_to_amount.is_some() { + let supply = compute_supply(self.0.term.short.filter()); + let percentiles = self + .0 + .term + .short + .compute_percentile_prices_from_standalone(supply); + if let Some(pp) = self.0.term.short.inner.price_percentiles.as_mut() { + pp.truncate_push(height, &percentiles)?; + } + } + + // Compute and push percentiles for "lth" + if self.0.term.long.price_to_amount.is_some() { + let supply = compute_supply(self.0.term.long.filter()); + let percentiles = self + .0 + .term + .long + .compute_percentile_prices_from_standalone(supply); + if let Some(pp) = self.0.term.long.inner.price_percentiles.as_mut() { + pp.truncate_push(height, &percentiles)?; + } + } + + Ok(()) } } diff --git a/crates/brk_grouper/src/address.rs b/crates/brk_grouper/src/address.rs index 125133d60..d692dd584 100644 --- a/crates/brk_grouper/src/address.rs +++ b/crates/brk_grouper/src/address.rs @@ -2,7 +2,7 @@ use brk_traversable::Traversable; use rayon::prelude::*; use vecdb::AnyExportableVec; -use crate::Filtered; +use crate::Filter; use super::{ByAmountRange, ByGreatEqualAmount, ByLowerThanAmount}; @@ -14,6 +14,24 @@ pub struct AddressGroups { } impl AddressGroups { + pub fn new(mut create: F) -> Self + where + F: FnMut(Filter) -> T, + { + Self { + ge_amount: ByGreatEqualAmount::new(&mut create), + amount_range: ByAmountRange::new(&mut create), + lt_amount: ByLowerThanAmount::new(&mut create), + } + } + + pub fn iter(&self) -> impl Iterator { + self.ge_amount + .iter() + .chain(self.amount_range.iter()) + .chain(self.lt_amount.iter()) + } + pub fn iter_mut(&mut self) -> impl Iterator { self.ge_amount .iter_mut() @@ -37,26 +55,6 @@ impl AddressGroups { } } -impl AddressGroups> { - pub fn iter_right(&self) -> impl Iterator { - self.amount_range - .iter_right() - .chain(self.lt_amount.iter_right()) - .chain(self.ge_amount.iter_right()) - } -} - -impl From> for AddressGroups> { - #[inline] - fn from(value: AddressGroups) -> Self { - Self { - amount_range: ByAmountRange::from(value.amount_range), - lt_amount: ByLowerThanAmount::from(value.lt_amount), - ge_amount: ByGreatEqualAmount::from(value.ge_amount), - } - } -} - impl Traversable for AddressGroups where ByGreatEqualAmount: brk_traversable::Traversable, diff --git a/crates/brk_grouper/src/amount_filter.rs b/crates/brk_grouper/src/amount_filter.rs new file mode 100644 index 000000000..817addbd1 --- /dev/null +++ b/crates/brk_grouper/src/amount_filter.rs @@ -0,0 +1,68 @@ +use std::ops::Range; + +use brk_types::Sats; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum AmountFilter { + LowerThan(Sats), + Range(Range), + GreaterOrEqual(Sats), +} + +impl AmountFilter { + pub fn contains(&self, sats: Sats) -> bool { + match self { + AmountFilter::LowerThan(max) => sats < *max, + AmountFilter::Range(r) => sats >= r.start && sats < r.end, + AmountFilter::GreaterOrEqual(min) => sats >= *min, + } + } + + pub fn includes(&self, other: &AmountFilter) -> bool { + match self { + AmountFilter::LowerThan(max) => match other { + AmountFilter::LowerThan(max2) => max >= max2, + AmountFilter::Range(range) => range.end <= *max, + AmountFilter::GreaterOrEqual(_) => false, + }, + AmountFilter::GreaterOrEqual(min) => match other { + AmountFilter::Range(range) => range.start >= *min, + AmountFilter::GreaterOrEqual(min2) => min <= min2, + AmountFilter::LowerThan(_) => false, + }, + AmountFilter::Range(_) => false, + } + } + + pub fn to_name_suffix(&self) -> String { + match self { + AmountFilter::LowerThan(s) if *s == Sats::_1 => "with_0sats".to_string(), + AmountFilter::LowerThan(s) => format!("under_{}", format_sats(*s)), + AmountFilter::GreaterOrEqual(s) => format!("above_{}", format_sats(*s)), + AmountFilter::Range(r) => { + format!("{}_{}", format_sats(r.start), format_sats(r.end)) + } + } + } +} + +fn format_sats(sats: Sats) -> String { + match sats { + s if s == Sats::ZERO => "0sats".to_string(), + s if s == Sats::_1 => "1sat".to_string(), + s if s == Sats::_10 => "10sats".to_string(), + s if s == Sats::_100 => "100sats".to_string(), + s if s == Sats::_1K => "1k_sats".to_string(), + s if s == Sats::_10K => "10k_sats".to_string(), + s if s == Sats::_100K => "100k_sats".to_string(), + s if s == Sats::_1M => "1m_sats".to_string(), + s if s == Sats::_10M => "10m_sats".to_string(), + s if s == Sats::_1BTC => "1btc".to_string(), + s if s == Sats::_10BTC => "10btc".to_string(), + s if s == Sats::_100BTC => "100btc".to_string(), + s if s == Sats::_1K_BTC => "1k_btc".to_string(), + s if s == Sats::_10K_BTC => "10k_btc".to_string(), + s if s == Sats::_100K_BTC => "100k_btc".to_string(), + _ => format!("{}sats", u64::from(sats)), + } +} diff --git a/crates/brk_grouper/src/by_address_type.rs b/crates/brk_grouper/src/by_address_type.rs index ce35bfdd9..d1fdf5959 100644 --- a/crates/brk_grouper/src/by_address_type.rs +++ b/crates/brk_grouper/src/by_address_type.rs @@ -6,7 +6,7 @@ use brk_types::OutputType; use rayon::prelude::*; use vecdb::AnyExportableVec; -use super::{Filter, Filtered}; +use super::Filter; pub const P2PK65: &str = "p2pk65"; pub const P2PK33: &str = "p2pk33"; @@ -30,6 +30,22 @@ pub struct ByAddressType { } impl ByAddressType { + pub fn new(mut create: F) -> Self + where + F: FnMut(Filter) -> T, + { + Self { + p2pk65: create(Filter::Type(OutputType::P2PK65)), + p2pk33: create(Filter::Type(OutputType::P2PK33)), + p2pkh: create(Filter::Type(OutputType::P2PKH)), + p2sh: create(Filter::Type(OutputType::P2SH)), + p2wpkh: create(Filter::Type(OutputType::P2WPKH)), + p2wsh: create(Filter::Type(OutputType::P2WSH)), + p2tr: create(Filter::Type(OutputType::P2TR)), + p2a: create(Filter::Type(OutputType::P2A)), + } + } + pub fn new_with_name(f: F) -> Result where F: Fn(&'static str) -> Result, @@ -215,38 +231,6 @@ impl ByAddressType { } } -impl ByAddressType> { - pub fn iter_right(&self) -> impl Iterator { - [ - &self.p2pk65.1, - &self.p2pk33.1, - &self.p2pkh.1, - &self.p2sh.1, - &self.p2wpkh.1, - &self.p2wsh.1, - &self.p2tr.1, - &self.p2a.1, - ] - .into_iter() - } -} - -impl From> for ByAddressType> { - #[inline] - fn from(value: ByAddressType) -> Self { - Self { - p2pk65: (Filter::Type(OutputType::P2PK65), value.p2pk65).into(), - p2pk33: (Filter::Type(OutputType::P2PK33), value.p2pk33).into(), - p2pkh: (Filter::Type(OutputType::P2PKH), value.p2pkh).into(), - p2sh: (Filter::Type(OutputType::P2SH), value.p2sh).into(), - p2wpkh: (Filter::Type(OutputType::P2WPKH), value.p2wpkh).into(), - p2wsh: (Filter::Type(OutputType::P2WSH), value.p2wsh).into(), - p2tr: (Filter::Type(OutputType::P2TR), value.p2tr).into(), - p2a: (Filter::Type(OutputType::P2A), value.p2a).into(), - } - } -} - impl Add for ByAddressType where T: Add, diff --git a/crates/brk_grouper/src/by_age_range.rs b/crates/brk_grouper/src/by_age_range.rs index 92cefb2b9..53cf3a833 100644 --- a/crates/brk_grouper/src/by_age_range.rs +++ b/crates/brk_grouper/src/by_age_range.rs @@ -1,9 +1,7 @@ use brk_traversable::Traversable; use rayon::iter::{IntoParallelIterator, ParallelIterator}; -use crate::Filtered; - -use super::Filter; +use super::{Filter, TimeFilter}; #[derive(Default, Clone, Traversable)] pub struct ByAgeRange { @@ -29,35 +27,35 @@ pub struct ByAgeRange { pub from_15y: T, } -impl From> for ByAgeRange> { - #[inline] - fn from(value: ByAgeRange) -> Self { +impl ByAgeRange { + pub fn new(mut create: F) -> Self + where + F: FnMut(Filter) -> T, + { Self { - up_to_1d: (Filter::LowerThan(1), value.up_to_1d).into(), - _1d_to_1w: (Filter::Range(1..7), value._1d_to_1w).into(), - _1w_to_1m: (Filter::Range(7..30), value._1w_to_1m).into(), - _1m_to_2m: (Filter::Range(30..2 * 30), value._1m_to_2m).into(), - _2m_to_3m: (Filter::Range(2 * 30..3 * 30), value._2m_to_3m).into(), - _3m_to_4m: (Filter::Range(3 * 30..4 * 30), value._3m_to_4m).into(), - _4m_to_5m: (Filter::Range(4 * 30..5 * 30), value._4m_to_5m).into(), - _5m_to_6m: (Filter::Range(5 * 30..6 * 30), value._5m_to_6m).into(), - _6m_to_1y: (Filter::Range(6 * 30..365), value._6m_to_1y).into(), - _1y_to_2y: (Filter::Range(365..2 * 365), value._1y_to_2y).into(), - _2y_to_3y: (Filter::Range(2 * 365..3 * 365), value._2y_to_3y).into(), - _3y_to_4y: (Filter::Range(3 * 365..4 * 365), value._3y_to_4y).into(), - _4y_to_5y: (Filter::Range(4 * 365..5 * 365), value._4y_to_5y).into(), - _5y_to_6y: (Filter::Range(5 * 365..6 * 365), value._5y_to_6y).into(), - _6y_to_7y: (Filter::Range(6 * 365..7 * 365), value._6y_to_7y).into(), - _7y_to_8y: (Filter::Range(7 * 365..8 * 365), value._7y_to_8y).into(), - _8y_to_10y: (Filter::Range(8 * 365..10 * 365), value._8y_to_10y).into(), - _10y_to_12y: (Filter::Range(10 * 365..12 * 365), value._10y_to_12y).into(), - _12y_to_15y: (Filter::Range(12 * 365..15 * 365), value._12y_to_15y).into(), - from_15y: (Filter::GreaterOrEqual(15 * 365), value.from_15y).into(), + up_to_1d: create(Filter::Time(TimeFilter::Range(0..1))), + _1d_to_1w: create(Filter::Time(TimeFilter::Range(1..7))), + _1w_to_1m: create(Filter::Time(TimeFilter::Range(7..30))), + _1m_to_2m: create(Filter::Time(TimeFilter::Range(30..2 * 30))), + _2m_to_3m: create(Filter::Time(TimeFilter::Range(2 * 30..3 * 30))), + _3m_to_4m: create(Filter::Time(TimeFilter::Range(3 * 30..4 * 30))), + _4m_to_5m: create(Filter::Time(TimeFilter::Range(4 * 30..5 * 30))), + _5m_to_6m: create(Filter::Time(TimeFilter::Range(5 * 30..6 * 30))), + _6m_to_1y: create(Filter::Time(TimeFilter::Range(6 * 30..365))), + _1y_to_2y: create(Filter::Time(TimeFilter::Range(365..2 * 365))), + _2y_to_3y: create(Filter::Time(TimeFilter::Range(2 * 365..3 * 365))), + _3y_to_4y: create(Filter::Time(TimeFilter::Range(3 * 365..4 * 365))), + _4y_to_5y: create(Filter::Time(TimeFilter::Range(4 * 365..5 * 365))), + _5y_to_6y: create(Filter::Time(TimeFilter::Range(5 * 365..6 * 365))), + _6y_to_7y: create(Filter::Time(TimeFilter::Range(6 * 365..7 * 365))), + _7y_to_8y: create(Filter::Time(TimeFilter::Range(7 * 365..8 * 365))), + _8y_to_10y: create(Filter::Time(TimeFilter::Range(8 * 365..10 * 365))), + _10y_to_12y: create(Filter::Time(TimeFilter::Range(10 * 365..12 * 365))), + _12y_to_15y: create(Filter::Time(TimeFilter::Range(12 * 365..15 * 365))), + from_15y: create(Filter::Time(TimeFilter::GreaterOrEqual(15 * 365))), } } -} -impl ByAgeRange { pub fn iter(&self) -> impl Iterator { [ &self.up_to_1d, @@ -140,30 +138,3 @@ impl ByAgeRange { } } -impl ByAgeRange> { - pub fn iter_right(&self) -> impl Iterator { - [ - &self.up_to_1d.1, - &self._1d_to_1w.1, - &self._1w_to_1m.1, - &self._1m_to_2m.1, - &self._2m_to_3m.1, - &self._3m_to_4m.1, - &self._4m_to_5m.1, - &self._5m_to_6m.1, - &self._6m_to_1y.1, - &self._1y_to_2y.1, - &self._2y_to_3y.1, - &self._3y_to_4y.1, - &self._4y_to_5y.1, - &self._5y_to_6y.1, - &self._6y_to_7y.1, - &self._7y_to_8y.1, - &self._8y_to_10y.1, - &self._10y_to_12y.1, - &self._12y_to_15y.1, - &self.from_15y.1, - ] - .into_iter() - } -} diff --git a/crates/brk_grouper/src/by_amount_range.rs b/crates/brk_grouper/src/by_amount_range.rs index 2d6452bf4..52d59a2d3 100644 --- a/crates/brk_grouper/src/by_amount_range.rs +++ b/crates/brk_grouper/src/by_amount_range.rs @@ -4,7 +4,7 @@ use brk_traversable::Traversable; use brk_types::Sats; use rayon::prelude::*; -use super::{Filter, Filtered}; +use super::{AmountFilter, Filter}; #[derive(Debug, Default, Clone, Traversable)] pub struct ByAmountRange { @@ -25,87 +25,30 @@ pub struct ByAmountRange { pub _100k_btc_or_more: T, } -impl From> for ByAmountRange> { - #[inline] - fn from(value: ByAmountRange) -> Self { - #[allow(clippy::inconsistent_digit_grouping)] +impl ByAmountRange { + pub fn new(mut create: F) -> Self + where + F: FnMut(Filter) -> T, + { Self { - _0sats: (Filter::LowerThan(Sats::_1.into()), value._0sats).into(), - _1sat_to_10sats: ( - Filter::Range(Sats::_1.into()..Sats::_10.into()), - value._1sat_to_10sats, - ) - .into(), - _10sats_to_100sats: ( - Filter::Range(Sats::_10.into()..Sats::_100.into()), - value._10sats_to_100sats, - ) - .into(), - _100sats_to_1k_sats: ( - Filter::Range(Sats::_100.into()..Sats::_1K.into()), - value._100sats_to_1k_sats, - ) - .into(), - _1k_sats_to_10k_sats: ( - Filter::Range(Sats::_1K.into()..Sats::_10K.into()), - value._1k_sats_to_10k_sats, - ) - .into(), - _10k_sats_to_100k_sats: ( - Filter::Range(Sats::_10K.into()..Sats::_100K.into()), - value._10k_sats_to_100k_sats, - ) - .into(), - _100k_sats_to_1m_sats: ( - Filter::Range(Sats::_100K.into()..Sats::_1M.into()), - value._100k_sats_to_1m_sats, - ) - .into(), - _1m_sats_to_10m_sats: ( - Filter::Range(Sats::_1M.into()..Sats::_10M.into()), - value._1m_sats_to_10m_sats, - ) - .into(), - _10m_sats_to_1btc: ( - Filter::Range(Sats::_10M.into()..Sats::_1BTC.into()), - value._10m_sats_to_1btc, - ) - .into(), - _1btc_to_10btc: ( - Filter::Range(Sats::_1BTC.into()..Sats::_10BTC.into()), - value._1btc_to_10btc, - ) - .into(), - _10btc_to_100btc: ( - Filter::Range(Sats::_10BTC.into()..Sats::_100BTC.into()), - value._10btc_to_100btc, - ) - .into(), - _100btc_to_1k_btc: ( - Filter::Range(Sats::_100BTC.into()..Sats::_1K_BTC.into()), - value._100btc_to_1k_btc, - ) - .into(), - _1k_btc_to_10k_btc: ( - Filter::Range(Sats::_1K_BTC.into()..Sats::_10K_BTC.into()), - value._1k_btc_to_10k_btc, - ) - .into(), - _10k_btc_to_100k_btc: ( - Filter::Range(Sats::_10K_BTC.into()..Sats::_100K_BTC.into()), - value._10k_btc_to_100k_btc, - ) - .into(), - _100k_btc_or_more: ( - Filter::GreaterOrEqual(Sats::_100K_BTC.into()), - value._100k_btc_or_more, - ) - .into(), + _0sats: create(Filter::Amount(AmountFilter::LowerThan(Sats::_1))), + _1sat_to_10sats: create(Filter::Amount(AmountFilter::Range(Sats::_1..Sats::_10))), + _10sats_to_100sats: create(Filter::Amount(AmountFilter::Range(Sats::_10..Sats::_100))), + _100sats_to_1k_sats: create(Filter::Amount(AmountFilter::Range(Sats::_100..Sats::_1K))), + _1k_sats_to_10k_sats: create(Filter::Amount(AmountFilter::Range(Sats::_1K..Sats::_10K))), + _10k_sats_to_100k_sats: create(Filter::Amount(AmountFilter::Range(Sats::_10K..Sats::_100K))), + _100k_sats_to_1m_sats: create(Filter::Amount(AmountFilter::Range(Sats::_100K..Sats::_1M))), + _1m_sats_to_10m_sats: create(Filter::Amount(AmountFilter::Range(Sats::_1M..Sats::_10M))), + _10m_sats_to_1btc: create(Filter::Amount(AmountFilter::Range(Sats::_10M..Sats::_1BTC))), + _1btc_to_10btc: create(Filter::Amount(AmountFilter::Range(Sats::_1BTC..Sats::_10BTC))), + _10btc_to_100btc: create(Filter::Amount(AmountFilter::Range(Sats::_10BTC..Sats::_100BTC))), + _100btc_to_1k_btc: create(Filter::Amount(AmountFilter::Range(Sats::_100BTC..Sats::_1K_BTC))), + _1k_btc_to_10k_btc: create(Filter::Amount(AmountFilter::Range(Sats::_1K_BTC..Sats::_10K_BTC))), + _10k_btc_to_100k_btc: create(Filter::Amount(AmountFilter::Range(Sats::_10K_BTC..Sats::_100K_BTC))), + _100k_btc_or_more: create(Filter::Amount(AmountFilter::GreaterOrEqual(Sats::_100K_BTC))), } } -} -impl ByAmountRange { #[allow(clippy::inconsistent_digit_grouping)] pub fn get_mut(&mut self, value: Sats) -> &mut T { if value == Sats::ZERO { @@ -229,29 +172,6 @@ impl ByAmountRange { } } -impl ByAmountRange> { - pub fn iter_right(&self) -> impl Iterator { - [ - &self._0sats.1, - &self._1sat_to_10sats.1, - &self._10sats_to_100sats.1, - &self._100sats_to_1k_sats.1, - &self._1k_sats_to_10k_sats.1, - &self._10k_sats_to_100k_sats.1, - &self._100k_sats_to_1m_sats.1, - &self._1m_sats_to_10m_sats.1, - &self._10m_sats_to_1btc.1, - &self._1btc_to_10btc.1, - &self._10btc_to_100btc.1, - &self._100btc_to_1k_btc.1, - &self._1k_btc_to_10k_btc.1, - &self._10k_btc_to_100k_btc.1, - &self._100k_btc_or_more.1, - ] - .into_iter() - } -} - impl Add for ByAmountRange where T: Add, diff --git a/crates/brk_grouper/src/by_epoch.rs b/crates/brk_grouper/src/by_epoch.rs index d42c2ca61..be50e7735 100644 --- a/crates/brk_grouper/src/by_epoch.rs +++ b/crates/brk_grouper/src/by_epoch.rs @@ -2,7 +2,7 @@ use brk_traversable::Traversable; use brk_types::{HalvingEpoch, Height}; use rayon::iter::{IntoParallelIterator, ParallelIterator}; -use super::{Filter, Filtered}; +use super::Filter; #[derive(Default, Clone, Traversable)] pub struct ByEpoch { @@ -13,20 +13,24 @@ pub struct ByEpoch { pub _4: T, } -impl From> for ByEpoch> { - #[inline] - fn from(value: ByEpoch) -> Self { +impl ByEpoch { + pub fn new(mut create: F) -> Self + where + F: FnMut(Filter) -> T, + { Self { - _0: (Filter::Epoch(HalvingEpoch::new(0)), value._0).into(), - _1: (Filter::Epoch(HalvingEpoch::new(1)), value._1).into(), - _2: (Filter::Epoch(HalvingEpoch::new(2)), value._2).into(), - _3: (Filter::Epoch(HalvingEpoch::new(3)), value._3).into(), - _4: (Filter::Epoch(HalvingEpoch::new(4)), value._4).into(), + _0: create(Filter::Epoch(HalvingEpoch::new(0))), + _1: create(Filter::Epoch(HalvingEpoch::new(1))), + _2: create(Filter::Epoch(HalvingEpoch::new(2))), + _3: create(Filter::Epoch(HalvingEpoch::new(3))), + _4: create(Filter::Epoch(HalvingEpoch::new(4))), } } -} -impl ByEpoch { + pub fn iter(&self) -> impl Iterator { + [&self._0, &self._1, &self._2, &self._3, &self._4].into_iter() + } + pub fn iter_mut(&mut self) -> impl Iterator { [ &mut self._0, @@ -69,9 +73,3 @@ impl ByEpoch { } } } - -impl ByEpoch> { - pub fn iter_right(&self) -> impl Iterator { - [&self._0.1, &self._1.1, &self._2.1, &self._3.1, &self._4.1].into_iter() - } -} diff --git a/crates/brk_grouper/src/by_ge_amount.rs b/crates/brk_grouper/src/by_ge_amount.rs index a11ee2ded..3da2c5f8c 100644 --- a/crates/brk_grouper/src/by_ge_amount.rs +++ b/crates/brk_grouper/src/by_ge_amount.rs @@ -1,7 +1,7 @@ use brk_traversable::Traversable; use brk_types::Sats; -use super::{Filter, Filtered}; +use super::{AmountFilter, Filter}; #[derive(Default, Clone, Traversable)] pub struct ByGreatEqualAmount { @@ -21,6 +21,46 @@ pub struct ByGreatEqualAmount { } impl ByGreatEqualAmount { + pub fn new(mut create: F) -> Self + where + F: FnMut(Filter) -> T, + { + Self { + _1sat: create(Filter::Amount(AmountFilter::GreaterOrEqual(Sats::_1))), + _10sats: create(Filter::Amount(AmountFilter::GreaterOrEqual(Sats::_10))), + _100sats: create(Filter::Amount(AmountFilter::GreaterOrEqual(Sats::_100))), + _1k_sats: create(Filter::Amount(AmountFilter::GreaterOrEqual(Sats::_1K))), + _10k_sats: create(Filter::Amount(AmountFilter::GreaterOrEqual(Sats::_10K))), + _100k_sats: create(Filter::Amount(AmountFilter::GreaterOrEqual(Sats::_100K))), + _1m_sats: create(Filter::Amount(AmountFilter::GreaterOrEqual(Sats::_1M))), + _10m_sats: create(Filter::Amount(AmountFilter::GreaterOrEqual(Sats::_10M))), + _1btc: create(Filter::Amount(AmountFilter::GreaterOrEqual(Sats::_1BTC))), + _10btc: create(Filter::Amount(AmountFilter::GreaterOrEqual(Sats::_10BTC))), + _100btc: create(Filter::Amount(AmountFilter::GreaterOrEqual(Sats::_100BTC))), + _1k_btc: create(Filter::Amount(AmountFilter::GreaterOrEqual(Sats::_1K_BTC))), + _10k_btc: create(Filter::Amount(AmountFilter::GreaterOrEqual(Sats::_10K_BTC))), + } + } + + pub fn iter(&self) -> impl Iterator { + [ + &self._1sat, + &self._10sats, + &self._100sats, + &self._1k_sats, + &self._10k_sats, + &self._100k_sats, + &self._1m_sats, + &self._10m_sats, + &self._1btc, + &self._10btc, + &self._100btc, + &self._1k_btc, + &self._10k_btc, + ] + .into_iter() + } + pub fn iter_mut(&mut self) -> impl Iterator { [ &mut self._1sat, @@ -40,49 +80,3 @@ impl ByGreatEqualAmount { .into_iter() } } - -impl ByGreatEqualAmount> { - pub fn iter_right(&self) -> impl Iterator { - [ - &self._1sat.1, - &self._10sats.1, - &self._100sats.1, - &self._1k_sats.1, - &self._10k_sats.1, - &self._100k_sats.1, - &self._1m_sats.1, - &self._10m_sats.1, - &self._1btc.1, - &self._10btc.1, - &self._100btc.1, - &self._1k_btc.1, - &self._10k_btc.1, - ] - .into_iter() - } -} - -impl From> for ByGreatEqualAmount> { - #[inline] - fn from(value: ByGreatEqualAmount) -> Self { - Self { - _1sat: (Filter::GreaterOrEqual(Sats::_1.into()), value._1sat).into(), - _10sats: (Filter::GreaterOrEqual(Sats::_10.into()), value._10sats).into(), - _100sats: (Filter::GreaterOrEqual(Sats::_100.into()), value._100sats).into(), - _1k_sats: (Filter::GreaterOrEqual(Sats::_1K.into()), value._1k_sats).into(), - _10k_sats: (Filter::GreaterOrEqual(Sats::_10K.into()), value._10k_sats).into(), - _100k_sats: (Filter::GreaterOrEqual(Sats::_100K.into()), value._100k_sats).into(), - _1m_sats: (Filter::GreaterOrEqual(Sats::_1M.into()), value._1m_sats).into(), - _10m_sats: (Filter::GreaterOrEqual(Sats::_10M.into()), value._10m_sats).into(), - _1btc: (Filter::GreaterOrEqual(Sats::_1BTC.into()), value._1btc).into(), - _10btc: (Filter::GreaterOrEqual(Sats::_10BTC.into()), value._10btc).into(), - _100btc: (Filter::GreaterOrEqual(Sats::_100BTC.into()), value._100btc).into(), - _1k_btc: (Filter::GreaterOrEqual(Sats::_1K_BTC.into()), value._1k_btc).into(), - _10k_btc: ( - Filter::GreaterOrEqual(Sats::_10K_BTC.into()), - value._10k_btc, - ) - .into(), - } - } -} diff --git a/crates/brk_grouper/src/by_lt_amount.rs b/crates/brk_grouper/src/by_lt_amount.rs index 5eaf35d7a..3599bad11 100644 --- a/crates/brk_grouper/src/by_lt_amount.rs +++ b/crates/brk_grouper/src/by_lt_amount.rs @@ -1,7 +1,7 @@ use brk_traversable::Traversable; use brk_types::Sats; -use super::{Filter, Filtered}; +use super::{AmountFilter, Filter}; #[derive(Default, Clone, Traversable)] pub struct ByLowerThanAmount { @@ -21,6 +21,46 @@ pub struct ByLowerThanAmount { } impl ByLowerThanAmount { + pub fn new(mut create: F) -> Self + where + F: FnMut(Filter) -> T, + { + Self { + _10sats: create(Filter::Amount(AmountFilter::LowerThan(Sats::_10))), + _100sats: create(Filter::Amount(AmountFilter::LowerThan(Sats::_100))), + _1k_sats: create(Filter::Amount(AmountFilter::LowerThan(Sats::_1K))), + _10k_sats: create(Filter::Amount(AmountFilter::LowerThan(Sats::_10K))), + _100k_sats: create(Filter::Amount(AmountFilter::LowerThan(Sats::_100K))), + _1m_sats: create(Filter::Amount(AmountFilter::LowerThan(Sats::_1M))), + _10m_sats: create(Filter::Amount(AmountFilter::LowerThan(Sats::_10M))), + _1btc: create(Filter::Amount(AmountFilter::LowerThan(Sats::_1BTC))), + _10btc: create(Filter::Amount(AmountFilter::LowerThan(Sats::_10BTC))), + _100btc: create(Filter::Amount(AmountFilter::LowerThan(Sats::_100BTC))), + _1k_btc: create(Filter::Amount(AmountFilter::LowerThan(Sats::_1K_BTC))), + _10k_btc: create(Filter::Amount(AmountFilter::LowerThan(Sats::_10K_BTC))), + _100k_btc: create(Filter::Amount(AmountFilter::LowerThan(Sats::_100K_BTC))), + } + } + + pub fn iter(&self) -> impl Iterator { + [ + &self._10sats, + &self._100sats, + &self._1k_sats, + &self._10k_sats, + &self._100k_sats, + &self._1m_sats, + &self._10m_sats, + &self._1btc, + &self._10btc, + &self._100btc, + &self._1k_btc, + &self._10k_btc, + &self._100k_btc, + ] + .into_iter() + } + pub fn iter_mut(&mut self) -> impl Iterator { [ &mut self._10sats, @@ -40,45 +80,3 @@ impl ByLowerThanAmount { .into_iter() } } - -impl ByLowerThanAmount> { - pub fn iter_right(&self) -> impl Iterator { - [ - &self._10sats.1, - &self._100sats.1, - &self._1k_sats.1, - &self._10k_sats.1, - &self._100k_sats.1, - &self._1m_sats.1, - &self._10m_sats.1, - &self._1btc.1, - &self._10btc.1, - &self._100btc.1, - &self._1k_btc.1, - &self._10k_btc.1, - &self._100k_btc.1, - ] - .into_iter() - } -} - -impl From> for ByLowerThanAmount> { - #[inline] - fn from(value: ByLowerThanAmount) -> Self { - Self { - _10sats: (Filter::LowerThan(Sats::_10.into()), value._10sats).into(), - _100sats: (Filter::LowerThan(Sats::_100.into()), value._100sats).into(), - _1k_sats: (Filter::LowerThan(Sats::_1K.into()), value._1k_sats).into(), - _10k_sats: (Filter::LowerThan(Sats::_10K.into()), value._10k_sats).into(), - _100k_sats: (Filter::LowerThan(Sats::_100K.into()), value._100k_sats).into(), - _1m_sats: (Filter::LowerThan(Sats::_1M.into()), value._1m_sats).into(), - _10m_sats: (Filter::LowerThan(Sats::_10M.into()), value._10m_sats).into(), - _1btc: (Filter::LowerThan(Sats::_1BTC.into()), value._1btc).into(), - _10btc: (Filter::LowerThan(Sats::_10BTC.into()), value._10btc).into(), - _100btc: (Filter::LowerThan(Sats::_100BTC.into()), value._100btc).into(), - _1k_btc: (Filter::LowerThan(Sats::_1K_BTC.into()), value._1k_btc).into(), - _10k_btc: (Filter::LowerThan(Sats::_10K_BTC.into()), value._10k_btc).into(), - _100k_btc: (Filter::LowerThan(Sats::_100K_BTC.into()), value._100k_btc).into(), - } - } -} diff --git a/crates/brk_grouper/src/by_max_age.rs b/crates/brk_grouper/src/by_max_age.rs index abad40848..132b92b0e 100644 --- a/crates/brk_grouper/src/by_max_age.rs +++ b/crates/brk_grouper/src/by_max_age.rs @@ -1,6 +1,4 @@ -use crate::Filtered; - -use super::Filter; +use super::{Filter, TimeFilter}; use brk_traversable::Traversable; #[derive(Default, Clone, Traversable)] @@ -26,6 +24,56 @@ pub struct ByMaxAge { } impl ByMaxAge { + pub fn new(mut create: F) -> Self + where + F: FnMut(Filter) -> T, + { + Self { + _1w: create(Filter::Time(TimeFilter::LowerThan(7))), + _1m: create(Filter::Time(TimeFilter::LowerThan(30))), + _2m: create(Filter::Time(TimeFilter::LowerThan(2 * 30))), + _3m: create(Filter::Time(TimeFilter::LowerThan(3 * 30))), + _4m: create(Filter::Time(TimeFilter::LowerThan(4 * 30))), + _5m: create(Filter::Time(TimeFilter::LowerThan(5 * 30))), + _6m: create(Filter::Time(TimeFilter::LowerThan(6 * 30))), + _1y: create(Filter::Time(TimeFilter::LowerThan(365))), + _2y: create(Filter::Time(TimeFilter::LowerThan(2 * 365))), + _3y: create(Filter::Time(TimeFilter::LowerThan(3 * 365))), + _4y: create(Filter::Time(TimeFilter::LowerThan(4 * 365))), + _5y: create(Filter::Time(TimeFilter::LowerThan(5 * 365))), + _6y: create(Filter::Time(TimeFilter::LowerThan(6 * 365))), + _7y: create(Filter::Time(TimeFilter::LowerThan(7 * 365))), + _8y: create(Filter::Time(TimeFilter::LowerThan(8 * 365))), + _10y: create(Filter::Time(TimeFilter::LowerThan(10 * 365))), + _12y: create(Filter::Time(TimeFilter::LowerThan(12 * 365))), + _15y: create(Filter::Time(TimeFilter::LowerThan(15 * 365))), + } + } + + pub fn iter(&self) -> impl Iterator { + [ + &self._1w, + &self._1m, + &self._2m, + &self._3m, + &self._4m, + &self._5m, + &self._6m, + &self._1y, + &self._2y, + &self._3y, + &self._4y, + &self._5y, + &self._6y, + &self._7y, + &self._8y, + &self._10y, + &self._12y, + &self._15y, + ] + .into_iter() + } + pub fn iter_mut(&mut self) -> impl Iterator { [ &mut self._1w, @@ -50,55 +98,3 @@ impl ByMaxAge { .into_iter() } } - -impl ByMaxAge> { - pub fn iter_right(&self) -> impl Iterator { - [ - &self._1w.1, - &self._1m.1, - &self._2m.1, - &self._3m.1, - &self._4m.1, - &self._5m.1, - &self._6m.1, - &self._1y.1, - &self._2y.1, - &self._3y.1, - &self._4y.1, - &self._5y.1, - &self._6y.1, - &self._7y.1, - &self._8y.1, - &self._10y.1, - &self._12y.1, - &self._15y.1, - ] - .into_iter() - } -} - -impl From> for ByMaxAge> { - #[inline] - fn from(value: ByMaxAge) -> Self { - Self { - _1w: (Filter::LowerThan(7), value._1w).into(), - _1m: (Filter::LowerThan(30), value._1m).into(), - _2m: (Filter::LowerThan(2 * 30), value._2m).into(), - _3m: (Filter::LowerThan(3 * 30), value._3m).into(), - _4m: (Filter::LowerThan(4 * 30), value._4m).into(), - _5m: (Filter::LowerThan(5 * 30), value._5m).into(), - _6m: (Filter::LowerThan(6 * 30), value._6m).into(), - _1y: (Filter::LowerThan(365), value._1y).into(), - _2y: (Filter::LowerThan(2 * 365), value._2y).into(), - _3y: (Filter::LowerThan(3 * 365), value._3y).into(), - _4y: (Filter::LowerThan(4 * 365), value._4y).into(), - _5y: (Filter::LowerThan(5 * 365), value._5y).into(), - _6y: (Filter::LowerThan(6 * 365), value._6y).into(), - _7y: (Filter::LowerThan(7 * 365), value._7y).into(), - _8y: (Filter::LowerThan(8 * 365), value._8y).into(), - _10y: (Filter::LowerThan(10 * 365), value._10y).into(), - _12y: (Filter::LowerThan(12 * 365), value._12y).into(), - _15y: (Filter::LowerThan(15 * 365), value._15y).into(), - } - } -} diff --git a/crates/brk_grouper/src/by_min_age.rs b/crates/brk_grouper/src/by_min_age.rs index d37802979..7e8549c66 100644 --- a/crates/brk_grouper/src/by_min_age.rs +++ b/crates/brk_grouper/src/by_min_age.rs @@ -1,8 +1,6 @@ use brk_traversable::Traversable; -use crate::Filtered; - -use super::Filter; +use super::{Filter, TimeFilter}; #[derive(Default, Clone, Traversable)] pub struct ByMinAge { @@ -27,6 +25,56 @@ pub struct ByMinAge { } impl ByMinAge { + pub fn new(mut create: F) -> Self + where + F: FnMut(Filter) -> T, + { + Self { + _1d: create(Filter::Time(TimeFilter::GreaterOrEqual(1))), + _1w: create(Filter::Time(TimeFilter::GreaterOrEqual(7))), + _1m: create(Filter::Time(TimeFilter::GreaterOrEqual(30))), + _2m: create(Filter::Time(TimeFilter::GreaterOrEqual(2 * 30))), + _3m: create(Filter::Time(TimeFilter::GreaterOrEqual(3 * 30))), + _4m: create(Filter::Time(TimeFilter::GreaterOrEqual(4 * 30))), + _5m: create(Filter::Time(TimeFilter::GreaterOrEqual(5 * 30))), + _6m: create(Filter::Time(TimeFilter::GreaterOrEqual(6 * 30))), + _1y: create(Filter::Time(TimeFilter::GreaterOrEqual(365))), + _2y: create(Filter::Time(TimeFilter::GreaterOrEqual(2 * 365))), + _3y: create(Filter::Time(TimeFilter::GreaterOrEqual(3 * 365))), + _4y: create(Filter::Time(TimeFilter::GreaterOrEqual(4 * 365))), + _5y: create(Filter::Time(TimeFilter::GreaterOrEqual(5 * 365))), + _6y: create(Filter::Time(TimeFilter::GreaterOrEqual(6 * 365))), + _7y: create(Filter::Time(TimeFilter::GreaterOrEqual(7 * 365))), + _8y: create(Filter::Time(TimeFilter::GreaterOrEqual(8 * 365))), + _10y: create(Filter::Time(TimeFilter::GreaterOrEqual(10 * 365))), + _12y: create(Filter::Time(TimeFilter::GreaterOrEqual(12 * 365))), + } + } + + pub fn iter(&self) -> impl Iterator { + [ + &self._1d, + &self._1w, + &self._1m, + &self._2m, + &self._3m, + &self._4m, + &self._5m, + &self._6m, + &self._1y, + &self._2y, + &self._3y, + &self._4y, + &self._5y, + &self._6y, + &self._7y, + &self._8y, + &self._10y, + &self._12y, + ] + .into_iter() + } + pub fn iter_mut(&mut self) -> impl Iterator { [ &mut self._1d, @@ -51,55 +99,3 @@ impl ByMinAge { .into_iter() } } - -impl ByMinAge> { - pub fn iter_right(&self) -> impl Iterator { - [ - &self._1d.1, - &self._1w.1, - &self._1m.1, - &self._2m.1, - &self._3m.1, - &self._4m.1, - &self._5m.1, - &self._6m.1, - &self._1y.1, - &self._2y.1, - &self._3y.1, - &self._4y.1, - &self._5y.1, - &self._6y.1, - &self._7y.1, - &self._8y.1, - &self._10y.1, - &self._12y.1, - ] - .into_iter() - } -} - -impl From> for ByMinAge> { - #[inline] - fn from(value: ByMinAge) -> Self { - Self { - _1d: (Filter::GreaterOrEqual(1), value._1d).into(), - _1w: (Filter::GreaterOrEqual(7), value._1w).into(), - _1m: (Filter::GreaterOrEqual(30), value._1m).into(), - _2m: (Filter::GreaterOrEqual(2 * 30), value._2m).into(), - _3m: (Filter::GreaterOrEqual(3 * 30), value._3m).into(), - _4m: (Filter::GreaterOrEqual(4 * 30), value._4m).into(), - _5m: (Filter::GreaterOrEqual(5 * 30), value._5m).into(), - _6m: (Filter::GreaterOrEqual(6 * 30), value._6m).into(), - _1y: (Filter::GreaterOrEqual(365), value._1y).into(), - _2y: (Filter::GreaterOrEqual(2 * 365), value._2y).into(), - _3y: (Filter::GreaterOrEqual(3 * 365), value._3y).into(), - _4y: (Filter::GreaterOrEqual(4 * 365), value._4y).into(), - _5y: (Filter::GreaterOrEqual(5 * 365), value._5y).into(), - _6y: (Filter::GreaterOrEqual(6 * 365), value._6y).into(), - _7y: (Filter::GreaterOrEqual(7 * 365), value._7y).into(), - _8y: (Filter::GreaterOrEqual(8 * 365), value._8y).into(), - _10y: (Filter::GreaterOrEqual(10 * 365), value._10y).into(), - _12y: (Filter::GreaterOrEqual(12 * 365), value._12y).into(), - } - } -} diff --git a/crates/brk_grouper/src/by_spendable_type.rs b/crates/brk_grouper/src/by_spendable_type.rs index 6350f403e..a970615ea 100644 --- a/crates/brk_grouper/src/by_spendable_type.rs +++ b/crates/brk_grouper/src/by_spendable_type.rs @@ -4,7 +4,7 @@ use brk_traversable::Traversable; use brk_types::OutputType; use rayon::iter::{IntoParallelIterator, ParallelIterator}; -use super::{Filter, Filtered}; +use super::Filter; #[derive(Default, Clone, Debug, Traversable)] pub struct BySpendableType { @@ -22,6 +22,25 @@ pub struct BySpendableType { } impl BySpendableType { + pub fn new(mut create: F) -> Self + where + F: FnMut(Filter) -> T, + { + Self { + p2pk65: create(Filter::Type(OutputType::P2PK65)), + p2pk33: create(Filter::Type(OutputType::P2PK33)), + p2pkh: create(Filter::Type(OutputType::P2PKH)), + p2ms: create(Filter::Type(OutputType::P2MS)), + p2sh: create(Filter::Type(OutputType::P2SH)), + p2wpkh: create(Filter::Type(OutputType::P2WPKH)), + p2wsh: create(Filter::Type(OutputType::P2WSH)), + p2tr: create(Filter::Type(OutputType::P2TR)), + p2a: create(Filter::Type(OutputType::P2A)), + unknown: create(Filter::Type(OutputType::Unknown)), + empty: create(Filter::Type(OutputType::Empty)), + } + } + pub fn get_mut(&mut self, output_type: OutputType) -> &mut T { match output_type { OutputType::P2PK65 => &mut self.p2pk65, @@ -39,6 +58,23 @@ impl BySpendableType { } } + pub fn iter(&self) -> impl Iterator { + [ + &self.p2pk65, + &self.p2pk33, + &self.p2pkh, + &self.p2ms, + &self.p2sh, + &self.p2wpkh, + &self.p2wsh, + &self.p2tr, + &self.p2a, + &self.unknown, + &self.empty, + ] + .into_iter() + } + pub fn iter_mut(&mut self) -> impl Iterator { [ &mut self.p2pk65, @@ -94,44 +130,6 @@ impl BySpendableType { } } -impl BySpendableType> { - pub fn iter_right(&self) -> impl Iterator { - [ - &self.p2pk65.1, - &self.p2pk33.1, - &self.p2pkh.1, - &self.p2ms.1, - &self.p2sh.1, - &self.p2wpkh.1, - &self.p2wsh.1, - &self.p2tr.1, - &self.p2a.1, - &self.unknown.1, - &self.empty.1, - ] - .into_iter() - } -} - -impl From> for BySpendableType> { - #[inline] - fn from(value: BySpendableType) -> Self { - Self { - p2pk65: (Filter::Type(OutputType::P2PK65), value.p2pk65).into(), - p2pk33: (Filter::Type(OutputType::P2PK33), value.p2pk33).into(), - p2pkh: (Filter::Type(OutputType::P2PKH), value.p2pkh).into(), - p2ms: (Filter::Type(OutputType::P2MS), value.p2ms).into(), - p2sh: (Filter::Type(OutputType::P2SH), value.p2sh).into(), - p2wpkh: (Filter::Type(OutputType::P2WPKH), value.p2wpkh).into(), - p2wsh: (Filter::Type(OutputType::P2WSH), value.p2wsh).into(), - p2tr: (Filter::Type(OutputType::P2TR), value.p2tr).into(), - p2a: (Filter::Type(OutputType::P2A), value.p2a).into(), - unknown: (Filter::Type(OutputType::Unknown), value.unknown).into(), - empty: (Filter::Type(OutputType::Empty), value.empty).into(), - } - } -} - impl Add for BySpendableType where T: Add, diff --git a/crates/brk_grouper/src/by_term.rs b/crates/brk_grouper/src/by_term.rs index 322cf114d..ec664ff29 100644 --- a/crates/brk_grouper/src/by_term.rs +++ b/crates/brk_grouper/src/by_term.rs @@ -1,8 +1,6 @@ use brk_traversable::Traversable; -use crate::Filtered; - -use super::Filter; +use super::{Filter, Term}; #[derive(Default, Clone, Traversable)] pub struct ByTerm { @@ -11,23 +9,21 @@ pub struct ByTerm { } impl ByTerm { + pub fn new(mut create: F) -> Self + where + F: FnMut(Filter) -> T, + { + Self { + short: create(Filter::Term(Term::Sth)), + long: create(Filter::Term(Term::Lth)), + } + } + + pub fn iter(&self) -> impl Iterator { + [&self.short, &self.long].into_iter() + } + pub fn iter_mut(&mut self) -> impl Iterator { [&mut self.short, &mut self.long].into_iter() } } - -impl ByTerm> { - pub fn iter_right(&self) -> impl Iterator { - [&self.short.1, &self.long.1].into_iter() - } -} - -impl From> for ByTerm> { - #[inline] - fn from(value: ByTerm) -> Self { - Self { - short: (Filter::LowerThan(5 * 30), value.short).into(), - long: (Filter::GreaterOrEqual(5 * 30), value.long).into(), - } - } -} diff --git a/crates/brk_grouper/src/cohort_context.rs b/crates/brk_grouper/src/cohort_context.rs new file mode 100644 index 000000000..420ded95a --- /dev/null +++ b/crates/brk_grouper/src/cohort_context.rs @@ -0,0 +1,17 @@ +/// Context for cohort naming - determines whether a prefix is needed. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CohortContext { + /// UTXO-based cohorts: uses "utxos_" prefix for Time/Amount filters + Utxo, + /// Address-based cohorts: uses "addrs_" prefix for Amount filters + Address, +} + +impl CohortContext { + pub fn prefix(&self) -> &'static str { + match self { + CohortContext::Utxo => "utxos", + CohortContext::Address => "addrs", + } + } +} diff --git a/crates/brk_grouper/src/filter.rs b/crates/brk_grouper/src/filter.rs index 73a1e2dd2..d0771e372 100644 --- a/crates/brk_grouper/src/filter.rs +++ b/crates/brk_grouper/src/filter.rs @@ -1,88 +1,109 @@ -use std::ops::Range; +use brk_types::{HalvingEpoch, OutputType, Sats}; -use brk_traversable::{Traversable, TreeNode}; -use brk_types::{HalvingEpoch, OutputType}; -use vecdb::AnyExportableVec; +use super::{AmountFilter, CohortContext, Term, TimeFilter}; #[derive(Debug, Clone, PartialEq, Eq)] pub enum Filter { All, - LowerThan(usize), - Range(Range), - GreaterOrEqual(usize), + Term(Term), + Time(TimeFilter), + Amount(AmountFilter), Epoch(HalvingEpoch), Type(OutputType), } impl Filter { - pub fn contains(&self, value: usize) -> bool { + pub fn is_all(&self) -> bool { + matches!(self, Filter::All) + } + + /// Returns true if this filter includes day 0 (only applicable to time-based filters) + pub fn includes_first_day(&self) -> bool { match self { - Filter::Range(r) => r.contains(&value), - Filter::LowerThan(max) => *max > value, - Filter::GreaterOrEqual(min) => *min <= value, Filter::All => true, - Filter::Epoch(_) | Filter::Type(_) => false, + Filter::Term(Term::Sth) => true, + Filter::Term(Term::Lth) => false, + Filter::Time(t) => t.includes_first_day(), + _ => false, } } - pub fn includes(&self, other: &Filter) -> bool { + pub fn to_name_suffix(&self) -> String { + match self { + Filter::All => String::new(), + Filter::Term(t) => t.to_name().to_string(), + Filter::Time(t) => t.to_name_suffix(), + Filter::Amount(a) => a.to_name_suffix(), + Filter::Epoch(e) => format!("epoch_{}", usize::from(*e)), + Filter::Type(t) => match t { + OutputType::P2MS => "p2ms_outputs".to_string(), + OutputType::Empty => "empty_outputs".to_string(), + OutputType::Unknown => "unknown_outputs".to_string(), + _ => format!("{:?}", t).to_lowercase(), + }, + } + } + + /// Returns the full name for this filter, including context-based prefix. + /// + /// Prefix rules: + /// - No prefix: `All`, `Term`, `Epoch`, `Type` + /// - `utxos_` prefix: `Time` or `Amount` with `CohortContext::Utxo` + /// - `addrs_` prefix: `Amount` with `CohortContext::Address` + pub fn to_full_name(&self, context: CohortContext) -> String { + let suffix = self.to_name_suffix(); + if suffix.is_empty() { + return suffix; + } + + let needs_prefix = match self { + Filter::All | Filter::Term(_) | Filter::Epoch(_) | Filter::Type(_) => false, + Filter::Time(_) => matches!(context, CohortContext::Utxo), + Filter::Amount(_) => true, + }; + + if needs_prefix { + format!("{}_{}", context.prefix(), suffix) + } else { + suffix + } + } + + /// Check if a time value (days) is contained by this filter + pub fn contains_time(&self, days: usize) -> bool { match self { Filter::All => true, - Filter::LowerThan(max) => match other { - Filter::LowerThan(max2) => max >= max2, - Filter::Range(range) => range.end <= *max, - Filter::All | Filter::GreaterOrEqual(_) | Filter::Epoch(_) | Filter::Type(_) => { - false - } - }, - Filter::GreaterOrEqual(min) => match other { - Filter::Range(range) => range.start >= *min, - Filter::GreaterOrEqual(min2) => min <= min2, - Filter::All | Filter::LowerThan(_) | Filter::Epoch(_) | Filter::Type(_) => false, - }, - Filter::Range(_) | Filter::Epoch(_) | Filter::Type(_) => false, + Filter::Term(Term::Sth) => days < Term::THRESHOLD_DAYS, + Filter::Term(Term::Lth) => days >= Term::THRESHOLD_DAYS, + Filter::Time(t) => t.contains(days), + _ => false, + } + } + + /// Check if an amount value (sats) is contained by this filter + pub fn contains_amount(&self, sats: Sats) -> bool { + match self { + Filter::All => true, + Filter::Amount(a) => a.contains(sats), + _ => false, + } + } + + /// Check if this filter includes another filter (for aggregation) + pub fn includes(&self, other: &Filter) -> bool { + match (self, other) { + (Filter::All, _) => true, + (Filter::Term(Term::Sth), Filter::Time(t)) => { + matches!(t, TimeFilter::LowerThan(d) if *d <= Term::THRESHOLD_DAYS) + || matches!(t, TimeFilter::Range(r) if r.end <= Term::THRESHOLD_DAYS) + } + (Filter::Term(Term::Lth), Filter::Time(t)) => { + matches!(t, TimeFilter::GreaterOrEqual(d) if *d >= Term::THRESHOLD_DAYS) + || matches!(t, TimeFilter::Range(r) if r.start >= Term::THRESHOLD_DAYS) + } + (Filter::Time(t1), Filter::Time(t2)) => t1.includes(t2), + (Filter::Amount(a1), Filter::Amount(a2)) => a1.includes(a2), + _ => false, } } } - -#[derive(Clone)] -pub struct Filtered(pub Filter, pub T); - -impl Filtered { - pub fn includes(&self, other: &Filter) -> bool { - self.0.includes(other) - } - - pub fn filter(&self) -> &Filter { - &self.0 - } - - pub fn unwrap(self) -> T { - self.1 - } - - pub fn t(&self) -> &T { - &self.1 - } - - pub fn mut_t(&mut self) -> &mut T { - &mut self.1 - } -} - -impl From<(Filter, T)> for Filtered { - #[inline] - fn from(value: (Filter, T)) -> Self { - Self(value.0, value.1) - } -} - -impl Traversable for Filtered { - fn to_tree_node(&self) -> TreeNode { - self.1.to_tree_node() - } - - fn iter_any_exportable(&self) -> impl Iterator { - self.1.iter_any_exportable() - } -} diff --git a/crates/brk_grouper/src/filtered.rs b/crates/brk_grouper/src/filtered.rs new file mode 100644 index 000000000..475ff1fb0 --- /dev/null +++ b/crates/brk_grouper/src/filtered.rs @@ -0,0 +1,21 @@ +use super::{CohortContext, Filter}; + +pub trait Filtered { + fn filter(&self) -> &Filter; + + fn is_all(&self) -> bool { + self.filter().is_all() + } + + fn includes_first_day(&self) -> bool { + self.filter().includes_first_day() + } + + fn name_suffix(&self) -> String { + self.filter().to_name_suffix() + } + + fn full_name(&self, context: CohortContext) -> String { + self.filter().to_full_name(context) + } +} diff --git a/crates/brk_grouper/src/lib.rs b/crates/brk_grouper/src/lib.rs index 6cd423784..0bc328e8a 100644 --- a/crates/brk_grouper/src/lib.rs +++ b/crates/brk_grouper/src/lib.rs @@ -1,6 +1,7 @@ #![doc = include_str!("../README.md")] mod address; +mod amount_filter; mod by_address_type; mod by_age_range; mod by_amount_range; @@ -14,10 +15,16 @@ mod by_spendable_type; mod by_term; mod by_type; mod by_unspendable_type; +mod cohort_context; mod filter; +mod filtered; +mod state_level; +mod term; +mod time_filter; mod utxo; pub use address::*; +pub use amount_filter::*; pub use by_address_type::*; pub use by_age_range::*; pub use by_amount_range::*; @@ -31,5 +38,10 @@ pub use by_spendable_type::*; pub use by_term::*; pub use by_type::*; pub use by_unspendable_type::*; +pub use cohort_context::*; pub use filter::*; +pub use filtered::*; +pub use state_level::*; +pub use term::*; +pub use time_filter::*; pub use utxo::*; diff --git a/crates/brk_grouper/src/state_level.rs b/crates/brk_grouper/src/state_level.rs new file mode 100644 index 000000000..1d6022452 --- /dev/null +++ b/crates/brk_grouper/src/state_level.rs @@ -0,0 +1,32 @@ +/// Controls the level of state tracking for a cohort. +/// +/// - `None`: No state tracking. Values are computed from stateful sub-cohorts. +/// - `PriceOnly`: Only tracks `price_to_amount` for percentile calculations. +/// Used by aggregate cohorts (all, sth, lth) that compute other values from sub-cohorts. +/// - `Full`: Full state tracking including supply, realized values, and `price_to_amount`. +/// Used by stateful cohorts like individual age ranges and epochs. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum StateLevel { + #[default] + None, + PriceOnly, + Full, +} + +impl StateLevel { + pub fn is_none(&self) -> bool { + matches!(self, StateLevel::None) + } + + pub fn is_price_only(&self) -> bool { + matches!(self, StateLevel::PriceOnly) + } + + pub fn is_full(&self) -> bool { + matches!(self, StateLevel::Full) + } + + pub fn has_price_to_amount(&self) -> bool { + matches!(self, StateLevel::PriceOnly | StateLevel::Full) + } +} diff --git a/crates/brk_grouper/src/term.rs b/crates/brk_grouper/src/term.rs new file mode 100644 index 000000000..f402800fc --- /dev/null +++ b/crates/brk_grouper/src/term.rs @@ -0,0 +1,20 @@ +/// Classification for short-term vs long-term holders. +/// The threshold is 150 days (approximately 5 months). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Term { + /// Short-Term Holder: < 150 days + Sth, + /// Long-Term Holder: >= 150 days + Lth, +} + +impl Term { + pub const THRESHOLD_DAYS: usize = 150; + + pub fn to_name(&self) -> &'static str { + match self { + Term::Sth => "sth", + Term::Lth => "lth", + } + } +} diff --git a/crates/brk_grouper/src/time_filter.rs b/crates/brk_grouper/src/time_filter.rs new file mode 100644 index 000000000..621b45303 --- /dev/null +++ b/crates/brk_grouper/src/time_filter.rs @@ -0,0 +1,114 @@ +use std::ops::Range; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum TimeFilter { + LowerThan(usize), + Range(Range), + GreaterOrEqual(usize), +} + +impl TimeFilter { + pub fn contains(&self, days: usize) -> bool { + match self { + TimeFilter::LowerThan(max) => days < *max, + TimeFilter::Range(r) => r.contains(&days), + TimeFilter::GreaterOrEqual(min) => days >= *min, + } + } + + pub fn includes(&self, other: &TimeFilter) -> bool { + match self { + TimeFilter::LowerThan(max) => match other { + TimeFilter::LowerThan(max2) => max >= max2, + TimeFilter::Range(range) => range.end <= *max, + TimeFilter::GreaterOrEqual(_) => false, + }, + TimeFilter::GreaterOrEqual(min) => match other { + TimeFilter::Range(range) => range.start >= *min, + TimeFilter::GreaterOrEqual(min2) => min <= min2, + TimeFilter::LowerThan(_) => false, + }, + TimeFilter::Range(_) => false, + } + } + + /// Returns true if this filter includes day 0 (UTXOs less than 1 day old) + pub fn includes_first_day(&self) -> bool { + match self { + TimeFilter::LowerThan(_) => true, + TimeFilter::Range(r) => r.start == 0, + TimeFilter::GreaterOrEqual(_) => false, + } + } + + pub fn to_name_suffix(&self) -> String { + match self { + // Special cases for common filters + TimeFilter::LowerThan(1) => "up_to_1d".to_string(), + TimeFilter::LowerThan(7) => "up_to_1w".to_string(), + TimeFilter::LowerThan(30) => "up_to_1m".to_string(), + TimeFilter::LowerThan(60) => "up_to_2m".to_string(), + TimeFilter::LowerThan(90) => "up_to_3m".to_string(), + TimeFilter::LowerThan(120) => "up_to_4m".to_string(), + TimeFilter::LowerThan(150) => "sth".to_string(), + TimeFilter::LowerThan(180) => "up_to_6m".to_string(), + TimeFilter::LowerThan(365) => "up_to_1y".to_string(), + TimeFilter::LowerThan(730) => "up_to_2y".to_string(), + TimeFilter::LowerThan(1095) => "up_to_3y".to_string(), + TimeFilter::LowerThan(1460) => "up_to_4y".to_string(), + TimeFilter::LowerThan(1825) => "up_to_5y".to_string(), + TimeFilter::LowerThan(2190) => "up_to_6y".to_string(), + TimeFilter::LowerThan(2555) => "up_to_7y".to_string(), + TimeFilter::LowerThan(2920) => "up_to_8y".to_string(), + TimeFilter::LowerThan(3650) => "up_to_10y".to_string(), + TimeFilter::LowerThan(4380) => "up_to_12y".to_string(), + TimeFilter::LowerThan(5475) => "up_to_15y".to_string(), + + TimeFilter::GreaterOrEqual(1) => "at_least_1d".to_string(), + TimeFilter::GreaterOrEqual(7) => "at_least_1w".to_string(), + TimeFilter::GreaterOrEqual(30) => "at_least_1m".to_string(), + TimeFilter::GreaterOrEqual(60) => "at_least_2m".to_string(), + TimeFilter::GreaterOrEqual(90) => "at_least_3m".to_string(), + TimeFilter::GreaterOrEqual(120) => "at_least_4m".to_string(), + TimeFilter::GreaterOrEqual(150) => "lth".to_string(), + TimeFilter::GreaterOrEqual(180) => "at_least_6m".to_string(), + TimeFilter::GreaterOrEqual(365) => "at_least_1y".to_string(), + TimeFilter::GreaterOrEqual(730) => "at_least_2y".to_string(), + TimeFilter::GreaterOrEqual(1095) => "at_least_3y".to_string(), + TimeFilter::GreaterOrEqual(1460) => "at_least_4y".to_string(), + TimeFilter::GreaterOrEqual(1825) => "at_least_5y".to_string(), + TimeFilter::GreaterOrEqual(2190) => "at_least_6y".to_string(), + TimeFilter::GreaterOrEqual(2555) => "at_least_7y".to_string(), + TimeFilter::GreaterOrEqual(2920) => "at_least_8y".to_string(), + TimeFilter::GreaterOrEqual(3650) => "at_least_10y".to_string(), + TimeFilter::GreaterOrEqual(4380) => "at_least_12y".to_string(), + TimeFilter::GreaterOrEqual(5475) => "at_least_15y".to_string(), + + // Range special cases + TimeFilter::Range(r) if *r == (0..1) => "up_to_1d".to_string(), + TimeFilter::Range(r) if *r == (1..7) => "1d_to_1w".to_string(), + TimeFilter::Range(r) if *r == (7..30) => "1w_to_1m".to_string(), + TimeFilter::Range(r) if *r == (30..60) => "1m_to_2m".to_string(), + TimeFilter::Range(r) if *r == (60..90) => "2m_to_3m".to_string(), + TimeFilter::Range(r) if *r == (90..120) => "3m_to_4m".to_string(), + TimeFilter::Range(r) if *r == (120..150) => "4m_to_5m".to_string(), + TimeFilter::Range(r) if *r == (150..180) => "5m_to_6m".to_string(), + TimeFilter::Range(r) if *r == (180..365) => "6m_to_1y".to_string(), + TimeFilter::Range(r) if *r == (365..730) => "1y_to_2y".to_string(), + TimeFilter::Range(r) if *r == (730..1095) => "2y_to_3y".to_string(), + TimeFilter::Range(r) if *r == (1095..1460) => "3y_to_4y".to_string(), + TimeFilter::Range(r) if *r == (1460..1825) => "4y_to_5y".to_string(), + TimeFilter::Range(r) if *r == (1825..2190) => "5y_to_6y".to_string(), + TimeFilter::Range(r) if *r == (2190..2555) => "6y_to_7y".to_string(), + TimeFilter::Range(r) if *r == (2555..2920) => "7y_to_8y".to_string(), + TimeFilter::Range(r) if *r == (2920..3650) => "8y_to_10y".to_string(), + TimeFilter::Range(r) if *r == (3650..4380) => "10y_to_12y".to_string(), + TimeFilter::Range(r) if *r == (4380..5475) => "12y_to_15y".to_string(), + + // Fallback generic names + TimeFilter::LowerThan(d) => format!("up_to_{}d", d), + TimeFilter::GreaterOrEqual(d) => format!("at_least_{}d", d), + TimeFilter::Range(r) => format!("{}d_to_{}d", r.start, r.end), + } + } +} diff --git a/crates/brk_grouper/src/utxo.rs b/crates/brk_grouper/src/utxo.rs index c46e508d9..2a71cc9d1 100644 --- a/crates/brk_grouper/src/utxo.rs +++ b/crates/brk_grouper/src/utxo.rs @@ -3,7 +3,7 @@ use rayon::prelude::*; use crate::{ ByAgeRange, ByAmountRange, ByEpoch, ByGreatEqualAmount, ByLowerThanAmount, ByMaxAge, ByMinAge, - BySpendableType, ByTerm, Filter, Filtered, + BySpendableType, ByTerm, Filter, }; #[derive(Default, Clone, Traversable)] @@ -15,12 +15,44 @@ pub struct UTXOGroups { pub ge_amount: ByGreatEqualAmount, pub amount_range: ByAmountRange, pub term: ByTerm, - pub _type: BySpendableType, + pub type_: BySpendableType, pub max_age: ByMaxAge, pub lt_amount: ByLowerThanAmount, } impl UTXOGroups { + pub fn new(mut create: F) -> Self + where + F: FnMut(Filter) -> T, + { + Self { + all: create(Filter::All), + age_range: ByAgeRange::new(&mut create), + epoch: ByEpoch::new(&mut create), + min_age: ByMinAge::new(&mut create), + ge_amount: ByGreatEqualAmount::new(&mut create), + amount_range: ByAmountRange::new(&mut create), + term: ByTerm::new(&mut create), + type_: BySpendableType::new(&mut create), + max_age: ByMaxAge::new(&mut create), + lt_amount: ByLowerThanAmount::new(&mut create), + } + } + + pub fn iter(&self) -> impl Iterator { + [&self.all] + .into_iter() + .chain(self.term.iter()) + .chain(self.max_age.iter()) + .chain(self.min_age.iter()) + .chain(self.ge_amount.iter()) + .chain(self.age_range.iter()) + .chain(self.epoch.iter()) + .chain(self.amount_range.iter()) + .chain(self.lt_amount.iter()) + .chain(self.type_.iter()) + } + pub fn iter_mut(&mut self) -> impl Iterator { [&mut self.all] .into_iter() @@ -32,7 +64,7 @@ impl UTXOGroups { .chain(self.epoch.iter_mut()) .chain(self.amount_range.iter_mut()) .chain(self.lt_amount.iter_mut()) - .chain(self._type.iter_mut()) + .chain(self.type_.iter_mut()) } pub fn iter_separate_mut(&mut self) -> impl Iterator { @@ -40,7 +72,7 @@ impl UTXOGroups { .iter_mut() .chain(self.epoch.iter_mut()) .chain(self.amount_range.iter_mut()) - .chain(self._type.iter_mut()) + .chain(self.type_.iter_mut()) } pub fn par_iter_separate_mut(&mut self) -> impl ParallelIterator @@ -51,7 +83,7 @@ impl UTXOGroups { .par_iter_mut() .chain(self.epoch.par_iter_mut()) .chain(self.amount_range.par_iter_mut()) - .chain(self._type.par_iter_mut()) + .chain(self.type_.par_iter_mut()) } pub fn iter_overlapping_mut(&mut self) -> impl Iterator { @@ -64,37 +96,3 @@ impl UTXOGroups { .chain(self.ge_amount.iter_mut()) } } - -impl UTXOGroups> { - pub fn iter_right(&self) -> impl Iterator { - [&self.all.1] - .into_iter() - .chain(self.term.iter_right()) - .chain(self.max_age.iter_right()) - .chain(self.min_age.iter_right()) - .chain(self.age_range.iter_right()) - .chain(self.epoch.iter_right()) - .chain(self.amount_range.iter_right()) - .chain(self._type.iter_right()) - .chain(self.lt_amount.iter_right()) - .chain(self.ge_amount.iter_right()) - } -} - -impl From> for UTXOGroups> { - #[inline] - fn from(value: UTXOGroups) -> Self { - Self { - all: (Filter::All, value.all).into(), - term: ByTerm::from(value.term), - max_age: ByMaxAge::from(value.max_age), - min_age: ByMinAge::from(value.min_age), - age_range: ByAgeRange::from(value.age_range), - epoch: ByEpoch::from(value.epoch), - amount_range: ByAmountRange::from(value.amount_range), - lt_amount: ByLowerThanAmount::from(value.lt_amount), - ge_amount: ByGreatEqualAmount::from(value.ge_amount), - _type: BySpendableType::from(value._type), - } - } -}