global: massive columnar rework part 3

This commit is contained in:
nym21
2026-08-11 18:15:29 +02:00
parent 09b50b6ede
commit 30ebe4e1ff
119 changed files with 22924 additions and 8503 deletions
+19067 -4709
View File
File diff suppressed because it is too large Load Diff
+9
View File
@@ -37,4 +37,13 @@ impl CohortContext {
Filter::Time(_) | Filter::Amount(_) => self.prefixed(name),
}
}
pub fn metric_name(&self, filter: &Filter, cohort: &str, metric: &str) -> String {
let cohort = self.full_name(filter, cohort);
if cohort.is_empty() {
metric.to_owned()
} else {
format!("{cohort}_{metric}")
}
}
}
@@ -1,11 +1,11 @@
use brk_cohort::UTXOGroupsWithoutAmountOrType;
use brk_cohort::{CohortContext, UTXOGroupsWithoutAmountOrType};
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{StoredF64, Version};
use vecdb::{Database, Rw, StorageMode};
use crate::{
distribution::metrics::{CumulativeUTXOColumnarMetricWithoutAmountOrType, utxo_metric_name},
distribution::metrics::CumulativeUTXOColumnarMetricWithoutAmountOrType,
indexes,
internal::{CachedWindowStartVec, LazyPerBlockCumulativeRolling, Windows},
};
@@ -31,7 +31,7 @@ impl CoindaysDestroyedByCohort {
version,
)?;
let cohorts = UTXOGroupsWithoutAmountOrType::new(|filter, cohort_name| {
let name = utxo_metric_name(&filter, cohort_name, "coindays_destroyed");
let name = CohortContext::Utxo.metric_name(&filter, cohort_name, "coindays_destroyed");
let source = cumulative
.matrices
.additive_source(&filter, &format!("{name}_cumulative"), version)
@@ -1,6 +1,6 @@
use brk_cohort::{
AmountRange, Filter, UTXO_AGGREGATE_FILTERS, UTXO_AGGREGATE_NAMES, UTXOAggregate,
UTXOAggregateId,
AmountRange, CohortContext, Filter, UTXO_AGGREGATE_FILTERS, UTXO_AGGREGATE_NAMES,
UTXOAggregate, UTXOAggregateId,
};
use brk_error::Result;
use brk_traversable::Traversable;
@@ -8,7 +8,7 @@ use brk_types::{Cents, Height, Sats, StoredF32, StoredF64, Version};
use vecdb::{AnyStoredVec, AnyVec, BinaryTransform, ColumnId, Database, Exit, Rw, StorageMode};
use crate::{
distribution::metrics::{UTXORows, utxo_metric_name},
distribution::metrics::UTXORows,
indexes,
internal::{
CachedWindowStartVec, ColumnarRollingWindows, Identity, LazyPerBlock, SatsToCents, Windows,
@@ -23,13 +23,13 @@ use super::{
#[derive(Traversable)]
pub struct ActivityVecs<M: StorageMode = Rw> {
pub transfer_volume: Box<CumulativeValueByCohort<M>>,
pub coindays_destroyed: Box<CoindaysDestroyedByCohort<M>>,
pub coindays_destroyed: CoindaysDestroyedByCohort<M>,
#[traversable(wrap = "transfer_volume", rename = "in_profit")]
pub transfer_volume_in_profit: Box<CoreCumulativeValueByCohort<M>>,
#[traversable(wrap = "transfer_volume", rename = "in_loss")]
pub transfer_volume_in_loss: Box<CoreCumulativeValueByCohort<M>>,
pub coinyears_destroyed: Box<UTXOAggregate<LazyPerBlock<StoredF64, StoredF64>>>,
pub dormancy: Box<UTXOAggregate<ColumnarRollingWindows<StoredF32, M>>>,
pub coinyears_destroyed: UTXOAggregate<LazyPerBlock<StoredF64, StoredF64>>,
pub dormancy: UTXOAggregate<ColumnarRollingWindows<StoredF32, M>>,
}
impl ActivityVecs {
@@ -48,12 +48,8 @@ impl ActivityVecs {
indexes,
cached_starts,
)?);
let coindays_destroyed = Box::new(CoindaysDestroyedByCohort::forced_import(
db,
version,
indexes,
cached_starts,
)?);
let coindays_destroyed =
CoindaysDestroyedByCohort::forced_import(db, version, indexes, cached_starts)?;
let transfer_volume_in_profit = Box::new(CoreCumulativeValueByCohort::forced_import(
db,
"transfer_volume_in_profit",
@@ -68,7 +64,7 @@ impl ActivityVecs {
indexes,
cached_starts,
)?);
let coinyears_destroyed = Box::new(UTXOAggregate::from_fn(|id| {
let coinyears_destroyed = UTXOAggregate::from_fn(|id| {
let filter = id.select(&UTXO_AGGREGATE_FILTERS);
let name = Self::aggregate_metric_name(id, "coinyears_destroyed");
LazyPerBlock::from_height_source::<Identity<StoredF64>, _>(
@@ -84,15 +80,15 @@ impl ActivityVecs {
.clone(),
indexes,
)
}));
let dormancy = Box::new(UTXOAggregate::try_from_fn(|id| {
});
let dormancy = UTXOAggregate::try_from_fn(|id| {
ColumnarRollingWindows::forced_import(
db,
&Self::aggregate_metric_name(id, "dormancy"),
Self::aggregate_version(aggregate_version, id),
indexes,
)
})?);
})?;
Ok(Self {
transfer_volume,
coindays_destroyed,
@@ -114,7 +110,7 @@ impl ActivityVecs {
}
fn aggregate_metric_name(id: UTXOAggregateId, metric: &str) -> String {
utxo_metric_name(
CohortContext::Utxo.metric_name(
id.select(&UTXO_AGGREGATE_FILTERS),
id.select(&UTXO_AGGREGATE_NAMES).id,
metric,
@@ -1,13 +1,11 @@
use brk_cohort::UTXOGroupsWithoutAmountOrType;
use brk_cohort::{CohortContext, UTXOGroupsWithoutAmountOrType};
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Cents, Sats, Version};
use vecdb::{AnyStoredVec, Database, Rw, StorageMode};
use crate::{
distribution::metrics::{
CumulativeUTXOValueColumnarMetricWithoutAmountOrType, UTXORows, utxo_metric_name,
},
distribution::metrics::{CumulativeUTXOValueColumnarMetricWithoutAmountOrType, UTXORows},
indexes,
internal::{CachedWindowStartVec, LazyValuePerBlockCumulativeRolling, Windows},
};
@@ -33,7 +31,7 @@ impl CoreCumulativeValueByCohort {
version,
)?;
let cohorts = UTXOGroupsWithoutAmountOrType::new(|filter, cohort_name| {
let name = utxo_metric_name(&filter, cohort_name, metric);
let name = CohortContext::Utxo.metric_name(&filter, cohort_name, metric);
let (sats, cents) = cumulative
.sources(&filter, &name, version)
.expect("supported core cumulative value cohort");
@@ -5,9 +5,7 @@ use brk_types::{Cents, Sats, Version};
use vecdb::{AnyStoredVec, Database, Rw, StorageMode};
use crate::{
distribution::metrics::{
ColumnarAmountValue, CumulativeUTXOValueColumnarMetric, UTXORows, utxo_metric_name,
},
distribution::metrics::{ColumnarAmountValue, CumulativeUTXOValueColumnarMetric, UTXORows},
indexes,
internal::{CachedWindowStartVec, LazyValuePerBlockCumulativeRolling, Windows},
};
@@ -34,7 +32,7 @@ impl CumulativeValueByCohort {
version,
)?;
let cohorts = UTXOGroups::new(|filter, cohort_name| {
let name = utxo_metric_name(&filter, cohort_name, metric);
let name = CohortContext::Utxo.metric_name(&filter, cohort_name, metric);
let (sats, cents) = cumulative
.sources(&filter, &name, version)
.expect("supported cumulative value cohort");
@@ -1,5 +1,6 @@
use brk_cohort::{
ByTerm, TermId, UTXO_AGGREGATE_FILTERS, UTXO_AGGREGATE_NAMES, UTXOAggregate, UTXOAggregateId,
ByTerm, CohortContext, TermId, UTXO_AGGREGATE_FILTERS, UTXO_AGGREGATE_NAMES, UTXOAggregate,
UTXOAggregateId,
};
use brk_error::Result;
use brk_traversable::Traversable;
@@ -15,8 +16,6 @@ use crate::{
internal::{ColumnarPerBlock, FiatType, LazyFiatPerBlock},
};
use super::super::utxo_metric_name;
#[derive(Deref, DerefMut, Traversable)]
pub struct AdditiveAggregateFiatPerBlock<C: FiatType, M: StorageMode = Rw> {
#[deref]
@@ -39,7 +38,7 @@ impl<C: FiatType> AdditiveAggregateFiatPerBlock<C> {
|source| {
let source = source.clone();
UTXOAggregate::from_fn(|aggregate| {
let name = utxo_metric_name(
let name = CohortContext::Utxo.metric_name(
aggregate.select(&UTXO_AGGREGATE_FILTERS),
aggregate.select(&UTXO_AGGREGATE_NAMES).id,
metric,
@@ -3,5 +3,3 @@ mod utxo_raw;
pub use aggregate::AdditiveAggregateFiatPerBlock;
pub(crate) use utxo_raw::AdditiveUTXORawVec;
use super::utxo_metric_name;
@@ -1,5 +1,6 @@
use brk_cohort::{
ByTerm, TermId, UTXO_AGGREGATE_FILTERS, UTXO_AGGREGATE_NAMES, UTXOAggregate, UTXOAggregateId,
ByTerm, CohortContext, TermId, UTXO_AGGREGATE_FILTERS, UTXO_AGGREGATE_NAMES, UTXOAggregate,
UTXOAggregateId,
};
use brk_error::Result;
use brk_traversable::Traversable;
@@ -18,8 +19,6 @@ use crate::{
},
};
use super::utxo_metric_name;
#[derive(Deref, DerefMut, Traversable)]
pub struct AdditiveAggregateFiatPerBlockCumulativeWithSums<C: FiatType, M: StorageMode = Rw> {
#[deref]
@@ -48,7 +47,7 @@ impl<C: FiatType> AdditiveAggregateFiatPerBlockCumulativeWithSums<C> {
|source| {
let source = source.clone();
UTXOAggregate::from_fn(|id| {
let name = utxo_metric_name(
let name = CohortContext::Utxo.metric_name(
id.select(&UTXO_AGGREGATE_FILTERS),
id.select(&UTXO_AGGREGATE_NAMES).id,
metric,
@@ -1,4 +1,6 @@
use brk_cohort::{UTXO_AGGREGATE_FILTERS, UTXO_AGGREGATE_NAMES, UTXOAggregate, UTXOAggregateId};
use brk_cohort::{
CohortContext, UTXO_AGGREGATE_FILTERS, UTXO_AGGREGATE_NAMES, UTXOAggregate, UTXOAggregateId,
};
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::Version;
@@ -12,8 +14,6 @@ use crate::{
internal::{ColumnarPerBlock, FiatType, LazyFiatPerBlock},
};
use super::utxo_metric_name;
#[derive(Deref, DerefMut, Traversable)]
pub struct AggregateFiatPerBlock<C: FiatType, M: StorageMode = Rw> {
#[deref]
@@ -35,7 +35,7 @@ impl<C: FiatType> AggregateFiatPerBlock<C> {
version,
|source| {
UTXOAggregate::from_fn(|id| {
let name = utxo_metric_name(
let name = CohortContext::Utxo.metric_name(
id.select(&UTXO_AGGREGATE_FILTERS),
id.select(&UTXO_AGGREGATE_NAMES).id,
metric,
@@ -7,5 +7,3 @@ pub use cumulative_fiat::AdditiveAggregateFiatPerBlockCumulativeWithSums;
pub use fiat::AggregateFiatPerBlock;
pub use percent::AggregatePercentPerBlock;
pub use price::AggregatePriceWithRatioPerBlock;
use super::utxo_metric_name;
@@ -1,4 +1,6 @@
use brk_cohort::{UTXO_AGGREGATE_FILTERS, UTXO_AGGREGATE_NAMES, UTXOAggregate, UTXOAggregateId};
use brk_cohort::{
CohortContext, UTXO_AGGREGATE_FILTERS, UTXO_AGGREGATE_NAMES, UTXOAggregate, UTXOAggregateId,
};
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Height, Version};
@@ -10,8 +12,6 @@ use crate::{
internal::{ColumnarPerBlock, FixedRatio, LazyColumnPercentPerBlock},
};
use super::utxo_metric_name;
#[derive(Deref, DerefMut, Traversable)]
pub struct AggregatePercentPerBlock<B: FixedRatio, M: StorageMode = Rw> {
#[deref]
@@ -38,7 +38,7 @@ impl<B: FixedRatio> AggregatePercentPerBlock<B> {
version,
|source| {
UTXOAggregate::from_fn(|id| {
let name = utxo_metric_name(
let name = CohortContext::Utxo.metric_name(
id.select(&UTXO_AGGREGATE_FILTERS),
id.select(&UTXO_AGGREGATE_NAMES).id,
metric,
@@ -1,4 +1,6 @@
use brk_cohort::{UTXO_AGGREGATE_FILTERS, UTXO_AGGREGATE_NAMES, UTXOAggregate, UTXOAggregateId};
use brk_cohort::{
CohortContext, UTXO_AGGREGATE_FILTERS, UTXO_AGGREGATE_NAMES, UTXOAggregate, UTXOAggregateId,
};
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Cents, Height, Version};
@@ -10,8 +12,6 @@ use crate::{
internal::{ColumnarPerBlock, LazyColumnPriceWithRatioPerBlock},
};
use super::utxo_metric_name;
#[derive(Deref, DerefMut, Traversable)]
pub struct AggregatePriceWithRatioPerBlock<M: StorageMode = Rw> {
#[deref]
@@ -39,7 +39,7 @@ impl AggregatePriceWithRatioPerBlock {
version,
|source| {
UTXOAggregate::from_fn(|id| {
let name = utxo_metric_name(
let name = CohortContext::Utxo.metric_name(
id.select(&UTXO_AGGREGATE_FILTERS),
id.select(&UTXO_AGGREGATE_NAMES).id,
metric,
@@ -67,8 +67,7 @@ where
cohort_name: &str,
metric: &str,
) -> String {
let cohort = context.full_name(filter, cohort_name);
format!("{cohort}_{metric}")
context.metric_name(filter, cohort_name, metric)
}
#[inline(always)]
@@ -37,12 +37,16 @@ impl<S: Clone> ColumnarAmountValue<S> {
)?;
let series = Amount::new(|filter, cohort_name| {
let amounts: Vec<_> = match AmountRangeId::matching(&filter) {
Some(amount) => vec![amount],
None => AmountRangeId::included_by(&filter).collect(),
};
let name = Self::metric_name(context, &filter, cohort_name, metric);
let (sats, cents) = values.sources(&format!("{name}_cumulative"), version, amounts);
let amounts = AmountRangeId::matching(&filter);
let (sats, cents) = match amounts {
Some(amount) => values.sources(&format!("{name}_cumulative"), version, [amount]),
None => values.sources(
&format!("{name}_cumulative"),
version,
AmountRangeId::included_by(&filter),
),
};
build(&name, sats, cents)
});
@@ -55,8 +59,7 @@ impl<S: Clone> ColumnarAmountValue<S> {
cohort_name: &str,
metric: &str,
) -> String {
let cohort = context.full_name(filter, cohort_name);
format!("{cohort}_{metric}")
context.metric_name(filter, cohort_name, metric)
}
#[inline(always)]
@@ -98,16 +98,15 @@ impl CumulativeUTXOValueColumnarMetric {
ReadableBoxedVec<Height, Sats>,
ReadableBoxedVec<Height, Cents>,
)> {
let columns: Vec<_> = UNDER_AMOUNT_FILTERS
let filter = UNDER_AMOUNT_FILTERS
.iter()
.chain(OVER_AMOUNT_FILTERS.iter())
.find(|candidate| *candidate == filter)
.map(|filter| AmountRangeId::included_by(filter).collect())?;
.find(|candidate| *candidate == filter)?;
Some(Self::matrix_sources(
&self.amount_range,
name,
version,
columns,
AmountRangeId::included_by(filter),
))
}
@@ -5,8 +5,6 @@ mod cumulative;
mod exact;
mod rows;
use brk_cohort::{CohortContext, Filter};
pub(crate) use additive::{
UTXOColumnarMetric, UTXOColumnarMetricWithoutAmount, UTXOColumnarMetricWithoutAmountOrType,
};
@@ -18,12 +16,3 @@ pub(crate) use cumulative::{
};
pub(crate) use exact::ExactUTXOColumnarMetric;
pub(crate) use rows::{UTXOAggregateRows, UTXORows};
pub(crate) fn utxo_metric_name(filter: &Filter, cohort_name: &str, metric: &str) -> String {
let cohort_name = CohortContext::Utxo.full_name(filter, cohort_name);
if cohort_name.is_empty() {
metric.to_owned()
} else {
format!("{cohort_name}_{metric}")
}
}
@@ -1,5 +1,6 @@
use brk_types::{Cents, PartsPerMillion32};
use crate::distribution::state::PercentileResult;
use crate::internal::PERCENTILES_LEN;
#[derive(Clone)]
@@ -10,3 +11,19 @@ pub(crate) struct CostBasisBlockData {
pub per_dollar: [Cents; PERCENTILES_LEN],
pub supply_density: PartsPerMillion32,
}
impl CostBasisBlockData {
#[inline(always)]
pub(crate) fn from_percentiles(
percentiles: PercentileResult,
supply_density: PartsPerMillion32,
) -> Self {
Self {
min: percentiles.min_price,
max: percentiles.max_price,
per_coin: percentiles.sat_prices,
per_dollar: percentiles.usd_prices,
supply_density,
}
}
}
@@ -1,11 +1,13 @@
use brk_cohort::{UTXO_AGGREGATE_FILTERS, UTXO_AGGREGATE_NAMES, UTXOAggregate, UTXOAggregateId};
use brk_cohort::{
CohortContext, UTXO_AGGREGATE_FILTERS, UTXO_AGGREGATE_NAMES, UTXOAggregate, UTXOAggregateId,
};
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Cents, PartsPerMillion32, Sats, Version};
use vecdb::{AnyStoredVec, AnyVec, Database, Rw, StorageMode};
use crate::{
distribution::{metrics::utxo_metric_name, state::UnrealizedState},
distribution::state::UnrealizedState,
indexes,
internal::{
ColumnarPerBlock, LazyColumnPerBlock, LazyColumnPercentPerBlock, PercentilesVecs, Price,
@@ -208,7 +210,7 @@ impl CostBasisVecs {
}
fn cohort_metric_name(id: UTXOAggregateId, metric: &str) -> String {
utxo_metric_name(
CohortContext::Utxo.metric_name(
id.select(&UTXO_AGGREGATE_FILTERS),
id.select(&UTXO_AGGREGATE_NAMES).id,
metric,
@@ -26,7 +26,7 @@ pub(crate) use columnar::{
CumulativeUTXOColumnarMetric, CumulativeUTXOColumnarMetricWithoutAmountOrType,
CumulativeUTXOValueColumnarMetric, CumulativeUTXOValueColumnarMetricWithoutAmountOrType,
ExactUTXOColumnarMetric, UTXOColumnarMetric, UTXOColumnarMetricWithoutAmount,
UTXOColumnarMetricWithoutAmountOrType, UTXORows, utxo_metric_name,
UTXOColumnarMetricWithoutAmountOrType, UTXORows,
};
pub(crate) use cost_basis::CostBasisBlockData;
pub use cost_basis::CostBasisVecs;
@@ -1,11 +1,11 @@
use brk_cohort::UTXOGroups;
use brk_cohort::{CohortContext, UTXOGroups};
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{StoredU64, Version};
use vecdb::{Database, Rw, StorageMode};
use crate::{
distribution::metrics::{CumulativeUTXOColumnarMetric, utxo_metric_name},
distribution::metrics::CumulativeUTXOColumnarMetric,
indexes,
internal::{CachedWindowStartVec, LazyPerBlockCumulativeRolling, Windows},
};
@@ -32,7 +32,7 @@ impl SpentOutputCount {
version,
)?;
let cohorts = UTXOGroups::new(|filter, cohort_name| {
let name = utxo_metric_name(&filter, cohort_name, "spent_utxo_count");
let name = CohortContext::Utxo.metric_name(&filter, cohort_name, "spent_utxo_count");
LazyPerBlockCumulativeRolling::from_boxed_cumulative_source(
&name,
version,
@@ -5,7 +5,7 @@ use brk_types::{PartsPerMillionSigned64, StoredI64, StoredU64, Version};
use vecdb::{Database, Rw, StorageMode};
use crate::{
distribution::metrics::{ColumnarAmount, UTXOColumnarMetric, utxo_metric_name},
distribution::metrics::{ColumnarAmount, UTXOColumnarMetric},
indexes,
internal::{CachedWindowStartVec, LazyPerBlockWithDeltas, Windows},
};
@@ -32,7 +32,7 @@ impl UnspentOutputCount {
) -> Result<Self> {
let matrices = UTXOColumnarMetric::forced_import(db, "utxo_count", version)?;
let cohorts = UTXOGroups::new(|filter, cohort_name| {
let name = utxo_metric_name(&filter, cohort_name, "utxo_count");
let name = CohortContext::Utxo.metric_name(&filter, cohort_name, "utxo_count");
LazyPerBlockWithDeltas::from_boxed_height_source(
&name,
version,
@@ -2,12 +2,12 @@ use std::path::Path;
use brk_cohort::{ByTerm, Filter, ProfitabilityRangeId, Term, UTXOAggregate};
use brk_error::Result;
use brk_types::{Cents, Date, PartsPerMillion32};
use brk_types::{Cents, Date};
use vecdb::ColumnId;
use crate::distribution::{
metrics::{CohortMetrics, CostBasisBlockData},
state::{PercentileResult, UTXOStates},
state::UTXOStates,
};
impl CohortMetrics {
@@ -31,9 +31,9 @@ impl CohortMetrics {
let fenwick = states.fenwick();
let (all_density, sth_density, lth_density) = fenwick.density(spot_price);
self.cost_basis.push(UTXOAggregate {
all: cost_basis_data(fenwick.percentiles_all(), all_density),
sth: cost_basis_data(fenwick.percentiles_sth(), sth_density),
lth: cost_basis_data(fenwick.percentiles_lth(), lth_density),
all: CostBasisBlockData::from_percentiles(fenwick.percentiles_all(), all_density),
sth: CostBasisBlockData::from_percentiles(fenwick.percentiles_sth(), sth_density),
lth: CostBasisBlockData::from_percentiles(fenwick.percentiles_lth(), lth_density),
});
let profitability = fenwick.profitability(spot_price);
@@ -50,17 +50,3 @@ impl CohortMetrics {
);
}
}
#[inline(always)]
fn cost_basis_data(
percentiles: PercentileResult,
supply_density: PartsPerMillion32,
) -> CostBasisBlockData {
CostBasisBlockData {
min: percentiles.min_price,
max: percentiles.max_price,
per_coin: percentiles.sat_prices,
per_dollar: percentiles.usd_prices,
supply_density,
}
}
@@ -1,134 +0,0 @@
use std::ops::Add;
use brk_cohort::{
ByTerm, ProfitabilityId, ProfitabilityRange, ProfitabilityRangeId, ProfitabilityRow,
};
use brk_types::{Bitcoin, Cents, Dollars, PartsPerMillionSigned32, Sats};
use vecdb::ColumnId;
pub(super) fn sum_terms<T>(rows: &ByTerm<ProfitabilityRange<T>>) -> ProfitabilityRange<T>
where
T: Add<Output = T> + Copy,
{
ProfitabilityRange::from_fn(|range| *range.select(&rows.short) + *range.select(&rows.long))
}
pub(super) fn unrealized_pnl_rows(
spot: Cents,
cap: &ByTerm<ProfitabilityRange<Dollars>>,
supply: &ByTerm<ProfitabilityRange<Sats>>,
) -> ByTerm<ProfitabilityRange<Dollars>> {
ByTerm {
short: unrealized_pnl_row(spot, &cap.short, &supply.short),
long: unrealized_pnl_row(spot, &cap.long, &supply.long),
}
}
fn unrealized_pnl_row(
spot: Cents,
cap: &ProfitabilityRange<Dollars>,
supply: &ProfitabilityRange<Sats>,
) -> ProfitabilityRange<Dollars> {
ProfitabilityRangeId::from_fn(|column| {
let market_value =
f64::from(Dollars::from(spot)) * f64::from(Bitcoin::from(*column.get(supply)));
let realized_cap = f64::from(*column.get(cap));
let pnl = if column.is_profit() {
market_value - realized_cap
} else {
realized_cap - market_value
}
.max(0.0);
Dollars::from(pnl)
})
}
pub(super) fn nupl_row(
spot: Cents,
cap: &ProfitabilityRange<Dollars>,
supply: &ProfitabilityRange<Sats>,
) -> ProfitabilityRow<PartsPerMillionSigned32> {
let cap = ProfitabilityRow::from_ranges(cap.clone());
let supply = ProfitabilityRow::from_ranges(supply.clone());
ProfitabilityId::from_fn(|column| {
let spot = spot.as_u128();
let supply = column.get(&supply).as_u128();
if spot == 0 || supply == 0 {
PartsPerMillionSigned32::ZERO
} else {
let realized_price =
Cents::from(*column.get(&cap)).as_u128() * Sats::ONE_BTC_U128 / supply;
PartsPerMillionSigned32::from((spot as f64 - realized_price as f64) / spot as f64)
}
})
}
#[cfg(test)]
mod tests {
use brk_cohort::{
ByTerm, PROFIT_COUNT, ProfitabilityId, ProfitabilityRangeId, ProfitabilityRow,
};
use brk_types::{Cents, Dollars, PartsPerMillionSigned32, Sats};
use vecdb::ColumnId;
use super::{nupl_row, sum_terms, unrealized_pnl_rows};
#[test]
fn expanded_thresholds_match_prefix_and_suffix_sums() {
let ranges = ProfitabilityRangeId::from_fn(|id| Sats::from(id.index() as u64 + 1));
let row = ProfitabilityRow::from_ranges(ranges.clone());
let sum = |values: &[Sats]| {
values
.iter()
.copied()
.fold(Sats::default(), |total, value| total + value)
};
let ranges: Vec<_> = ranges.iter().copied().collect();
for (threshold, &column) in ProfitabilityId::profit_ids().iter().enumerate() {
assert_eq!(
*column.get(&row),
sum(&ranges[..PROFIT_COUNT + 1 - threshold])
);
}
for (threshold, &column) in ProfitabilityId::loss_ids().iter().enumerate() {
assert_eq!(
*column.get(&row),
sum(&ranges[PROFIT_COUNT + 1 + threshold..])
);
}
}
#[test]
fn derived_rows_preserve_profit_and_loss_polarity() {
let supply = ProfitabilityRangeId::from_fn(|_| Sats::ONE_BTC);
let cap = ProfitabilityRangeId::from_fn(|column| {
Dollars::from(if column.is_profit() { 1.0 } else { 3.0 })
});
let spot = Cents::from(200_u64);
let cap = ByTerm {
short: cap.clone(),
long: cap.clone(),
};
let supply = ByTerm {
short: supply.clone(),
long: supply.clone(),
};
let pnl = unrealized_pnl_rows(spot, &cap, &supply);
let all_cap = sum_terms(&cap);
let all_supply = sum_terms(&supply);
let nupl = nupl_row(spot, &all_cap, &all_supply);
for column in ProfitabilityRangeId::ALL {
assert_eq!(*column.get(&pnl.short), Dollars::from(1.0));
assert_eq!(*column.get(&pnl.long), Dollars::from(1.0));
}
for column in ProfitabilityId::ALL {
assert_eq!(
*column.get(&nupl),
PartsPerMillionSigned32::from(if column.is_profit() { 0.5 } else { -0.5 })
);
}
}
}
@@ -1,5 +1,4 @@
mod column_id;
mod compute;
mod vecs;
pub use vecs::ProfitabilityVecs;
@@ -1,14 +1,15 @@
use std::ops::AddAssign;
use std::ops::{Add, AddAssign};
use brk_cohort::{
ByTerm, ProfitabilityId, ProfitabilityRange, ProfitabilityRow, UTXOAggregate, UTXOAggregateId,
ByTerm, ProfitabilityId, ProfitabilityRange, ProfitabilityRangeId, ProfitabilityRow,
UTXOAggregate, UTXOAggregateId,
};
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Cents, Dollars, Height, PartsPerMillionSigned32, Sats, Version};
use brk_types::{Bitcoin, Cents, Dollars, Height, PartsPerMillionSigned32, Sats, Version};
use vecdb::{
AnyStoredVec, AnyVec, CachedBoxedVec, Database, PcoVec, PcoVecValue, ReadOnlyColumnarVec,
ReadableBoxedVec, Rw, StorageMode,
AnyStoredVec, AnyVec, CachedBoxedVec, ColumnId, Database, PcoVec, PcoVecValue,
ReadOnlyColumnarVec, ReadableBoxedVec, Rw, StorageMode,
};
use crate::{
@@ -19,10 +20,7 @@ use crate::{
},
};
use super::{
TermProfitabilityRangeId,
compute::{nupl_row, sum_terms, unrealized_pnl_rows},
};
use super::TermProfitabilityRangeId;
const VERSION: Version = Version::new(7);
@@ -176,10 +174,10 @@ impl ProfitabilityVecs {
supply: ByTerm<ProfitabilityRange<Sats>>,
realized_cap: ByTerm<ProfitabilityRange<Dollars>>,
) {
let all_supply = sum_terms(&supply);
let all_realized_cap = sum_terms(&realized_cap);
let unrealized_pnl = unrealized_pnl_rows(spot, &realized_cap, &supply);
let nupl = nupl_row(spot, &all_realized_cap, &all_supply);
let all_supply = Self::sum_terms(&supply);
let all_realized_cap = Self::sum_terms(&realized_cap);
let unrealized_pnl = Self::unrealized_pnl_rows(spot, &realized_cap, &supply);
let nupl = Self::nupl_row(spot, &all_realized_cap, &all_supply);
self.supply.push(supply);
self.realized_cap.push(realized_cap);
@@ -195,4 +193,131 @@ impl ProfitabilityVecs {
self.nupl.stored_mut(),
]
}
fn sum_terms<T>(rows: &ByTerm<ProfitabilityRange<T>>) -> ProfitabilityRange<T>
where
T: Add<Output = T> + Copy,
{
ProfitabilityRange::from_fn(|range| *range.select(&rows.short) + *range.select(&rows.long))
}
fn unrealized_pnl_rows(
spot: Cents,
cap: &ByTerm<ProfitabilityRange<Dollars>>,
supply: &ByTerm<ProfitabilityRange<Sats>>,
) -> ByTerm<ProfitabilityRange<Dollars>> {
ByTerm {
short: Self::unrealized_pnl_row(spot, &cap.short, &supply.short),
long: Self::unrealized_pnl_row(spot, &cap.long, &supply.long),
}
}
fn unrealized_pnl_row(
spot: Cents,
cap: &ProfitabilityRange<Dollars>,
supply: &ProfitabilityRange<Sats>,
) -> ProfitabilityRange<Dollars> {
ProfitabilityRangeId::from_fn(|column| {
let market_value =
f64::from(Dollars::from(spot)) * f64::from(Bitcoin::from(*column.get(supply)));
let realized_cap = f64::from(*column.get(cap));
let pnl = if column.is_profit() {
market_value - realized_cap
} else {
realized_cap - market_value
}
.max(0.0);
Dollars::from(pnl)
})
}
fn nupl_row(
spot: Cents,
cap: &ProfitabilityRange<Dollars>,
supply: &ProfitabilityRange<Sats>,
) -> ProfitabilityRow<PartsPerMillionSigned32> {
let cap = ProfitabilityRow::from_ranges(cap.clone());
let supply = ProfitabilityRow::from_ranges(supply.clone());
ProfitabilityId::from_fn(|column| {
let spot = spot.as_u128();
let supply = column.get(&supply).as_u128();
if spot == 0 || supply == 0 {
PartsPerMillionSigned32::ZERO
} else {
let realized_price =
Cents::from(*column.get(&cap)).as_u128() * Sats::ONE_BTC_U128 / supply;
PartsPerMillionSigned32::from((spot as f64 - realized_price as f64) / spot as f64)
}
})
}
}
#[cfg(test)]
mod tests {
use brk_cohort::{
ByTerm, PROFIT_COUNT, ProfitabilityId, ProfitabilityRangeId, ProfitabilityRow,
};
use brk_types::{Cents, Dollars, PartsPerMillionSigned32, Sats};
use vecdb::ColumnId;
use super::ProfitabilityVecs;
#[test]
fn expanded_thresholds_match_prefix_and_suffix_sums() {
let ranges = ProfitabilityRangeId::from_fn(|id| Sats::from(id.index() as u64 + 1));
let row = ProfitabilityRow::from_ranges(ranges.clone());
let sum = |values: &[Sats]| {
values
.iter()
.copied()
.fold(Sats::default(), |total, value| total + value)
};
let ranges: Vec<_> = ranges.iter().copied().collect();
for (threshold, &column) in ProfitabilityId::profit_ids().iter().enumerate() {
assert_eq!(
*column.get(&row),
sum(&ranges[..PROFIT_COUNT + 1 - threshold])
);
}
for (threshold, &column) in ProfitabilityId::loss_ids().iter().enumerate() {
assert_eq!(
*column.get(&row),
sum(&ranges[PROFIT_COUNT + 1 + threshold..])
);
}
}
#[test]
fn derived_rows_preserve_profit_and_loss_polarity() {
let supply = ProfitabilityRangeId::from_fn(|_| Sats::ONE_BTC);
let cap = ProfitabilityRangeId::from_fn(|column| {
Dollars::from(if column.is_profit() { 1.0 } else { 3.0 })
});
let spot = Cents::from(200_u64);
let cap = ByTerm {
short: cap.clone(),
long: cap.clone(),
};
let supply = ByTerm {
short: supply.clone(),
long: supply.clone(),
};
let pnl = ProfitabilityVecs::unrealized_pnl_rows(spot, &cap, &supply);
let all_cap = ProfitabilityVecs::sum_terms(&cap);
let all_supply = ProfitabilityVecs::sum_terms(&supply);
let nupl = ProfitabilityVecs::nupl_row(spot, &all_cap, &all_supply);
for column in ProfitabilityRangeId::ALL {
assert_eq!(*column.get(&pnl.short), Dollars::from(1.0));
assert_eq!(*column.get(&pnl.long), Dollars::from(1.0));
}
for column in ProfitabilityId::ALL {
assert_eq!(
*column.get(&nupl),
PartsPerMillionSigned32::from(if column.is_profit() { 0.5 } else { -0.5 })
);
}
}
}
@@ -1,4 +1,6 @@
use brk_cohort::{Filter, TERM_NAMES, Term, UTXO_ALL_NAME, UTXOAllAndSth, UTXOAllAndSthId};
use brk_cohort::{
CohortContext, Filter, TERM_NAMES, Term, UTXO_ALL_NAME, UTXOAllAndSth, UTXOAllAndSthId,
};
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Cents, Height, StoredF64, Version};
@@ -7,7 +9,6 @@ use vecdb::{
};
use crate::{
distribution::metrics::utxo_metric_name,
indexes,
internal::{
CachedWindowStartVec, ColumnarPerBlockCumulativeRolling, ColumnarRollingWindows,
@@ -120,10 +121,14 @@ impl AdjustedSoprVecs {
fn cohort_metric_name(id: UTXOAllAndSthId, metric: &str) -> String {
match id {
UTXOAllAndSthId::All => utxo_metric_name(&Filter::All, UTXO_ALL_NAME.id, metric),
UTXOAllAndSthId::Sth => {
utxo_metric_name(&Filter::Term(Term::Sth), TERM_NAMES.short.id, metric)
UTXOAllAndSthId::All => {
CohortContext::Utxo.metric_name(&Filter::All, UTXO_ALL_NAME.id, metric)
}
UTXOAllAndSthId::Sth => CohortContext::Utxo.metric_name(
&Filter::Term(Term::Sth),
TERM_NAMES.short.id,
metric,
),
}
}
@@ -1,8 +1,8 @@
use brk_cohort::{
AGE_RANGE_FILTERS, AgeRange, AgeRangeId, ByEntry, ByEpoch, CLASS_FILTERS, Class, ClassId,
ENTRY_FILTERS, EPOCH_FILTERS, EntryId, EpochId, Filter, OVER_AGE_FILTERS, OverAge, OverAgeId,
Term, UNDER_AGE_FILTERS, UTXOAggregate, UTXOAggregateId, UTXOGroupsWithoutAmountOrType,
UnderAge, UnderAgeId,
CohortContext, ENTRY_FILTERS, EPOCH_FILTERS, EntryId, EpochId, Filter, OVER_AGE_FILTERS,
OverAge, OverAgeId, Term, UNDER_AGE_FILTERS, UTXOAggregate, UTXOAggregateId,
UTXOGroupsWithoutAmountOrType, UnderAge, UnderAgeId,
};
use brk_error::Result;
use brk_traversable::Traversable;
@@ -13,7 +13,6 @@ use vecdb::{
};
use crate::{
distribution::metrics::utxo_metric_name,
indexes,
internal::{ColumnarPerBlock, Identity, LazyColumnPerBlock, LazyPerBlock, RatioCents64},
};
@@ -121,7 +120,7 @@ impl Sopr24hVecs {
})?;
let cohorts = UTXOGroupsWithoutAmountOrType::new(|filter, cohort_name| {
let name = utxo_metric_name(&filter, cohort_name, "sopr_24h");
let name = CohortContext::Utxo.metric_name(&filter, cohort_name, "sopr_24h");
let version = Self::cohort_version(version, &filter);
match &filter {
Filter::All => {
@@ -1,11 +1,11 @@
use brk_cohort::UTXOGroups;
use brk_cohort::{CohortContext, UTXOGroups};
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Cents, CentsSigned, PartsPerMillionSigned64, Version};
use vecdb::{Database, Rw, StorageMode};
use crate::{
distribution::metrics::{UTXOColumnarMetric, utxo_metric_name},
distribution::metrics::UTXOColumnarMetric,
indexes,
internal::{CachedWindowStartVec, LazyFiatPerBlockWithDeltas, Windows},
};
@@ -28,7 +28,7 @@ impl RealizedCapByCohort {
) -> Result<Self> {
let matrices = UTXOColumnarMetric::forced_import(db, "realized_cap_cents", version)?;
let cohorts = UTXOGroups::new(|filter, cohort_name| {
let name = utxo_metric_name(&filter, cohort_name, "realized_cap");
let name = CohortContext::Utxo.metric_name(&filter, cohort_name, "realized_cap");
LazyFiatPerBlockWithDeltas::from_boxed_cents_source(
&name,
version,
@@ -19,7 +19,7 @@ use crate::{
metrics::{
AdditiveAggregateFiatPerBlockCumulativeWithSums, AdditiveUTXORawVec,
AggregatePercentPerBlock, AggregatePriceWithRatioPerBlock, ColumnarAmount,
RealizedBlockData, RealizedTotals, UTXORows, utxo_metric_name,
RealizedBlockData, RealizedTotals, UTXORows,
},
},
indexes,
@@ -222,14 +222,14 @@ impl RealizedVecs {
let mvrv = price.cohorts.map_named(|filter, cohort_name, price| {
LazyPerBlock::from_lazy::<Identity<StoredF32>, PartsPerMillion64>(
&utxo_metric_name(filter, cohort_name, "mvrv"),
&CohortContext::Utxo.metric_name(filter, cohort_name, "mvrv"),
Self::cohort_version(version, filter),
&price.ratio,
)
});
let negative_loss = UTXOGroupsWithoutAmountOrType::new(|filter, cohort_name| {
let loss = loss.cohorts.get(&filter).expect("realized-loss cohort");
let name = utxo_metric_name(&filter, cohort_name, "realized_loss_neg");
let name = CohortContext::Utxo.metric_name(&filter, cohort_name, "realized_loss_neg");
let version = Self::cohort_version(version, &filter) + Version::ONE;
let base = LazyVec::transformed::<NegCentsUnsignedToDollars>(
&name,
@@ -248,7 +248,7 @@ impl RealizedVecs {
});
let cap_to_own_mcap = UTXOAggregate::from_fn(|id| {
let filter = id.select(&UTXO_AGGREGATE_FILTERS);
let name = utxo_metric_name(
let name = CohortContext::Utxo.metric_name(
filter,
id.select(&UTXO_AGGREGATE_NAMES).id,
"realized_cap_to_own_mcap",
@@ -268,7 +268,7 @@ impl RealizedVecs {
});
let net_pnl_change_1m_to_mcap = UTXOAggregate::from_fn(|id| {
let filter = id.select(&UTXO_AGGREGATE_FILTERS);
let name = utxo_metric_name(
let name = CohortContext::Utxo.metric_name(
filter,
id.select(&UTXO_AGGREGATE_NAMES).id,
"net_pnl_change_1m_to_mcap",
@@ -343,7 +343,7 @@ impl RealizedVecs {
}
fn aggregate_metric_name(id: UTXOAggregateId, metric: &str) -> String {
utxo_metric_name(
CohortContext::Utxo.metric_name(
id.select(&UTXO_AGGREGATE_FILTERS),
id.select(&UTXO_AGGREGATE_NAMES).id,
metric,
@@ -1,11 +1,11 @@
use brk_cohort::UTXOGroups;
use brk_cohort::{CohortContext, UTXOGroups};
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Cents, Version};
use vecdb::{Database, Rw, StorageMode};
use crate::{
distribution::metrics::{CumulativeUTXOColumnarMetric, utxo_metric_name},
distribution::metrics::CumulativeUTXOColumnarMetric,
indexes,
internal::{CachedWindowStartVec, LazyFiatPerBlockCumulativeWithSums, Windows},
};
@@ -32,7 +32,7 @@ impl CumulativeRealizedByCohort {
version,
)?;
let cohorts = UTXOGroups::new(|filter, cohort_name| {
let name = utxo_metric_name(&filter, cohort_name, metric);
let name = CohortContext::Utxo.metric_name(&filter, cohort_name, metric);
let source = cumulative
.matrices
.additive_source(&filter, &format!("{name}_cumulative_cents"), version)
@@ -1,11 +1,11 @@
use brk_cohort::UTXOGroupsWithoutAmountOrType;
use brk_cohort::{CohortContext, UTXOGroupsWithoutAmountOrType};
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{CentsSigned, PartsPerMillionSigned64, Version};
use vecdb::{Database, Rw, StorageMode};
use crate::{
distribution::metrics::{CumulativeUTXOColumnarMetricWithoutAmountOrType, utxo_metric_name},
distribution::metrics::CumulativeUTXOColumnarMetricWithoutAmountOrType,
indexes,
internal::{CachedWindowStartVec, LazyFiatPerBlockCumulativeWithSumsAndDeltas, Windows},
};
@@ -38,7 +38,7 @@ impl CumulativeNetRealizedByCohort {
version,
)?;
let cohorts = UTXOGroupsWithoutAmountOrType::new(|filter, cohort_name| {
let name = utxo_metric_name(&filter, cohort_name, "net_realized_pnl");
let name = CohortContext::Utxo.metric_name(&filter, cohort_name, "net_realized_pnl");
let source = cumulative
.matrices
.additive_source(&filter, &format!("{name}_cumulative_cents"), version)
@@ -1,13 +1,11 @@
use brk_cohort::UTXOGroups;
use brk_cohort::{CohortContext, UTXOGroups};
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Cents, Height, Version};
use vecdb::{CachedBoxedVec, Database, Rw, StorageMode};
use crate::{
distribution::metrics::{ExactUTXOColumnarMetric, utxo_metric_name},
indexes,
internal::LazyPriceWithRatioPerBlock,
distribution::metrics::ExactUTXOColumnarMetric, indexes, internal::LazyPriceWithRatioPerBlock,
};
#[derive(Traversable)]
@@ -28,7 +26,7 @@ impl RealizedPriceByCohort {
let version = version + Version::ONE;
let matrices = ExactUTXOColumnarMetric::forced_import(db, "realized_price_cents", version)?;
let cohorts = UTXOGroups::new(|filter, cohort_name| {
let name = utxo_metric_name(&filter, cohort_name, "realized_price");
let name = CohortContext::Utxo.metric_name(&filter, cohort_name, "realized_price");
LazyPriceWithRatioPerBlock::from_boxed_height_source(
&name,
version,
@@ -1,11 +1,11 @@
use brk_cohort::UTXOGroupsWithoutAmountOrType;
use brk_cohort::{CohortContext, UTXOGroupsWithoutAmountOrType};
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Cents, Version};
use vecdb::{Database, Rw, StorageMode};
use crate::{
distribution::metrics::{CumulativeUTXOColumnarMetricWithoutAmountOrType, utxo_metric_name},
distribution::metrics::CumulativeUTXOColumnarMetricWithoutAmountOrType,
indexes,
internal::{CachedWindowStartVec, LazyFiatPerBlockCumulativeWithSums, Windows},
};
@@ -32,7 +32,7 @@ impl CumulativeValueDestroyedByCohort {
version,
)?;
let cohorts = UTXOGroupsWithoutAmountOrType::new(|filter, cohort_name| {
let name = utxo_metric_name(&filter, cohort_name, metric);
let name = CohortContext::Utxo.metric_name(&filter, cohort_name, metric);
let source = cumulative
.matrices
.additive_source(&filter, &format!("{name}_cumulative_cents"), version)
@@ -1,4 +1,6 @@
use brk_cohort::{UTXO_AGGREGATE_FILTERS, UTXO_AGGREGATE_NAMES, UTXOAggregate, UTXOAggregateId};
use brk_cohort::{
CohortContext, UTXO_AGGREGATE_FILTERS, UTXO_AGGREGATE_NAMES, UTXOAggregate, UTXOAggregateId,
};
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Dollars, Height, PartsPerMillion32, PartsPerMillionSigned32, Version};
@@ -8,7 +10,6 @@ use vecdb::{
};
use crate::{
distribution::metrics::utxo_metric_name,
indexes,
internal::{ColumnarPerBlock, FixedRatio, LazyPercentPerBlock},
};
@@ -82,7 +83,7 @@ impl GrossPnlComposition {
indexes: &indexes::Vecs,
) -> UTXOAggregate<LazyPercentPerBlock<B>> {
UTXOAggregate::from_fn(|id| {
let name = utxo_metric_name(
let name = CohortContext::Utxo.metric_name(
id.select(&UTXO_AGGREGATE_FILTERS),
id.select(&UTXO_AGGREGATE_NAMES).id,
metric,
@@ -1,4 +1,6 @@
use brk_cohort::{UTXO_AGGREGATE_FILTERS, UTXO_AGGREGATE_NAMES, UTXOAggregate, UTXOAggregateId};
use brk_cohort::{
CohortContext, UTXO_AGGREGATE_FILTERS, UTXO_AGGREGATE_NAMES, UTXOAggregate, UTXOAggregateId,
};
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Height, PartsPerMillion32, Sats, Version};
@@ -8,7 +10,6 @@ use vecdb::{
};
use crate::{
distribution::metrics::utxo_metric_name,
indexes,
internal::{ColumnarPerBlock, LazyPercentPerBlock, RatioSats},
};
@@ -71,7 +72,7 @@ impl SupplyProfitabilityShares {
indexes: &indexes::Vecs,
) -> UTXOAggregate<LazyPercentPerBlock<PartsPerMillion32>> {
UTXOAggregate::from_fn(|id| {
let name = utxo_metric_name(
let name = CohortContext::Utxo.metric_name(
id.select(&UTXO_AGGREGATE_FILTERS),
id.select(&UTXO_AGGREGATE_NAMES).id,
metric,
@@ -1,6 +1,6 @@
use brk_cohort::{
ByTerm, TERM_FILTERS, TERM_NAMES, TermId, UTXO_AGGREGATE_FILTERS, UTXO_AGGREGATE_NAMES,
UTXOAggregate, UTXOAggregateId,
ByTerm, CohortContext, TERM_FILTERS, TERM_NAMES, TermId, UTXO_AGGREGATE_FILTERS,
UTXO_AGGREGATE_NAMES, UTXOAggregate, UTXOAggregateId,
};
use brk_error::Result;
use brk_traversable::Traversable;
@@ -8,10 +8,7 @@ use brk_types::{Cents, Height, PartsPerMillion32, PartsPerMillionSigned32, Versi
use vecdb::{AnyStoredVec, BinaryTransform, Database, Exit, Rw, StorageMode};
use crate::{
distribution::{
AllChainCache,
metrics::{AggregatePercentPerBlock, utxo_metric_name},
},
distribution::{AllChainCache, metrics::AggregatePercentPerBlock},
indexes,
internal::{
ColumnarPerBlock, LazyColumnPercentPerBlock, LazyPercentPerBlock, RatioCents, RatioDollars,
@@ -162,15 +159,18 @@ impl RelativeVecs {
> {
ColumnarPerBlock::forced_import(db, &format!("{metric}_ppm_by_term"), version, |source| {
ByTerm::from_fn(|id| {
let name =
utxo_metric_name(id.select(&TERM_FILTERS), id.select(&TERM_NAMES).id, metric);
let name = CohortContext::Utxo.metric_name(
id.select(&TERM_FILTERS),
id.select(&TERM_NAMES).id,
metric,
);
LazyColumnPercentPerBlock::new(&name, version, source, id, indexes)
})
})
}
fn aggregate_metric_name(id: UTXOAggregateId, metric: &str) -> String {
utxo_metric_name(
CohortContext::Utxo.metric_name(
id.select(&UTXO_AGGREGATE_FILTERS),
id.select(&UTXO_AGGREGATE_NAMES).id,
metric,
@@ -1,11 +1,11 @@
use brk_cohort::{Filter, UTXOGroupsWithoutAmount};
use brk_cohort::{CohortContext, Filter, UTXOGroupsWithoutAmount};
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Cents, Height, Sats, Version};
use vecdb::{AnyStoredVec, CachedBoxedVec, Database, Rw, StorageMode};
use crate::{
distribution::metrics::{UTXOColumnarMetricWithoutAmount, UTXORows, utxo_metric_name},
distribution::metrics::{UTXOColumnarMetricWithoutAmount, UTXORows},
indexes,
internal::LazySpotValuePerBlock,
};
@@ -29,7 +29,7 @@ impl SupplyByCohort {
let matrices =
UTXOColumnarMetricWithoutAmount::forced_import(db, &format!("{metric}_sats"), version)?;
let cohorts = UTXOGroupsWithoutAmount::new(|filter, cohort_name| {
let name = utxo_metric_name(&filter, cohort_name, metric);
let name = CohortContext::Utxo.metric_name(&filter, cohort_name, metric);
let source = matrices
.additive_source(&filter, &format!("{name}_sats"), version)
.expect("supported supply cohort");
@@ -5,7 +5,7 @@ use brk_types::{Cents, Height, Sats, Version};
use vecdb::{AnyStoredVec, CachedBoxedVec, Database, Rw, StorageMode};
use crate::{
distribution::metrics::{ColumnarAmount, UTXOColumnarMetric, UTXORows, utxo_metric_name},
distribution::metrics::{ColumnarAmount, UTXOColumnarMetric, UTXORows},
indexes,
internal::LazySpotValuePerBlock,
};
@@ -30,7 +30,7 @@ impl SupplyTotal {
) -> Result<(Self, AllSupplyCache)> {
let matrices = UTXOColumnarMetric::forced_import(db, "supply_sats", version)?;
let cohorts = UTXOGroups::new(|filter, cohort_name| {
let name = utxo_metric_name(&filter, cohort_name, "supply");
let name = CohortContext::Utxo.metric_name(&filter, cohort_name, "supply");
LazySpotValuePerBlock::from_boxed_sats_source(
&name,
version,
@@ -1,13 +1,13 @@
use std::ops::AddAssign;
use brk_cohort::UTXOGroupsWithoutAmount;
use brk_cohort::{CohortContext, UTXOGroupsWithoutAmount};
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::Version;
use vecdb::{Database, PcoVecValue, Rw, StorageMode};
use crate::{
distribution::metrics::{UTXOColumnarMetricWithoutAmount, utxo_metric_name},
distribution::metrics::UTXOColumnarMetricWithoutAmount,
indexes,
internal::{FiatType, LazyFiatPerBlock},
};
@@ -39,7 +39,7 @@ where
version,
)?;
let cohorts = UTXOGroupsWithoutAmount::new(|filter, cohort_name| {
let name = utxo_metric_name(&filter, cohort_name, metric);
let name = CohortContext::Utxo.metric_name(&filter, cohort_name, metric);
let source = matrices
.additive_source(&filter, &format!("{name}_cents"), version)
.expect("supported unrealized cohort");
@@ -1,4 +1,4 @@
use brk_cohort::{Filter, UTXOAggregate, UTXOGroups, UTXOGroupsWithoutAmount};
use brk_cohort::{CohortContext, Filter, UTXOAggregate, UTXOGroups, UTXOGroupsWithoutAmount};
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{
@@ -11,7 +11,6 @@ use crate::{
distribution::{
metrics::{
AdditiveAggregateFiatPerBlock, AdditiveUTXORawVec, AggregateFiatPerBlock, UTXORows,
utxo_metric_name,
},
state::UnrealizedState,
},
@@ -95,13 +94,13 @@ impl UnrealizedVecs {
AggregateFiatPerBlock::forced_import(db, "net_sentiment", aggregate_version, indexes)?;
let nupl = realized_price.map_named(|filter, cohort_name, price| {
LazyRatioPerBlock::from_lazy_source::<MvrvToNupl, PartsPerMillion64>(
&utxo_metric_name(filter, cohort_name, "nupl"),
&CohortContext::Utxo.metric_name(filter, cohort_name, "nupl"),
Self::cohort_version(version, filter) + Version::new(5),
&price.ppm,
)
});
let negative_loss = UTXOGroupsWithoutAmount::new(|filter, cohort_name| {
let name = utxo_metric_name(&filter, cohort_name, "unrealized_loss_neg");
let name = CohortContext::Utxo.metric_name(&filter, cohort_name, "unrealized_loss_neg");
LazyPerBlock::from_lazy::<NegCentsUnsignedToDollars, Cents>(
&name,
Self::cohort_version(version, &filter),
@@ -1,12 +1,11 @@
use brk_cohort::UTXOGroupsWithoutAmountOrType;
use brk_cohort::{CohortContext, UTXOGroupsWithoutAmountOrType};
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{CentsSigned, Version};
use vecdb::{Database, Rw, StorageMode};
use crate::{
distribution::metrics::{UTXOColumnarMetricWithoutAmountOrType, utxo_metric_name},
indexes,
distribution::metrics::UTXOColumnarMetricWithoutAmountOrType, indexes,
internal::LazyFiatPerBlock,
};
@@ -31,7 +30,7 @@ impl NetUnrealizedByCohort {
version,
)?;
let cohorts = UTXOGroupsWithoutAmountOrType::new(|filter, cohort_name| {
let name = utxo_metric_name(&filter, cohort_name, metric);
let name = CohortContext::Utxo.metric_name(&filter, cohort_name, metric);
let source = matrices
.additive_source(&filter, &format!("{name}_cents"), version)
.expect("supported net unrealized cohort");
@@ -30,7 +30,7 @@ const TREE_SIZE: usize = TIER0_COUNT + TIER1_COUNT + OVERFLOW; // 190,001
/// Fenwick tree node for combined cost basis tracking.
#[derive(Clone, Copy, Default)]
pub(super) struct CostBasisNode {
struct CostBasisNode {
all_sats: i64,
sth_sats: i64,
all_usd: i128,
@@ -0,0 +1,68 @@
use brk_cohort::{AgeRange, AgeRangeId};
use vecdb::ColumnId;
use super::{HOURS_PER_DAY, MINIMUM_DURATION_DAYS};
#[derive(Clone, Copy, Debug)]
pub(crate) struct AgeBand {
pub(crate) lower: f64,
pub(crate) upper: f64,
}
impl AgeBand {
pub(crate) fn all() -> AgeRange<Self> {
AgeRange::from_fn(|id| {
let bound = id.bounds();
Self {
lower: bound.start as f64 / HOURS_PER_DAY,
upper: if id == AgeRangeId::Over15Y {
f64::INFINITY
} else {
bound.end as f64 / HOURS_PER_DAY
},
}
})
}
#[inline]
pub(crate) fn mobility(exposure: f64) -> f64 {
if exposure.is_nan() || exposure <= 0.0 {
0.0
} else {
(-(-exposure).exp_m1()).min(1.0 - 1e-12)
}
}
pub(crate) fn horizon_mobility(
hazards: &AgeRange<f64>,
start_band: AgeRangeId,
horizon: f64,
bounds: &AgeRange<Self>,
) -> f64 {
let start = *start_band.select(bounds);
let mut age = if start.upper.is_finite() {
(start.lower + start.upper) / 2.0
} else {
start.lower
};
let mut remaining = horizon;
let mut exposure = 0.0;
for &band in &AgeRangeId::ALL[start_band.index()..] {
if remaining <= 0.0 {
break;
}
let bound = *band.select(bounds);
let duration = if bound.upper.is_finite() {
remaining.min((bound.upper - age).max(MINIMUM_DURATION_DAYS))
} else {
remaining
};
exposure += band.select(hazards).max(0.0) * duration;
remaining -= duration;
age = bound.upper;
}
Self::mobility(exposure)
}
}
@@ -8,7 +8,6 @@ use brk_cohort::{AgeRange, AgeRangeId, ByTerm, TERM_FILTERS, UTXOAggregate};
use super::super::cointime;
use super::{
AGE_COHORT_COUNT, AgeBand, AggregateSources, HorizonId, Horizons, MINIMUM_DURATION_DAYS, Vecs,
age_bounds_days, horizon_mobility, mobility,
};
use crate::{
distribution,
@@ -195,7 +194,7 @@ impl Vecs {
let genesis_timestamp = timestamps
.collect_one(Height::ZERO)
.unwrap_or(Timestamp::ZERO);
let bounds = age_bounds_days();
let bounds = AgeBand::all();
let mut chunk_start = start;
while chunk_start < source_end {
let chunk_end = (chunk_start + WRITE_INTERVAL).min(source_end);
@@ -219,7 +218,7 @@ impl Vecs {
for offset in 0..(chunk_end - chunk_start) {
let hazards = AgeRange::from_fn(|id| {
spending_rate(
Self::spending_rate(
id.select(&transfer_batches)[offset],
*id.get(&coinday_batch[offset]),
)
@@ -228,10 +227,12 @@ impl Vecs {
.difference_in_days_between_float(genesis_timestamp)
.max(MINIMUM_DURATION_DAYS);
let exposures = DecayFit::exposures(&hazards, network_age, &bounds);
let mobilities = AgeRange::from_fn(|id| mobility(*id.select(&exposures)));
let mobilities = AgeRange::from_fn(|id| AgeBand::mobility(*id.select(&exposures)));
let horizon_mobilities: Horizons<AgeRange<f64>> = HorizonId::from_fn(|horizon| {
let horizon = horizon.days();
AgeRange::from_fn(|age| horizon_mobility(&hazards, age, horizon, &bounds))
AgeRange::from_fn(|age| {
AgeBand::horizon_mobility(&hazards, age, horizon, &bounds)
})
});
self.age_range.spending_rate.push(AgeRangeId::from_fn(|id| {
StoredF64::from(*id.select(&hazards))
@@ -305,6 +306,16 @@ impl Vecs {
.into_iter()
.chain(self.aggregate_sources.primary_vecs_mut())
}
#[inline]
fn spending_rate(transfer_volume: Sats, coindays_created: StoredF64) -> f64 {
let exposure = f64::from(coindays_created);
if exposure > 0.0 {
(f64::from(Bitcoin::from(transfer_volume)) / exposure).max(0.0)
} else {
0.0
}
}
}
impl AggregateSources {
@@ -364,16 +375,6 @@ impl AggregateSources {
}
}
#[inline]
fn spending_rate(transfer_volume: Sats, coindays_created: StoredF64) -> f64 {
let exposure = f64::from(coindays_created);
if exposure > 0.0 {
(f64::from(Bitcoin::from(transfer_volume)) / exposure).max(0.0)
} else {
0.0
}
}
impl DecayFit {
fn fit(hazards: &AgeRange<f64>, network_age: f64, bounds: &AgeRange<AgeBand>) -> Option<Self> {
let mut total_duration = 0.0;
@@ -511,26 +512,27 @@ mod tests {
#[test]
fn mobility_is_the_complement_of_survival() {
assert_eq!(mobility(0.0), 0.0);
assert!((mobility(2.0_f64.ln()) - 0.5).abs() < 1e-12);
assert!((mobility(1e-15) - 1e-15).abs() < 1e-27);
assert!(mobility(1_000.0) < 1.0);
assert_eq!(mobility(f64::INFINITY), 1.0 - 1e-12);
assert_eq!(mobility(f64::NAN), 0.0);
assert_eq!(AgeBand::mobility(0.0), 0.0);
assert!((AgeBand::mobility(2.0_f64.ln()) - 0.5).abs() < 1e-12);
assert!((AgeBand::mobility(1e-15) - 1e-15).abs() < 1e-27);
assert!(AgeBand::mobility(1_000.0) < 1.0);
assert_eq!(AgeBand::mobility(f64::INFINITY), 1.0 - 1e-12);
assert_eq!(AgeBand::mobility(f64::NAN), 0.0);
}
#[test]
fn fixed_horizon_compounds_hazards_across_age_ranges() {
let bounds = age_bounds_days();
let bounds = AgeBand::all();
let hazards = AgeRange::from_fn(|_| 0.01);
let probability = horizon_mobility(&hazards, AgeRangeId::From1DTo1W, 30.0, &bounds);
let probability =
AgeBand::horizon_mobility(&hazards, AgeRangeId::From1DTo1W, 30.0, &bounds);
assert!((probability - mobility(0.3)).abs() < 1e-12);
assert!((probability - AgeBand::mobility(0.3)).abs() < 1e-12);
}
#[test]
fn decay_fit_recovers_an_exponential_lifetime() {
let bounds = age_bounds_days();
let bounds = AgeBand::all();
let expected_tau = 1_000.0;
let hazards = AgeRange::from_fn(|id| {
let band = *id.select(&bounds);
@@ -549,7 +551,7 @@ mod tests {
#[test]
fn oldest_cohort_exposure_is_its_observed_tail_lifetime() {
let bounds = age_bounds_days();
let bounds = AgeBand::all();
let hazards = AgeRange::from_fn(|id| {
let band = *id.select(&bounds);
let age = if band.upper.is_finite() {
@@ -10,8 +10,8 @@ use vecdb::{
};
use super::{
AgeRangeVecs, AggregateSources, AggregateVecs, HorizonId, HorizonVecs, Mobility, MobilityId,
SpendingExposureSeries, Vecs, mobility,
AgeBand, AgeRangeVecs, AggregateSources, AggregateVecs, HorizonId, HorizonVecs, Mobility,
MobilityId, SpendingExposureSeries, Vecs,
};
use crate::{
indexes,
@@ -28,7 +28,7 @@ struct ExposureToMobility;
impl UnaryTransform<StoredF64, StoredF64> for ExposureToMobility {
#[inline(always)]
fn apply(exposure: StoredF64) -> StoredF64 {
StoredF64::from(mobility(*exposure))
StoredF64::from(AgeBand::mobility(*exposure))
}
}
@@ -1,11 +1,10 @@
mod age_band;
mod compute;
mod horizon;
mod import;
mod vecs;
use brk_cohort::{AgeRange, AgeRangeId};
use vecdb::ColumnId;
pub(crate) use age_band::AgeBand;
pub(crate) use brk_cohort::AGE_RANGE_COUNT as AGE_COHORT_COUNT;
pub use horizon::{HorizonId, Horizons};
pub use vecs::{
@@ -16,66 +15,3 @@ pub use vecs::{
pub(crate) const HORIZON_COUNT: usize = HorizonId::ALL.len();
pub(crate) const HOURS_PER_DAY: f64 = 24.0;
pub(crate) const MINIMUM_DURATION_DAYS: f64 = 1.0 / HOURS_PER_DAY;
#[derive(Clone, Copy, Debug)]
pub(crate) struct AgeBand {
pub lower: f64,
pub upper: f64,
}
pub(crate) fn age_bounds_days() -> AgeRange<AgeBand> {
AgeRange::from_fn(|id| {
let bound = id.bounds();
AgeBand {
lower: bound.start as f64 / HOURS_PER_DAY,
upper: if id == AgeRangeId::Over15Y {
f64::INFINITY
} else {
bound.end as f64 / HOURS_PER_DAY
},
}
})
}
#[inline]
pub(crate) fn mobility(exposure: f64) -> f64 {
if exposure.is_nan() || exposure <= 0.0 {
0.0
} else {
(-(-exposure).exp_m1()).min(1.0 - 1e-12)
}
}
pub(crate) fn horizon_mobility(
hazards: &AgeRange<f64>,
start_band: AgeRangeId,
horizon: f64,
bounds: &AgeRange<AgeBand>,
) -> f64 {
let start = *start_band.select(bounds);
let mut age = if start.upper.is_finite() {
(start.lower + start.upper) / 2.0
} else {
start.lower
};
let mut remaining = horizon;
let mut exposure = 0.0;
for &band in &AgeRangeId::ALL[start_band.index()..] {
if remaining <= 0.0 {
break;
}
let bound = *band.select(bounds);
let upper = bound.upper;
let duration = if upper.is_finite() {
remaining.min((upper - age).max(MINIMUM_DURATION_DAYS))
} else {
remaining
};
exposure += band.select(hazards).max(0.0) * duration;
remaining -= duration;
age = upper;
}
mobility(exposure)
}
@@ -1,6 +1,6 @@
use brk_error::Result;
use brk_indexer::Indexer;
use brk_types::{Bitcoin, StoredF64};
use brk_types::{Bitcoin, Height, StoredF64};
use vecdb::Exit;
use super::Vecs;
@@ -10,7 +10,7 @@ use crate::{
};
pub(crate) fn compute_rest(
starting_height: brk_types::Height,
starting_height: Height,
created: &PerBlockCumulativeRolling<StoredF64>,
consumed: &PerBlockCumulativeRolling<StoredF64>,
stored: &mut PerBlockCumulativeRolling<StoredF64>,
@@ -5,7 +5,7 @@ use brk_error::Result;
use brk_types::{Cents, Height, StoredF64, Version};
use vecdb::{
CachedBoxedVec, ColumnId, Database, ImportableVec, PcoVec, PcoVecValue, ReadOnlyClone,
ReadableBoxedVec, ReadableCloneableVec, ReadableColumnarVec,
ReadOnlyColumnarVec, ReadableBoxedVec, ReadableCloneableVec, ReadableColumnarVec,
};
use super::{AwakeVecs, CohortVecs, DormantVecs, Sources, Vecs};
@@ -49,7 +49,7 @@ impl Sources {
}
fn additive_source<T>(
source: &vecdb::ReadOnlyColumnarVec<PcoVec<Height, T>, TermId>,
source: &ReadOnlyColumnarVec<PcoVec<Height, T>, TermId>,
name: &str,
version: Version,
aggregate: UTXOAggregateId,
@@ -1,6 +1,6 @@
use brk_error::Result;
use brk_types::{Bitcoin, Cents, Height, Version};
use vecdb::{CachedBoxedVec, Database};
use vecdb::{CachedBoxedVec, Database, ReadableCloneableVec};
use super::Vecs;
use crate::{
@@ -16,7 +16,7 @@ impl Vecs {
indexes: &indexes::Vecs,
spot_price: &CachedBoxedVec<Height, Cents>,
all_chain: &AllChainCache,
cointime_cap: &(impl vecdb::ReadableCloneableVec<Height, Cents> + 'static),
cointime_cap: &(impl ReadableCloneableVec<Height, Cents> + 'static),
) -> Result<Self> {
macro_rules! import {
($name:expr) => {
+3 -3
View File
@@ -1,7 +1,7 @@
use brk_traversable::Traversable;
use brk_types::{
Day1, Day3, Epoch, Halving, Height, Hour1, Hour4, Hour12, Minute10, Minute30, Month1, Month3,
Month6, StoredU64, Timestamp, Version, Week1, Year1, Year10,
Date, Day1, Day3, Epoch, Halving, Height, Hour1, Hour4, Hour12, Minute10, Minute30, Month1,
Month3, Month6, StoredU64, Timestamp, Version, Week1, Year1, Year10,
};
use vecdb::{CachedBoxedVec, CachedReadableVec, CachedVec, LazyVec, ReadableBoxedVec, VecValue};
@@ -94,7 +94,7 @@ impl Vecs {
}
pub(crate) fn day1_from_timestamp(timestamp: Timestamp) -> Day1 {
Day1::try_from(brk_types::Date::from(timestamp)).unwrap()
Day1::try_from(Date::from(timestamp)).unwrap()
}
pub(crate) fn month1_from_timestamp(timestamp: Timestamp) -> Month1 {
+3 -3
View File
@@ -1,6 +1,6 @@
use brk_indexer::Indexer;
use brk_traversable::Traversable;
use brk_types::{TxIndex, Txid, Version};
use brk_types::{TxInIndex, TxIndex, TxOutIndex, Txid, Version};
use vecdb::{LazyVec, ReadableCloneableVec};
use crate::internal::LazyIndexCountVec;
@@ -8,8 +8,8 @@ use crate::internal::LazyIndexCountVec;
#[derive(Clone, Traversable)]
pub struct Vecs {
pub identity: LazyVec<TxIndex, TxIndex, TxIndex, Txid>,
pub input_count: LazyIndexCountVec<TxIndex, brk_types::TxInIndex>,
pub output_count: LazyIndexCountVec<TxIndex, brk_types::TxOutIndex>,
pub input_count: LazyIndexCountVec<TxIndex, TxInIndex>,
pub output_count: LazyIndexCountVec<TxIndex, TxOutIndex>,
}
impl Vecs {
@@ -1,3 +1,6 @@
use std::result::Result as StdResult;
use brk_error::Result;
use brk_traversable::Traversable;
#[derive(Clone, Traversable)]
@@ -15,9 +18,7 @@ impl<A> DistributionStats<A> {
pub const SUFFIXES: [&'static str; 7] =
["min", "max", "pct10", "pct25", "median", "pct75", "pct90"];
pub fn try_from_fn<E>(
mut f: impl FnMut(&str) -> std::result::Result<A, E>,
) -> std::result::Result<Self, E> {
pub fn try_from_fn<E>(mut f: impl FnMut(&str) -> StdResult<A, E>) -> StdResult<Self, E> {
Ok(Self {
min: f(Self::SUFFIXES[0])?,
max: f(Self::SUFFIXES[1])?,
@@ -30,10 +31,7 @@ impl<A> DistributionStats<A> {
}
/// Apply a fallible operation to each of the 7 fields.
pub fn try_for_each_mut(
&mut self,
mut f: impl FnMut(&mut A) -> brk_error::Result<()>,
) -> brk_error::Result<()> {
pub fn try_for_each_mut(&mut self, mut f: impl FnMut(&mut A) -> Result<()>) -> Result<()> {
f(&mut self.min)?;
f(&mut self.max)?;
f(&mut self.pct10)?;
@@ -2,7 +2,7 @@ use std::collections::VecDeque;
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Height, VSize, get_percentile, get_weighted_percentile};
use brk_types::{Height, StoredU64, VSize, get_percentile, get_weighted_percentile};
use derive_more::{Deref, DerefMut};
use schemars::JsonSchema;
use vecdb::{
@@ -38,12 +38,12 @@ impl<T: NumericValue + JsonSchema> PerBlockDistribution<T> {
max_from: Height,
source: &impl ReadableVec<A, T>,
first_indexes: &impl ReadableVec<Height, A>,
count_indexes: &impl ReadableVec<Height, brk_types::StoredU64>,
count_indexes: &impl ReadableVec<Height, StoredU64>,
exit: &Exit,
skip_count: usize,
) -> Result<()>
where
A: VecIndex + VecValue + brk_types::CheckedSub<A>,
A: VecIndex + VecValue + CheckedSub<A>,
{
let DistributionStats {
min,
@@ -95,8 +95,7 @@ impl<T: NumericValue + JsonSchema> PerBlockDistribution<T> {
let fi_len = first_indexes.len();
let first_indexes_batch: Vec<A> = first_indexes.collect_range_at(start, fi_len);
let count_indexes_batch: Vec<brk_types::StoredU64> =
count_indexes.collect_range_at(start, fi_len);
let count_indexes_batch: Vec<StoredU64> = count_indexes.collect_range_at(start, fi_len);
let zero = T::from(0_usize);
let mut values: Vec<T> = Vec::new();
@@ -163,12 +162,12 @@ impl<T: NumericValue + JsonSchema> PerBlockDistribution<T> {
source: &impl ReadableVec<A, T>,
vsize_source: &impl ReadableVec<A, VSize>,
first_indexes: &impl ReadableVec<Height, A>,
count_indexes: &impl ReadableVec<Height, brk_types::StoredU64>,
count_indexes: &impl ReadableVec<Height, StoredU64>,
exit: &Exit,
skip_count: usize,
) -> Result<()>
where
A: VecIndex + VecValue + brk_types::CheckedSub<A>,
A: VecIndex + VecValue + CheckedSub<A>,
{
let DistributionStats {
min,
@@ -223,8 +222,7 @@ impl<T: NumericValue + JsonSchema> PerBlockDistribution<T> {
let fi_len = first_indexes.len();
let first_indexes_batch: Vec<A> = first_indexes.collect_range_at(start, fi_len);
let count_indexes_batch: Vec<brk_types::StoredU64> =
count_indexes.collect_range_at(start, fi_len);
let count_indexes_batch: Vec<StoredU64> = count_indexes.collect_range_at(start, fi_len);
let zero = T::from(0_usize);
let mut values: Vec<T> = Vec::new();
@@ -294,13 +292,13 @@ impl<T: NumericValue + JsonSchema> PerBlockDistribution<T> {
max_from: Height,
source: &(impl ReadableVec<A, T> + Sized),
first_indexes: &impl ReadableVec<Height, A>,
count_indexes: &impl ReadableVec<Height, brk_types::StoredU64>,
count_indexes: &impl ReadableVec<Height, StoredU64>,
n_blocks: usize,
exit: &Exit,
) -> Result<()>
where
T: CheckedSub,
A: VecIndex + VecValue + brk_types::CheckedSub<A>,
A: VecIndex + VecValue + CheckedSub<A>,
{
let DistributionStats {
min,
@@ -341,8 +339,7 @@ impl<T: NumericValue + JsonSchema> PerBlockDistribution<T> {
let batch_start = start.saturating_sub(n_blocks - 1);
let first_indexes_batch: Vec<A> = first_indexes.collect_range_at(batch_start, fi_len);
let count_indexes_all: Vec<brk_types::StoredU64> =
count_indexes.collect_range_at(batch_start, fi_len);
let count_indexes_all: Vec<StoredU64> = count_indexes.collect_range_at(batch_start, fi_len);
let zero = T::from(0_usize);
@@ -3,7 +3,8 @@ use brk_traversable::Traversable;
use brk_types::{Height, StoredF32, Version};
use derive_more::{Deref, DerefMut};
use vecdb::{
BinaryTransform, Database, Exit, ReadableCloneableVec, ReadableVec, Rw, StorageMode, VecValue,
BinaryTransform, Database, EagerVec, Exit, PcoVec, ReadableCloneableVec, ReadableVec, Rw,
StorageMode, VecValue,
};
use crate::{
@@ -73,7 +74,7 @@ impl<B: FixedRatio> PercentPerBlock<B> {
C: VecValue,
A: VecValue,
f64: From<C> + From<A>,
vecdb::EagerVec<vecdb::PcoVec<Height, B>>: ComputeDrawdown<Height>,
EagerVec<PcoVec<Height, B>>: ComputeDrawdown<Height>,
{
self.ppm
.height
@@ -1,5 +1,5 @@
use brk_traversable::Traversable;
use brk_types::{Height, StoredF32, Version};
use brk_types::{Height, PartsPerMillionSigned64, StoredF32, Version};
use derive_more::{Deref, DerefMut};
use vecdb::{
BinaryTransform, CachedBoxedVec, ReadableCloneableVec, ReadableVec, TypedVec, UnaryTransform,
@@ -186,7 +186,7 @@ impl<B: FixedRatio> LazyPercentPerBlock<B> {
}
}
impl LazyPercentPerBlock<brk_types::PartsPerMillionSigned64> {
impl LazyPercentPerBlock<PartsPerMillionSigned64> {
pub(crate) fn from_lazy_cagr(name: &str, version: Version, years: u8, source: &Self) -> Self {
match years {
2 => Self::from_lazy_percent::<Cagr<2>>(name, version, source),
@@ -109,10 +109,7 @@ impl Price<LazyPerBlock<Cents>> {
indexes: &indexes::Vecs,
) -> Self
where
V: TypedVec<I = brk_types::Height, T = Cents>
+ ReadableVec<brk_types::Height, Cents>
+ Clone
+ 'static,
V: TypedVec<I = Height, T = Cents> + ReadableVec<Height, Cents> + Clone + 'static,
{
let cents = LazyPerBlock::from_height_source::<crate::internal::Identity<Cents>, _>(
&format!("{name}_cents"),
@@ -136,10 +133,7 @@ impl Price<LazyPerBlock<Cents>> {
indexes: &indexes::Vecs,
) -> Self
where
V: TypedVec<I = brk_types::Height, T = Cents>
+ ReadableVec<brk_types::Height, Cents>
+ Clone
+ 'static,
V: TypedVec<I = Height, T = Cents> + ReadableVec<Height, Cents> + Clone + 'static,
{
let cents = LazyPerBlock::from_uncached_height_source::<crate::internal::Identity<Cents>, _>(
&format!("{name}_cents"),
@@ -1,126 +0,0 @@
//! RollingWindows - newtype on Windows with PerBlock per window duration.
//!
//! Each of the 4 windows (24h, 1w, 1m, 1y) contains a height-level vec plus
//! all 17 LazyAggVec index views.
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::Version;
use derive_more::{Deref, DerefMut};
use schemars::JsonSchema;
use vecdb::{Database, Rw, StorageMode};
use crate::{
blocks::lookback::LazyWindowStartVec,
indexes,
internal::{
ColumnarPerBlock, ComputedVecValue, LazyColumnPerBlock, NumericValue, PerBlock,
WindowFrom1wId, WindowId, Windows, WindowsFrom1w,
},
};
pub use crate::blocks::lookback::CachedWindowStartVec;
/// Rolling window start heights — the 4 height-ago vecs (24h, 1w, 1m, 1y).
#[derive(Deref, DerefMut)]
pub struct WindowStarts<'a>(pub Windows<&'a LazyWindowStartVec>);
/// 4 rolling window vecs (24h, 1w, 1m, 1y), each with height data + all 17 index views.
#[derive(Deref, DerefMut, Traversable)]
#[traversable(transparent)]
pub struct RollingWindows<T, M: StorageMode = Rw>(pub Windows<PerBlock<T, M>>)
where
T: ComputedVecValue + PartialOrd + JsonSchema;
impl<T> RollingWindows<T>
where
T: NumericValue + JsonSchema,
{
pub(crate) fn forced_import(
db: &Database,
name: &str,
version: Version,
indexes: &indexes::Vecs,
) -> Result<Self> {
Ok(Self(Windows::try_from_fn(|suffix| {
PerBlock::forced_import(db, &format!("{name}_{suffix}"), version, indexes)
})?))
}
}
/// Four named rolling-window views backed by one columnar source.
#[derive(Deref, DerefMut, Traversable)]
#[traversable(transparent)]
pub struct ColumnarRollingWindows<T, M: StorageMode = Rw>(
pub ColumnarPerBlock<T, WindowId, Windows<LazyColumnPerBlock<T, WindowId>>, M>,
)
where
T: NumericValue + JsonSchema;
impl<T> ColumnarRollingWindows<T>
where
T: NumericValue + JsonSchema,
{
pub(crate) fn forced_import(
db: &Database,
name: &str,
version: Version,
indexes: &indexes::Vecs,
) -> Result<Self> {
Ok(Self(ColumnarPerBlock::forced_import(
db,
name,
version,
|source| {
WindowId::series(|window| {
LazyColumnPerBlock::new(
&format!("{name}_{}", window.suffix()),
version,
source,
window,
indexes,
)
})
},
)?))
}
}
/// The 1w, 1m, and 1y views backed by one columnar source.
#[derive(Deref, DerefMut, Traversable)]
#[traversable(transparent)]
pub struct ColumnarRollingWindowsFrom1w<T, M: StorageMode = Rw>(
pub ColumnarPerBlock<T, WindowFrom1wId, WindowsFrom1w<LazyColumnPerBlock<T, WindowFrom1wId>>, M>,
)
where
T: NumericValue + JsonSchema;
impl<T> ColumnarRollingWindowsFrom1w<T>
where
T: NumericValue + JsonSchema,
{
pub(crate) fn forced_import(
db: &Database,
name: &str,
version: Version,
indexes: &indexes::Vecs,
) -> Result<Self> {
Ok(Self(ColumnarPerBlock::forced_import(
db,
name,
version,
|source| {
WindowFrom1wId::series(|window| {
LazyColumnPerBlock::new(
&format!("{name}_{}", window.suffix()),
version,
source,
window,
indexes,
)
})
},
)?))
}
}
@@ -0,0 +1,48 @@
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::Version;
use derive_more::{Deref, DerefMut};
use schemars::JsonSchema;
use vecdb::{Database, Rw, StorageMode};
use crate::{
indexes,
internal::{ColumnarPerBlock, LazyColumnPerBlock, NumericValue, WindowId, Windows},
};
#[derive(Deref, DerefMut, Traversable)]
#[traversable(transparent)]
pub struct ColumnarRollingWindows<T, M: StorageMode = Rw>(
pub ColumnarPerBlock<T, WindowId, Windows<LazyColumnPerBlock<T, WindowId>>, M>,
)
where
T: NumericValue + JsonSchema;
impl<T> ColumnarRollingWindows<T>
where
T: NumericValue + JsonSchema,
{
pub(crate) fn forced_import(
db: &Database,
name: &str,
version: Version,
indexes: &indexes::Vecs,
) -> Result<Self> {
Ok(Self(ColumnarPerBlock::forced_import(
db,
name,
version,
|source| {
WindowId::series(|window| {
LazyColumnPerBlock::new(
&format!("{name}_{}", window.suffix()),
version,
source,
window,
indexes,
)
})
},
)?))
}
}
@@ -0,0 +1,48 @@
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::Version;
use derive_more::{Deref, DerefMut};
use schemars::JsonSchema;
use vecdb::{Database, Rw, StorageMode};
use crate::{
indexes,
internal::{ColumnarPerBlock, LazyColumnPerBlock, NumericValue, WindowFrom1wId, WindowsFrom1w},
};
#[derive(Deref, DerefMut, Traversable)]
#[traversable(transparent)]
pub struct ColumnarRollingWindowsFrom1w<T, M: StorageMode = Rw>(
pub ColumnarPerBlock<T, WindowFrom1wId, WindowsFrom1w<LazyColumnPerBlock<T, WindowFrom1wId>>, M>,
)
where
T: NumericValue + JsonSchema;
impl<T> ColumnarRollingWindowsFrom1w<T>
where
T: NumericValue + JsonSchema,
{
pub(crate) fn forced_import(
db: &Database,
name: &str,
version: Version,
indexes: &indexes::Vecs,
) -> Result<Self> {
Ok(Self(ColumnarPerBlock::forced_import(
db,
name,
version,
|source| {
WindowFrom1wId::series(|window| {
LazyColumnPerBlock::new(
&format!("{name}_{}", window.suffix()),
version,
source,
window,
indexes,
)
})
},
)?))
}
}
@@ -0,0 +1,10 @@
mod columnar;
mod columnar_from_1w;
mod rolling;
mod starts;
pub use crate::blocks::lookback::CachedWindowStartVec;
pub use columnar::ColumnarRollingWindows;
pub use columnar_from_1w::ColumnarRollingWindowsFrom1w;
pub use rolling::RollingWindows;
pub use starts::WindowStarts;
@@ -0,0 +1,33 @@
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::Version;
use derive_more::{Deref, DerefMut};
use schemars::JsonSchema;
use vecdb::{Database, Rw, StorageMode};
use crate::{
indexes,
internal::{ComputedVecValue, NumericValue, PerBlock, Windows},
};
#[derive(Deref, DerefMut, Traversable)]
#[traversable(transparent)]
pub struct RollingWindows<T, M: StorageMode = Rw>(pub Windows<PerBlock<T, M>>)
where
T: ComputedVecValue + PartialOrd + JsonSchema;
impl<T> RollingWindows<T>
where
T: NumericValue + JsonSchema,
{
pub(crate) fn forced_import(
db: &Database,
name: &str,
version: Version,
indexes: &indexes::Vecs,
) -> Result<Self> {
Ok(Self(Windows::try_from_fn(|suffix| {
PerBlock::forced_import(db, &format!("{name}_{suffix}"), version, indexes)
})?))
}
}
@@ -0,0 +1,6 @@
use derive_more::{Deref, DerefMut};
use crate::{blocks::lookback::LazyWindowStartVec, internal::Windows};
#[derive(Deref, DerefMut)]
pub struct WindowStarts<'a>(pub Windows<&'a LazyWindowStartVec>);
@@ -88,18 +88,18 @@ where
ReadableBoxedVec<Height, Sats>,
ReadableBoxedVec<Height, Cents>,
) {
let columns: Vec<_> = columns.into_iter().collect();
let columns: Box<[_]> = columns.into_iter().collect();
let sats = Self::typed_source::<StoredU64ToSats, Sats>(
sats,
&format!("{name}_sats"),
version,
columns.iter().copied(),
&columns,
);
let cents = Self::typed_source::<StoredU64ToCents, Cents>(
cents,
&format!("{name}_cents"),
version,
columns,
&columns,
);
(sats, cents)
}
@@ -108,20 +108,19 @@ where
source: &ReadOnlyColumnarVec<PcoVec<Height, StoredU64>, C>,
name: &str,
version: Version,
columns: impl IntoIterator<Item = C>,
columns: &[C],
) -> ReadableBoxedVec<Height, T>
where
F: UnaryTransform<StoredU64, T>,
T: VecValue,
{
let columns: Vec<_> = columns.into_iter().collect();
let raw = if columns.len() == 1 {
source
.column(name, version, columns[0])
.read_only_boxed_clone()
} else {
source
.sum_columns(name, version, columns)
.sum_columns(name, version, columns.iter().copied())
.read_only_boxed_clone()
};
LazyVec::transformed::<F>(name, version, raw).read_only_boxed_clone()
@@ -1,154 +0,0 @@
use brk_types::{
Bitcoin, Cents, CentsSigned, Dollars, Sats, SatsFract, SatsSigned, StoredF32, StoredU64,
};
use vecdb::{BinaryTransform, UnaryTransform, unlikely};
pub struct SatsToBitcoin;
impl UnaryTransform<Sats, Bitcoin> for SatsToBitcoin {
#[inline(always)]
fn apply(sats: Sats) -> Bitcoin {
Bitcoin::from(sats)
}
}
pub struct StoredU64ToSats;
impl UnaryTransform<StoredU64, Sats> for StoredU64ToSats {
#[inline(always)]
fn apply(value: StoredU64) -> Sats {
Sats::new(value.into())
}
}
pub struct StoredU64ToCents;
impl UnaryTransform<StoredU64, Cents> for StoredU64ToCents {
#[inline(always)]
fn apply(value: StoredU64) -> Cents {
Cents::new(value.into())
}
}
pub struct SatsSignedToBitcoin;
impl UnaryTransform<SatsSigned, Bitcoin> for SatsSignedToBitcoin {
#[inline(always)]
fn apply(sats: SatsSigned) -> Bitcoin {
Bitcoin::from(sats)
}
}
pub struct AvgSatsToBtc;
impl UnaryTransform<StoredF32, Bitcoin> for AvgSatsToBtc {
#[inline(always)]
fn apply(sats: StoredF32) -> Bitcoin {
Bitcoin::from(f64::from(sats) / Sats::ONE_BTC_U128 as f64)
}
}
pub struct AvgCentsToUsd;
impl UnaryTransform<StoredF32, Dollars> for AvgCentsToUsd {
#[inline(always)]
fn apply(cents: StoredF32) -> Dollars {
Dollars::from(f64::from(cents) / 100.0)
}
}
pub struct SatsToCents;
impl BinaryTransform<Sats, Cents, Cents> for SatsToCents {
#[inline(always)]
fn apply(sats: Sats, price_cents: Cents) -> Cents {
if unlikely(price_cents.is_nan()) {
Cents::NAN
} else {
Cents::from(sats.as_u128() * price_cents.as_u128() / Sats::ONE_BTC_U128)
}
}
}
pub struct CentsUnsignedToDollars;
impl UnaryTransform<Cents, Dollars> for CentsUnsignedToDollars {
#[inline(always)]
fn apply(cents: Cents) -> Dollars {
cents.into()
}
}
pub struct NegCentsUnsignedToDollars;
impl UnaryTransform<Cents, Dollars> for NegCentsUnsignedToDollars {
#[inline(always)]
fn apply(cents: Cents) -> Dollars {
-Dollars::from(cents)
}
}
pub struct CentsSignedToDollars;
impl UnaryTransform<CentsSigned, Dollars> for CentsSignedToDollars {
#[inline(always)]
fn apply(cents: CentsSigned) -> Dollars {
cents.into()
}
}
pub struct CentsUnsignedToSats;
impl UnaryTransform<Cents, Sats> for CentsUnsignedToSats {
#[inline(always)]
fn apply(cents: Cents) -> Sats {
if unlikely(cents.is_nan()) {
panic!("Cents::NAN cannot be converted to whole Sats");
}
let dollars = Dollars::from(cents);
if dollars == Dollars::ZERO {
Sats::ZERO
} else {
Sats::ONE_BTC / dollars
}
}
}
pub struct CentsTimesTenths<const V: u16>;
impl<const V: u16> UnaryTransform<Cents, Cents> for CentsTimesTenths<V> {
#[inline(always)]
fn apply(c: Cents) -> Cents {
if unlikely(c.is_nan()) {
Cents::NAN
} else {
Cents::from(c.as_u128() * V as u128 / 10)
}
}
}
pub struct DollarsToSatsFract;
impl UnaryTransform<Dollars, SatsFract> for DollarsToSatsFract {
#[inline(always)]
fn apply(usd: Dollars) -> SatsFract {
SatsFract::ONE_BTC / usd
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cents_outputs_propagate_nan() {
assert!(SatsToCents::apply(Sats::ONE_BTC, Cents::NAN).is_nan());
assert!(CentsTimesTenths::<24>::apply(Cents::NAN).is_nan());
}
#[test]
#[should_panic(expected = "Cents::NAN cannot be converted to whole Sats")]
fn whole_sats_reject_nan() {
CentsUnsignedToSats::apply(Cents::NAN);
}
}
@@ -0,0 +1,11 @@
use brk_types::{Dollars, StoredF32};
use vecdb::UnaryTransform;
pub struct AvgCentsToUsd;
impl UnaryTransform<StoredF32, Dollars> for AvgCentsToUsd {
#[inline(always)]
fn apply(cents: StoredF32) -> Dollars {
Dollars::from(f64::from(cents) / 100.0)
}
}
@@ -0,0 +1,11 @@
use brk_types::{Bitcoin, Sats, StoredF32};
use vecdb::UnaryTransform;
pub struct AvgSatsToBtc;
impl UnaryTransform<StoredF32, Bitcoin> for AvgSatsToBtc {
#[inline(always)]
fn apply(sats: StoredF32) -> Bitcoin {
Bitcoin::from(f64::from(sats) / Sats::ONE_BTC_U128 as f64)
}
}
@@ -0,0 +1,11 @@
use brk_types::{CentsSigned, Dollars};
use vecdb::UnaryTransform;
pub struct CentsSignedToDollars;
impl UnaryTransform<CentsSigned, Dollars> for CentsSignedToDollars {
#[inline(always)]
fn apply(cents: CentsSigned) -> Dollars {
cents.into()
}
}
@@ -0,0 +1,15 @@
use brk_types::Cents;
use vecdb::{UnaryTransform, unlikely};
pub struct CentsTimesTenths<const V: u16>;
impl<const V: u16> UnaryTransform<Cents, Cents> for CentsTimesTenths<V> {
#[inline(always)]
fn apply(cents: Cents) -> Cents {
if unlikely(cents.is_nan()) {
Cents::NAN
} else {
Cents::from(cents.as_u128() * V as u128 / 10)
}
}
}
@@ -0,0 +1,11 @@
use brk_types::{Cents, Dollars};
use vecdb::UnaryTransform;
pub struct CentsUnsignedToDollars;
impl UnaryTransform<Cents, Dollars> for CentsUnsignedToDollars {
#[inline(always)]
fn apply(cents: Cents) -> Dollars {
cents.into()
}
}
@@ -0,0 +1,19 @@
use brk_types::{Cents, Dollars, Sats};
use vecdb::{UnaryTransform, unlikely};
pub struct CentsUnsignedToSats;
impl UnaryTransform<Cents, Sats> for CentsUnsignedToSats {
#[inline(always)]
fn apply(cents: Cents) -> Sats {
if unlikely(cents.is_nan()) {
panic!("Cents::NAN cannot be converted to whole Sats");
}
let dollars = Dollars::from(cents);
if dollars == Dollars::ZERO {
Sats::ZERO
} else {
Sats::ONE_BTC / dollars
}
}
}
@@ -0,0 +1,11 @@
use brk_types::{Dollars, SatsFract};
use vecdb::UnaryTransform;
pub struct DollarsToSatsFract;
impl UnaryTransform<Dollars, SatsFract> for DollarsToSatsFract {
#[inline(always)]
fn apply(dollars: Dollars) -> SatsFract {
SatsFract::ONE_BTC / dollars
}
}
@@ -0,0 +1,47 @@
mod avg_cents_to_usd;
mod avg_sats_to_btc;
mod cents_signed_to_dollars;
mod cents_times_tenths;
mod cents_unsigned_to_dollars;
mod cents_unsigned_to_sats;
mod dollars_to_sats_fract;
mod neg_cents_unsigned_to_dollars;
mod sats_signed_to_bitcoin;
mod sats_to_bitcoin;
mod sats_to_cents;
mod stored_u64_to_cents;
mod stored_u64_to_sats;
pub use avg_cents_to_usd::AvgCentsToUsd;
pub use avg_sats_to_btc::AvgSatsToBtc;
pub use cents_signed_to_dollars::CentsSignedToDollars;
pub use cents_times_tenths::CentsTimesTenths;
pub use cents_unsigned_to_dollars::CentsUnsignedToDollars;
pub use cents_unsigned_to_sats::CentsUnsignedToSats;
pub use dollars_to_sats_fract::DollarsToSatsFract;
pub use neg_cents_unsigned_to_dollars::NegCentsUnsignedToDollars;
pub use sats_signed_to_bitcoin::SatsSignedToBitcoin;
pub use sats_to_bitcoin::SatsToBitcoin;
pub use sats_to_cents::SatsToCents;
pub use stored_u64_to_cents::StoredU64ToCents;
pub use stored_u64_to_sats::StoredU64ToSats;
#[cfg(test)]
mod tests {
use brk_types::{Cents, Sats};
use vecdb::{BinaryTransform, UnaryTransform};
use super::{CentsTimesTenths, CentsUnsignedToSats, SatsToCents};
#[test]
fn cents_outputs_propagate_nan() {
assert!(SatsToCents::apply(Sats::ONE_BTC, Cents::NAN).is_nan());
assert!(CentsTimesTenths::<24>::apply(Cents::NAN).is_nan());
}
#[test]
#[should_panic(expected = "Cents::NAN cannot be converted to whole Sats")]
fn whole_sats_reject_nan() {
CentsUnsignedToSats::apply(Cents::NAN);
}
}
@@ -0,0 +1,11 @@
use brk_types::{Cents, Dollars};
use vecdb::UnaryTransform;
pub struct NegCentsUnsignedToDollars;
impl UnaryTransform<Cents, Dollars> for NegCentsUnsignedToDollars {
#[inline(always)]
fn apply(cents: Cents) -> Dollars {
-Dollars::from(cents)
}
}
@@ -0,0 +1,11 @@
use brk_types::{Bitcoin, SatsSigned};
use vecdb::UnaryTransform;
pub struct SatsSignedToBitcoin;
impl UnaryTransform<SatsSigned, Bitcoin> for SatsSignedToBitcoin {
#[inline(always)]
fn apply(sats: SatsSigned) -> Bitcoin {
Bitcoin::from(sats)
}
}
@@ -0,0 +1,11 @@
use brk_types::{Bitcoin, Sats};
use vecdb::UnaryTransform;
pub struct SatsToBitcoin;
impl UnaryTransform<Sats, Bitcoin> for SatsToBitcoin {
#[inline(always)]
fn apply(sats: Sats) -> Bitcoin {
Bitcoin::from(sats)
}
}
@@ -0,0 +1,15 @@
use brk_types::{Cents, Sats};
use vecdb::{BinaryTransform, unlikely};
pub struct SatsToCents;
impl BinaryTransform<Sats, Cents, Cents> for SatsToCents {
#[inline(always)]
fn apply(sats: Sats, price_cents: Cents) -> Cents {
if unlikely(price_cents.is_nan()) {
Cents::NAN
} else {
Cents::from(sats.as_u128() * price_cents.as_u128() / Sats::ONE_BTC_U128)
}
}
}
@@ -0,0 +1,11 @@
use brk_types::{Cents, StoredU64};
use vecdb::UnaryTransform;
pub struct StoredU64ToCents;
impl UnaryTransform<StoredU64, Cents> for StoredU64ToCents {
#[inline(always)]
fn apply(value: StoredU64) -> Cents {
Cents::new(value.into())
}
}
@@ -0,0 +1,11 @@
use brk_types::{Sats, StoredU64};
use vecdb::UnaryTransform;
pub struct StoredU64ToSats;
impl UnaryTransform<StoredU64, Sats> for StoredU64ToSats {
#[inline(always)]
fn apply(value: StoredU64) -> Sats {
Sats::new(value.into())
}
}
@@ -0,0 +1,168 @@
use std::cmp::Ordering;
use brk_types::Day1;
use vecdb::{ReadableVec, VecValue};
use super::{ModeId, Modes, Percentiles, WeightedModeId, WeightedModes};
const MINIMUM_DAYS: usize = 365;
const PERCENTILES: Percentiles<f64> = Percentiles {
pct95: 0.95,
pct98: 0.98,
pct99: 0.99,
pct99_5: 0.995,
pct99_9: 0.999,
};
pub(super) type Thresholds = Modes<Option<Percentiles<f64>>>;
pub(super) struct Calibration {
histories: Modes<Vec<f64>>,
}
impl Calibration {
pub(super) fn from_sources<T, U>(
raw: &impl ReadableVec<Day1, Option<T>>,
weighted: &WeightedModes<&dyn ReadableVec<Day1, Option<U>>>,
end: usize,
) -> Self
where
T: VecValue,
U: VecValue,
f64: From<T> + From<U>,
{
let mut histories = Modes::from_fn(|_| Vec::new());
histories.raw = Self::history(raw, end);
for id in WeightedModeId::ALL {
let source = weighted.select(id);
*histories.select_mut(id.mode()) = Self::history(*source, end);
}
Self { histories }
}
pub(super) fn loss_shares<T, U>(
raw: &impl ReadableVec<Day1, Option<T>>,
weighted: &WeightedModes<&dyn ReadableVec<Day1, Option<U>>>,
day: Day1,
) -> Modes<Option<f64>>
where
T: VecValue,
U: VecValue,
f64: From<T> + From<U>,
{
let mut shares = Modes::from_fn(|_| None);
shares.raw = Self::loss_share(raw, day);
for id in WeightedModeId::ALL {
let source = weighted.select(id);
*shares.select_mut(id.mode()) = Self::loss_share(*source, day);
}
shares
}
pub(super) fn thresholds(&self, current: &Modes<Option<f64>>) -> Thresholds {
Modes::from_fn(|mode| {
let history = self.histories.select(mode);
(current.select(mode).is_some() && history.len() >= MINIMUM_DAYS).then(|| {
Percentiles::from_fn(|percentile| {
Self::quantile(history, *percentile.select(&PERCENTILES))
.expect("non-empty history")
})
})
})
}
pub(super) fn observe(&mut self, shares: Modes<Option<f64>>) {
for mode in ModeId::ALL {
let history = self.histories.select_mut(mode);
if let Some(share) = *shares.select(mode) {
Self::insert_sorted(history, share.clamp(0.0, 1.0));
}
}
}
fn history<T>(source: &(impl ReadableVec<Day1, Option<T>> + ?Sized), end: usize) -> Vec<f64>
where
T: VecValue,
f64: From<T>,
{
let mut history = Vec::with_capacity(end);
source.for_each_range_dyn_at(0, end, &mut |value| {
if let Some(value) = value.map(f64::from).filter(|value| value.is_finite()) {
history.push(value);
}
});
history.sort_unstable_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal));
history
}
fn loss_share<T>(
source: &(impl ReadableVec<Day1, Option<T>> + ?Sized),
day: Day1,
) -> Option<f64>
where
T: VecValue,
f64: From<T>,
{
source
.collect_one(day)
.flatten()
.map(f64::from)
.filter(|value| value.is_finite())
}
fn quantile(sorted: &[f64], percentile: f64) -> Option<f64> {
if sorted.is_empty() {
return None;
}
let position = percentile.clamp(0.0, 1.0) * (sorted.len() - 1) as f64;
let lower = position.floor() as usize;
let upper = position.ceil() as usize;
let fraction = position - lower as f64;
Some(sorted[lower] * (1.0 - fraction) + sorted[upper] * fraction)
}
fn insert_sorted(values: &mut Vec<f64>, value: f64) {
let index = values
.binary_search_by(|candidate| candidate.partial_cmp(&value).unwrap_or(Ordering::Less))
.unwrap_or_else(|index| index);
values.insert(index, value);
}
}
#[cfg(test)]
mod tests {
use super::{Calibration, MINIMUM_DAYS};
use crate::models::bedrock::Modes;
#[test]
fn quantile_linearly_interpolates() {
assert_eq!(Calibration::quantile(&[0.0, 1.0], 0.95), Some(0.95));
assert_eq!(Calibration::quantile(&[], 0.95), None);
}
#[test]
fn missing_share_does_not_update_history() {
let mut calibration = Calibration {
histories: Modes::from_fn(|_| Vec::new()),
};
let shares = Modes::from_fn(|_| None);
assert!(calibration.thresholds(&shares).iter().all(Option::is_none));
calibration.observe(shares);
assert!(calibration.histories.iter().all(Vec::is_empty));
}
#[test]
fn a_year_of_history_enables_thresholds() {
let calibration = Calibration {
histories: Modes::from_fn(|_| vec![0.5; MINIMUM_DAYS]),
};
let shares = Modes::from_fn(|_| Some(0.5));
let thresholds = calibration.thresholds(&shares);
assert!(thresholds.iter().all(|values| values.is_some()));
assert!(
thresholds
.iter()
.all(|values| values.as_ref().unwrap().pct95 == 0.5)
);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,182 @@
use brk_types::{Cents, CentsCompact, Sats, StoredF64, UrpdRaw};
use super::{
DayUrpds, LEVEL_IDS, Levels, LossPercentileId, ModeId, ModeResult, Modes, Percentiles,
PriceBands, Thresholds,
};
const LEVEL_PERCENTILES: Levels<f64> = Levels {
pct10: 0.1,
pct20: 0.2,
pct30: 0.3,
pct40: 0.4,
pct50: 0.5,
pct60: 0.6,
pct70: 0.7,
pct80: 0.8,
pct90: 0.9,
};
pub(super) struct DayResult {
pub(super) by_mode: Modes<ModeResult>,
}
impl DayResult {
pub(super) fn from_thresholds(thresholds: &Thresholds) -> Self {
Self {
by_mode: Modes::from_fn(|mode| ModeResult {
loss_threshold: match thresholds.select(mode) {
Some(values) => Percentiles::from_fn(|percentile| {
StoredF64::from(*percentile.select(values))
}),
None => Percentiles::from_fn(|_| StoredF64::NAN),
},
prices: PriceBands::from_fn(|_| Cents::NAN),
}),
}
}
pub(super) fn evaluate(&mut self, urpds: &DayUrpds, thresholds: &Thresholds) {
for mode in ModeId::ALL {
let urpd = urpds.mode(mode);
let denominator = urpd.map.values().copied().map(u64::from).sum::<u64>();
let Some(thresholds) = thresholds.select(mode) else {
continue;
};
if denominator == 0
|| !urpd
.map
.iter()
.any(|(price, sats)| price.inner() != 0 && *sats != Sats::ZERO)
{
continue;
}
let mut remaining_loss = denominator;
let mut floors = Percentiles::from_fn(|_| Cents::NAN);
let mut p95_floor = None;
for (price, sats) in &urpd.map {
remaining_loss -= u64::from(*sats);
let remaining_share = remaining_loss as f64 / denominator as f64;
for percentile in LossPercentileId::ALL {
let floor = percentile.select_mut(&mut floors);
if floor.is_nan() && remaining_share <= *percentile.select(thresholds) {
*floor = Cents::from(*price);
if percentile == LossPercentileId::Pct95 {
p95_floor = Some(*price);
}
}
}
if floors.iter().all(|floor| !floor.is_nan()) {
break;
}
}
let mode_result = self.by_mode.select_mut(mode);
mode_result.prices.floor = floors;
if let Some(p95_floor) = p95_floor {
mode_result.prices.level = Self::conditional_levels(urpd, p95_floor);
}
}
}
fn conditional_levels(urpd: &UrpdRaw, lower: CentsCompact) -> Levels<Cents> {
let mut levels = Levels::from_fn(|_| Cents::NAN);
let total = urpd
.map
.range(lower..)
.map(|(_, sats)| u64::from(*sats))
.sum::<u64>();
if total == 0 {
return levels;
}
let mut cumulative = 0_u64;
let mut percentiles = LEVEL_IDS.iter().copied().peekable();
for (price, sats) in urpd.map.range(lower..) {
let sats = u64::from(*sats);
if sats == 0 {
continue;
}
cumulative += sats;
while let Some(percentile) = percentiles.peek().copied()
&& cumulative as f64 >= total as f64 * *percentile.select(&LEVEL_PERCENTILES)
{
*percentile.select_mut(&mut levels) = Cents::from(*price);
percentiles.next();
}
if percentiles.peek().is_none() {
break;
}
}
levels
}
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use brk_cohort::ByTerm;
use brk_types::{Cents, CentsCompact, Sats, StoredF64, UrpdRaw};
use super::DayResult;
use crate::models::bedrock::{
DayUrpds, Levels, Modes, Percentiles, WeightedModes, WeightedPair,
};
fn repeated_urpds<const N: usize>(entries: [(u32, u64); N]) -> DayUrpds {
let map = entries
.into_iter()
.map(|(price, sats)| (CentsCompact::new(price), Sats::from(sats)))
.collect::<BTreeMap<_, _>>();
DayUrpds {
raw: UrpdRaw { map: map.clone() },
all: WeightedModes::from_fn(|_| UrpdRaw { map: map.clone() }),
term: ByTerm {
short: WeightedPair::from_fn(|_| UrpdRaw { map: map.clone() }),
long: WeightedPair::from_fn(|_| UrpdRaw { map: map.clone() }),
},
}
}
#[test]
fn calibrated_loss_share_sets_floor_and_levels() {
let urpds = repeated_urpds([(100, 50), (200, 50)]);
let thresholds = Modes::from_fn(|_| Some(Percentiles::from_fn(|_| 0.5)));
let mut result = DayResult::from_thresholds(&thresholds);
result.evaluate(&urpds, &thresholds);
let result = &result.by_mode.coinflow;
assert_eq!(
result.loss_threshold,
Percentiles::from_fn(|_| StoredF64::from(0.5))
);
assert_eq!(
result.prices.floor,
Percentiles::from_fn(|_| Cents::new(100))
);
assert_eq!(
result.prices.level,
Levels {
pct10: Cents::new(100),
pct20: Cents::new(100),
pct30: Cents::new(100),
pct40: Cents::new(100),
pct50: Cents::new(100),
pct60: Cents::new(200),
pct70: Cents::new(200),
pct80: Cents::new(200),
pct90: Cents::new(200),
}
);
}
#[test]
fn zero_cost_distribution_stays_missing() {
let urpds = repeated_urpds([(0, 100)]);
let thresholds = Modes::from_fn(|_| Some(Percentiles::from_fn(|_| 1.0)));
let mut result = DayResult::from_thresholds(&thresholds);
result.evaluate(&urpds, &thresholds);
assert!(result.by_mode.raw.prices.floor.pct95.is_nan());
}
}
@@ -0,0 +1,301 @@
use std::{
collections::BTreeMap,
fs,
io::{Error, ErrorKind},
path::Path,
};
use brk_cohort::{
AgeRange, AgeRangeId, ByTerm, CohortContext, TERM_FILTERS, TERM_NAMES, UTXO_ALL_NAME,
UTXOAggregate,
};
use brk_error::Result;
use brk_types::{CentsCompact, Date, Sats, UrpdRaw, UrpdWeight, Version};
use vecdb::ColumnId;
use super::{ModeId, Modes, WeightedModeId, WeightedModes, WeightedPair};
use crate::distribution::UTXOStates;
const VERSION_FILE: &str = "bedrock_urpd.version";
pub(super) type ModeWeights = Modes<Option<AgeRange<f64>>>;
pub(super) type WeightedUrpdNames = UTXOAggregate<WeightedPair<String>>;
struct WeightedMasses {
all: WeightedModes<f64>,
term: ByTerm<WeightedPair<f64>>,
}
impl Default for WeightedMasses {
fn default() -> Self {
Self {
all: WeightedModes::from_fn(|_| 0.0),
term: ByTerm::default(),
}
}
}
pub(super) struct DayUrpds {
pub(super) raw: UrpdRaw,
pub(super) all: WeightedModes<UrpdRaw>,
pub(super) term: ByTerm<WeightedPair<UrpdRaw>>,
}
impl DayUrpds {
pub(super) fn mode(&self, mode: ModeId) -> &UrpdRaw {
match mode {
ModeId::Raw => &self.raw,
_ => self.all.select(mode.weighted().expect("weighted mode")),
}
}
pub(super) fn names() -> WeightedUrpdNames {
UTXOAggregate {
all: WeightedPair::from_fn(|weight| Self::weighted_name(weight, UTXO_ALL_NAME.id)),
sth: WeightedPair::from_fn(|weight| Self::weighted_name(weight, TERM_NAMES.short.id)),
lth: WeightedPair::from_fn(|weight| Self::weighted_name(weight, TERM_NAMES.long.id)),
}
}
pub(super) fn weighted_name(weight: UrpdWeight, cohort: &str) -> String {
debug_assert!(weight.is_weighted());
if cohort == UTXO_ALL_NAME.id {
format!("bedrock_{}", weight.as_str())
} else {
format!("bedrock_{}_{cohort}", weight.as_str())
}
}
pub(super) fn read(
distribution_states_path: &Path,
date: Date,
weights: &ModeWeights,
) -> Result<Self> {
let raw = UrpdRaw::read(distribution_states_path, UTXO_ALL_NAME.id, date)?;
let mut weighted = BTreeMap::new();
for &age in AgeRangeId::ALL {
let cohort = CohortContext::Utxo.prefixed(age.name().id);
let source = UrpdRaw::read(distribution_states_path, &cohort, date)?;
let is_short = TERM_FILTERS.short.includes(age.filter());
for (price, sats) in source.map {
Self::add_weighted_entry(&mut weighted, price, sats, age, is_short, weights);
}
}
Ok(Self::finalize(raw, weighted))
}
pub(super) fn current(utxos: &UTXOStates, weights: &ModeWeights) -> Self {
Self::from_age_entries(utxos.age_range_urpd_entries(), weights)
}
pub(super) fn from_age_entries(
entries: impl IntoIterator<Item = (AgeRangeId, CentsCompact, Sats)>,
weights: &ModeWeights,
) -> Self {
let mut raw = UrpdRaw::default();
let mut weighted = BTreeMap::new();
for (age, price, sats) in entries {
*raw.map.entry(price).or_default() += sats;
let is_short = TERM_FILTERS.short.includes(age.filter());
Self::add_weighted_entry(&mut weighted, price, sats, age, is_short, weights);
}
Self::finalize(raw, weighted)
}
pub(super) fn write(
&self,
states_path: &Path,
names: &WeightedUrpdNames,
date: Date,
) -> Result<()> {
Self::write_pair(
states_path,
&names.all,
date,
&self.all.cointime,
&self.all.coinflow,
)?;
Self::write_pair(
states_path,
&names.sth,
date,
&self.term.short.cointime,
&self.term.short.coinflow,
)?;
Self::write_pair(
states_path,
&names.lth,
date,
&self.term.long.cointime,
&self.term.long.coinflow,
)
}
pub(super) fn stored_version(states_path: &Path) -> Result<Option<Version>> {
let path = states_path.join(VERSION_FILE);
if !path.exists() {
return Ok(None);
}
Ok(Some(Version::try_from(path.as_path())?))
}
pub(super) fn write_version(states_path: &Path, version: Version) -> Result<()> {
fs::create_dir_all(states_path)?;
Ok(version.write(&states_path.join(VERSION_FILE))?)
}
pub(super) fn reset(states_path: &Path, names: &WeightedUrpdNames) -> Result<()> {
for name in names.iter().flat_map(WeightedPair::iter) {
Self::remove_dir(states_path, name)?;
}
for id in WeightedModeId::COINFLOW_HORIZONS {
Self::remove_dir(states_path, &format!("bedrock_{}", id.mode().name()))?;
}
Ok(())
}
fn add_weighted_entry(
weighted: &mut BTreeMap<CentsCompact, WeightedMasses>,
price: CentsCompact,
sats: Sats,
age: AgeRangeId,
is_short: bool,
weights: &ModeWeights,
) {
let mass = u64::from(sats) as f64;
let bucket = weighted.entry(price).or_default();
for id in WeightedModeId::ALL {
let mode = id.mode();
if let Some(mode_weights) = weights.select(mode) {
let weighted_mass = mass * *age.select(mode_weights);
*bucket.all.select_mut(id) += weighted_mass;
let term = if is_short {
&mut bucket.term.short
} else {
&mut bucket.term.long
};
match mode {
ModeId::Cointime => term.cointime += weighted_mass,
ModeId::Coinflow => term.coinflow += weighted_mass,
_ => {}
}
}
}
}
fn finalize(raw: UrpdRaw, weighted: BTreeMap<CentsCompact, WeightedMasses>) -> Self {
let mut all = WeightedModes::from_fn(|_| UrpdRaw::default());
let mut term = ByTerm::<WeightedPair<UrpdRaw>>::default();
for (price, masses) in weighted {
for id in WeightedModeId::ALL {
let distribution = all.select_mut(id);
Self::insert_mass(price, distribution, *masses.all.select(id));
}
Self::insert_pair(price, &mut term.short, &masses.term.short);
Self::insert_pair(price, &mut term.long, &masses.term.long);
}
Self { raw, all, term }
}
fn insert_pair(
price: CentsCompact,
distributions: &mut WeightedPair<UrpdRaw>,
masses: &WeightedPair<f64>,
) {
Self::insert_mass(price, &mut distributions.cointime, masses.cointime);
Self::insert_mass(price, &mut distributions.coinflow, masses.coinflow);
}
fn insert_mass(price: CentsCompact, distribution: &mut UrpdRaw, mass: f64) {
let sats = Self::floor_sats(mass);
if sats != Sats::ZERO {
distribution.map.insert(price, sats);
}
}
fn floor_sats(mass: f64) -> Sats {
debug_assert!(mass.is_finite() && mass >= 0.0);
Sats::from(mass.floor() as u64)
}
fn write_pair(
states_path: &Path,
names: &WeightedPair<String>,
date: Date,
cointime: &UrpdRaw,
coinflow: &UrpdRaw,
) -> Result<()> {
Self::write_one(states_path, &names.cointime, date, cointime)?;
Self::write_one(states_path, &names.coinflow, date, coinflow)
}
fn write_one(states_path: &Path, name: &str, date: Date, distribution: &UrpdRaw) -> Result<()> {
UrpdRaw::write(
states_path,
name,
date,
distribution.map.iter().map(|(&price, &sats)| (price, sats)),
)
}
fn remove_dir(states_path: &Path, name: &str) -> Result<()> {
let path = UrpdRaw::dir(states_path, name);
match fs::remove_dir_all(&path) {
Ok(()) => Ok(()),
Err(error) if error.kind() == ErrorKind::NotFound => Ok(()),
Err(error) => Err(Error::new(
error.kind(),
format!("Cannot reset URPD '{}': {error}", path.display()),
)
.into()),
}
}
}
#[cfg(test)]
mod tests {
use brk_cohort::{AgeRange, AgeRangeId};
use brk_types::{CentsCompact, Sats};
use super::{DayUrpds, ModeWeights};
#[test]
fn weighted_sats_are_floored_after_summing() {
assert_eq!(DayUrpds::floor_sats(0.6 + 0.6), Sats::from(1_u64));
assert_eq!(DayUrpds::floor_sats(0.6), Sats::ZERO);
}
#[test]
fn current_entries_build_raw_and_weighted_urpds() {
let weights = ModeWeights::from_fn(|_| Some(AgeRange::from_fn(|_| 0.5)));
let price = CentsCompact::new(100);
let urpds = DayUrpds::from_age_entries(
[
(AgeRangeId::Under1H, price, Sats::from(3_u64)),
(AgeRangeId::From5MTo6M, price, Sats::from(5_u64)),
],
&weights,
);
assert_eq!(urpds.raw.map[&price], Sats::from(8_u64));
assert_eq!(urpds.all.cointime.map[&price], Sats::from(4_u64));
assert_eq!(urpds.term.short.cointime.map[&price], Sats::from(1_u64));
assert_eq!(urpds.term.long.cointime.map[&price], Sats::from(2_u64));
}
#[test]
fn names_cover_only_stored_aggregate_weights() {
let names = DayUrpds::names();
assert_eq!(names.all.cointime, "bedrock_cointime");
assert_eq!(names.all.coinflow, "bedrock_coinflow");
assert_eq!(names.sth.cointime, "bedrock_cointime_sth");
assert_eq!(names.lth.coinflow, "bedrock_coinflow_lth");
}
}
@@ -4,10 +4,7 @@ use brk_error::Result;
use brk_types::Version;
use vecdb::Database;
use super::{
price::LazyColumnPrice,
vecs::{LossPercentileId, ModeVecs, Modes, PriceBandId, Vecs},
};
use super::{LossPercentileId, ModeVecs, Modes, PriceBandId, Vecs, price::LazyColumnPrice};
use crate::{
indexes,
internal::{ColumnarDailyMetric, DailyMappings, LazyColumnDailyMetric},
@@ -0,0 +1,57 @@
use super::Levels;
pub(crate) const LEVEL_COUNT: usize = 9;
pub(crate) const LEVEL_IDS: [LevelId; LEVEL_COUNT] = [
LevelId::Pct10,
LevelId::Pct20,
LevelId::Pct30,
LevelId::Pct40,
LevelId::Pct50,
LevelId::Pct60,
LevelId::Pct70,
LevelId::Pct80,
LevelId::Pct90,
];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum LevelId {
Pct10,
Pct20,
Pct30,
Pct40,
Pct50,
Pct60,
Pct70,
Pct80,
Pct90,
}
impl LevelId {
pub(super) fn select<T>(self, values: &Levels<T>) -> &T {
match self {
Self::Pct10 => &values.pct10,
Self::Pct20 => &values.pct20,
Self::Pct30 => &values.pct30,
Self::Pct40 => &values.pct40,
Self::Pct50 => &values.pct50,
Self::Pct60 => &values.pct60,
Self::Pct70 => &values.pct70,
Self::Pct80 => &values.pct80,
Self::Pct90 => &values.pct90,
}
}
pub(super) fn select_mut<T>(self, values: &mut Levels<T>) -> &mut T {
match self {
Self::Pct10 => &mut values.pct10,
Self::Pct20 => &mut values.pct20,
Self::Pct30 => &mut values.pct30,
Self::Pct40 => &mut values.pct40,
Self::Pct50 => &mut values.pct50,
Self::Pct60 => &mut values.pct60,
Self::Pct70 => &mut values.pct70,
Self::Pct80 => &mut values.pct80,
Self::Pct90 => &mut values.pct90,
}
}
}
@@ -1,6 +1,11 @@
use std::{fmt, str};
use brk_traversable::Traversable;
use schemars::JsonSchema;
use serde::Serialize;
use vecdb::Formattable;
use super::LevelId;
#[derive(Debug, Clone, Copy, PartialEq, Traversable, Serialize, JsonSchema)]
pub struct Levels<T> {
@@ -14,3 +19,45 @@ pub struct Levels<T> {
pub pct80: T,
pub pct90: T,
}
impl_named_row_formattable!(Levels {
pct10,
pct20,
pct30,
pct40,
pct50,
pct60,
pct70,
pct80,
pct90,
});
impl<T> Levels<T> {
pub(super) fn from_fn(mut create: impl FnMut(LevelId) -> T) -> Self {
Self {
pct10: create(LevelId::Pct10),
pct20: create(LevelId::Pct20),
pct30: create(LevelId::Pct30),
pct40: create(LevelId::Pct40),
pct50: create(LevelId::Pct50),
pct60: create(LevelId::Pct60),
pct70: create(LevelId::Pct70),
pct80: create(LevelId::Pct80),
pct90: create(LevelId::Pct90),
}
}
pub(super) fn map<U>(self, mut map: impl FnMut(T) -> U) -> Levels<U> {
Levels {
pct10: map(self.pct10),
pct20: map(self.pct20),
pct30: map(self.pct30),
pct40: map(self.pct40),
pct50: map(self.pct50),
pct60: map(self.pct60),
pct70: map(self.pct70),
pct80: map(self.pct80),
pct90: map(self.pct90),
}
}
}
@@ -0,0 +1,120 @@
use brk_types::Version;
use vecdb::{ColumnId, VecValue};
use super::Percentiles;
const PERCENTILE_COUNT: usize = 5;
const LOSS_PERCENTILE_IDS: [LossPercentileId; PERCENTILE_COUNT] = [
LossPercentileId::Pct95,
LossPercentileId::Pct98,
LossPercentileId::Pct99,
LossPercentileId::Pct99_5,
LossPercentileId::Pct99_9,
];
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum LossPercentileId {
Pct95,
Pct98,
Pct99,
Pct99_5,
Pct99_9,
}
impl LossPercentileId {
pub(super) const ALL: [Self; PERCENTILE_COUNT] = LOSS_PERCENTILE_IDS;
pub(super) const fn suffix(self) -> &'static str {
match self {
Self::Pct95 => "pct95",
Self::Pct98 => "pct98",
Self::Pct99 => "pct99",
Self::Pct99_5 => "pct99_5",
Self::Pct99_9 => "pct99_9",
}
}
pub(super) fn select<T>(self, values: &Percentiles<T>) -> &T {
match self {
Self::Pct95 => &values.pct95,
Self::Pct98 => &values.pct98,
Self::Pct99 => &values.pct99,
Self::Pct99_5 => &values.pct99_5,
Self::Pct99_9 => &values.pct99_9,
}
}
pub(super) fn select_mut<T>(self, values: &mut Percentiles<T>) -> &mut T {
match self {
Self::Pct95 => &mut values.pct95,
Self::Pct98 => &mut values.pct98,
Self::Pct99 => &mut values.pct99,
Self::Pct99_5 => &mut values.pct99_5,
Self::Pct99_9 => &mut values.pct99_9,
}
}
pub(super) fn series<T>(create: impl FnMut(Self) -> T) -> Percentiles<T> {
Percentiles::from_fn(create)
}
}
impl ColumnId for LossPercentileId {
type Row<T>
= Percentiles<T>
where
T: VecValue;
const VERSION: Version = Version::ONE;
const ALL: &'static [Self] = &LOSS_PERCENTILE_IDS;
#[inline]
fn index(self) -> usize {
self as usize
}
#[inline]
fn get<T: VecValue>(self, row: &Self::Row<T>) -> &T {
self.select(row)
}
#[inline]
fn get_mut<T: VecValue>(self, row: &mut Self::Row<T>) -> &mut T {
self.select_mut(row)
}
#[inline]
fn from_fn<T, F>(create: F) -> Self::Row<T>
where
T: VecValue,
F: FnMut(Self) -> T,
{
Percentiles::from_fn(create)
}
#[inline]
fn map<T, U, F>(row: Self::Row<T>, create: F) -> Self::Row<U>
where
T: VecValue,
U: VecValue,
F: FnMut(T) -> U,
{
row.map(create)
}
}
#[cfg(test)]
mod tests {
use vecdb::ColumnId;
use super::{LOSS_PERCENTILE_IDS, LossPercentileId};
#[test]
fn storage_order_matches_public_order() {
assert_eq!(LossPercentileId::ALL, LOSS_PERCENTILE_IDS);
let row = LossPercentileId::from_fn(|id| id);
for id in LossPercentileId::ALL {
assert_eq!(id.get(&row), &id);
}
}
}
+66 -21
View File
@@ -1,48 +1,93 @@
macro_rules! impl_named_row_formattable {
($row:ident { $($field:ident),+ $(,)? }) => {
impl<T: Formattable> Formattable for $row<T> {
fn write_to(&self, output: &mut Vec<u8>) {
output.push(b'{');
let mut first = true;
$(
if !first {
output.push(b',');
}
first = false;
output.extend_from_slice(concat!("\"", stringify!($field), "\":").as_bytes());
self.$field.fmt_json(output);
)+
let _ = first;
output.push(b'}');
}
fn fmt_csv(&self, output: &mut String) -> fmt::Result {
let mut json = Vec::new();
self.write_to(&mut json);
let json = str::from_utf8(&json).map_err(|_| fmt::Error)?;
output.push('"');
for character in json.chars() {
if character == '"' {
output.push('"');
}
output.push(character);
}
output.push('"');
Ok(())
}
}
};
}
mod calibration;
mod compute;
mod day_result;
mod day_urpds;
mod import;
mod level_id;
mod levels;
mod loss_percentile_id;
mod mode_id;
mod mode_result;
mod mode_vecs;
mod modes;
mod percentiles;
mod price;
mod price_band_id;
mod price_bands;
mod vecs;
mod weighted;
mod weighted_pair;
pub(super) use levels::Levels;
pub(super) use mode_vecs::ModeVecs;
pub(super) use modes::Modes;
pub(super) use percentiles::Percentiles;
pub(super) use price_bands::PriceBands;
pub(super) use weighted::WeightedModes;
use calibration::{Calibration, Thresholds};
use day_result::DayResult;
use day_urpds::{DayUrpds, ModeWeights};
use level_id::{LEVEL_COUNT, LEVEL_IDS, LevelId};
use levels::Levels;
use loss_percentile_id::LossPercentileId;
use mode_id::{MODE_COUNT, ModeId};
use mode_result::ModeResult;
use mode_vecs::ModeVecs;
use modes::Modes;
use percentiles::Percentiles;
use price_band_id::PriceBandId;
use price_bands::PriceBands;
use weighted::{WeightedModeId, WeightedModes};
use weighted_pair::WeightedPair;
use std::path::PathBuf;
use brk_cohort::{AgeRangeId, UTXO_ALL_NAME};
use brk_cohort::AgeRangeId;
use brk_error::Result;
use brk_types::{Cohort, Date, Day1, UrpdRaw, UrpdWeight};
use vecdb::{ColumnId, ReadableVec, StorageMode};
use self::compute::resolve_age_value;
use crate::Computer;
pub use vecs::Vecs;
pub(crate) fn weighted_urpd_name(weight: UrpdWeight, cohort: &str) -> String {
debug_assert!(weight.is_weighted());
if cohort == UTXO_ALL_NAME.id {
format!("bedrock_{}", weight.as_str())
} else {
format!("bedrock_{}_{cohort}", weight.as_str())
}
}
impl<M: StorageMode> Computer<M> {
/// Directory containing a persisted aggregate Bedrock-weighted URPD.
pub fn bedrock_urpd_dir(&self, weight: UrpdWeight, cohort: &Cohort) -> PathBuf {
UrpdRaw::dir(
&self.models.bedrock.states_path,
&weighted_urpd_name(weight, cohort),
&DayUrpds::weighted_name(weight, cohort),
)
}
@@ -55,7 +100,7 @@ impl<M: StorageMode> Computer<M> {
) -> Result<UrpdRaw> {
UrpdRaw::read(
&self.models.bedrock.states_path,
&weighted_urpd_name(weight, cohort),
&DayUrpds::weighted_name(weight, cohort),
date,
)
}
@@ -84,7 +129,7 @@ impl<M: StorageMode> Computer<M> {
UrpdWeight::Raw => Some(1.0),
UrpdWeight::Cointime => {
let cohort = age.select(&self.frameworks.cointime.age_range.activity.wakefulness);
resolve_age_value(cohort.day1.collect_one(day).flatten(), supply)
Vecs::<M>::resolve_age_value(cohort.day1.collect_one(day).flatten(), supply)
.map(|value| value.clamp(0.0, 1.0))
}
UrpdWeight::Coinflow => {
@@ -96,7 +141,7 @@ impl<M: StorageMode> Computer<M> {
.spending_exposure
.mobility,
);
resolve_age_value(cohort.day1.0.collect_one(day).flatten(), supply)
Vecs::<M>::resolve_age_value(cohort.day1.0.collect_one(day).flatten(), supply)
.map(|value| value.clamp(0.0, 1.0))
}
}
@@ -0,0 +1,108 @@
use super::WeightedModeId;
pub(crate) const MODE_COUNT: usize = 10;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
#[repr(u8)]
pub(crate) enum ModeId {
Raw,
Cointime,
Coinflow,
Coinflow8Y,
Coinflow4Y,
Coinflow2Y,
Coinflow1Y,
Coinflow6M,
Coinflow3M,
Coinflow1M,
}
impl ModeId {
pub(super) const ALL: [Self; MODE_COUNT] = [
Self::Raw,
Self::Cointime,
Self::Coinflow,
Self::Coinflow8Y,
Self::Coinflow4Y,
Self::Coinflow2Y,
Self::Coinflow1Y,
Self::Coinflow6M,
Self::Coinflow3M,
Self::Coinflow1M,
];
pub(super) const fn name(self) -> &'static str {
match self {
Self::Raw => "raw",
Self::Cointime => "cointime",
Self::Coinflow => "coinflow",
Self::Coinflow8Y => "coinflow_8y",
Self::Coinflow4Y => "coinflow_4y",
Self::Coinflow2Y => "coinflow_2y",
Self::Coinflow1Y => "coinflow_1y",
Self::Coinflow6M => "coinflow_6m",
Self::Coinflow3M => "coinflow_3m",
Self::Coinflow1M => "coinflow_1m",
}
}
pub(super) const fn weighted(self) -> Option<WeightedModeId> {
match self {
Self::Raw => None,
Self::Cointime => Some(WeightedModeId::Cointime),
Self::Coinflow => Some(WeightedModeId::Coinflow),
Self::Coinflow8Y => Some(WeightedModeId::Coinflow8Y),
Self::Coinflow4Y => Some(WeightedModeId::Coinflow4Y),
Self::Coinflow2Y => Some(WeightedModeId::Coinflow2Y),
Self::Coinflow1Y => Some(WeightedModeId::Coinflow1Y),
Self::Coinflow6M => Some(WeightedModeId::Coinflow6M),
Self::Coinflow3M => Some(WeightedModeId::Coinflow3M),
Self::Coinflow1M => Some(WeightedModeId::Coinflow1M),
}
}
}
#[cfg(test)]
mod tests {
use std::convert::Infallible;
use super::ModeId;
use crate::models::bedrock::{Modes, WeightedModeId};
#[test]
fn ids_match_named_fields_and_storage_names() {
assert_eq!(
WeightedModeId::ALL.map(WeightedModeId::mode).as_slice(),
&ModeId::ALL[1..]
);
assert_eq!(
WeightedModeId::COINFLOW_HORIZONS
.map(WeightedModeId::mode)
.as_slice(),
&ModeId::ALL[3..]
);
let mut modes = Modes::try_from_fn(|id| Ok::<_, Infallible>((id, false))).unwrap();
for id in ModeId::ALL {
let mode = modes.select_mut(id);
assert_eq!(mode.0, id);
mode.1 = true;
}
assert!(modes.iter().all(|(_, visited)| *visited));
assert_eq!(
ModeId::ALL.map(ModeId::name),
[
"raw",
"cointime",
"coinflow",
"coinflow_8y",
"coinflow_4y",
"coinflow_2y",
"coinflow_1y",
"coinflow_6m",
"coinflow_3m",
"coinflow_1m",
]
);
}
}
@@ -0,0 +1,8 @@
use brk_types::{Cents, StoredF64};
use super::{Percentiles, PriceBands};
pub(super) struct ModeResult {
pub(super) loss_threshold: Percentiles<StoredF64>,
pub(super) prices: PriceBands<Cents>,
}
@@ -3,11 +3,7 @@ use brk_types::{Cents, StoredF64};
use derive_more::{Deref, DerefMut};
use vecdb::{Rw, StorageMode};
use super::{
Percentiles, PriceBands,
price::LazyColumnPrice,
vecs::{LossPercentileId, PriceBandId},
};
use super::{LossPercentileId, Percentiles, PriceBandId, PriceBands, price::LazyColumnPrice};
use crate::internal::{ColumnarDailyMetric, LazyColumnDailyMetric};
#[derive(Deref, DerefMut, Traversable)]
@@ -1,7 +1,7 @@
use brk_traversable::Traversable;
use derive_more::{Deref, DerefMut};
use super::WeightedModes;
use super::{ModeId, WeightedModes};
#[derive(Deref, DerefMut, Traversable)]
pub struct Modes<T> {
@@ -11,3 +11,45 @@ pub struct Modes<T> {
#[traversable(flatten)]
pub weighted: WeightedModes<T>,
}
impl<T> Modes<T> {
pub(super) fn from_fn(mut create: impl FnMut(ModeId) -> T) -> Self {
Self {
raw: create(ModeId::Raw),
weighted: WeightedModes::from_fn(|id| create(id.mode())),
}
}
pub(super) fn try_from_fn<E>(
mut create: impl FnMut(ModeId) -> Result<T, E>,
) -> Result<Self, E> {
Ok(Self {
raw: create(ModeId::Raw)?,
weighted: WeightedModes::try_from_fn(|id| create(id.mode()))?,
})
}
pub(super) fn select_mut(&mut self, id: ModeId) -> &mut T {
match id {
ModeId::Raw => &mut self.raw,
_ => self
.weighted
.select_mut(id.weighted().expect("weighted mode")),
}
}
pub(super) fn select(&self, id: ModeId) -> &T {
match id {
ModeId::Raw => &self.raw,
_ => self.weighted.select(id.weighted().expect("weighted mode")),
}
}
pub(super) fn iter(&self) -> impl Iterator<Item = &T> {
std::iter::once(&self.raw).chain(self.weighted.iter())
}
pub(super) fn iter_mut(&mut self) -> impl Iterator<Item = &mut T> {
std::iter::once(&mut self.raw).chain(self.weighted.iter_mut())
}
}
@@ -1,6 +1,11 @@
use std::{fmt, str};
use brk_traversable::Traversable;
use schemars::JsonSchema;
use serde::Serialize;
use vecdb::Formattable;
use super::LossPercentileId;
#[derive(Debug, Clone, Copy, PartialEq, Traversable, Serialize, JsonSchema)]
pub struct Percentiles<T> {
@@ -10,3 +15,44 @@ pub struct Percentiles<T> {
pub pct99_5: T,
pub pct99_9: T,
}
impl_named_row_formattable!(Percentiles {
pct95,
pct98,
pct99,
pct99_5,
pct99_9,
});
impl<T> Percentiles<T> {
pub(super) fn from_fn(mut create: impl FnMut(LossPercentileId) -> T) -> Self {
Self {
pct95: create(LossPercentileId::Pct95),
pct98: create(LossPercentileId::Pct98),
pct99: create(LossPercentileId::Pct99),
pct99_5: create(LossPercentileId::Pct99_5),
pct99_9: create(LossPercentileId::Pct99_9),
}
}
pub(super) fn iter(&self) -> impl Iterator<Item = &T> {
[
&self.pct95,
&self.pct98,
&self.pct99,
&self.pct99_5,
&self.pct99_9,
]
.into_iter()
}
pub(super) fn map<U>(self, mut map: impl FnMut(T) -> U) -> Percentiles<U> {
Percentiles {
pct95: map(self.pct95),
pct98: map(self.pct98),
pct99: map(self.pct99),
pct99_5: map(self.pct99_5),
pct99_9: map(self.pct99_9),
}
}
}
@@ -0,0 +1,180 @@
use brk_types::Version;
use vecdb::{ColumnId, VecValue};
use super::{LEVEL_COUNT, LevelId, PriceBands};
const PERCENTILE_COUNT: usize = 5;
const PRICE_BAND_COUNT: usize = PERCENTILE_COUNT + LEVEL_COUNT;
const PRICE_BAND_IDS: [PriceBandId; PRICE_BAND_COUNT] = [
PriceBandId::FloorPct95,
PriceBandId::FloorPct98,
PriceBandId::FloorPct99,
PriceBandId::FloorPct99_5,
PriceBandId::FloorPct99_9,
PriceBandId::LevelPct10,
PriceBandId::LevelPct20,
PriceBandId::LevelPct30,
PriceBandId::LevelPct40,
PriceBandId::LevelPct50,
PriceBandId::LevelPct60,
PriceBandId::LevelPct70,
PriceBandId::LevelPct80,
PriceBandId::LevelPct90,
];
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum PriceBandId {
FloorPct95,
FloorPct98,
FloorPct99,
FloorPct99_5,
FloorPct99_9,
LevelPct10,
LevelPct20,
LevelPct30,
LevelPct40,
LevelPct50,
LevelPct60,
LevelPct70,
LevelPct80,
LevelPct90,
}
impl PriceBandId {
pub(super) const fn suffix(self) -> &'static str {
match self {
Self::FloorPct95 => "floor_pct95",
Self::FloorPct98 => "floor_pct98",
Self::FloorPct99 => "floor_pct99",
Self::FloorPct99_5 => "floor_pct99_5",
Self::FloorPct99_9 => "floor_pct99_9",
Self::LevelPct10 => "level_pct10",
Self::LevelPct20 => "level_pct20",
Self::LevelPct30 => "level_pct30",
Self::LevelPct40 => "level_pct40",
Self::LevelPct50 => "level_pct50",
Self::LevelPct60 => "level_pct60",
Self::LevelPct70 => "level_pct70",
Self::LevelPct80 => "level_pct80",
Self::LevelPct90 => "level_pct90",
}
}
pub(super) fn select<T>(self, values: &PriceBands<T>) -> &T {
match self {
Self::FloorPct95 => &values.floor.pct95,
Self::FloorPct98 => &values.floor.pct98,
Self::FloorPct99 => &values.floor.pct99,
Self::FloorPct99_5 => &values.floor.pct99_5,
Self::FloorPct99_9 => &values.floor.pct99_9,
Self::LevelPct10 => &values.level.pct10,
Self::LevelPct20 => &values.level.pct20,
Self::LevelPct30 => &values.level.pct30,
Self::LevelPct40 => &values.level.pct40,
Self::LevelPct50 => &values.level.pct50,
Self::LevelPct60 => &values.level.pct60,
Self::LevelPct70 => &values.level.pct70,
Self::LevelPct80 => &values.level.pct80,
Self::LevelPct90 => &values.level.pct90,
}
}
pub(super) fn select_mut<T>(self, values: &mut PriceBands<T>) -> &mut T {
match self {
Self::FloorPct95 => &mut values.floor.pct95,
Self::FloorPct98 => &mut values.floor.pct98,
Self::FloorPct99 => &mut values.floor.pct99,
Self::FloorPct99_5 => &mut values.floor.pct99_5,
Self::FloorPct99_9 => &mut values.floor.pct99_9,
Self::LevelPct10 => &mut values.level.pct10,
Self::LevelPct20 => &mut values.level.pct20,
Self::LevelPct30 => &mut values.level.pct30,
Self::LevelPct40 => &mut values.level.pct40,
Self::LevelPct50 => &mut values.level.pct50,
Self::LevelPct60 => &mut values.level.pct60,
Self::LevelPct70 => &mut values.level.pct70,
Self::LevelPct80 => &mut values.level.pct80,
Self::LevelPct90 => &mut values.level.pct90,
}
}
pub(super) fn series<T>(create: impl FnMut(Self) -> T) -> PriceBands<T> {
PriceBands::from_fn(create)
}
}
impl From<LevelId> for PriceBandId {
fn from(value: LevelId) -> Self {
match value {
LevelId::Pct10 => Self::LevelPct10,
LevelId::Pct20 => Self::LevelPct20,
LevelId::Pct30 => Self::LevelPct30,
LevelId::Pct40 => Self::LevelPct40,
LevelId::Pct50 => Self::LevelPct50,
LevelId::Pct60 => Self::LevelPct60,
LevelId::Pct70 => Self::LevelPct70,
LevelId::Pct80 => Self::LevelPct80,
LevelId::Pct90 => Self::LevelPct90,
}
}
}
impl ColumnId for PriceBandId {
type Row<T>
= PriceBands<T>
where
T: VecValue;
const VERSION: Version = Version::ONE;
const ALL: &'static [Self] = &PRICE_BAND_IDS;
#[inline]
fn index(self) -> usize {
self as usize
}
#[inline]
fn get<T: VecValue>(self, row: &Self::Row<T>) -> &T {
self.select(row)
}
#[inline]
fn get_mut<T: VecValue>(self, row: &mut Self::Row<T>) -> &mut T {
self.select_mut(row)
}
#[inline]
fn from_fn<T, F>(create: F) -> Self::Row<T>
where
T: VecValue,
F: FnMut(Self) -> T,
{
PriceBands::from_fn(create)
}
#[inline]
fn map<T, U, F>(row: Self::Row<T>, create: F) -> Self::Row<U>
where
T: VecValue,
U: VecValue,
F: FnMut(T) -> U,
{
row.map(create)
}
}
#[cfg(test)]
mod tests {
use vecdb::ColumnId;
use super::{PRICE_BAND_IDS, PriceBandId};
#[test]
fn storage_order_matches_public_order() {
assert_eq!(PriceBandId::ALL, PRICE_BAND_IDS);
let row = PriceBandId::from_fn(|id| id);
for &id in PriceBandId::ALL {
assert_eq!(id.get(&row), &id);
}
}
}
@@ -1,11 +1,38 @@
use std::{fmt, str};
use brk_traversable::Traversable;
use schemars::JsonSchema;
use serde::Serialize;
use vecdb::Formattable;
use super::{Levels, Percentiles};
use super::{LevelId, Levels, Percentiles, PriceBandId};
#[derive(Debug, Clone, Copy, PartialEq, Traversable, Serialize, JsonSchema)]
pub struct PriceBands<T> {
pub floor: Percentiles<T>,
pub level: Levels<T>,
}
impl_named_row_formattable!(PriceBands { floor, level });
impl<T> PriceBands<T> {
pub(super) fn from_fn(mut create: impl FnMut(PriceBandId) -> T) -> Self {
Self {
floor: Percentiles {
pct95: create(PriceBandId::FloorPct95),
pct98: create(PriceBandId::FloorPct98),
pct99: create(PriceBandId::FloorPct99),
pct99_5: create(PriceBandId::FloorPct99_5),
pct99_9: create(PriceBandId::FloorPct99_9),
},
level: Levels::from_fn(|id: LevelId| create(id.into())),
}
}
pub(super) fn map<U>(self, mut map: impl FnMut(T) -> U) -> PriceBands<U> {
PriceBands {
floor: self.floor.map(&mut map),
level: self.level.map(map),
}
}
}
+32 -812
View File
@@ -1,716 +1,11 @@
use std::path::PathBuf;
use brk_traversable::Traversable;
use brk_types::Version;
use brk_types::Sats;
use derive_more::{Deref, DerefMut};
use vecdb::{ColumnId, Formattable, Rw, StorageMode, VecValue};
use vecdb::{Rw, StorageMode};
pub(super) use super::{Levels, ModeVecs, Modes, Percentiles, PriceBands, WeightedModes};
macro_rules! impl_named_row_formattable {
($row:ident { $($field:ident),+ $(,)? }) => {
impl<T: Formattable> Formattable for $row<T> {
fn write_to(&self, output: &mut Vec<u8>) {
output.push(b'{');
let mut first = true;
$(
if !first {
output.push(b',');
}
first = false;
output.extend_from_slice(concat!("\"", stringify!($field), "\":").as_bytes());
self.$field.fmt_json(output);
)+
let _ = first;
output.push(b'}');
}
fn fmt_csv(&self, output: &mut String) -> std::fmt::Result {
let mut json = Vec::new();
self.write_to(&mut json);
let json = std::str::from_utf8(&json).map_err(|_| std::fmt::Error)?;
output.push('"');
for character in json.chars() {
if character == '"' {
output.push('"');
}
output.push(character);
}
output.push('"');
Ok(())
}
}
};
}
pub(crate) const MODE_COUNT: usize = 10;
pub(crate) const PERCENTILE_COUNT: usize = 5;
pub(crate) const LEVEL_COUNT: usize = 9;
const WEIGHTED_MODE_COUNT: usize = MODE_COUNT - 1;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
#[repr(u8)]
pub(super) enum ModeId {
Raw,
Cointime,
Coinflow,
Coinflow8Y,
Coinflow4Y,
Coinflow2Y,
Coinflow1Y,
Coinflow6M,
Coinflow3M,
Coinflow1M,
}
impl ModeId {
pub(super) const ALL: [Self; MODE_COUNT] = [
Self::Raw,
Self::Cointime,
Self::Coinflow,
Self::Coinflow8Y,
Self::Coinflow4Y,
Self::Coinflow2Y,
Self::Coinflow1Y,
Self::Coinflow6M,
Self::Coinflow3M,
Self::Coinflow1M,
];
pub(super) const fn name(self) -> &'static str {
match self {
Self::Raw => "raw",
Self::Cointime => "cointime",
Self::Coinflow => "coinflow",
Self::Coinflow8Y => "coinflow_8y",
Self::Coinflow4Y => "coinflow_4y",
Self::Coinflow2Y => "coinflow_2y",
Self::Coinflow1Y => "coinflow_1y",
Self::Coinflow6M => "coinflow_6m",
Self::Coinflow3M => "coinflow_3m",
Self::Coinflow1M => "coinflow_1m",
}
}
pub(super) const fn weighted(self) -> Option<WeightedModeId> {
match self {
Self::Raw => None,
Self::Cointime => Some(WeightedModeId::Cointime),
Self::Coinflow => Some(WeightedModeId::Coinflow),
Self::Coinflow8Y => Some(WeightedModeId::Coinflow8Y),
Self::Coinflow4Y => Some(WeightedModeId::Coinflow4Y),
Self::Coinflow2Y => Some(WeightedModeId::Coinflow2Y),
Self::Coinflow1Y => Some(WeightedModeId::Coinflow1Y),
Self::Coinflow6M => Some(WeightedModeId::Coinflow6M),
Self::Coinflow3M => Some(WeightedModeId::Coinflow3M),
Self::Coinflow1M => Some(WeightedModeId::Coinflow1M),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum WeightedModeId {
Cointime,
Coinflow,
Coinflow8Y,
Coinflow4Y,
Coinflow2Y,
Coinflow1Y,
Coinflow6M,
Coinflow3M,
Coinflow1M,
}
impl WeightedModeId {
pub(super) const ALL: [Self; WEIGHTED_MODE_COUNT] = [
Self::Cointime,
Self::Coinflow,
Self::Coinflow8Y,
Self::Coinflow4Y,
Self::Coinflow2Y,
Self::Coinflow1Y,
Self::Coinflow6M,
Self::Coinflow3M,
Self::Coinflow1M,
];
pub(super) const COINFLOW_HORIZONS: [Self; 7] = [
Self::Coinflow8Y,
Self::Coinflow4Y,
Self::Coinflow2Y,
Self::Coinflow1Y,
Self::Coinflow6M,
Self::Coinflow3M,
Self::Coinflow1M,
];
pub(super) const fn mode(self) -> ModeId {
match self {
Self::Cointime => ModeId::Cointime,
Self::Coinflow => ModeId::Coinflow,
Self::Coinflow8Y => ModeId::Coinflow8Y,
Self::Coinflow4Y => ModeId::Coinflow4Y,
Self::Coinflow2Y => ModeId::Coinflow2Y,
Self::Coinflow1Y => ModeId::Coinflow1Y,
Self::Coinflow6M => ModeId::Coinflow6M,
Self::Coinflow3M => ModeId::Coinflow3M,
Self::Coinflow1M => ModeId::Coinflow1M,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum LossPercentileId {
Pct95,
Pct98,
Pct99,
Pct99_5,
Pct99_9,
}
impl LossPercentileId {
pub(super) const fn suffix(self) -> &'static str {
match self {
Self::Pct95 => "pct95",
Self::Pct98 => "pct98",
Self::Pct99 => "pct99",
Self::Pct99_5 => "pct99_5",
Self::Pct99_9 => "pct99_9",
}
}
pub(super) fn select<T>(self, values: &Percentiles<T>) -> &T {
match self {
Self::Pct95 => &values.pct95,
Self::Pct98 => &values.pct98,
Self::Pct99 => &values.pct99,
Self::Pct99_5 => &values.pct99_5,
Self::Pct99_9 => &values.pct99_9,
}
}
pub(super) fn select_mut<T>(self, values: &mut Percentiles<T>) -> &mut T {
match self {
Self::Pct95 => &mut values.pct95,
Self::Pct98 => &mut values.pct98,
Self::Pct99 => &mut values.pct99,
Self::Pct99_5 => &mut values.pct99_5,
Self::Pct99_9 => &mut values.pct99_9,
}
}
}
const LOSS_PERCENTILE_IDS: [LossPercentileId; PERCENTILE_COUNT] = [
LossPercentileId::Pct95,
LossPercentileId::Pct98,
LossPercentileId::Pct99,
LossPercentileId::Pct99_5,
LossPercentileId::Pct99_9,
];
impl ColumnId for LossPercentileId {
type Row<T>
= Percentiles<T>
where
T: VecValue;
const VERSION: Version = Version::ONE;
const ALL: &'static [Self] = &LOSS_PERCENTILE_IDS;
#[inline]
fn index(self) -> usize {
self as usize
}
#[inline]
fn get<T: VecValue>(self, row: &Self::Row<T>) -> &T {
self.select(row)
}
#[inline]
fn get_mut<T: VecValue>(self, row: &mut Self::Row<T>) -> &mut T {
self.select_mut(row)
}
#[inline]
fn from_fn<T, F>(create: F) -> Self::Row<T>
where
T: VecValue,
F: FnMut(Self) -> T,
{
Percentiles::from_fn(create)
}
#[inline]
fn map<T, U, F>(row: Self::Row<T>, create: F) -> Self::Row<U>
where
T: VecValue,
U: VecValue,
F: FnMut(T) -> U,
{
row.map(create)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum PriceBandId {
FloorPct95,
FloorPct98,
FloorPct99,
FloorPct99_5,
FloorPct99_9,
LevelPct10,
LevelPct20,
LevelPct30,
LevelPct40,
LevelPct50,
LevelPct60,
LevelPct70,
LevelPct80,
LevelPct90,
}
impl PriceBandId {
pub(super) const fn suffix(self) -> &'static str {
match self {
Self::FloorPct95 => "floor_pct95",
Self::FloorPct98 => "floor_pct98",
Self::FloorPct99 => "floor_pct99",
Self::FloorPct99_5 => "floor_pct99_5",
Self::FloorPct99_9 => "floor_pct99_9",
Self::LevelPct10 => "level_pct10",
Self::LevelPct20 => "level_pct20",
Self::LevelPct30 => "level_pct30",
Self::LevelPct40 => "level_pct40",
Self::LevelPct50 => "level_pct50",
Self::LevelPct60 => "level_pct60",
Self::LevelPct70 => "level_pct70",
Self::LevelPct80 => "level_pct80",
Self::LevelPct90 => "level_pct90",
}
}
pub(super) fn select<T>(self, values: &PriceBands<T>) -> &T {
match self {
Self::FloorPct95 => &values.floor.pct95,
Self::FloorPct98 => &values.floor.pct98,
Self::FloorPct99 => &values.floor.pct99,
Self::FloorPct99_5 => &values.floor.pct99_5,
Self::FloorPct99_9 => &values.floor.pct99_9,
Self::LevelPct10 => &values.level.pct10,
Self::LevelPct20 => &values.level.pct20,
Self::LevelPct30 => &values.level.pct30,
Self::LevelPct40 => &values.level.pct40,
Self::LevelPct50 => &values.level.pct50,
Self::LevelPct60 => &values.level.pct60,
Self::LevelPct70 => &values.level.pct70,
Self::LevelPct80 => &values.level.pct80,
Self::LevelPct90 => &values.level.pct90,
}
}
pub(super) fn select_mut<T>(self, values: &mut PriceBands<T>) -> &mut T {
match self {
Self::FloorPct95 => &mut values.floor.pct95,
Self::FloorPct98 => &mut values.floor.pct98,
Self::FloorPct99 => &mut values.floor.pct99,
Self::FloorPct99_5 => &mut values.floor.pct99_5,
Self::FloorPct99_9 => &mut values.floor.pct99_9,
Self::LevelPct10 => &mut values.level.pct10,
Self::LevelPct20 => &mut values.level.pct20,
Self::LevelPct30 => &mut values.level.pct30,
Self::LevelPct40 => &mut values.level.pct40,
Self::LevelPct50 => &mut values.level.pct50,
Self::LevelPct60 => &mut values.level.pct60,
Self::LevelPct70 => &mut values.level.pct70,
Self::LevelPct80 => &mut values.level.pct80,
Self::LevelPct90 => &mut values.level.pct90,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum LevelId {
Pct10,
Pct20,
Pct30,
Pct40,
Pct50,
Pct60,
Pct70,
Pct80,
Pct90,
}
impl LevelId {
pub(super) fn select<T>(self, values: &Levels<T>) -> &T {
match self {
Self::Pct10 => &values.pct10,
Self::Pct20 => &values.pct20,
Self::Pct30 => &values.pct30,
Self::Pct40 => &values.pct40,
Self::Pct50 => &values.pct50,
Self::Pct60 => &values.pct60,
Self::Pct70 => &values.pct70,
Self::Pct80 => &values.pct80,
Self::Pct90 => &values.pct90,
}
}
pub(super) fn select_mut<T>(self, values: &mut Levels<T>) -> &mut T {
match self {
Self::Pct10 => &mut values.pct10,
Self::Pct20 => &mut values.pct20,
Self::Pct30 => &mut values.pct30,
Self::Pct40 => &mut values.pct40,
Self::Pct50 => &mut values.pct50,
Self::Pct60 => &mut values.pct60,
Self::Pct70 => &mut values.pct70,
Self::Pct80 => &mut values.pct80,
Self::Pct90 => &mut values.pct90,
}
}
const fn price_band(self) -> PriceBandId {
match self {
Self::Pct10 => PriceBandId::LevelPct10,
Self::Pct20 => PriceBandId::LevelPct20,
Self::Pct30 => PriceBandId::LevelPct30,
Self::Pct40 => PriceBandId::LevelPct40,
Self::Pct50 => PriceBandId::LevelPct50,
Self::Pct60 => PriceBandId::LevelPct60,
Self::Pct70 => PriceBandId::LevelPct70,
Self::Pct80 => PriceBandId::LevelPct80,
Self::Pct90 => PriceBandId::LevelPct90,
}
}
}
const PRICE_BAND_COUNT: usize = PERCENTILE_COUNT + LEVEL_COUNT;
const PRICE_BAND_IDS: [PriceBandId; PRICE_BAND_COUNT] = [
PriceBandId::FloorPct95,
PriceBandId::FloorPct98,
PriceBandId::FloorPct99,
PriceBandId::FloorPct99_5,
PriceBandId::FloorPct99_9,
PriceBandId::LevelPct10,
PriceBandId::LevelPct20,
PriceBandId::LevelPct30,
PriceBandId::LevelPct40,
PriceBandId::LevelPct50,
PriceBandId::LevelPct60,
PriceBandId::LevelPct70,
PriceBandId::LevelPct80,
PriceBandId::LevelPct90,
];
pub(super) const LEVEL_IDS: [LevelId; LEVEL_COUNT] = [
LevelId::Pct10,
LevelId::Pct20,
LevelId::Pct30,
LevelId::Pct40,
LevelId::Pct50,
LevelId::Pct60,
LevelId::Pct70,
LevelId::Pct80,
LevelId::Pct90,
];
impl ColumnId for PriceBandId {
type Row<T>
= PriceBands<T>
where
T: VecValue;
const VERSION: Version = Version::ONE;
const ALL: &'static [Self] = &PRICE_BAND_IDS;
#[inline]
fn index(self) -> usize {
self as usize
}
#[inline]
fn get<T: VecValue>(self, row: &Self::Row<T>) -> &T {
self.select(row)
}
#[inline]
fn get_mut<T: VecValue>(self, row: &mut Self::Row<T>) -> &mut T {
self.select_mut(row)
}
#[inline]
fn from_fn<T, F>(create: F) -> Self::Row<T>
where
T: VecValue,
F: FnMut(Self) -> T,
{
PriceBands::from_fn(create)
}
#[inline]
fn map<T, U, F>(row: Self::Row<T>, create: F) -> Self::Row<U>
where
T: VecValue,
U: VecValue,
F: FnMut(T) -> U,
{
row.map(create)
}
}
impl_named_row_formattable!(Percentiles {
pct95,
pct98,
pct99,
pct99_5,
pct99_9,
});
impl<T> Percentiles<T> {
pub(super) fn from_fn(mut create: impl FnMut(LossPercentileId) -> T) -> Self {
Self {
pct95: create(LossPercentileId::Pct95),
pct98: create(LossPercentileId::Pct98),
pct99: create(LossPercentileId::Pct99),
pct99_5: create(LossPercentileId::Pct99_5),
pct99_9: create(LossPercentileId::Pct99_9),
}
}
pub(super) fn iter(&self) -> impl Iterator<Item = &T> {
[
&self.pct95,
&self.pct98,
&self.pct99,
&self.pct99_5,
&self.pct99_9,
]
.into_iter()
}
fn map<U>(self, mut map: impl FnMut(T) -> U) -> Percentiles<U> {
Percentiles {
pct95: map(self.pct95),
pct98: map(self.pct98),
pct99: map(self.pct99),
pct99_5: map(self.pct99_5),
pct99_9: map(self.pct99_9),
}
}
}
impl_named_row_formattable!(Levels {
pct10,
pct20,
pct30,
pct40,
pct50,
pct60,
pct70,
pct80,
pct90,
});
impl<T> Levels<T> {
pub(super) fn from_fn(mut create: impl FnMut(LevelId) -> T) -> Self {
Self {
pct10: create(LevelId::Pct10),
pct20: create(LevelId::Pct20),
pct30: create(LevelId::Pct30),
pct40: create(LevelId::Pct40),
pct50: create(LevelId::Pct50),
pct60: create(LevelId::Pct60),
pct70: create(LevelId::Pct70),
pct80: create(LevelId::Pct80),
pct90: create(LevelId::Pct90),
}
}
fn map<U>(self, mut map: impl FnMut(T) -> U) -> Levels<U> {
Levels {
pct10: map(self.pct10),
pct20: map(self.pct20),
pct30: map(self.pct30),
pct40: map(self.pct40),
pct50: map(self.pct50),
pct60: map(self.pct60),
pct70: map(self.pct70),
pct80: map(self.pct80),
pct90: map(self.pct90),
}
}
}
impl_named_row_formattable!(PriceBands { floor, level });
impl<T> PriceBands<T> {
pub(super) fn from_fn(mut create: impl FnMut(PriceBandId) -> T) -> Self {
Self {
floor: Percentiles {
pct95: create(PriceBandId::FloorPct95),
pct98: create(PriceBandId::FloorPct98),
pct99: create(PriceBandId::FloorPct99),
pct99_5: create(PriceBandId::FloorPct99_5),
pct99_9: create(PriceBandId::FloorPct99_9),
},
level: Levels::from_fn(|id| create(id.price_band())),
}
}
fn map<U>(self, mut map: impl FnMut(T) -> U) -> PriceBands<U> {
PriceBands {
floor: self.floor.map(&mut map),
level: self.level.map(map),
}
}
}
impl LossPercentileId {
pub(super) fn series<T>(mut create: impl FnMut(Self) -> T) -> Percentiles<T> {
Percentiles::from_fn(&mut create)
}
}
impl PriceBandId {
pub(super) fn series<T>(mut create: impl FnMut(Self) -> T) -> PriceBands<T> {
PriceBands::from_fn(&mut create)
}
}
impl<T> WeightedModes<T> {
pub(super) fn from_fn(mut create: impl FnMut(WeightedModeId) -> T) -> Self {
Self {
cointime: create(WeightedModeId::Cointime),
coinflow: create(WeightedModeId::Coinflow),
coinflow_8y: create(WeightedModeId::Coinflow8Y),
coinflow_4y: create(WeightedModeId::Coinflow4Y),
coinflow_2y: create(WeightedModeId::Coinflow2Y),
coinflow_1y: create(WeightedModeId::Coinflow1Y),
coinflow_6m: create(WeightedModeId::Coinflow6M),
coinflow_3m: create(WeightedModeId::Coinflow3M),
coinflow_1m: create(WeightedModeId::Coinflow1M),
}
}
pub(super) fn try_from_fn<E>(
mut create: impl FnMut(WeightedModeId) -> Result<T, E>,
) -> Result<Self, E> {
Ok(Self {
cointime: create(WeightedModeId::Cointime)?,
coinflow: create(WeightedModeId::Coinflow)?,
coinflow_8y: create(WeightedModeId::Coinflow8Y)?,
coinflow_4y: create(WeightedModeId::Coinflow4Y)?,
coinflow_2y: create(WeightedModeId::Coinflow2Y)?,
coinflow_1y: create(WeightedModeId::Coinflow1Y)?,
coinflow_6m: create(WeightedModeId::Coinflow6M)?,
coinflow_3m: create(WeightedModeId::Coinflow3M)?,
coinflow_1m: create(WeightedModeId::Coinflow1M)?,
})
}
pub(super) fn select_mut(&mut self, id: WeightedModeId) -> &mut T {
match id {
WeightedModeId::Cointime => &mut self.cointime,
WeightedModeId::Coinflow => &mut self.coinflow,
WeightedModeId::Coinflow8Y => &mut self.coinflow_8y,
WeightedModeId::Coinflow4Y => &mut self.coinflow_4y,
WeightedModeId::Coinflow2Y => &mut self.coinflow_2y,
WeightedModeId::Coinflow1Y => &mut self.coinflow_1y,
WeightedModeId::Coinflow6M => &mut self.coinflow_6m,
WeightedModeId::Coinflow3M => &mut self.coinflow_3m,
WeightedModeId::Coinflow1M => &mut self.coinflow_1m,
}
}
pub(super) fn select(&self, id: WeightedModeId) -> &T {
match id {
WeightedModeId::Cointime => &self.cointime,
WeightedModeId::Coinflow => &self.coinflow,
WeightedModeId::Coinflow8Y => &self.coinflow_8y,
WeightedModeId::Coinflow4Y => &self.coinflow_4y,
WeightedModeId::Coinflow2Y => &self.coinflow_2y,
WeightedModeId::Coinflow1Y => &self.coinflow_1y,
WeightedModeId::Coinflow6M => &self.coinflow_6m,
WeightedModeId::Coinflow3M => &self.coinflow_3m,
WeightedModeId::Coinflow1M => &self.coinflow_1m,
}
}
pub(super) fn iter(&self) -> impl Iterator<Item = &T> {
[
&self.cointime,
&self.coinflow,
&self.coinflow_8y,
&self.coinflow_4y,
&self.coinflow_2y,
&self.coinflow_1y,
&self.coinflow_6m,
&self.coinflow_3m,
&self.coinflow_1m,
]
.into_iter()
}
pub(super) fn iter_mut(&mut self) -> impl Iterator<Item = &mut T> {
[
&mut self.cointime,
&mut self.coinflow,
&mut self.coinflow_8y,
&mut self.coinflow_4y,
&mut self.coinflow_2y,
&mut self.coinflow_1y,
&mut self.coinflow_6m,
&mut self.coinflow_3m,
&mut self.coinflow_1m,
]
.into_iter()
}
}
impl<T> Modes<T> {
pub(super) fn from_fn(mut create: impl FnMut(ModeId) -> T) -> Self {
Self {
raw: create(ModeId::Raw),
weighted: WeightedModes::from_fn(|id| create(id.mode())),
}
}
pub(super) fn try_from_fn<E>(
mut create: impl FnMut(ModeId) -> Result<T, E>,
) -> Result<Self, E> {
Ok(Self {
raw: create(ModeId::Raw)?,
weighted: WeightedModes::try_from_fn(|id| create(id.mode()))?,
})
}
pub(super) fn select_mut(&mut self, id: ModeId) -> &mut T {
match id {
ModeId::Raw => &mut self.raw,
_ => self
.weighted
.select_mut(id.weighted().expect("weighted mode")),
}
}
pub(super) fn select(&self, id: ModeId) -> &T {
match id {
ModeId::Raw => &self.raw,
_ => self.weighted.select(id.weighted().expect("weighted mode")),
}
}
pub(super) fn iter(&self) -> impl Iterator<Item = &T> {
std::iter::once(&self.raw).chain(self.weighted.iter())
}
pub(super) fn iter_mut(&mut self) -> impl Iterator<Item = &mut T> {
std::iter::once(&mut self.raw).chain(self.weighted.iter_mut())
}
}
use super::{ModeVecs, Modes};
#[derive(Deref, DerefMut, Traversable)]
pub struct Vecs<M: StorageMode = Rw> {
@@ -723,127 +18,52 @@ pub struct Vecs<M: StorageMode = Rw> {
pub modes: Modes<ModeVecs<M>>,
}
impl<M: StorageMode> Vecs<M> {
pub(crate) fn resolve_age_value<T>(value: Option<T>, supply: Sats) -> Option<f64>
where
f64: From<T>,
{
match value.map(f64::from) {
Some(value) if value.is_finite() => Some(value),
_ if supply == Sats::ZERO => Some(0.0),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use std::convert::Infallible;
use brk_types::{Sats, StoredF64};
use vecdb::Rw;
use vecdb::ColumnId;
use super::{
LOSS_PERCENTILE_IDS, Levels, LossPercentileId, ModeId, Modes, PRICE_BAND_IDS, Percentiles,
PriceBandId, PriceBands, WeightedModeId,
};
use super::Vecs;
#[test]
fn bedrock_columns_match_public_band_order() {
assert_eq!(LossPercentileId::ALL, LOSS_PERCENTILE_IDS);
let percentiles = LossPercentileId::from_fn(|percentile| percentile);
fn empty_age_cohort_uses_zero_weight() {
assert_eq!(
percentiles,
Percentiles {
pct95: LossPercentileId::Pct95,
pct98: LossPercentileId::Pct98,
pct99: LossPercentileId::Pct99,
pct99_5: LossPercentileId::Pct99_5,
pct99_9: LossPercentileId::Pct99_9,
},
Vecs::<Rw>::resolve_age_value::<StoredF64>(None, Sats::ZERO),
Some(0.0)
);
assert_eq!(PriceBandId::ALL, PRICE_BAND_IDS);
let bands = PriceBandId::from_fn(|band| band);
assert_eq!(
bands,
PriceBands {
floor: Percentiles {
pct95: PriceBandId::FloorPct95,
pct98: PriceBandId::FloorPct98,
pct99: PriceBandId::FloorPct99,
pct99_5: PriceBandId::FloorPct99_5,
pct99_9: PriceBandId::FloorPct99_9,
},
level: Levels {
pct10: PriceBandId::LevelPct10,
pct20: PriceBandId::LevelPct20,
pct30: PriceBandId::LevelPct30,
pct40: PriceBandId::LevelPct40,
pct50: PriceBandId::LevelPct50,
pct60: PriceBandId::LevelPct60,
pct70: PriceBandId::LevelPct70,
pct80: PriceBandId::LevelPct80,
pct90: PriceBandId::LevelPct90,
},
},
);
let suffixes = LossPercentileId::from_fn(LossPercentileId::suffix);
assert_eq!(
suffixes,
Percentiles {
pct95: "pct95",
pct98: "pct98",
pct99: "pct99",
pct99_5: "pct99_5",
pct99_9: "pct99_9",
},
);
let suffixes = PriceBandId::from_fn(PriceBandId::suffix);
assert_eq!(
suffixes,
PriceBands {
floor: Percentiles {
pct95: "floor_pct95",
pct98: "floor_pct98",
pct99: "floor_pct99",
pct99_5: "floor_pct99_5",
pct99_9: "floor_pct99_9",
},
level: Levels {
pct10: "level_pct10",
pct20: "level_pct20",
pct30: "level_pct30",
pct40: "level_pct40",
pct50: "level_pct50",
pct60: "level_pct60",
pct70: "level_pct70",
pct80: "level_pct80",
pct90: "level_pct90",
},
},
Vecs::<Rw>::resolve_age_value(Some(StoredF64::NAN), Sats::ZERO),
Some(0.0)
);
}
#[test]
fn mode_ids_match_named_fields_and_storage_names() {
fn non_empty_age_cohort_requires_finite_weight() {
let supply = Sats::from(1_u64);
assert_eq!(
WeightedModeId::ALL.map(WeightedModeId::mode).as_slice(),
&ModeId::ALL[1..]
Vecs::<Rw>::resolve_age_value::<StoredF64>(None, supply),
None
);
assert_eq!(
WeightedModeId::COINFLOW_HORIZONS
.map(WeightedModeId::mode)
.as_slice(),
&ModeId::ALL[3..]
Vecs::<Rw>::resolve_age_value(Some(StoredF64::NAN), supply),
None
);
let mut modes = Modes::try_from_fn(|id| Ok::<_, Infallible>((id, false))).unwrap();
for id in ModeId::ALL {
let mode = modes.select_mut(id);
assert_eq!(mode.0, id);
mode.1 = true;
}
assert!(modes.iter().all(|(_, visited)| *visited));
assert_eq!(
ModeId::ALL.map(ModeId::name),
[
"raw",
"cointime",
"coinflow",
"coinflow_8y",
"coinflow_4y",
"coinflow_2y",
"coinflow_1y",
"coinflow_6m",
"coinflow_3m",
"coinflow_1m",
]
Vecs::<Rw>::resolve_age_value(Some(StoredF64::from(0.25)), supply),
Some(0.25)
);
}
}
@@ -1,3 +1,5 @@
mod mode_id;
mod modes;
pub(crate) use mode_id::WeightedModeId;
pub(crate) use modes::WeightedModes;
@@ -0,0 +1,54 @@
use super::super::{MODE_COUNT, ModeId};
const WEIGHTED_MODE_COUNT: usize = MODE_COUNT - 1;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum WeightedModeId {
Cointime,
Coinflow,
Coinflow8Y,
Coinflow4Y,
Coinflow2Y,
Coinflow1Y,
Coinflow6M,
Coinflow3M,
Coinflow1M,
}
impl WeightedModeId {
pub(crate) const ALL: [Self; WEIGHTED_MODE_COUNT] = [
Self::Cointime,
Self::Coinflow,
Self::Coinflow8Y,
Self::Coinflow4Y,
Self::Coinflow2Y,
Self::Coinflow1Y,
Self::Coinflow6M,
Self::Coinflow3M,
Self::Coinflow1M,
];
pub(crate) const COINFLOW_HORIZONS: [Self; 7] = [
Self::Coinflow8Y,
Self::Coinflow4Y,
Self::Coinflow2Y,
Self::Coinflow1Y,
Self::Coinflow6M,
Self::Coinflow3M,
Self::Coinflow1M,
];
pub(crate) const fn mode(self) -> ModeId {
match self {
Self::Cointime => ModeId::Cointime,
Self::Coinflow => ModeId::Coinflow,
Self::Coinflow8Y => ModeId::Coinflow8Y,
Self::Coinflow4Y => ModeId::Coinflow4Y,
Self::Coinflow2Y => ModeId::Coinflow2Y,
Self::Coinflow1Y => ModeId::Coinflow1Y,
Self::Coinflow6M => ModeId::Coinflow6M,
Self::Coinflow3M => ModeId::Coinflow3M,
Self::Coinflow1M => ModeId::Coinflow1M,
}
}
}
@@ -1,5 +1,7 @@
use brk_traversable::Traversable;
use super::WeightedModeId;
#[derive(Traversable)]
pub struct WeightedModes<T> {
pub cointime: T,
@@ -12,3 +14,93 @@ pub struct WeightedModes<T> {
pub coinflow_3m: T,
pub coinflow_1m: T,
}
impl<T> WeightedModes<T> {
pub(crate) fn from_fn(mut create: impl FnMut(WeightedModeId) -> T) -> Self {
Self {
cointime: create(WeightedModeId::Cointime),
coinflow: create(WeightedModeId::Coinflow),
coinflow_8y: create(WeightedModeId::Coinflow8Y),
coinflow_4y: create(WeightedModeId::Coinflow4Y),
coinflow_2y: create(WeightedModeId::Coinflow2Y),
coinflow_1y: create(WeightedModeId::Coinflow1Y),
coinflow_6m: create(WeightedModeId::Coinflow6M),
coinflow_3m: create(WeightedModeId::Coinflow3M),
coinflow_1m: create(WeightedModeId::Coinflow1M),
}
}
pub(crate) fn try_from_fn<E>(
mut create: impl FnMut(WeightedModeId) -> Result<T, E>,
) -> Result<Self, E> {
Ok(Self {
cointime: create(WeightedModeId::Cointime)?,
coinflow: create(WeightedModeId::Coinflow)?,
coinflow_8y: create(WeightedModeId::Coinflow8Y)?,
coinflow_4y: create(WeightedModeId::Coinflow4Y)?,
coinflow_2y: create(WeightedModeId::Coinflow2Y)?,
coinflow_1y: create(WeightedModeId::Coinflow1Y)?,
coinflow_6m: create(WeightedModeId::Coinflow6M)?,
coinflow_3m: create(WeightedModeId::Coinflow3M)?,
coinflow_1m: create(WeightedModeId::Coinflow1M)?,
})
}
pub(crate) fn select_mut(&mut self, id: WeightedModeId) -> &mut T {
match id {
WeightedModeId::Cointime => &mut self.cointime,
WeightedModeId::Coinflow => &mut self.coinflow,
WeightedModeId::Coinflow8Y => &mut self.coinflow_8y,
WeightedModeId::Coinflow4Y => &mut self.coinflow_4y,
WeightedModeId::Coinflow2Y => &mut self.coinflow_2y,
WeightedModeId::Coinflow1Y => &mut self.coinflow_1y,
WeightedModeId::Coinflow6M => &mut self.coinflow_6m,
WeightedModeId::Coinflow3M => &mut self.coinflow_3m,
WeightedModeId::Coinflow1M => &mut self.coinflow_1m,
}
}
pub(crate) fn select(&self, id: WeightedModeId) -> &T {
match id {
WeightedModeId::Cointime => &self.cointime,
WeightedModeId::Coinflow => &self.coinflow,
WeightedModeId::Coinflow8Y => &self.coinflow_8y,
WeightedModeId::Coinflow4Y => &self.coinflow_4y,
WeightedModeId::Coinflow2Y => &self.coinflow_2y,
WeightedModeId::Coinflow1Y => &self.coinflow_1y,
WeightedModeId::Coinflow6M => &self.coinflow_6m,
WeightedModeId::Coinflow3M => &self.coinflow_3m,
WeightedModeId::Coinflow1M => &self.coinflow_1m,
}
}
pub(crate) fn iter(&self) -> impl Iterator<Item = &T> {
[
&self.cointime,
&self.coinflow,
&self.coinflow_8y,
&self.coinflow_4y,
&self.coinflow_2y,
&self.coinflow_1y,
&self.coinflow_6m,
&self.coinflow_3m,
&self.coinflow_1m,
]
.into_iter()
}
pub(crate) fn iter_mut(&mut self) -> impl Iterator<Item = &mut T> {
[
&mut self.cointime,
&mut self.coinflow,
&mut self.coinflow_8y,
&mut self.coinflow_4y,
&mut self.coinflow_2y,
&mut self.coinflow_1y,
&mut self.coinflow_6m,
&mut self.coinflow_3m,
&mut self.coinflow_1m,
]
.into_iter()
}
}
@@ -0,0 +1,20 @@
use brk_types::UrpdWeight;
#[derive(Default)]
pub(super) struct WeightedPair<T> {
pub(super) cointime: T,
pub(super) coinflow: T,
}
impl<T> WeightedPair<T> {
pub(super) fn from_fn(mut create: impl FnMut(UrpdWeight) -> T) -> Self {
Self {
cointime: create(UrpdWeight::Cointime),
coinflow: create(UrpdWeight::Coinflow),
}
}
pub(super) fn iter(&self) -> impl Iterator<Item = &T> {
[&self.cointime, &self.coinflow].into_iter()
}
}

Some files were not shown because too many files have changed in this diff Show More