global: snapshot

This commit is contained in:
nym21
2026-03-06 14:40:52 +01:00
parent a935573ef8
commit 92cb184a5c
31 changed files with 741 additions and 1257 deletions
@@ -8,7 +8,11 @@ use rayon::prelude::*;
use vecdb::{AnyStoredVec, AnyVec, Database, Exit, ReadableVec, Rw, StorageMode, WritableVec};
use crate::{
blocks, distribution::state::AddressCohortState, indexes, internal::ComputedFromHeight, prices,
blocks,
distribution::state::{AddressCohortState, CoreRealizedState},
indexes,
internal::ComputedFromHeight,
prices,
};
use crate::distribution::metrics::{CoreCohortMetrics, ImportConfig};
@@ -19,7 +23,7 @@ pub struct AddressCohortVecs<M: StorageMode = Rw> {
starting_height: Option<Height>,
#[traversable(skip)]
pub state: Option<Box<AddressCohortState>>,
pub state: Option<Box<AddressCohortState<CoreRealizedState>>>,
#[traversable(flatten)]
pub metrics: CoreCohortMetrics<M>,
@@ -20,7 +20,7 @@ use crate::distribution::metrics::{
use super::{percentiles::PercentileCache, vecs::UTXOCohortVecs};
use crate::distribution::state::UTXOCohortState;
use crate::distribution::state::{CoreRealizedState, RealizedState, UTXOCohortState};
const VERSION: Version = Version::new(0);
@@ -36,18 +36,18 @@ const VERSION: Version = Version::new(0);
/// - min_age: basic
#[derive(Traversable)]
pub struct UTXOCohorts<M: StorageMode = Rw> {
pub all: UTXOCohortVecs<AllCohortMetrics<M>>,
pub sth: UTXOCohortVecs<ExtendedAdjustedCohortMetrics<M>>,
pub lth: UTXOCohortVecs<ExtendedCohortMetrics<M>>,
pub age_range: ByAgeRange<UTXOCohortVecs<BasicCohortMetrics<M>>>,
pub max_age: ByMaxAge<UTXOCohortVecs<CompleteCohortMetrics<M>>>,
pub min_age: ByMinAge<UTXOCohortVecs<CompleteCohortMetrics<M>>>,
pub ge_amount: ByGreatEqualAmount<UTXOCohortVecs<CoreCohortMetrics<M>>>,
pub amount_range: ByAmountRange<UTXOCohortVecs<CoreCohortMetrics<M>>>,
pub lt_amount: ByLowerThanAmount<UTXOCohortVecs<CoreCohortMetrics<M>>>,
pub epoch: ByEpoch<UTXOCohortVecs<CoreCohortMetrics<M>>>,
pub class: ByClass<UTXOCohortVecs<CoreCohortMetrics<M>>>,
pub type_: BySpendableType<UTXOCohortVecs<MinimalCohortMetrics<M>>>,
pub all: UTXOCohortVecs<AllCohortMetrics<M>, RealizedState>,
pub sth: UTXOCohortVecs<ExtendedAdjustedCohortMetrics<M>, RealizedState>,
pub lth: UTXOCohortVecs<ExtendedCohortMetrics<M>, RealizedState>,
pub age_range: ByAgeRange<UTXOCohortVecs<BasicCohortMetrics<M>, RealizedState>>,
pub max_age: ByMaxAge<UTXOCohortVecs<CompleteCohortMetrics<M>, RealizedState>>,
pub min_age: ByMinAge<UTXOCohortVecs<CompleteCohortMetrics<M>, RealizedState>>,
pub ge_amount: ByGreatEqualAmount<UTXOCohortVecs<CoreCohortMetrics<M>, CoreRealizedState>>,
pub amount_range: ByAmountRange<UTXOCohortVecs<CoreCohortMetrics<M>, CoreRealizedState>>,
pub lt_amount: ByLowerThanAmount<UTXOCohortVecs<CoreCohortMetrics<M>, CoreRealizedState>>,
pub epoch: ByEpoch<UTXOCohortVecs<CoreCohortMetrics<M>, CoreRealizedState>>,
pub class: ByClass<UTXOCohortVecs<CoreCohortMetrics<M>, CoreRealizedState>>,
pub type_: BySpendableType<UTXOCohortVecs<MinimalCohortMetrics<M>, CoreRealizedState>>,
#[traversable(skip)]
pub(super) percentile_cache: PercentileCache,
/// Cached partition_point positions for tick_tock boundary searches.
@@ -86,7 +86,7 @@ impl UTXOCohorts<Rw> {
// Helper for separate cohorts with BasicCohortMetrics + full state
let basic_separate =
|f: Filter, name: &'static str| -> Result<UTXOCohortVecs<BasicCohortMetrics>> {
|f: Filter, name: &'static str| -> Result<UTXOCohortVecs<BasicCohortMetrics, RealizedState>> {
let full_name = CohortContext::Utxo.full_name(&f, name);
let cfg = ImportConfig {
db,
@@ -105,7 +105,7 @@ impl UTXOCohorts<Rw> {
let age_range = ByAgeRange::try_new(&basic_separate)?;
let core_separate =
|f: Filter, name: &'static str| -> Result<UTXOCohortVecs<CoreCohortMetrics>> {
|f: Filter, name: &'static str| -> Result<UTXOCohortVecs<CoreCohortMetrics, CoreRealizedState>> {
let full_name = CohortContext::Utxo.full_name(&f, name);
let cfg = ImportConfig {
db,
@@ -126,7 +126,7 @@ impl UTXOCohorts<Rw> {
let class = ByClass::try_new(&core_separate)?;
let type_ = BySpendableType::try_new(
&|f: Filter, name: &'static str| -> Result<UTXOCohortVecs<MinimalCohortMetrics>> {
&|f: Filter, name: &'static str| -> Result<UTXOCohortVecs<MinimalCohortMetrics, CoreRealizedState>> {
let full_name = CohortContext::Utxo.full_name(&f, name);
let cfg = ImportConfig {
db,
@@ -181,7 +181,7 @@ impl UTXOCohorts<Rw> {
// max_age: CompleteCohortMetrics (no state, aggregates from age_range)
let max_age = {
ByMaxAge::try_new(&|f: Filter, name: &'static str| -> Result<_> {
ByMaxAge::try_new(&|f: Filter, name: &'static str| -> Result<UTXOCohortVecs<CompleteCohortMetrics, RealizedState>> {
let full_name = CohortContext::Utxo.full_name(&f, name);
let cfg = ImportConfig {
db,
@@ -199,7 +199,7 @@ impl UTXOCohorts<Rw> {
// min_age: CompleteCohortMetrics
let min_age = {
ByMinAge::try_new(&|f: Filter, name: &'static str| -> Result<_> {
ByMinAge::try_new(&|f: Filter, name: &'static str| -> Result<UTXOCohortVecs<CompleteCohortMetrics, RealizedState>> {
let full_name = CohortContext::Utxo.full_name(&f, name);
let cfg = ImportConfig {
db,
@@ -217,7 +217,7 @@ impl UTXOCohorts<Rw> {
// ge_amount, lt_amount: CoreCohortMetrics (no state)
let core_no_state =
|f: Filter, name: &'static str| -> Result<UTXOCohortVecs<CoreCohortMetrics>> {
|f: Filter, name: &'static str| -> Result<UTXOCohortVecs<CoreCohortMetrics, CoreRealizedState>> {
let full_name = CohortContext::Utxo.full_name(&f, name);
let cfg = ImportConfig {
db,
@@ -398,15 +398,15 @@ impl UTXOCohorts<Rw> {
let tasks: Vec<Box<dyn FnOnce() -> Result<()> + Send + '_>> = vec![
Box::new(|| {
let sources = filter_sources_from(ar.iter(), None);
all.metrics.compute_net_sentiment_from_others_dyn(si, &sources, exit)
all.metrics.compute_net_sentiment_from_others(si, &sources, exit)
}),
Box::new(|| {
let sources = filter_sources_from(ar.iter(), Some(sth.metrics.filter()));
sth.metrics.compute_net_sentiment_from_others_dyn(si, &sources, exit)
sth.metrics.compute_net_sentiment_from_others(si, &sources, exit)
}),
Box::new(|| {
let sources = filter_sources_from(ar.iter(), Some(lth.metrics.filter()));
lth.metrics.compute_net_sentiment_from_others_dyn(si, &sources, exit)
lth.metrics.compute_net_sentiment_from_others(si, &sources, exit)
}),
];
@@ -597,26 +597,24 @@ impl UTXOCohorts<Rw> {
}
}
/// Filter source cohorts by an optional filter, returning dyn CohortMetricsBase refs.
/// Filter source cohorts by an optional filter.
/// If filter is None, returns all sources (used for "all" aggregate).
fn filter_sources_from<'a, M: CohortMetricsBase + 'a>(
sources: impl Iterator<Item = &'a UTXOCohortVecs<M>>,
sources: impl Iterator<Item = &'a UTXOCohortVecs<M, RealizedState>>,
filter: Option<&Filter>,
) -> Vec<&'a dyn CohortMetricsBase> {
) -> Vec<&'a M> {
match filter {
Some(f) => sources
.filter(|v| f.includes(v.metrics.filter()))
.map(|v| &v.metrics as &dyn CohortMetricsBase)
.collect(),
None => sources
.map(|v| &v.metrics as &dyn CohortMetricsBase)
.map(|v| &v.metrics)
.collect(),
None => sources.map(|v| &v.metrics).collect(),
}
}
/// Filter CoreCohortMetrics source cohorts by an optional filter.
fn filter_core_sources_from<'a>(
sources: impl Iterator<Item = &'a UTXOCohortVecs<CoreCohortMetrics>>,
sources: impl Iterator<Item = &'a UTXOCohortVecs<CoreCohortMetrics, CoreRealizedState>>,
filter: Option<&Filter>,
) -> Vec<&'a CoreCohortMetrics> {
match filter {
@@ -4,7 +4,7 @@ use vecdb::{Rw, VecIndex};
use crate::distribution::{
compute::PriceRangeMax,
state::{BlockState, CohortState, Transacted},
state::{BlockState, SendPrecomputed, Transacted},
};
use super::groups::UTXOCohorts;
@@ -51,7 +51,7 @@ impl UTXOCohorts<Rw> {
let peak_price = price_range_max.max_between(receive_height, send_height);
// Pre-compute once for age_range, epoch, year (all share sent.spendable_supply)
if let Some(pre) = CohortState::precompute_send(
if let Some(pre) = SendPrecomputed::new(
&sent.spendable_supply,
current_price,
prev_price,
@@ -6,52 +6,83 @@ use vecdb::{Exit, ReadableVec};
use crate::{blocks, distribution::state::UTXOCohortState, prices};
use crate::distribution::metrics::{CohortMetricsBase, CompleteCohortMetrics, CoreCohortMetrics, MinimalCohortMetrics};
use crate::distribution::metrics::{
CohortMetricsBase, CompleteCohortMetrics, CoreCohortMetrics, MinimalCohortMetrics,
};
use crate::distribution::state::{CoreRealizedState, RealizedOps, RealizedState};
use super::super::traits::DynCohortVecs;
#[derive(Traversable)]
pub struct UTXOCohortVecs<Metrics> {
/// Starting height when state was imported
pub struct UTXOCohortVecs<Metrics, R: RealizedOps> {
#[traversable(skip)]
state_starting_height: Option<Height>,
/// Runtime state for block-by-block processing (separate cohorts only)
#[traversable(skip)]
pub state: Option<Box<UTXOCohortState>>,
pub state: Option<Box<UTXOCohortState<R>>>,
/// Metric vectors
#[traversable(flatten)]
pub metrics: Metrics,
}
impl<Metrics> UTXOCohortVecs<Metrics> {
/// Create a new UTXOCohortVecs with state and metrics.
pub(crate) fn new(state: Option<Box<UTXOCohortState>>, metrics: Metrics) -> Self {
// --- Shared state helpers (identical across all DynCohortVecs impls) ---
impl<Metrics, R: RealizedOps> UTXOCohortVecs<Metrics, R> {
pub(crate) fn new(state: Option<Box<UTXOCohortState<R>>>, metrics: Metrics) -> Self {
Self {
state_starting_height: None,
state,
metrics,
}
}
fn reset_state_impl(&mut self) {
self.state_starting_height = Some(Height::ZERO);
if let Some(state) = self.state.as_mut() {
state.reset();
}
}
fn write_state_impl(&mut self, height: Height, cleanup: bool) -> Result<()> {
if let Some(state) = self.state.as_mut() {
state.write(height, cleanup)?;
}
Ok(())
}
fn reset_cost_basis_impl(&mut self) -> Result<()> {
if let Some(state) = self.state.as_mut() {
state.reset_cost_basis_data_if_needed()?;
}
Ok(())
}
fn reset_iteration_impl(&mut self) {
if let Some(state) = self.state.as_mut() {
state.reset_single_iteration_values();
}
}
}
impl<Metrics: CohortMetricsBase + Traversable> Filtered for UTXOCohortVecs<Metrics> {
// --- Blanket impl for CohortMetricsBase types (always use full RealizedState) ---
impl<Metrics: CohortMetricsBase + Traversable> Filtered
for UTXOCohortVecs<Metrics, RealizedState>
{
fn filter(&self) -> &Filter {
self.metrics.filter()
}
}
impl<Metrics: CohortMetricsBase + Traversable> DynCohortVecs for UTXOCohortVecs<Metrics> {
impl<Metrics: CohortMetricsBase + Traversable> DynCohortVecs
for UTXOCohortVecs<Metrics, RealizedState>
{
fn min_stateful_height_len(&self) -> usize {
self.metrics.min_stateful_height_len()
}
fn reset_state_starting_height(&mut self) {
self.state_starting_height = Some(Height::ZERO);
if let Some(state) = self.state.as_mut() {
state.reset();
}
self.reset_state_impl();
}
fn import_state(&mut self, starting_height: Height) -> Result<Height> {
@@ -132,8 +163,6 @@ impl<Metrics: CohortMetricsBase + Traversable> DynCohortVecs for UTXOCohortVecs<
) -> Result<()> {
self.metrics
.compute_rest_part1(blocks, prices, starting_indexes, exit)?;
// Separate cohorts (with state) compute net_sentiment = greed - pain directly.
// Aggregate cohorts get it via weighted average in groups.rs.
if self.state.is_some() {
self.metrics
.compute_net_sentiment_height(starting_indexes, exit)?;
@@ -142,79 +171,78 @@ impl<Metrics: CohortMetricsBase + Traversable> DynCohortVecs for UTXOCohortVecs<
}
fn write_state(&mut self, height: Height, cleanup: bool) -> Result<()> {
if let Some(state) = self.state.as_mut() {
state.write(height, cleanup)?;
}
Ok(())
self.write_state_impl(height, cleanup)
}
fn reset_cost_basis_data_if_needed(&mut self) -> Result<()> {
if let Some(state) = self.state.as_mut() {
state.reset_cost_basis_data_if_needed()?;
}
Ok(())
self.reset_cost_basis_impl()
}
fn reset_single_iteration_values(&mut self) {
if let Some(state) = self.state.as_mut() {
state.reset_single_iteration_values();
}
self.reset_iteration_impl();
}
}
impl Filtered for UTXOCohortVecs<MinimalCohortMetrics> {
// --- Shared import_state for non-blanket impls (direct field access) ---
macro_rules! impl_import_state {
() => {
fn import_state(&mut self, starting_height: Height) -> Result<Height> {
if let Some(state) = self.state.as_mut() {
if let Some(mut prev_height) = starting_height.decremented() {
prev_height = state.import_at_or_before(prev_height)?;
state.supply.value = self
.metrics
.supply
.total
.sats
.height
.collect_one(prev_height)
.unwrap();
state.supply.utxo_count = *self
.metrics
.outputs
.utxo_count
.height
.collect_one(prev_height)
.unwrap();
state.restore_realized_cap();
let result = prev_height.incremented();
self.state_starting_height = Some(result);
Ok(result)
} else {
self.state_starting_height = Some(Height::ZERO);
Ok(Height::ZERO)
}
} else {
self.state_starting_height = Some(starting_height);
Ok(starting_height)
}
}
};
}
// --- MinimalCohortMetrics: uses CoreRealizedState ---
impl Filtered for UTXOCohortVecs<MinimalCohortMetrics, CoreRealizedState> {
fn filter(&self) -> &Filter {
&self.metrics.filter
}
}
impl DynCohortVecs for UTXOCohortVecs<MinimalCohortMetrics> {
impl DynCohortVecs for UTXOCohortVecs<MinimalCohortMetrics, CoreRealizedState> {
fn min_stateful_height_len(&self) -> usize {
self.metrics.min_stateful_height_len()
}
fn reset_state_starting_height(&mut self) {
self.state_starting_height = Some(Height::ZERO);
if let Some(state) = self.state.as_mut() {
state.reset();
}
self.reset_state_impl();
}
fn import_state(&mut self, starting_height: Height) -> Result<Height> {
if let Some(state) = self.state.as_mut() {
if let Some(mut prev_height) = starting_height.decremented() {
prev_height = state.import_at_or_before(prev_height)?;
state.supply.value = self
.metrics
.supply
.total
.sats
.height
.collect_one(prev_height)
.unwrap();
state.supply.utxo_count = *self
.metrics
.outputs
.utxo_count
.height
.collect_one(prev_height)
.unwrap();
state.restore_realized_cap();
let result = prev_height.incremented();
self.state_starting_height = Some(result);
Ok(result)
} else {
self.state_starting_height = Some(Height::ZERO);
Ok(Height::ZERO)
}
} else {
self.state_starting_height = Some(starting_height);
Ok(starting_height)
}
}
impl_import_state!();
fn validate_computed_versions(&mut self, base_version: Version) -> Result<()> {
self.metrics.validate_computed_versions(base_version)
@@ -268,79 +296,36 @@ impl DynCohortVecs for UTXOCohortVecs<MinimalCohortMetrics> {
}
fn write_state(&mut self, height: Height, cleanup: bool) -> Result<()> {
if let Some(state) = self.state.as_mut() {
state.write(height, cleanup)?;
}
Ok(())
self.write_state_impl(height, cleanup)
}
fn reset_cost_basis_data_if_needed(&mut self) -> Result<()> {
if let Some(state) = self.state.as_mut() {
state.reset_cost_basis_data_if_needed()?;
}
Ok(())
self.reset_cost_basis_impl()
}
fn reset_single_iteration_values(&mut self) {
if let Some(state) = self.state.as_mut() {
state.reset_single_iteration_values();
}
self.reset_iteration_impl();
}
}
impl Filtered for UTXOCohortVecs<CoreCohortMetrics> {
// --- CoreCohortMetrics: uses CoreRealizedState ---
impl Filtered for UTXOCohortVecs<CoreCohortMetrics, CoreRealizedState> {
fn filter(&self) -> &Filter {
&self.metrics.filter
}
}
impl DynCohortVecs for UTXOCohortVecs<CoreCohortMetrics> {
impl DynCohortVecs for UTXOCohortVecs<CoreCohortMetrics, CoreRealizedState> {
fn min_stateful_height_len(&self) -> usize {
self.metrics.min_stateful_height_len()
}
fn reset_state_starting_height(&mut self) {
self.state_starting_height = Some(Height::ZERO);
if let Some(state) = self.state.as_mut() {
state.reset();
}
self.reset_state_impl();
}
fn import_state(&mut self, starting_height: Height) -> Result<Height> {
if let Some(state) = self.state.as_mut() {
if let Some(mut prev_height) = starting_height.decremented() {
prev_height = state.import_at_or_before(prev_height)?;
state.supply.value = self
.metrics
.supply
.total
.sats
.height
.collect_one(prev_height)
.unwrap();
state.supply.utxo_count = *self
.metrics
.outputs
.utxo_count
.height
.collect_one(prev_height)
.unwrap();
state.restore_realized_cap();
let result = prev_height.incremented();
self.state_starting_height = Some(result);
Ok(result)
} else {
self.state_starting_height = Some(Height::ZERO);
Ok(Height::ZERO)
}
} else {
self.state_starting_height = Some(starting_height);
Ok(starting_height)
}
}
impl_import_state!();
fn validate_computed_versions(&mut self, base_version: Version) -> Result<()> {
self.metrics.validate_computed_versions(base_version)
@@ -395,79 +380,36 @@ impl DynCohortVecs for UTXOCohortVecs<CoreCohortMetrics> {
}
fn write_state(&mut self, height: Height, cleanup: bool) -> Result<()> {
if let Some(state) = self.state.as_mut() {
state.write(height, cleanup)?;
}
Ok(())
self.write_state_impl(height, cleanup)
}
fn reset_cost_basis_data_if_needed(&mut self) -> Result<()> {
if let Some(state) = self.state.as_mut() {
state.reset_cost_basis_data_if_needed()?;
}
Ok(())
self.reset_cost_basis_impl()
}
fn reset_single_iteration_values(&mut self) {
if let Some(state) = self.state.as_mut() {
state.reset_single_iteration_values();
}
self.reset_iteration_impl();
}
}
impl Filtered for UTXOCohortVecs<CompleteCohortMetrics> {
// --- CompleteCohortMetrics: uses full RealizedState ---
impl Filtered for UTXOCohortVecs<CompleteCohortMetrics, RealizedState> {
fn filter(&self) -> &Filter {
&self.metrics.filter
}
}
impl DynCohortVecs for UTXOCohortVecs<CompleteCohortMetrics> {
impl DynCohortVecs for UTXOCohortVecs<CompleteCohortMetrics, RealizedState> {
fn min_stateful_height_len(&self) -> usize {
self.metrics.min_stateful_height_len()
}
fn reset_state_starting_height(&mut self) {
self.state_starting_height = Some(Height::ZERO);
if let Some(state) = self.state.as_mut() {
state.reset();
}
self.reset_state_impl();
}
fn import_state(&mut self, starting_height: Height) -> Result<Height> {
if let Some(state) = self.state.as_mut() {
if let Some(mut prev_height) = starting_height.decremented() {
prev_height = state.import_at_or_before(prev_height)?;
state.supply.value = self
.metrics
.supply
.total
.sats
.height
.collect_one(prev_height)
.unwrap();
state.supply.utxo_count = *self
.metrics
.outputs
.utxo_count
.height
.collect_one(prev_height)
.unwrap();
state.restore_realized_cap();
let result = prev_height.incremented();
self.state_starting_height = Some(result);
Ok(result)
} else {
self.state_starting_height = Some(Height::ZERO);
Ok(Height::ZERO)
}
} else {
self.state_starting_height = Some(starting_height);
Ok(starting_height)
}
}
impl_import_state!();
fn validate_computed_versions(&mut self, base_version: Version) -> Result<()> {
self.metrics.validate_computed_versions(base_version)
@@ -530,22 +472,14 @@ impl DynCohortVecs for UTXOCohortVecs<CompleteCohortMetrics> {
}
fn write_state(&mut self, height: Height, cleanup: bool) -> Result<()> {
if let Some(state) = self.state.as_mut() {
state.write(height, cleanup)?;
}
Ok(())
self.write_state_impl(height, cleanup)
}
fn reset_cost_basis_data_if_needed(&mut self) -> Result<()> {
if let Some(state) = self.state.as_mut() {
state.reset_cost_basis_data_if_needed()?;
}
Ok(())
self.reset_cost_basis_impl()
}
fn reset_single_iteration_values(&mut self) {
if let Some(state) = self.state.as_mut() {
state.reset_single_iteration_values();
}
self.reset_iteration_impl();
}
}
@@ -81,18 +81,8 @@ impl ActivityMetrics {
self.core
.compute_from_stateful(starting_indexes, &core_refs, exit)?;
macro_rules! sum_others {
($($field:tt).+) => {
self.$($field).+.compute_sum_of_others(
starting_indexes.height,
&others.iter().map(|v| &v.$($field).+).collect::<Vec<_>>(),
exit,
)?
};
}
sum_others!(coinblocks_destroyed.height);
sum_others!(coindays_destroyed.height);
sum_others!(self, starting_indexes, others, exit; coinblocks_destroyed.height);
sum_others!(self, starting_indexes, others, exit; coindays_destroyed.height);
Ok(())
}
@@ -1,143 +0,0 @@
use brk_cohort::Filter;
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Cents, Dollars, Height, Indexes, Sats, Version};
use rayon::prelude::*;
use vecdb::{AnyStoredVec, Exit, ReadableVec, Rw, StorageMode};
use crate::{blocks, prices};
use crate::distribution::metrics::{
ActivityMetrics, CohortMetricsBase, CostBasisBase, ImportConfig, OutputsMetrics, RealizedBase,
RealizedWithAdjusted, RelativeWithRelToAll, SupplyMetrics, UnrealizedBase,
};
/// Cohort metrics with adjusted realized (no extended).
/// Used by: max_age cohorts.
#[derive(Traversable)]
pub struct AdjustedCohortMetrics<M: StorageMode = Rw> {
#[traversable(skip)]
pub filter: Filter,
pub supply: Box<SupplyMetrics<M>>,
pub outputs: Box<OutputsMetrics<M>>,
pub activity: Box<ActivityMetrics<M>>,
pub realized: Box<RealizedWithAdjusted<M>>,
pub cost_basis: Box<CostBasisBase<M>>,
pub unrealized: Box<UnrealizedBase<M>>,
pub relative: Box<RelativeWithRelToAll<M>>,
}
impl CohortMetricsBase for AdjustedCohortMetrics {
fn filter(&self) -> &Filter {
&self.filter
}
fn supply(&self) -> &SupplyMetrics {
&self.supply
}
fn supply_mut(&mut self) -> &mut SupplyMetrics {
&mut self.supply
}
fn outputs(&self) -> &OutputsMetrics {
&self.outputs
}
fn outputs_mut(&mut self) -> &mut OutputsMetrics {
&mut self.outputs
}
fn activity(&self) -> &ActivityMetrics {
&self.activity
}
fn activity_mut(&mut self) -> &mut ActivityMetrics {
&mut self.activity
}
fn realized_base(&self) -> &RealizedBase {
&self.realized
}
fn realized_base_mut(&mut self) -> &mut RealizedBase {
&mut self.realized
}
fn unrealized_base(&self) -> &UnrealizedBase {
&self.unrealized
}
fn unrealized_base_mut(&mut self) -> &mut UnrealizedBase {
&mut self.unrealized
}
fn cost_basis_base(&self) -> &CostBasisBase {
&self.cost_basis
}
fn cost_basis_base_mut(&mut self) -> &mut CostBasisBase {
&mut self.cost_basis
}
fn validate_computed_versions(&mut self, base_version: Version) -> Result<()> {
self.supply.validate_computed_versions(base_version)?;
self.activity.validate_computed_versions(base_version)?;
Ok(())
}
fn collect_all_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
let mut vecs: Vec<&mut dyn AnyStoredVec> = Vec::new();
vecs.extend(self.supply.par_iter_mut().collect::<Vec<_>>());
vecs.extend(self.outputs.par_iter_mut().collect::<Vec<_>>());
vecs.extend(self.activity.par_iter_mut().collect::<Vec<_>>());
vecs.extend(self.realized.collect_vecs_mut());
vecs.extend(self.cost_basis.collect_vecs_mut());
vecs.extend(self.unrealized.collect_vecs_mut());
vecs
}
}
impl AdjustedCohortMetrics {
pub(crate) fn forced_import(cfg: &ImportConfig) -> Result<Self> {
let supply = SupplyMetrics::forced_import(cfg)?;
let unrealized = UnrealizedBase::forced_import(cfg)?;
let realized = RealizedWithAdjusted::forced_import(cfg)?;
let relative = RelativeWithRelToAll::forced_import(cfg)?;
Ok(Self {
filter: cfg.filter.clone(),
supply: Box::new(supply),
outputs: Box::new(OutputsMetrics::forced_import(cfg)?),
activity: Box::new(ActivityMetrics::forced_import(cfg)?),
realized: Box::new(realized),
cost_basis: Box::new(CostBasisBase::forced_import(cfg)?),
unrealized: Box::new(unrealized),
relative: Box::new(relative),
})
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn compute_rest_part2(
&mut self,
blocks: &blocks::Vecs,
prices: &prices::Vecs,
starting_indexes: &Indexes,
height_to_market_cap: &impl ReadableVec<Height, Dollars>,
up_to_1h_value_created: &impl ReadableVec<Height, Cents>,
up_to_1h_value_destroyed: &impl ReadableVec<Height, Cents>,
all_supply_sats: &impl ReadableVec<Height, Sats>,
exit: &Exit,
) -> Result<()> {
self.realized.compute_rest_part2(
blocks,
prices,
starting_indexes,
&self.supply.total.btc.height,
height_to_market_cap,
up_to_1h_value_created,
up_to_1h_value_destroyed,
exit,
)?;
self.relative.compute(
starting_indexes.height,
&self.unrealized,
&self.realized.base,
&self.supply.total.sats.height,
height_to_market_cap,
all_supply_sats,
exit,
)?;
Ok(())
}
}
@@ -5,16 +5,16 @@ use brk_types::{Cents, Dollars, Height, Indexes, Version};
use rayon::prelude::*;
use vecdb::{AnyStoredVec, Exit, ReadableVec, Rw, StorageMode};
use crate::{blocks, distribution::state::CohortState, prices};
use crate::{blocks, distribution::state::{CohortState, RealizedState}, prices};
use crate::distribution::metrics::{
ActivityMetrics, CohortMetricsBase, CostBasisBase, CostBasisWithExtended, ImportConfig,
OutputsMetrics, RealizedBase, RealizedWithExtendedAdjusted, RelativeForAll, SupplyMetrics,
UnrealizedBase,
OutputsMetrics, RealizedAdjusted, RealizedBase, RealizedWithExtended, RelativeForAll,
SupplyMetrics, UnrealizedBase,
};
/// All-cohort metrics: extended + adjusted realized, extended cost basis,
/// relative for-all (no rel_to_all).
/// All-cohort metrics: extended realized + adjusted (as composable add-on),
/// extended cost basis, relative for-all (no rel_to_all).
/// Used by: the "all" cohort.
#[derive(Traversable)]
pub struct AllCohortMetrics<M: StorageMode = Rw> {
@@ -23,83 +23,14 @@ pub struct AllCohortMetrics<M: StorageMode = Rw> {
pub supply: Box<SupplyMetrics<M>>,
pub outputs: Box<OutputsMetrics<M>>,
pub activity: Box<ActivityMetrics<M>>,
pub realized: Box<RealizedWithExtendedAdjusted<M>>,
pub realized: Box<RealizedWithExtended<M>>,
pub cost_basis: Box<CostBasisWithExtended<M>>,
pub unrealized: Box<UnrealizedBase<M>>,
pub adjusted: Box<RealizedAdjusted<M>>,
pub relative: Box<RelativeForAll<M>>,
}
impl CohortMetricsBase for AllCohortMetrics {
fn filter(&self) -> &Filter {
&self.filter
}
fn supply(&self) -> &SupplyMetrics {
&self.supply
}
fn supply_mut(&mut self) -> &mut SupplyMetrics {
&mut self.supply
}
fn outputs(&self) -> &OutputsMetrics {
&self.outputs
}
fn outputs_mut(&mut self) -> &mut OutputsMetrics {
&mut self.outputs
}
fn activity(&self) -> &ActivityMetrics {
&self.activity
}
fn activity_mut(&mut self) -> &mut ActivityMetrics {
&mut self.activity
}
fn realized_base(&self) -> &RealizedBase {
&self.realized
}
fn realized_base_mut(&mut self) -> &mut RealizedBase {
&mut self.realized
}
fn unrealized_base(&self) -> &UnrealizedBase {
&self.unrealized
}
fn unrealized_base_mut(&mut self) -> &mut UnrealizedBase {
&mut self.unrealized
}
fn cost_basis_base(&self) -> &CostBasisBase {
&self.cost_basis
}
fn cost_basis_base_mut(&mut self) -> &mut CostBasisBase {
&mut self.cost_basis
}
fn validate_computed_versions(&mut self, base_version: Version) -> Result<()> {
self.supply.validate_computed_versions(base_version)?;
self.activity.validate_computed_versions(base_version)?;
self.cost_basis.validate_computed_versions(base_version)?;
Ok(())
}
fn compute_then_truncate_push_unrealized_states(
&mut self,
height: Height,
height_price: Cents,
state: &mut CohortState,
is_day_boundary: bool,
) -> Result<()> {
self.compute_and_push_unrealized_base(height, height_price, state)?;
self.cost_basis
.extended
.truncate_push_percentiles(height, state, is_day_boundary)?;
Ok(())
}
fn collect_all_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
let mut vecs: Vec<&mut dyn AnyStoredVec> = Vec::new();
vecs.extend(self.supply.par_iter_mut().collect::<Vec<_>>());
vecs.extend(self.outputs.par_iter_mut().collect::<Vec<_>>());
vecs.extend(self.activity.par_iter_mut().collect::<Vec<_>>());
vecs.extend(self.realized.collect_vecs_mut());
vecs.extend(self.cost_basis.base.collect_vecs_mut());
vecs.extend(self.cost_basis.extended.collect_vecs_mut());
vecs.extend(self.unrealized.collect_vecs_mut());
vecs
}
}
impl_cohort_metrics_base!(AllCohortMetrics, extended_cost_basis);
impl AllCohortMetrics {
/// Import the "all" cohort metrics with a pre-imported supply.
@@ -111,7 +42,8 @@ impl AllCohortMetrics {
supply: SupplyMetrics,
) -> Result<Self> {
let unrealized = UnrealizedBase::forced_import(cfg)?;
let realized = RealizedWithExtendedAdjusted::forced_import(cfg)?;
let realized = RealizedWithExtended::forced_import(cfg)?;
let adjusted = RealizedAdjusted::forced_import(cfg)?;
let relative = RelativeForAll::forced_import(cfg)?;
@@ -123,6 +55,7 @@ impl AllCohortMetrics {
realized: Box::new(realized),
cost_basis: Box::new(CostBasisWithExtended::forced_import(cfg)?),
unrealized: Box::new(unrealized),
adjusted: Box::new(adjusted),
relative: Box::new(relative),
})
}
@@ -144,6 +77,14 @@ impl AllCohortMetrics {
starting_indexes,
&self.supply.total.btc.height,
height_to_market_cap,
exit,
)?;
self.adjusted.compute_rest_part2(
blocks,
starting_indexes,
&self.realized.value_created.height,
&self.realized.value_destroyed.height,
up_to_1h_value_created,
up_to_1h_value_destroyed,
exit,
@@ -27,62 +27,7 @@ pub struct BasicCohortMetrics<M: StorageMode = Rw> {
pub relative: Box<RelativeWithRelToAll<M>>,
}
impl CohortMetricsBase for BasicCohortMetrics {
fn filter(&self) -> &Filter {
&self.filter
}
fn supply(&self) -> &SupplyMetrics {
&self.supply
}
fn supply_mut(&mut self) -> &mut SupplyMetrics {
&mut self.supply
}
fn outputs(&self) -> &OutputsMetrics {
&self.outputs
}
fn outputs_mut(&mut self) -> &mut OutputsMetrics {
&mut self.outputs
}
fn activity(&self) -> &ActivityMetrics {
&self.activity
}
fn activity_mut(&mut self) -> &mut ActivityMetrics {
&mut self.activity
}
fn realized_base(&self) -> &RealizedBase {
&self.realized
}
fn realized_base_mut(&mut self) -> &mut RealizedBase {
&mut self.realized
}
fn unrealized_base(&self) -> &UnrealizedBase {
&self.unrealized
}
fn unrealized_base_mut(&mut self) -> &mut UnrealizedBase {
&mut self.unrealized
}
fn cost_basis_base(&self) -> &CostBasisBase {
&self.cost_basis
}
fn cost_basis_base_mut(&mut self) -> &mut CostBasisBase {
&mut self.cost_basis
}
fn validate_computed_versions(&mut self, base_version: Version) -> Result<()> {
self.supply.validate_computed_versions(base_version)?;
self.activity.validate_computed_versions(base_version)?;
Ok(())
}
fn collect_all_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
let mut vecs: Vec<&mut dyn AnyStoredVec> = Vec::new();
vecs.extend(self.supply.par_iter_mut().collect::<Vec<_>>());
vecs.extend(self.outputs.par_iter_mut().collect::<Vec<_>>());
vecs.extend(self.activity.par_iter_mut().collect::<Vec<_>>());
vecs.extend(self.realized.collect_vecs_mut());
vecs.extend(self.cost_basis.collect_vecs_mut());
vecs.extend(self.unrealized.collect_vecs_mut());
vecs
}
}
impl_cohort_metrics_base!(BasicCohortMetrics, base_cost_basis);
impl BasicCohortMetrics {
pub(crate) fn forced_import(cfg: &ImportConfig) -> Result<Self> {
@@ -104,10 +49,6 @@ impl BasicCohortMetrics {
})
}
pub(crate) fn par_iter_mut(&mut self) -> impl ParallelIterator<Item = &mut dyn AnyStoredVec> {
self.collect_all_vecs_mut().into_par_iter()
}
pub(crate) fn compute_rest_part2(
&mut self,
blocks: &blocks::Vecs,
@@ -139,28 +80,4 @@ impl BasicCohortMetrics {
Ok(())
}
pub(crate) fn compute_from_stateful(
&mut self,
starting_indexes: &Indexes,
others: &[&Self],
exit: &Exit,
) -> Result<()> {
macro_rules! aggregate {
($field:ident) => {
self.$field.compute_from_stateful(
starting_indexes,
&others.iter().map(|v| &*v.$field).collect::<Vec<_>>(),
exit,
)?
};
}
aggregate!(supply);
aggregate!(outputs);
aggregate!(activity);
aggregate!(realized);
aggregate!(unrealized);
aggregate!(cost_basis);
Ok(())
}
}
@@ -78,10 +78,10 @@ impl CompleteCohortMetrics {
}
/// Aggregate Complete-tier metrics from Source cohort refs.
pub(crate) fn compute_from_sources(
pub(crate) fn compute_from_sources<T: CohortMetricsBase>(
&mut self,
starting_indexes: &Indexes,
others: &[&dyn CohortMetricsBase],
others: &[&T],
exit: &Exit,
) -> Result<()> {
// Supply, outputs, activity: use their existing compute_from_stateful
@@ -5,7 +5,7 @@ use brk_types::{Cents, Dollars, Height, Indexes, Sats, Version};
use rayon::prelude::*;
use vecdb::{AnyStoredVec, Exit, ReadableVec, Rw, StorageMode};
use crate::{blocks, distribution::state::CohortState, prices};
use crate::{blocks, distribution::state::{CohortState, RealizedState}, prices};
use crate::distribution::metrics::{
ActivityMetrics, CohortMetricsBase, CostBasisBase, CostBasisWithExtended, ImportConfig,
@@ -28,78 +28,7 @@ pub struct ExtendedCohortMetrics<M: StorageMode = Rw> {
pub relative: Box<RelativeWithExtended<M>>,
}
impl CohortMetricsBase for ExtendedCohortMetrics {
fn filter(&self) -> &Filter {
&self.filter
}
fn supply(&self) -> &SupplyMetrics {
&self.supply
}
fn supply_mut(&mut self) -> &mut SupplyMetrics {
&mut self.supply
}
fn outputs(&self) -> &OutputsMetrics {
&self.outputs
}
fn outputs_mut(&mut self) -> &mut OutputsMetrics {
&mut self.outputs
}
fn activity(&self) -> &ActivityMetrics {
&self.activity
}
fn activity_mut(&mut self) -> &mut ActivityMetrics {
&mut self.activity
}
fn realized_base(&self) -> &RealizedBase {
&self.realized
}
fn realized_base_mut(&mut self) -> &mut RealizedBase {
&mut self.realized
}
fn unrealized_base(&self) -> &UnrealizedBase {
&self.unrealized
}
fn unrealized_base_mut(&mut self) -> &mut UnrealizedBase {
&mut self.unrealized
}
fn cost_basis_base(&self) -> &CostBasisBase {
&self.cost_basis
}
fn cost_basis_base_mut(&mut self) -> &mut CostBasisBase {
&mut self.cost_basis
}
fn validate_computed_versions(&mut self, base_version: Version) -> Result<()> {
self.supply.validate_computed_versions(base_version)?;
self.activity.validate_computed_versions(base_version)?;
self.cost_basis.validate_computed_versions(base_version)?;
Ok(())
}
fn compute_then_truncate_push_unrealized_states(
&mut self,
height: Height,
height_price: Cents,
state: &mut CohortState,
is_day_boundary: bool,
) -> Result<()> {
self.compute_and_push_unrealized_base(height, height_price, state)?;
self.cost_basis
.extended
.truncate_push_percentiles(height, state, is_day_boundary)?;
Ok(())
}
fn collect_all_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
let mut vecs: Vec<&mut dyn AnyStoredVec> = Vec::new();
vecs.extend(self.supply.par_iter_mut().collect::<Vec<_>>());
vecs.extend(self.outputs.par_iter_mut().collect::<Vec<_>>());
vecs.extend(self.activity.par_iter_mut().collect::<Vec<_>>());
vecs.extend(self.realized.collect_vecs_mut());
vecs.extend(self.cost_basis.base.collect_vecs_mut());
vecs.extend(self.cost_basis.extended.collect_vecs_mut());
vecs.extend(self.unrealized.collect_vecs_mut());
vecs
}
}
impl_cohort_metrics_base!(ExtendedCohortMetrics, extended_cost_basis);
impl ExtendedCohortMetrics {
pub(crate) fn forced_import(cfg: &ImportConfig) -> Result<Self> {
@@ -2,121 +2,40 @@ use brk_cohort::Filter;
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Cents, Dollars, Height, Indexes, Sats, Version};
use rayon::prelude::*;
use derive_more::{Deref, DerefMut};
use vecdb::{AnyStoredVec, Exit, ReadableVec, Rw, StorageMode};
use crate::{blocks, distribution::state::CohortState, prices};
use crate::{blocks, distribution::state::{CohortState, RealizedState}, prices};
use crate::distribution::metrics::{
ActivityMetrics, CohortMetricsBase, CostBasisBase, CostBasisWithExtended, ImportConfig,
OutputsMetrics, RealizedBase, RealizedWithExtendedAdjusted, RelativeWithExtended,
SupplyMetrics, UnrealizedBase,
ActivityMetrics, CohortMetricsBase, CostBasisBase, ImportConfig, OutputsMetrics, RealizedAdjusted,
RealizedBase, SupplyMetrics, UnrealizedBase,
};
use super::ExtendedCohortMetrics;
/// Cohort metrics with extended + adjusted realized, extended cost basis.
/// Wraps `ExtendedCohortMetrics` and adds adjusted SOPR as a composable add-on.
/// Used by: sth cohort.
#[derive(Traversable)]
#[derive(Deref, DerefMut, Traversable)]
pub struct ExtendedAdjustedCohortMetrics<M: StorageMode = Rw> {
#[traversable(skip)]
pub filter: Filter,
pub supply: Box<SupplyMetrics<M>>,
pub outputs: Box<OutputsMetrics<M>>,
pub activity: Box<ActivityMetrics<M>>,
pub realized: Box<RealizedWithExtendedAdjusted<M>>,
pub cost_basis: Box<CostBasisWithExtended<M>>,
pub unrealized: Box<UnrealizedBase<M>>,
pub relative: Box<RelativeWithExtended<M>>,
#[deref]
#[deref_mut]
#[traversable(flatten)]
pub inner: ExtendedCohortMetrics<M>,
#[traversable(flatten)]
pub adjusted: Box<RealizedAdjusted<M>>,
}
impl CohortMetricsBase for ExtendedAdjustedCohortMetrics {
fn filter(&self) -> &Filter {
&self.filter
}
fn supply(&self) -> &SupplyMetrics {
&self.supply
}
fn supply_mut(&mut self) -> &mut SupplyMetrics {
&mut self.supply
}
fn outputs(&self) -> &OutputsMetrics {
&self.outputs
}
fn outputs_mut(&mut self) -> &mut OutputsMetrics {
&mut self.outputs
}
fn activity(&self) -> &ActivityMetrics {
&self.activity
}
fn activity_mut(&mut self) -> &mut ActivityMetrics {
&mut self.activity
}
fn realized_base(&self) -> &RealizedBase {
&self.realized
}
fn realized_base_mut(&mut self) -> &mut RealizedBase {
&mut self.realized
}
fn unrealized_base(&self) -> &UnrealizedBase {
&self.unrealized
}
fn unrealized_base_mut(&mut self) -> &mut UnrealizedBase {
&mut self.unrealized
}
fn cost_basis_base(&self) -> &CostBasisBase {
&self.cost_basis
}
fn cost_basis_base_mut(&mut self) -> &mut CostBasisBase {
&mut self.cost_basis
}
fn validate_computed_versions(&mut self, base_version: Version) -> Result<()> {
self.supply.validate_computed_versions(base_version)?;
self.activity.validate_computed_versions(base_version)?;
self.cost_basis.validate_computed_versions(base_version)?;
Ok(())
}
fn compute_then_truncate_push_unrealized_states(
&mut self,
height: Height,
height_price: Cents,
state: &mut CohortState,
is_day_boundary: bool,
) -> Result<()> {
self.compute_and_push_unrealized_base(height, height_price, state)?;
self.cost_basis
.extended
.truncate_push_percentiles(height, state, is_day_boundary)?;
Ok(())
}
fn collect_all_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
let mut vecs: Vec<&mut dyn AnyStoredVec> = Vec::new();
vecs.extend(self.supply.par_iter_mut().collect::<Vec<_>>());
vecs.extend(self.outputs.par_iter_mut().collect::<Vec<_>>());
vecs.extend(self.activity.par_iter_mut().collect::<Vec<_>>());
vecs.extend(self.realized.collect_vecs_mut());
vecs.extend(self.cost_basis.base.collect_vecs_mut());
vecs.extend(self.cost_basis.extended.collect_vecs_mut());
vecs.extend(self.unrealized.collect_vecs_mut());
vecs
}
}
impl_cohort_metrics_base!(ExtendedAdjustedCohortMetrics, deref_extended_cost_basis);
impl ExtendedAdjustedCohortMetrics {
pub(crate) fn forced_import(cfg: &ImportConfig) -> Result<Self> {
let supply = SupplyMetrics::forced_import(cfg)?;
let unrealized = UnrealizedBase::forced_import(cfg)?;
let realized = RealizedWithExtendedAdjusted::forced_import(cfg)?;
let relative = RelativeWithExtended::forced_import(cfg)?;
let inner = ExtendedCohortMetrics::forced_import(cfg)?;
let adjusted = RealizedAdjusted::forced_import(cfg)?;
Ok(Self {
filter: cfg.filter.clone(),
supply: Box::new(supply),
outputs: Box::new(OutputsMetrics::forced_import(cfg)?),
activity: Box::new(ActivityMetrics::forced_import(cfg)?),
realized: Box::new(realized),
cost_basis: Box::new(CostBasisWithExtended::forced_import(cfg)?),
unrealized: Box::new(unrealized),
relative: Box::new(relative),
inner,
adjusted: Box::new(adjusted),
})
}
@@ -132,25 +51,22 @@ impl ExtendedAdjustedCohortMetrics {
all_supply_sats: &impl ReadableVec<Height, Sats>,
exit: &Exit,
) -> Result<()> {
self.realized.compute_rest_part2(
self.inner.compute_rest_part2(
blocks,
prices,
starting_indexes,
&self.supply.total.btc.height,
height_to_market_cap,
up_to_1h_value_created,
up_to_1h_value_destroyed,
all_supply_sats,
exit,
)?;
self.relative.compute(
starting_indexes.height,
&self.unrealized,
&self.realized.base,
&self.supply.total.sats.height,
height_to_market_cap,
all_supply_sats,
&self.supply.total.usd.height,
self.adjusted.compute_rest_part2(
blocks,
starting_indexes,
&self.inner.realized.value_created.height,
&self.inner.realized.value_destroyed.height,
up_to_1h_value_created,
up_to_1h_value_destroyed,
exit,
)?;
@@ -1,4 +1,3 @@
mod adjusted;
mod all;
mod basic;
mod complete;
@@ -7,7 +6,6 @@ mod extended;
mod extended_adjusted;
mod minimal;
pub use adjusted::*;
pub use all::*;
pub use basic::*;
pub use complete::*;
@@ -4,7 +4,7 @@ use brk_types::{Cents, Height, Indexes, Version};
use vecdb::{AnyStoredVec, AnyVec, Exit, Rw, StorageMode, WritableVec};
use crate::{
distribution::state::CohortState,
distribution::state::{CohortState, RealizedState},
internal::{ComputedFromHeight, Price},
};
@@ -35,7 +35,7 @@ impl CostBasisBase {
pub(crate) fn truncate_push_minmax(
&mut self,
height: Height,
state: &CohortState,
state: &CohortState<RealizedState>,
) -> Result<()> {
self.min.cents.height.truncate_push(
height,
@@ -4,7 +4,7 @@ use brk_types::{Cents, Height, Version};
use vecdb::{AnyStoredVec, Rw, StorageMode};
use crate::{
distribution::state::CohortState,
distribution::state::{CohortState, RealizedState},
internal::{PERCENTILES_LEN, PercentilesVecs},
};
@@ -41,7 +41,7 @@ impl CostBasisExtended {
pub(crate) fn truncate_push_percentiles(
&mut self,
height: Height,
state: &mut CohortState,
state: &mut CohortState<RealizedState>,
is_day_boundary: bool,
) -> Result<()> {
let computed = if is_day_boundary {
@@ -1,4 +1,146 @@
/// Aggregate a field by summing the same field across `others`.
macro_rules! sum_others {
($self_:ident, $si:ident, $others:ident, $exit:ident; $($field:tt).+) => {
$self_.$($field).+.compute_sum_of_others(
$si.height,
&$others.iter().map(|v| &v.$($field).+).collect::<Vec<_>>(),
$exit,
)?
};
}
mod activity;
/// DRY macro for `CohortMetricsBase` impl on cohort metric types.
///
/// All types share the same 13 accessor methods and common `collect_all_vecs_mut` shape.
/// Two variants handle the cost basis difference:
///
/// - `base_cost_basis`: `CostBasisBase` only (no percentiles, no cost_basis version check)
/// - `extended_cost_basis`: `CostBasisWithExtended` (percentiles + cost_basis version check)
/// - `deref_extended_cost_basis`: Deref wrapper delegating to `self.inner` (avoids DerefMut borrow conflicts)
macro_rules! impl_cohort_metrics_base {
($type:ident, base_cost_basis) => {
impl CohortMetricsBase for $type {
impl_cohort_metrics_base!(@accessors);
fn validate_computed_versions(&mut self, base_version: Version) -> Result<()> {
self.supply.validate_computed_versions(base_version)?;
self.activity.validate_computed_versions(base_version)?;
Ok(())
}
fn collect_all_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
let mut vecs: Vec<&mut dyn AnyStoredVec> = Vec::new();
vecs.extend(self.supply.par_iter_mut().collect::<Vec<_>>());
vecs.extend(self.outputs.par_iter_mut().collect::<Vec<_>>());
vecs.extend(self.activity.par_iter_mut().collect::<Vec<_>>());
vecs.extend(self.realized.collect_vecs_mut());
vecs.extend(self.cost_basis.collect_vecs_mut());
vecs.extend(self.unrealized.collect_vecs_mut());
vecs
}
}
};
($type:ident, extended_cost_basis) => {
impl CohortMetricsBase for $type {
impl_cohort_metrics_base!(@accessors);
fn validate_computed_versions(&mut self, base_version: Version) -> Result<()> {
self.supply.validate_computed_versions(base_version)?;
self.activity.validate_computed_versions(base_version)?;
self.cost_basis.validate_computed_versions(base_version)?;
Ok(())
}
fn compute_then_truncate_push_unrealized_states(
&mut self,
height: Height,
height_price: Cents,
state: &mut CohortState<RealizedState>,
is_day_boundary: bool,
) -> Result<()> {
self.compute_and_push_unrealized_base(height, height_price, state)?;
self.cost_basis
.extended
.truncate_push_percentiles(height, state, is_day_boundary)?;
Ok(())
}
fn collect_all_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
let mut vecs: Vec<&mut dyn AnyStoredVec> = Vec::new();
vecs.extend(self.supply.par_iter_mut().collect::<Vec<_>>());
vecs.extend(self.outputs.par_iter_mut().collect::<Vec<_>>());
vecs.extend(self.activity.par_iter_mut().collect::<Vec<_>>());
vecs.extend(self.realized.collect_vecs_mut());
vecs.extend(self.cost_basis.base.collect_vecs_mut());
vecs.extend(self.cost_basis.extended.collect_vecs_mut());
vecs.extend(self.unrealized.collect_vecs_mut());
vecs
}
}
};
($type:ident, deref_extended_cost_basis) => {
impl CohortMetricsBase for $type {
impl_cohort_metrics_base!(@deref_accessors);
fn validate_computed_versions(&mut self, base_version: Version) -> Result<()> {
self.inner.validate_computed_versions(base_version)
}
fn compute_then_truncate_push_unrealized_states(
&mut self,
height: Height,
height_price: Cents,
state: &mut CohortState<RealizedState>,
is_day_boundary: bool,
) -> Result<()> {
self.inner.compute_then_truncate_push_unrealized_states(
height, height_price, state, is_day_boundary,
)
}
fn collect_all_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
self.inner.collect_all_vecs_mut()
}
}
};
(@accessors) => {
fn filter(&self) -> &Filter { &self.filter }
fn supply(&self) -> &SupplyMetrics { &self.supply }
fn supply_mut(&mut self) -> &mut SupplyMetrics { &mut self.supply }
fn outputs(&self) -> &OutputsMetrics { &self.outputs }
fn outputs_mut(&mut self) -> &mut OutputsMetrics { &mut self.outputs }
fn activity(&self) -> &ActivityMetrics { &self.activity }
fn activity_mut(&mut self) -> &mut ActivityMetrics { &mut self.activity }
fn realized_base(&self) -> &RealizedBase { &self.realized }
fn realized_base_mut(&mut self) -> &mut RealizedBase { &mut self.realized }
fn unrealized_base(&self) -> &UnrealizedBase { &self.unrealized }
fn unrealized_base_mut(&mut self) -> &mut UnrealizedBase { &mut self.unrealized }
fn cost_basis_base(&self) -> &CostBasisBase { &self.cost_basis }
fn cost_basis_base_mut(&mut self) -> &mut CostBasisBase { &mut self.cost_basis }
};
(@deref_accessors) => {
fn filter(&self) -> &Filter { self.inner.filter() }
fn supply(&self) -> &SupplyMetrics { self.inner.supply() }
fn supply_mut(&mut self) -> &mut SupplyMetrics { self.inner.supply_mut() }
fn outputs(&self) -> &OutputsMetrics { self.inner.outputs() }
fn outputs_mut(&mut self) -> &mut OutputsMetrics { self.inner.outputs_mut() }
fn activity(&self) -> &ActivityMetrics { self.inner.activity() }
fn activity_mut(&mut self) -> &mut ActivityMetrics { self.inner.activity_mut() }
fn realized_base(&self) -> &RealizedBase { self.inner.realized_base() }
fn realized_base_mut(&mut self) -> &mut RealizedBase { self.inner.realized_base_mut() }
fn unrealized_base(&self) -> &UnrealizedBase { self.inner.unrealized_base() }
fn unrealized_base_mut(&mut self) -> &mut UnrealizedBase { self.inner.unrealized_base_mut() }
fn cost_basis_base(&self) -> &CostBasisBase { self.inner.cost_basis_base() }
fn cost_basis_base_mut(&mut self) -> &mut CostBasisBase { self.inner.cost_basis_base_mut() }
};
}
mod cohort;
mod config;
mod cost_basis;
@@ -23,7 +165,7 @@ use brk_error::Result;
use brk_types::{Cents, Height, Indexes, Version};
use vecdb::{AnyStoredVec, Exit};
use crate::{blocks, distribution::state::CohortState, prices};
use crate::{blocks, distribution::state::{CohortState, RealizedState}, prices};
pub trait CohortMetricsBase: Send + Sync {
fn filter(&self) -> &Filter;
@@ -47,7 +189,7 @@ pub trait CohortMetricsBase: Send + Sync {
&mut self,
height: Height,
height_price: Cents,
state: &mut CohortState,
state: &mut CohortState<RealizedState>,
) -> Result<()> {
state.apply_pending();
self.cost_basis_base_mut()
@@ -63,7 +205,7 @@ pub trait CohortMetricsBase: Send + Sync {
&mut self,
height: Height,
height_price: Cents,
state: &mut CohortState,
state: &mut CohortState<RealizedState>,
_is_day_boundary: bool,
) -> Result<()> {
self.compute_and_push_unrealized_base(height, height_price, state)
@@ -81,7 +223,7 @@ pub trait CohortMetricsBase: Send + Sync {
.min(self.cost_basis_base().min_stateful_height_len())
}
fn truncate_push(&mut self, height: Height, state: &CohortState) -> Result<()> {
fn truncate_push(&mut self, height: Height, state: &CohortState<RealizedState>) -> Result<()> {
self.supply_mut()
.truncate_push(height, state.supply.value)?;
self.outputs_mut()
@@ -97,39 +239,11 @@ pub trait CohortMetricsBase: Send + Sync {
Ok(())
}
/// Compute net_sentiment.height as capital-weighted average of component cohorts (same type).
fn compute_net_sentiment_from_others(
/// Compute net_sentiment.height as capital-weighted average of component cohorts.
fn compute_net_sentiment_from_others<T: CohortMetricsBase>(
&mut self,
starting_indexes: &Indexes,
others: &[&Self],
exit: &Exit,
) -> Result<()>
where
Self: Sized,
{
let weights: Vec<_> = others
.iter()
.map(|o| &o.realized_base().realized_cap.height)
.collect();
let values: Vec<_> = others
.iter()
.map(|o| &o.unrealized_base().net_sentiment.cents.height)
.collect();
self.unrealized_base_mut()
.net_sentiment
.cents
.height
.compute_weighted_average_of_others(starting_indexes.height, &weights, &values, exit)?;
Ok(())
}
/// Compute net_sentiment.height as capital-weighted average from heterogeneous sources.
fn compute_net_sentiment_from_others_dyn(
&mut self,
starting_indexes: &Indexes,
others: &[&dyn CohortMetricsBase],
others: &[&T],
exit: &Exit,
) -> Result<()> {
let weights: Vec<_> = others
@@ -196,17 +310,13 @@ pub trait CohortMetricsBase: Send + Sync {
Ok(())
}
/// Compute aggregate base metrics from heterogeneous source cohorts.
/// Uses only base fields (supply, outputs, activity, realized_base, unrealized_base, cost_basis_base).
fn compute_base_from_others(
/// Compute aggregate base metrics from source cohorts.
fn compute_base_from_others<T: CohortMetricsBase>(
&mut self,
starting_indexes: &Indexes,
others: &[&dyn CohortMetricsBase],
others: &[&T],
exit: &Exit,
) -> Result<()>
where
Self: Sized,
{
) -> Result<()> {
macro_rules! aggregate {
($self_mut:ident, $accessor:ident) => {
self.$self_mut().compute_from_stateful(
@@ -38,7 +38,7 @@ impl RealizedAdjusted {
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn compute_rest_part2_adj(
pub(crate) fn compute_rest_part2(
&mut self,
blocks: &blocks::Vecs,
starting_indexes: &Indexes,
@@ -179,22 +179,12 @@ impl RealizedComplete {
self.core
.compute_from_stateful(starting_indexes, &core_refs, exit)?;
macro_rules! sum_others {
($($field:tt).+) => {
self.$($field).+.compute_sum_of_others(
starting_indexes.height,
&others.iter().map(|v| &v.$($field).+).collect::<Vec<_>>(),
exit,
)?
};
}
sum_others!(profit_value_created.height);
sum_others!(profit_value_destroyed.height);
sum_others!(loss_value_created.height);
sum_others!(loss_value_destroyed.height);
sum_others!(sent_in_profit.base.sats.height);
sum_others!(sent_in_loss.base.sats.height);
sum_others!(self, starting_indexes, others, exit; profit_value_created.height);
sum_others!(self, starting_indexes, others, exit; profit_value_destroyed.height);
sum_others!(self, starting_indexes, others, exit; loss_value_created.height);
sum_others!(self, starting_indexes, others, exit; loss_value_destroyed.height);
sum_others!(self, starting_indexes, others, exit; sent_in_profit.base.sats.height);
sum_others!(self, starting_indexes, others, exit; sent_in_loss.base.sats.height);
Ok(())
}
@@ -10,7 +10,7 @@ use vecdb::{
use crate::{
blocks,
distribution::state::RealizedState,
distribution::state::RealizedOps,
internal::{
CentsUnsignedToDollars, ComputedFromHeight, ComputedFromHeightCumulative,
ComputedFromHeightRatio, FiatFromHeight, Identity, LazyFromHeight,
@@ -122,7 +122,7 @@ impl CoreRealized {
.min(self.realized_loss.height.len())
}
pub(crate) fn truncate_push(&mut self, height: Height, state: &RealizedState) -> Result<()> {
pub(crate) fn truncate_push(&mut self, height: Height, state: &impl RealizedOps) -> Result<()> {
self.realized_cap_cents
.height
.truncate_push(height, state.cap())?;
@@ -149,19 +149,9 @@ impl CoreRealized {
others: &[&Self],
exit: &Exit,
) -> Result<()> {
macro_rules! sum_others {
($($field:tt).+) => {
self.$($field).+.compute_sum_of_others(
starting_indexes.height,
&others.iter().map(|v| &v.$($field).+).collect::<Vec<_>>(),
exit,
)?
};
}
sum_others!(realized_cap_cents.height);
sum_others!(realized_profit.height);
sum_others!(realized_loss.height);
sum_others!(self, starting_indexes, others, exit; realized_cap_cents.height);
sum_others!(self, starting_indexes, others, exit; realized_profit.height);
sum_others!(self, starting_indexes, others, exit; realized_loss.height);
Ok(())
}
@@ -6,9 +6,10 @@ use vecdb::{Exit, ReadableVec, Rw, StorageMode};
use crate::{
blocks,
internal::{
ComputedFromHeightRatioPercentiles, ComputedFromHeightRatioStdDevBands,
PercentFromHeight, RatioCents64, RatioDollarsBp32, RollingWindows,
ComputedFromHeightRatioFull, PercentFromHeight, RatioCents64, RatioDollarsBp32,
RollingWindows,
},
prices,
};
use crate::distribution::metrics::ImportConfig;
@@ -24,10 +25,8 @@ pub struct RealizedExtended<M: StorageMode = Rw> {
pub realized_profit_to_loss_ratio: RollingWindows<StoredF64, M>,
pub realized_price_ratio_percentiles: ComputedFromHeightRatioPercentiles<M>,
pub realized_price_ratio_std_dev: ComputedFromHeightRatioStdDevBands<M>,
pub investor_price_ratio_percentiles: ComputedFromHeightRatioPercentiles<M>,
pub investor_price_ratio_std_dev: ComputedFromHeightRatioStdDevBands<M>,
pub realized_price_ratio: ComputedFromHeightRatioFull<M>,
pub investor_price_ratio: ComputedFromHeightRatioFull<M>,
}
impl RealizedExtended {
@@ -39,27 +38,13 @@ impl RealizedExtended {
realized_loss_sum: cfg.import_rolling("realized_loss", Version::ONE)?,
realized_profit_to_loss_ratio: cfg
.import_rolling("realized_profit_to_loss_ratio", Version::ONE)?,
realized_price_ratio_percentiles:
ComputedFromHeightRatioPercentiles::forced_import(
cfg.db,
&cfg.name("realized_price"),
cfg.version + Version::ONE,
cfg.indexes,
)?,
realized_price_ratio_std_dev: ComputedFromHeightRatioStdDevBands::forced_import(
realized_price_ratio: ComputedFromHeightRatioFull::forced_import(
cfg.db,
&cfg.name("realized_price"),
cfg.version + Version::ONE,
cfg.indexes,
)?,
investor_price_ratio_percentiles:
ComputedFromHeightRatioPercentiles::forced_import(
cfg.db,
&cfg.name("investor_price"),
cfg.version,
cfg.indexes,
)?,
investor_price_ratio_std_dev: ComputedFromHeightRatioStdDevBands::forced_import(
investor_price_ratio: ComputedFromHeightRatioFull::forced_import(
cfg.db,
&cfg.name("investor_price"),
cfg.version,
@@ -69,10 +54,11 @@ impl RealizedExtended {
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn compute_rest_part2_ext(
pub(crate) fn compute_rest_part2(
&mut self,
base: &RealizedBase,
blocks: &blocks::Vecs,
prices: &prices::Vecs,
starting_indexes: &Indexes,
height_to_market_cap: &impl ReadableVec<Height, Dollars>,
exit: &Exit,
@@ -117,35 +103,21 @@ impl RealizedExtended {
)?;
}
// Realized price ratio: percentiles + stddev
self.realized_price_ratio_percentiles.compute(
// Realized price: ratio + percentiles + stddev bands
self.realized_price_ratio.compute_rest(
blocks,
prices,
starting_indexes,
exit,
&base.realized_price_ratio.ratio.height,
&base.realized_price.cents.height,
)?;
self.realized_price_ratio_std_dev.compute(
blocks,
starting_indexes,
exit,
&base.realized_price_ratio.ratio.height,
&base.realized_price.cents.height,
)?;
// Investor price ratio: percentiles + stddev
self.investor_price_ratio_percentiles.compute(
// Investor price: ratio + percentiles + stddev bands
self.investor_price_ratio.compute_rest(
blocks,
prices,
starting_indexes,
exit,
&base.investor_price_ratio.ratio.height,
&base.investor_price.cents.height,
)?;
self.investor_price_ratio_std_dev.compute(
blocks,
starting_indexes,
exit,
&base.investor_price_ratio.ratio.height,
&base.investor_price.cents.height,
)?;
@@ -4,9 +4,7 @@ mod complete;
mod core;
mod extended;
mod with_adjusted;
mod with_extended;
mod with_extended_adjusted;
pub use adjusted::*;
pub use base::*;
@@ -14,6 +12,4 @@ pub use complete::*;
pub use core::*;
pub use extended::*;
pub use with_adjusted::*;
pub use with_extended::*;
pub use with_extended_adjusted::*;
@@ -1,64 +0,0 @@
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Bitcoin, Cents, Dollars, Height, Indexes};
use derive_more::{Deref, DerefMut};
use vecdb::{Exit, ReadableVec, Rw, StorageMode};
use crate::{blocks, prices};
use crate::distribution::metrics::ImportConfig;
use super::{RealizedAdjusted, RealizedBase};
/// Realized metrics with guaranteed adjusted (no Option).
#[derive(Deref, DerefMut, Traversable)]
pub struct RealizedWithAdjusted<M: StorageMode = Rw> {
#[deref]
#[deref_mut]
#[traversable(flatten)]
pub base: RealizedBase<M>,
#[traversable(flatten)]
pub adjusted: RealizedAdjusted<M>,
}
impl RealizedWithAdjusted {
pub(crate) fn forced_import(cfg: &ImportConfig) -> Result<Self> {
let base = RealizedBase::forced_import(cfg)?;
let adjusted = RealizedAdjusted::forced_import(cfg)?;
Ok(Self { base, adjusted })
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn compute_rest_part2(
&mut self,
blocks: &blocks::Vecs,
prices: &prices::Vecs,
starting_indexes: &Indexes,
height_to_supply: &impl ReadableVec<Height, Bitcoin>,
height_to_market_cap: &impl ReadableVec<Height, Dollars>,
up_to_1h_value_created: &impl ReadableVec<Height, Cents>,
up_to_1h_value_destroyed: &impl ReadableVec<Height, Cents>,
exit: &Exit,
) -> Result<()> {
self.base.compute_rest_part2_base(
blocks,
prices,
starting_indexes,
height_to_supply,
height_to_market_cap,
exit,
)?;
self.adjusted.compute_rest_part2_adj(
blocks,
starting_indexes,
&self.base.value_created.height,
&self.base.value_destroyed.height,
up_to_1h_value_created,
up_to_1h_value_destroyed,
exit,
)?;
Ok(())
}
}
@@ -47,9 +47,10 @@ impl RealizedWithExtended {
exit,
)?;
self.extended.compute_rest_part2_ext(
self.extended.compute_rest_part2(
&self.base,
blocks,
prices,
starting_indexes,
height_to_market_cap,
exit,
@@ -1,79 +0,0 @@
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Bitcoin, Cents, Dollars, Height, Indexes};
use derive_more::{Deref, DerefMut};
use vecdb::{Exit, ReadableVec, Rw, StorageMode};
use crate::{blocks, prices};
use crate::distribution::metrics::ImportConfig;
use super::{RealizedAdjusted, RealizedBase, RealizedExtended};
/// Realized metrics with guaranteed extended AND adjusted (no Options).
#[derive(Deref, DerefMut, Traversable)]
pub struct RealizedWithExtendedAdjusted<M: StorageMode = Rw> {
#[deref]
#[deref_mut]
#[traversable(flatten)]
pub base: RealizedBase<M>,
#[traversable(flatten)]
pub extended: RealizedExtended<M>,
#[traversable(flatten)]
pub adjusted: RealizedAdjusted<M>,
}
impl RealizedWithExtendedAdjusted {
pub(crate) fn forced_import(cfg: &ImportConfig) -> Result<Self> {
let base = RealizedBase::forced_import(cfg)?;
let extended = RealizedExtended::forced_import(cfg)?;
let adjusted = RealizedAdjusted::forced_import(cfg)?;
Ok(Self {
base,
extended,
adjusted,
})
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn compute_rest_part2(
&mut self,
blocks: &blocks::Vecs,
prices: &prices::Vecs,
starting_indexes: &Indexes,
height_to_supply: &impl ReadableVec<Height, Bitcoin>,
height_to_market_cap: &impl ReadableVec<Height, Dollars>,
up_to_1h_value_created: &impl ReadableVec<Height, Cents>,
up_to_1h_value_destroyed: &impl ReadableVec<Height, Cents>,
exit: &Exit,
) -> Result<()> {
self.base.compute_rest_part2_base(
blocks,
prices,
starting_indexes,
height_to_supply,
height_to_market_cap,
exit,
)?;
self.extended.compute_rest_part2_ext(
&self.base,
blocks,
starting_indexes,
height_to_market_cap,
exit,
)?;
self.adjusted.compute_rest_part2_adj(
blocks,
starting_indexes,
&self.base.value_created.height,
&self.base.value_destroyed.height,
up_to_1h_value_created,
up_to_1h_value_destroyed,
exit,
)?;
Ok(())
}
}
@@ -150,18 +150,8 @@ impl UnrealizedBase {
.compute_from_stateful(starting_indexes, &complete_refs, exit)?;
// Source-only: invested_capital
macro_rules! sum_others {
($($field:tt).+) => {
self.$($field).+.compute_sum_of_others(
starting_indexes.height,
&others.iter().map(|v| &v.$($field).+).collect::<Vec<_>>(),
exit,
)?
};
}
sum_others!(invested_capital_in_profit.cents.height);
sum_others!(invested_capital_in_loss.cents.height);
sum_others!(self, starting_indexes, others, exit; invested_capital_in_profit.cents.height);
sum_others!(self, starting_indexes, others, exit; invested_capital_in_loss.cents.height);
// Source-only: raw BytesVec aggregation
let start = self
@@ -112,20 +112,10 @@ impl UnrealizedComplete {
others: &[&Self],
exit: &Exit,
) -> Result<()> {
macro_rules! sum_others {
($($field:tt).+) => {
self.$($field).+.compute_sum_of_others(
starting_indexes.height,
&others.iter().map(|v| &v.$($field).+).collect::<Vec<_>>(),
exit,
)?
};
}
sum_others!(supply_in_profit.sats.height);
sum_others!(supply_in_loss.sats.height);
sum_others!(unrealized_profit.cents.height);
sum_others!(unrealized_loss.cents.height);
sum_others!(self, starting_indexes, others, exit; supply_in_profit.sats.height);
sum_others!(self, starting_indexes, others, exit; supply_in_loss.sats.height);
sum_others!(self, starting_indexes, others, exit; unrealized_profit.cents.height);
sum_others!(self, starting_indexes, others, exit; unrealized_loss.cents.height);
Ok(())
}
@@ -4,17 +4,18 @@ use brk_error::Result;
use brk_types::{Age, Cents, FundedAddressData, Sats, SupplyState};
use vecdb::unlikely;
use super::{super::cost_basis::RealizedState, base::CohortState};
use super::super::cost_basis::RealizedOps;
use super::base::CohortState;
/// Significant digits for address cost basis prices (after rounding to dollars).
const COST_BASIS_PRICE_DIGITS: i32 = 4;
pub struct AddressCohortState {
pub struct AddressCohortState<R: RealizedOps> {
pub addr_count: u64,
pub inner: CohortState,
pub inner: CohortState<R>,
}
impl AddressCohortState {
impl<R: RealizedOps> AddressCohortState<R> {
pub(crate) fn new(path: &Path, name: &str) -> Self {
Self {
addr_count: 0,
@@ -29,7 +30,7 @@ impl AddressCohortState {
self.inner.sent = Sats::ZERO;
self.inner.satblocks_destroyed = Sats::ZERO;
self.inner.satdays_destroyed = Sats::ZERO;
self.inner.realized = RealizedState::default();
self.inner.realized = R::default();
}
pub(crate) fn send(
@@ -3,7 +3,7 @@ use std::{collections::BTreeMap, path::Path};
use brk_error::Result;
use brk_types::{Age, Cents, CentsCompact, CentsSats, CentsSquaredSats, CostBasisSnapshot, Height, Sats, SupplyState};
use super::super::cost_basis::{CostBasisData, Percentiles, RealizedState, UnrealizedState};
use super::super::cost_basis::{CostBasisData, Percentiles, RealizedOps, UnrealizedState};
pub struct SendPrecomputed {
pub sats: Sats,
@@ -15,20 +15,54 @@ pub struct SendPrecomputed {
pub prev_investor_cap: CentsSquaredSats,
}
pub struct CohortState {
impl SendPrecomputed {
/// Pre-compute values for send_utxo when the same supply/prices are shared
/// across multiple cohorts (age_range, epoch, class).
pub(crate) fn new(
supply: &SupplyState,
current_price: Cents,
prev_price: Cents,
ath: Cents,
age: Age,
) -> Option<Self> {
if supply.utxo_count == 0 || supply.value == Sats::ZERO {
return None;
}
let sats = supply.value;
let current_ps = CentsSats::from_price_sats(current_price, sats);
let prev_ps = CentsSats::from_price_sats(prev_price, sats);
let ath_ps = if ath == current_price {
current_ps
} else {
CentsSats::from_price_sats(ath, sats)
};
let prev_investor_cap = prev_ps.to_investor_cap(prev_price);
Some(Self {
sats,
prev_price,
age,
current_ps,
prev_ps,
ath_ps,
prev_investor_cap,
})
}
}
pub struct CohortState<R: RealizedOps> {
pub supply: SupplyState,
pub realized: RealizedState,
pub realized: R,
pub sent: Sats,
pub satblocks_destroyed: Sats,
pub satdays_destroyed: Sats,
cost_basis_data: CostBasisData,
}
impl CohortState {
impl<R: RealizedOps> CohortState<R> {
pub(crate) fn new(path: &Path, name: &str) -> Self {
Self {
supply: SupplyState::default(),
realized: RealizedState::default(),
realized: R::default(),
sent: Sats::ZERO,
satblocks_destroyed: Sats::ZERO,
satdays_destroyed: Sats::ZERO,
@@ -47,7 +81,6 @@ impl CohortState {
}
/// Restore realized cap from cost_basis_data after import.
/// Uses the exact persisted values instead of recomputing from the map.
pub(crate) fn restore_realized_cap(&mut self) {
self.realized.set_cap_raw(self.cost_basis_data.cap_raw());
self.realized
@@ -170,38 +203,6 @@ impl CohortState {
}
}
/// Pre-computed values for send_utxo when the same supply/prices are shared
/// across multiple cohorts (age_range, epoch, year).
pub(crate) fn precompute_send(
supply: &SupplyState,
current_price: Cents,
prev_price: Cents,
ath: Cents,
age: Age,
) -> Option<SendPrecomputed> {
if supply.utxo_count == 0 || supply.value == Sats::ZERO {
return None;
}
let sats = supply.value;
let current_ps = CentsSats::from_price_sats(current_price, sats);
let prev_ps = CentsSats::from_price_sats(prev_price, sats);
let ath_ps = if ath == current_price {
current_ps
} else {
CentsSats::from_price_sats(ath, sats)
};
let prev_investor_cap = prev_ps.to_investor_cap(prev_price);
Some(SendPrecomputed {
sats,
prev_price,
age,
current_ps,
prev_ps,
ath_ps,
prev_investor_cap,
})
}
pub(crate) fn send_utxo_precomputed(
&mut self,
supply: &SupplyState,
@@ -227,7 +228,7 @@ impl CohortState {
ath: Cents,
age: Age,
) {
if let Some(pre) = Self::precompute_send(supply, current_price, prev_price, ath, age) {
if let Some(pre) = SendPrecomputed::new(supply, current_price, prev_price, ath, age) {
self.send_utxo_precomputed(supply, &pre);
} else if supply.utxo_count > 0 {
self.supply -= supply;
@@ -4,12 +4,13 @@ use brk_error::Result;
use brk_types::{Sats, SupplyState};
use derive_more::{Deref, DerefMut};
use super::{super::cost_basis::RealizedState, base::CohortState};
use super::super::cost_basis::RealizedOps;
use super::base::CohortState;
#[derive(Deref, DerefMut)]
pub struct UTXOCohortState(CohortState);
pub struct UTXOCohortState<R: RealizedOps>(pub(crate) CohortState<R>);
impl UTXOCohortState {
impl<R: RealizedOps> UTXOCohortState<R> {
pub(crate) fn new(path: &Path, name: &str) -> Self {
Self(CohortState::new(path, name))
}
@@ -24,6 +25,6 @@ impl UTXOCohortState {
self.0.sent = Sats::ZERO;
self.0.satblocks_destroyed = Sats::ZERO;
self.0.satdays_destroyed = Sats::ZERO;
self.0.realized = RealizedState::default();
self.0.realized = R::default();
}
}
@@ -2,19 +2,131 @@ use std::cmp::Ordering;
use brk_types::{Cents, CentsSats, CentsSquaredSats, Sats};
/// Realized state using u128 for raw cent*sat values internally.
/// This avoids overflow and defers division to output time for efficiency.
/// Trait for realized state operations, implemented by both Core and Full variants.
/// Core skips extra fields (value_created/destroyed, peak_regret, sent_in_profit/loss, investor_cap).
pub trait RealizedOps: Default + Clone + Send + Sync + 'static {
fn cap(&self) -> Cents;
fn profit(&self) -> Cents;
fn loss(&self) -> Cents;
fn set_cap_raw(&mut self, cap_raw: CentsSats);
fn set_investor_cap_raw(&mut self, investor_cap_raw: CentsSquaredSats);
fn reset_single_iteration_values(&mut self);
fn increment(&mut self, price: Cents, sats: Sats);
fn increment_snapshot(&mut self, price_sats: CentsSats, investor_cap: CentsSquaredSats);
fn decrement_snapshot(&mut self, price_sats: CentsSats, investor_cap: CentsSquaredSats);
fn receive(&mut self, price: Cents, sats: Sats) {
self.increment(price, sats);
}
fn send(
&mut self,
sats: Sats,
current_ps: CentsSats,
prev_ps: CentsSats,
ath_ps: CentsSats,
prev_investor_cap: CentsSquaredSats,
);
}
/// Core realized state: only cap, profit, loss (48 bytes).
/// Used by CoreCohortMetrics and MinimalCohortMetrics cohorts
/// (epoch, class, amount_range, type_ — ~50 separate cohorts).
#[derive(Debug, Default, Clone)]
pub struct CoreRealizedState {
cap_raw: u128,
profit_raw: u128,
loss_raw: u128,
}
impl RealizedOps for CoreRealizedState {
#[inline]
fn cap(&self) -> Cents {
if self.cap_raw == 0 {
return Cents::ZERO;
}
Cents::new((self.cap_raw / Sats::ONE_BTC_U128) as u64)
}
#[inline]
fn profit(&self) -> Cents {
if self.profit_raw == 0 {
return Cents::ZERO;
}
Cents::new((self.profit_raw / Sats::ONE_BTC_U128) as u64)
}
#[inline]
fn loss(&self) -> Cents {
if self.loss_raw == 0 {
return Cents::ZERO;
}
Cents::new((self.loss_raw / Sats::ONE_BTC_U128) as u64)
}
#[inline]
fn set_cap_raw(&mut self, cap_raw: CentsSats) {
self.cap_raw = cap_raw.inner();
}
#[inline]
fn set_investor_cap_raw(&mut self, _investor_cap_raw: CentsSquaredSats) {
// no-op for Core
}
#[inline]
fn reset_single_iteration_values(&mut self) {
self.profit_raw = 0;
self.loss_raw = 0;
}
#[inline]
fn increment(&mut self, price: Cents, sats: Sats) {
if sats.is_zero() {
return;
}
let price_sats = CentsSats::from_price_sats(price, sats);
self.cap_raw += price_sats.as_u128();
}
#[inline]
fn increment_snapshot(&mut self, price_sats: CentsSats, _investor_cap: CentsSquaredSats) {
self.cap_raw += price_sats.as_u128();
}
#[inline]
fn decrement_snapshot(&mut self, price_sats: CentsSats, _investor_cap: CentsSquaredSats) {
self.cap_raw -= price_sats.as_u128();
}
#[inline]
fn send(
&mut self,
_sats: Sats,
current_ps: CentsSats,
prev_ps: CentsSats,
_ath_ps: CentsSats,
_prev_investor_cap: CentsSquaredSats,
) {
match current_ps.cmp(&prev_ps) {
Ordering::Greater => {
self.profit_raw += (current_ps - prev_ps).as_u128();
}
Ordering::Less => {
self.loss_raw += (prev_ps - current_ps).as_u128();
}
Ordering::Equal => {}
}
self.cap_raw -= prev_ps.as_u128();
}
}
/// Full realized state (~160 bytes).
/// Used by BasicCohortMetrics and CompleteCohortMetrics cohorts
/// (age_range — 21 separate cohorts).
#[derive(Debug, Default, Clone)]
pub struct RealizedState {
/// Raw realized cap: Σ(price × sats)
cap_raw: u128,
core: CoreRealizedState,
/// Raw investor cap: Σ(price² × sats)
/// investor_price = investor_cap_raw / cap_raw (gives cents directly)
investor_cap_raw: CentsSquaredSats,
/// Raw realized profit (cents * sats)
profit_raw: u128,
/// Raw realized loss (cents * sats)
loss_raw: u128,
/// sell_price × sats for profit cases
profit_value_created_raw: u128,
/// cost_basis × sats for profit cases
@@ -31,43 +143,120 @@ pub struct RealizedState {
sent_in_loss: Sats,
}
impl RealizedState {
/// Get realized cap as CentsUnsigned (divides by ONE_BTC).
impl RealizedOps for RealizedState {
#[inline]
pub(crate) fn cap(&self) -> Cents {
if self.cap_raw == 0 {
return Cents::ZERO;
}
Cents::new((self.cap_raw / Sats::ONE_BTC_U128) as u64)
fn cap(&self) -> Cents {
self.core.cap()
}
/// Set cap_raw directly from persisted value.
#[inline]
pub(crate) fn set_cap_raw(&mut self, cap_raw: CentsSats) {
self.cap_raw = cap_raw.inner();
fn profit(&self) -> Cents {
self.core.profit()
}
/// Set investor_cap_raw directly from persisted value.
#[inline]
pub(crate) fn set_investor_cap_raw(&mut self, investor_cap_raw: CentsSquaredSats) {
fn loss(&self) -> Cents {
self.core.loss()
}
#[inline]
fn set_cap_raw(&mut self, cap_raw: CentsSats) {
self.core.set_cap_raw(cap_raw);
}
#[inline]
fn set_investor_cap_raw(&mut self, investor_cap_raw: CentsSquaredSats) {
self.investor_cap_raw = investor_cap_raw;
}
#[inline]
fn reset_single_iteration_values(&mut self) {
self.core.reset_single_iteration_values();
self.profit_value_created_raw = 0;
self.profit_value_destroyed_raw = 0;
self.loss_value_created_raw = 0;
self.loss_value_destroyed_raw = 0;
self.peak_regret_raw = 0;
self.sent_in_profit = Sats::ZERO;
self.sent_in_loss = Sats::ZERO;
}
#[inline]
fn increment(&mut self, price: Cents, sats: Sats) {
if sats.is_zero() {
return;
}
let price_sats = CentsSats::from_price_sats(price, sats);
self.core.cap_raw += price_sats.as_u128();
self.investor_cap_raw += price_sats.to_investor_cap(price);
}
#[inline]
fn increment_snapshot(&mut self, price_sats: CentsSats, investor_cap: CentsSquaredSats) {
self.core.cap_raw += price_sats.as_u128();
self.investor_cap_raw += investor_cap;
}
#[inline]
fn decrement_snapshot(&mut self, price_sats: CentsSats, investor_cap: CentsSquaredSats) {
self.core.cap_raw -= price_sats.as_u128();
self.investor_cap_raw -= investor_cap;
}
#[inline]
fn send(
&mut self,
sats: Sats,
current_ps: CentsSats,
prev_ps: CentsSats,
ath_ps: CentsSats,
prev_investor_cap: CentsSquaredSats,
) {
match current_ps.cmp(&prev_ps) {
Ordering::Greater => {
self.core.profit_raw += (current_ps - prev_ps).as_u128();
self.profit_value_created_raw += current_ps.as_u128();
self.profit_value_destroyed_raw += prev_ps.as_u128();
self.sent_in_profit += sats;
}
Ordering::Less => {
self.core.loss_raw += (prev_ps - current_ps).as_u128();
self.loss_value_created_raw += current_ps.as_u128();
self.loss_value_destroyed_raw += prev_ps.as_u128();
self.sent_in_loss += sats;
}
Ordering::Equal => {
// Break-even: count as profit side (arbitrary but consistent)
self.profit_value_created_raw += current_ps.as_u128();
self.profit_value_destroyed_raw += prev_ps.as_u128();
self.sent_in_profit += sats;
}
}
// Track peak regret: (peak - sell_price) × sats
self.peak_regret_raw += (ath_ps - current_ps).as_u128();
// Inline decrement to avoid recomputation
self.core.cap_raw -= prev_ps.as_u128();
self.investor_cap_raw -= prev_investor_cap;
}
}
impl RealizedState {
/// Get investor price as CentsUnsigned.
/// investor_price = Σ(price² × sats) / Σ(price × sats)
/// This is the dollar-weighted average acquisition price.
#[inline]
pub(crate) fn investor_price(&self) -> Cents {
if self.cap_raw == 0 {
if self.core.cap_raw == 0 {
return Cents::ZERO;
}
Cents::new((self.investor_cap_raw / self.cap_raw) as u64)
Cents::new((self.investor_cap_raw / self.core.cap_raw) as u64)
}
/// Get raw realized cap for aggregation.
#[inline]
pub(crate) fn cap_raw(&self) -> CentsSats {
CentsSats::new(self.cap_raw)
CentsSats::new(self.core.cap_raw)
}
/// Get raw investor cap for aggregation.
@@ -76,24 +265,6 @@ impl RealizedState {
self.investor_cap_raw
}
/// Get realized profit as CentsUnsigned.
#[inline]
pub(crate) fn profit(&self) -> Cents {
if self.profit_raw == 0 {
return Cents::ZERO;
}
Cents::new((self.profit_raw / Sats::ONE_BTC_U128) as u64)
}
/// Get realized loss as CentsUnsigned.
#[inline]
pub(crate) fn loss(&self) -> Cents {
if self.loss_raw == 0 {
return Cents::ZERO;
}
Cents::new((self.loss_raw / Sats::ONE_BTC_U128) as u64)
}
/// Get profit value created as CentsUnsigned (sell_price × sats for profit cases).
#[inline]
pub(crate) fn profit_value_created(&self) -> Cents {
@@ -104,7 +275,6 @@ impl RealizedState {
}
/// Get profit value destroyed as CentsUnsigned (cost_basis × sats for profit cases).
/// This is also known as profit_flow.
#[inline]
pub(crate) fn profit_value_destroyed(&self) -> Cents {
if self.profit_value_destroyed_raw == 0 {
@@ -123,7 +293,6 @@ impl RealizedState {
}
/// Get loss value destroyed as CentsUnsigned (cost_basis × sats for loss cases).
/// This is also known as capitulation_flow.
#[inline]
pub(crate) fn loss_value_destroyed(&self) -> Cents {
if self.loss_value_destroyed_raw == 0 {
@@ -133,8 +302,6 @@ impl RealizedState {
}
/// Get realized peak regret as CentsUnsigned.
/// This is Σ((peak - sell_price) × sats) - how much more could have been made
/// by selling at peak instead of when actually sold.
#[inline]
pub(crate) fn peak_regret(&self) -> Cents {
if self.peak_regret_raw == 0 {
@@ -154,93 +321,4 @@ impl RealizedState {
pub(crate) fn sent_in_loss(&self) -> Sats {
self.sent_in_loss
}
pub(crate) fn reset_single_iteration_values(&mut self) {
self.profit_raw = 0;
self.loss_raw = 0;
self.profit_value_created_raw = 0;
self.profit_value_destroyed_raw = 0;
self.loss_value_created_raw = 0;
self.loss_value_destroyed_raw = 0;
self.peak_regret_raw = 0;
self.sent_in_profit = Sats::ZERO;
self.sent_in_loss = Sats::ZERO;
}
/// Increment using pre-computed values (for UTXO path)
#[inline]
pub(crate) fn increment(&mut self, price: Cents, sats: Sats) {
if sats.is_zero() {
return;
}
let price_sats = CentsSats::from_price_sats(price, sats);
self.cap_raw += price_sats.as_u128();
self.investor_cap_raw += price_sats.to_investor_cap(price);
}
/// Increment using pre-computed snapshot values (for address path)
#[inline]
pub(crate) fn increment_snapshot(
&mut self,
price_sats: CentsSats,
investor_cap: CentsSquaredSats,
) {
self.cap_raw += price_sats.as_u128();
self.investor_cap_raw += investor_cap;
}
/// Decrement using pre-computed snapshot values (for address path)
#[inline]
pub(crate) fn decrement_snapshot(
&mut self,
price_sats: CentsSats,
investor_cap: CentsSquaredSats,
) {
self.cap_raw -= price_sats.as_u128();
self.investor_cap_raw -= investor_cap;
}
#[inline]
pub(crate) fn receive(&mut self, price: Cents, sats: Sats) {
self.increment(price, sats);
}
/// Send with pre-computed typed values. Inlines decrement to avoid recomputation.
#[inline]
pub(crate) fn send(
&mut self,
sats: Sats,
current_ps: CentsSats,
prev_ps: CentsSats,
ath_ps: CentsSats,
prev_investor_cap: CentsSquaredSats,
) {
match current_ps.cmp(&prev_ps) {
Ordering::Greater => {
self.profit_raw += (current_ps - prev_ps).as_u128();
self.profit_value_created_raw += current_ps.as_u128();
self.profit_value_destroyed_raw += prev_ps.as_u128();
self.sent_in_profit += sats;
}
Ordering::Less => {
self.loss_raw += (prev_ps - current_ps).as_u128();
self.loss_value_created_raw += current_ps.as_u128();
self.loss_value_destroyed_raw += prev_ps.as_u128();
self.sent_in_loss += sats;
}
Ordering::Equal => {
// Break-even: count as profit side (arbitrary but consistent)
self.profit_value_created_raw += current_ps.as_u128();
self.profit_value_destroyed_raw += prev_ps.as_u128();
self.sent_in_profit += sats;
}
}
// Track peak regret: (peak - sell_price) × sats
self.peak_regret_raw += (ath_ps - current_ps).as_u128();
// Inline decrement to avoid recomputation
self.cap_raw -= prev_ps.as_u128();
self.investor_cap_raw -= prev_investor_cap;
}
}
@@ -38,12 +38,13 @@ impl Vecs {
// Find min_height via binary search (first_txoutindex is monotonically non-decreasing)
let first_txoutindex_vec = &indexer.vecs.outputs.first_txoutindex;
let total_heights = target_height.to_usize() + 1;
let min_height = if min_txoutindex == 0 {
Height::ZERO
} else if min_txoutindex >= starting_indexes.txoutindex.to_usize() {
starting_indexes.height
} else {
let mut lo = 0usize;
let mut hi = total_heights;
let mut hi = starting_indexes.height.to_usize() + 1;
while lo < hi {
let mid = lo + (hi - lo) / 2;
if first_txoutindex_vec.collect_one_at(mid).unwrap().to_usize() <= min_txoutindex {
+73 -51
View File
@@ -612,10 +612,7 @@ fn gen_read_only_clone(input: &DeriveInput) -> proc_macro2::TokenStream {
return gen_read_only_clone_for_m(name, generics, data, mode_param);
}
// Collect generic type params that have NO trait bounds.
// Container types (ByDcaClass<T>, Price<U>) have unbounded params.
// Leaf types (LazyPercentiles<I: VecIndex, T: ComputedVecValue, ...>) have bounded params.
// Only generate ReadOnlyClone for container-like types (all params unbounded).
// Collect all generic type params.
let type_params: Vec<&syn::TypeParam> = generics
.params
.iter()
@@ -629,31 +626,43 @@ fn gen_read_only_clone(input: &DeriveInput) -> proc_macro2::TokenStream {
return quote! {};
}
// If any type param has bounds (inline or in where clause), skip —
// this is a leaf/computation type, not a container.
if type_params.iter().any(|tp| !tp.bounds.is_empty()) {
// Determine which type params have bounds (inline or via where clause).
// Bounded params are "leaf" params — kept as-is in the ReadOnly target.
// Unbounded params are "container" params — mapped through ReadOnlyClone.
let where_bounded: Vec<&syn::Ident> = if let Some(where_clause) = &generics.where_clause {
where_clause
.predicates
.iter()
.filter_map(|pred| {
if let syn::WherePredicate::Type(pt) = pred
&& let Type::Path(tp) = &pt.bounded_ty
&& let Some(seg) = tp.path.segments.first()
{
type_params
.iter()
.find(|p| p.ident == seg.ident)
.map(|p| &p.ident)
} else {
None
}
})
.collect()
} else {
Vec::new()
};
let container_params: Vec<&syn::Ident> = type_params
.iter()
.filter(|tp| tp.bounds.is_empty() && !where_bounded.contains(&&tp.ident))
.map(|tp| &tp.ident)
.collect();
// If no container params, this is a pure leaf type — skip.
if container_params.is_empty() {
return quote! {};
}
// Also check where clause for bounds on any type param.
if let Some(where_clause) = &generics.where_clause {
let param_names: Vec<&syn::Ident> = type_params.iter().map(|tp| &tp.ident).collect();
let has_where_bounds = where_clause.predicates.iter().any(|pred| {
if let syn::WherePredicate::Type(pt) = pred
&& let Type::Path(tp) = &pt.bounded_ty
&& let Some(seg) = tp.path.segments.first()
{
return param_names.iter().any(|p| seg.ident == **p);
}
false
});
if has_where_bounds {
return quote! {};
}
}
let param_idents: Vec<&syn::Ident> = type_params.iter().map(|tp| &tp.ident).collect();
gen_read_only_clone_for_generics(name, generics, data, &param_idents)
gen_read_only_clone_for_generics(name, generics, data, &container_params)
}
/// Generate `ReadOnlyClone` for types with `M: StorageMode`.
@@ -789,33 +798,40 @@ fn is_field_skipped(field: &syn::Field) -> bool {
/// Generate `ReadOnlyClone` for types with generic type params but no `M: StorageMode`.
///
/// Each generic type param T gets a `ReadOnlyClone` bound.
/// `type ReadOnly = Self<T::ReadOnly, ...>` for each type param.
/// Fields containing any type param use `.read_only_clone()`, others use `.clone()`.
/// `container_params` are unbounded type params that get `ReadOnlyClone` bounds and are
/// mapped to `T::ReadOnly` in the target type.
/// Bounded type params (leaf params) are kept as-is — they don't change across storage modes.
/// Fields containing container params use `.read_only_clone()`, others use `.clone()`.
fn gen_read_only_clone_for_generics(
name: &syn::Ident,
generics: &syn::Generics,
data: &syn::DataStruct,
type_params: &[&syn::Ident],
container_params: &[&syn::Ident],
) -> proc_macro2::TokenStream {
// Check if any field actually references a type param (otherwise skip).
let has_generic_field = match &data.fields {
Fields::Named(named) => named
.named
.iter()
.any(|f| type_params.iter().any(|tp| type_contains_ident(&f.ty, tp))),
Fields::Unnamed(unnamed) => unnamed
.unnamed
.iter()
.any(|f| type_params.iter().any(|tp| type_contains_ident(&f.ty, tp))),
// Check if any non-skipped field references a container param (otherwise skip).
let has_container_field = match &data.fields {
Fields::Named(named) => named.named.iter().any(|f| {
!is_field_skipped(f)
&& container_params
.iter()
.any(|tp| type_contains_ident(&f.ty, tp))
}),
Fields::Unnamed(unnamed) => unnamed.unnamed.iter().any(|f| {
!is_field_skipped(f)
&& container_params
.iter()
.any(|tp| type_contains_ident(&f.ty, tp))
}),
Fields::Unit => false,
};
if !has_generic_field {
if !has_container_field {
return quote! {};
}
// Impl generics: add ReadOnlyClone bound to type params.
let is_container = |ident: &syn::Ident| container_params.iter().any(|cp| *cp == ident);
// Impl generics: add ReadOnlyClone bound to container params, keep bounds for leaf params.
let impl_params: Vec<proc_macro2::TokenStream> = generics
.params
.iter()
@@ -823,10 +839,12 @@ fn gen_read_only_clone_for_generics(
syn::GenericParam::Type(tp) => {
let ident = &tp.ident;
let bounds = &tp.bounds;
if bounds.is_empty() {
if is_container(ident) {
quote! { #ident: vecdb::ReadOnlyClone }
} else if bounds.is_empty() {
quote! { #ident }
} else {
quote! { #ident: #bounds + vecdb::ReadOnlyClone }
quote! { #ident: #bounds }
}
}
syn::GenericParam::Lifetime(lt) => quote! { #lt },
@@ -858,14 +876,18 @@ fn gen_read_only_clone_for_generics(
})
.collect();
// ReadOnly type args: replace each type param T with <T as ReadOnlyClone>::ReadOnly.
// ReadOnly type args: map container params to ReadOnly, keep leaf params as-is.
let ro_ty_args: Vec<proc_macro2::TokenStream> = generics
.params
.iter()
.map(|p| match p {
syn::GenericParam::Type(tp) => {
let id = &tp.ident;
quote! { <#id as vecdb::ReadOnlyClone>::ReadOnly }
if is_container(id) {
quote! { <#id as vecdb::ReadOnlyClone>::ReadOnly }
} else {
quote! { #id }
}
}
syn::GenericParam::Lifetime(lt) => {
let lt = &lt.lifetime;
@@ -880,9 +902,9 @@ fn gen_read_only_clone_for_generics(
let where_clause = &generics.where_clause;
// Field-level: if field type contains any type param → read_only_clone, else → clone.
let field_contains_any_param =
|ty: &Type| type_params.iter().any(|tp| type_contains_ident(ty, tp));
// Field-level: if field type contains any container param → read_only_clone, else → clone.
let field_contains_container_param =
|ty: &Type| container_params.iter().any(|tp| type_contains_ident(ty, tp));
let body = match &data.fields {
Fields::Named(named) => {
@@ -893,7 +915,7 @@ fn gen_read_only_clone_for_generics(
let field_name = f.ident.as_ref().unwrap();
if is_field_skipped(f) {
quote! { #field_name: Default::default() }
} else if field_contains_any_param(&f.ty) {
} else if field_contains_container_param(&f.ty) {
if is_box_type(&f.ty) {
quote! { #field_name: Box::new(vecdb::ReadOnlyClone::read_only_clone(&*self.#field_name)) }
} else {
@@ -915,7 +937,7 @@ fn gen_read_only_clone_for_generics(
let idx = syn::Index::from(i);
if is_field_skipped(f) {
quote! { Default::default() }
} else if field_contains_any_param(&f.ty) {
} else if field_contains_container_param(&f.ty) {
if is_box_type(&f.ty) {
quote! { Box::new(vecdb::ReadOnlyClone::read_only_clone(&*self.#idx)) }
} else {