mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-08-12 02:03:09 -07:00
global: massive columnar rework part 3
This commit is contained in:
+19067
-4709
File diff suppressed because it is too large
Load Diff
@@ -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)]
|
||||
|
||||
+3
-4
@@ -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,
|
||||
|
||||
+4
-3
@@ -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) => {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user