brk: metric trimming part X + 4

This commit is contained in:
nym21
2026-07-31 17:44:14 +02:00
parent cc5b6da341
commit be3136d579
848 changed files with 10364 additions and 10754 deletions
Generated
-4
View File
@@ -1291,8 +1291,6 @@ checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
[[package]]
name = "fjall"
version = "3.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "420a84699b8ccbb1ed573e38e88f4f23637b45beab6432066452f834be469c57"
dependencies = [
"byteorder-lite",
"byteview",
@@ -2029,8 +2027,6 @@ checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
[[package]]
name = "lsm-tree"
version = "3.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "055a908d502129cf63bedae52f2db222e4436d2da32a69df9b84ac9fb9147761"
dependencies = [
"byteorder-lite",
"byteview",
+1 -1
View File
@@ -64,7 +64,7 @@ color-eyre = "0.6.5"
corepc-jsonrpc = { package = "jsonrpc", version = "0.19.0", features = ["simple_http"], default-features = false }
corepc-types = { version = "0.15.0", features = ["std"], default-features = false }
derive_more = { version = "2.1.1", features = ["deref", "deref_mut"] }
fjall = "3.1.8"
fjall = { path = "../fjall", version = "3.1.8" }
indexmap = { version = "2.14.0", features = ["serde"] }
jiff = { version = "0.2.34", features = ["perf-inline", "tz-system"], default-features = false }
owo-colors = "4.3.0"
+2 -1
View File
@@ -109,7 +109,8 @@ impl<'a> Ctx<'a> {
for &name in BLOCK_FIELDS {
obj.insert(
name.into(),
self.block_field(name, None, &[]).expect("known block field"),
self.block_field(name, None, &[])
.expect("known block field"),
);
}
Value::Object(obj)
+1 -1
View File
@@ -46,7 +46,7 @@ fn run() -> Result<()> {
.map(|n| n.get())
.unwrap_or(2)
/ 2)
.max(1);
.max(1);
for block in reader.range_with(start, end, parser_threads)?.iter() {
let block = block?;
let line = formatter.format(&Ctx::new(&block, network))?;
+1 -3
View File
@@ -16,9 +16,7 @@ impl Mode {
));
}
if compact && n_fields == 0 {
return Err(Error::Parse(
"--compact requires at least one field".into(),
));
return Err(Error::Parse("--compact requires at least one field".into()));
}
Ok(if pretty {
Self::Pretty
+2 -1
View File
@@ -181,7 +181,8 @@ fn ex(cmd: &str, note: &str) {
}
fn bold(s: &str) -> String {
s.if_supports_color(Stream::Stdout, |t| t.bold()).to_string()
s.if_supports_color(Stream::Stdout, |t| t.bold())
.to_string()
}
fn dim(s: &str) -> String {
+257 -206
View File
@@ -1388,42 +1388,6 @@ impl CapCapitalizedGrossLossMvrvNetPeakPriceProfitSellSoprPattern {
}
}
/// Pattern struct for repeated tree structure.
pub struct CapCapitalizedGrossLossMvrvNetPeakPriceProfitSellSoprPattern2 {
pub cap: CentsDeltaToUsdPattern,
pub capitalized: PricePattern,
pub gross_pnl: BlockCumulativeSumPattern,
pub loss: BlockCumulativeNegativeSumPattern,
pub mvrv: SeriesPattern1<StoredF32>,
pub net_pnl: BlockChangeCumulativeDeltaSumPattern,
pub peak_regret: BlockCumulativeSumPattern,
pub price: CentsPpmRatioSatsUsdPattern,
pub profit: BlockCumulativeSumPattern,
pub profit_to_loss_ratio: _1m1w1y24hPattern<StoredF64>,
pub sell_side_risk_ratio: _1m1w1y24hPattern8,
pub sopr: RatioValuePattern2,
}
impl CapCapitalizedGrossLossMvrvNetPeakPriceProfitSellSoprPattern2 {
/// Create a new pattern node with accumulated series name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
cap: CentsDeltaToUsdPattern::new(client.clone(), _m(&acc, "realized_cap")),
capitalized: PricePattern::new(client.clone(), _m(&acc, "capitalized_price")),
gross_pnl: BlockCumulativeSumPattern::new(client.clone(), _m(&acc, "realized_gross_pnl")),
loss: BlockCumulativeNegativeSumPattern::new(client.clone(), _m(&acc, "realized_loss")),
mvrv: SeriesPattern1::new(client.clone(), _m(&acc, "mvrv")),
net_pnl: BlockChangeCumulativeDeltaSumPattern::new(client.clone(), _m(&acc, "net")),
peak_regret: BlockCumulativeSumPattern::new(client.clone(), _m(&acc, "realized_peak_regret")),
price: CentsPpmRatioSatsUsdPattern::new(client.clone(), _m(&acc, "realized_price")),
profit: BlockCumulativeSumPattern::new(client.clone(), _m(&acc, "realized_profit")),
profit_to_loss_ratio: _1m1w1y24hPattern::new(client.clone(), _m(&acc, "realized_profit_to_loss_ratio")),
sell_side_risk_ratio: _1m1w1y24hPattern8::new(client.clone(), _m(&acc, "sell_side_risk_ratio")),
sopr: RatioValuePattern2::new(client.clone(), acc.clone()),
}
}
}
/// Pattern struct for repeated tree structure.
pub struct EmptyOpP2aP2msP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshUnknownPattern2 {
pub empty: _1m1w1y24hPercentPpmRatioPattern,
@@ -1877,32 +1841,6 @@ pub struct ActiveInputOutputSpendablePattern {
pub spendable_output_to_reused_addr_share: _1m1w1y24hPercentPpmRatioPattern,
}
/// Pattern struct for repeated tree structure.
pub struct ActivityCostInvestedOutputsRealizedSupplyUnrealizedPattern2 {
pub activity: CoindaysCoinyearsDormancyTransferPattern,
pub cost_basis: InMaxMinPerSupplyPattern,
pub invested_capital: InPattern,
pub outputs: SpentUnspentPattern,
pub realized: CapCapitalizedGrossLossMvrvNetPeakPriceProfitSellSoprPattern2,
pub supply: DeltaDominanceHalfInTotalPattern2,
pub unrealized: CapitalizedGrossInvestedLossNetNuplProfitSentimentPattern2,
}
impl ActivityCostInvestedOutputsRealizedSupplyUnrealizedPattern2 {
/// Create a new pattern node with accumulated series name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
activity: CoindaysCoinyearsDormancyTransferPattern::new(client.clone(), acc.clone()),
cost_basis: InMaxMinPerSupplyPattern::new(client.clone(), acc.clone()),
invested_capital: InPattern::new(client.clone(), _m(&acc, "invested_capital_in")),
outputs: SpentUnspentPattern::new(client.clone(), acc.clone()),
realized: CapCapitalizedGrossLossMvrvNetPeakPriceProfitSellSoprPattern2::new(client.clone(), acc.clone()),
supply: DeltaDominanceHalfInTotalPattern2::new(client.clone(), _m(&acc, "supply")),
unrealized: CapitalizedGrossInvestedLossNetNuplProfitSentimentPattern2::new(client.clone(), acc.clone()),
}
}
}
/// Pattern struct for repeated tree structure.
pub struct CapLossMvrvNetPriceProfitSoprPattern {
pub cap: CentsDeltaUsdPattern,
@@ -2033,30 +1971,6 @@ impl<T: DeserializeOwned> MaxMedianMinPct10Pct25Pct75Pct90Pattern<T> {
}
}
/// Pattern struct for repeated tree structure.
pub struct ActivityAddrOutputsRealizedSupplyUnrealizedPattern {
pub activity: TransferPattern,
pub addr_count: BaseDeltaPattern,
pub outputs: SpentUnspentPattern,
pub realized: CapLossMvrvPriceProfitPattern,
pub supply: DeltaDominanceTotalPattern,
pub unrealized: NuplPattern,
}
impl ActivityAddrOutputsRealizedSupplyUnrealizedPattern {
/// Create a new pattern node with accumulated series name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
activity: TransferPattern::new(client.clone(), _m(&acc, "transfer_volume")),
addr_count: BaseDeltaPattern::new(client.clone(), _m(&acc, "addr_count")),
outputs: SpentUnspentPattern::new(client.clone(), acc.clone()),
realized: CapLossMvrvPriceProfitPattern::new(client.clone(), acc.clone()),
supply: DeltaDominanceTotalPattern::new(client.clone(), _m(&acc, "supply")),
unrealized: NuplPattern::new(client.clone(), _m(&acc, "nupl")),
}
}
}
/// Pattern struct for repeated tree structure.
pub struct AverageBlockCumulativeInSumPattern {
pub average: _1m1w1y24hPattern3,
@@ -2241,6 +2155,28 @@ impl ActivityOutputsRealizedSupplyUnrealizedPattern {
}
}
/// Pattern struct for repeated tree structure.
pub struct ActivityAddrOutputsRealizedSupplyPattern {
pub activity: TransferPattern,
pub addr_count: BaseDeltaPattern,
pub outputs: UnspentPattern,
pub realized: CapLossProfitPattern,
pub supply: DeltaDominanceTotalPattern,
}
impl ActivityAddrOutputsRealizedSupplyPattern {
/// Create a new pattern node with accumulated series name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
activity: TransferPattern::new(client.clone(), _m(&acc, "transfer_volume")),
addr_count: BaseDeltaPattern::new(client.clone(), _m(&acc, "addr_count")),
outputs: UnspentPattern::new(client.clone(), _m(&acc, "utxo_count")),
realized: CapLossProfitPattern::new(client.clone(), _m(&acc, "realized")),
supply: DeltaDominanceTotalPattern::new(client.clone(), _m(&acc, "supply")),
}
}
}
/// Pattern struct for repeated tree structure.
pub struct ActivityOutputsRealizedSupplyUnrealizedPattern3 {
pub activity: TransferPattern,
@@ -3013,6 +2949,24 @@ impl BlocksDominanceRewardsPattern {
}
}
/// Pattern struct for repeated tree structure.
pub struct CapLossProfitPattern {
pub cap: CentsDeltaUsdPattern,
pub loss: BlockCumulativeSumPattern,
pub profit: BlockCumulativeSumPattern,
}
impl CapLossProfitPattern {
/// Create a new pattern node with accumulated series name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
cap: CentsDeltaUsdPattern::new(client.clone(), _m(&acc, "cap")),
loss: BlockCumulativeSumPattern::new(client.clone(), _m(&acc, "loss")),
profit: BlockCumulativeSumPattern::new(client.clone(), _m(&acc, "profit")),
}
}
}
/// Pattern struct for repeated tree structure.
pub struct CentsSatsUsdPattern3 {
pub cents: SeriesPattern2<Cents>,
@@ -3726,22 +3680,6 @@ impl PpmRatioPattern {
}
}
/// Pattern struct for repeated tree structure.
pub struct RatioValuePattern2 {
pub ratio: _1m1w1y24hPattern<StoredF64>,
pub value_destroyed: AverageBlockCumulativeSumPattern<Cents>,
}
impl RatioValuePattern2 {
/// Create a new pattern node with accumulated series name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
ratio: _1m1w1y24hPattern::new(client.clone(), _m(&acc, "sopr")),
value_destroyed: AverageBlockCumulativeSumPattern::new(client.clone(), _m(&acc, "value_destroyed")),
}
}
}
/// Pattern struct for repeated tree structure.
pub struct RatioValuePattern {
pub ratio: _24hPattern,
@@ -3908,6 +3846,20 @@ impl TransferPattern {
}
}
/// Pattern struct for repeated tree structure.
pub struct UnspentPattern {
pub unspent_count: BaseDeltaPattern,
}
impl UnspentPattern {
/// Create a new pattern node with accumulated series name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
unspent_count: BaseDeltaPattern::new(client.clone(), acc.clone()),
}
}
}
// Series tree
/// Series tree node.
@@ -5952,8 +5904,10 @@ pub struct SeriesTree_Cointime_AgeRange {
pub _3m_to_4m: CoindaysLivelinessRatioSupplyVaultednessPattern,
pub _4m_to_5m: CoindaysLivelinessRatioSupplyVaultednessPattern,
pub _5m_to_6m: CoindaysLivelinessRatioSupplyVaultednessPattern,
pub _6m_to_1y: CoindaysLivelinessRatioSupplyVaultednessPattern,
pub _1y_to_2y: CoindaysLivelinessRatioSupplyVaultednessPattern,
pub _6m_to_9m: CoindaysLivelinessRatioSupplyVaultednessPattern,
pub _9m_to_1y: CoindaysLivelinessRatioSupplyVaultednessPattern,
pub _1y_to_18m: CoindaysLivelinessRatioSupplyVaultednessPattern,
pub _18m_to_2y: CoindaysLivelinessRatioSupplyVaultednessPattern,
pub _2y_to_3y: CoindaysLivelinessRatioSupplyVaultednessPattern,
pub _3y_to_4y: CoindaysLivelinessRatioSupplyVaultednessPattern,
pub _4y_to_5y: CoindaysLivelinessRatioSupplyVaultednessPattern,
@@ -5978,8 +5932,10 @@ impl SeriesTree_Cointime_AgeRange {
_3m_to_4m: CoindaysLivelinessRatioSupplyVaultednessPattern::new(client.clone(), "utxos_3m_to_4m_old".to_string()),
_4m_to_5m: CoindaysLivelinessRatioSupplyVaultednessPattern::new(client.clone(), "utxos_4m_to_5m_old".to_string()),
_5m_to_6m: CoindaysLivelinessRatioSupplyVaultednessPattern::new(client.clone(), "utxos_5m_to_6m_old".to_string()),
_6m_to_1y: CoindaysLivelinessRatioSupplyVaultednessPattern::new(client.clone(), "utxos_6m_to_1y_old".to_string()),
_1y_to_2y: CoindaysLivelinessRatioSupplyVaultednessPattern::new(client.clone(), "utxos_1y_to_2y_old".to_string()),
_6m_to_9m: CoindaysLivelinessRatioSupplyVaultednessPattern::new(client.clone(), "utxos_6m_to_9m_old".to_string()),
_9m_to_1y: CoindaysLivelinessRatioSupplyVaultednessPattern::new(client.clone(), "utxos_9m_to_1y_old".to_string()),
_1y_to_18m: CoindaysLivelinessRatioSupplyVaultednessPattern::new(client.clone(), "utxos_1y_to_18m_old".to_string()),
_18m_to_2y: CoindaysLivelinessRatioSupplyVaultednessPattern::new(client.clone(), "utxos_18m_to_2y_old".to_string()),
_2y_to_3y: CoindaysLivelinessRatioSupplyVaultednessPattern::new(client.clone(), "utxos_2y_to_3y_old".to_string()),
_3y_to_4y: CoindaysLivelinessRatioSupplyVaultednessPattern::new(client.clone(), "utxos_3y_to_4y_old".to_string()),
_4y_to_5y: CoindaysLivelinessRatioSupplyVaultednessPattern::new(client.clone(), "utxos_4y_to_5y_old".to_string()),
@@ -6157,8 +6113,10 @@ pub struct SeriesTree_Coinflow_AgeRange {
pub _3m_to_4m: MobilitySpendingSupplyPattern,
pub _4m_to_5m: MobilitySpendingSupplyPattern,
pub _5m_to_6m: MobilitySpendingSupplyPattern,
pub _6m_to_1y: MobilitySpendingSupplyPattern,
pub _1y_to_2y: MobilitySpendingSupplyPattern,
pub _6m_to_9m: MobilitySpendingSupplyPattern,
pub _9m_to_1y: MobilitySpendingSupplyPattern,
pub _1y_to_18m: MobilitySpendingSupplyPattern,
pub _18m_to_2y: MobilitySpendingSupplyPattern,
pub _2y_to_3y: MobilitySpendingSupplyPattern,
pub _3y_to_4y: MobilitySpendingSupplyPattern,
pub _4y_to_5y: MobilitySpendingSupplyPattern,
@@ -6183,8 +6141,10 @@ impl SeriesTree_Coinflow_AgeRange {
_3m_to_4m: MobilitySpendingSupplyPattern::new(client.clone(), "utxos_3m_to_4m_old".to_string()),
_4m_to_5m: MobilitySpendingSupplyPattern::new(client.clone(), "utxos_4m_to_5m_old".to_string()),
_5m_to_6m: MobilitySpendingSupplyPattern::new(client.clone(), "utxos_5m_to_6m_old".to_string()),
_6m_to_1y: MobilitySpendingSupplyPattern::new(client.clone(), "utxos_6m_to_1y_old".to_string()),
_1y_to_2y: MobilitySpendingSupplyPattern::new(client.clone(), "utxos_1y_to_2y_old".to_string()),
_6m_to_9m: MobilitySpendingSupplyPattern::new(client.clone(), "utxos_6m_to_9m_old".to_string()),
_9m_to_1y: MobilitySpendingSupplyPattern::new(client.clone(), "utxos_9m_to_1y_old".to_string()),
_1y_to_18m: MobilitySpendingSupplyPattern::new(client.clone(), "utxos_1y_to_18m_old".to_string()),
_18m_to_2y: MobilitySpendingSupplyPattern::new(client.clone(), "utxos_18m_to_2y_old".to_string()),
_2y_to_3y: MobilitySpendingSupplyPattern::new(client.clone(), "utxos_2y_to_3y_old".to_string()),
_3y_to_4y: MobilitySpendingSupplyPattern::new(client.clone(), "utxos_3y_to_4y_old".to_string()),
_4y_to_5y: MobilitySpendingSupplyPattern::new(client.clone(), "utxos_4y_to_5y_old".to_string()),
@@ -8219,7 +8179,7 @@ impl SeriesTree_Cohorts {
pub struct SeriesTree_Cohorts_Utxo {
pub all: SeriesTree_Cohorts_Utxo_All,
pub sth: SeriesTree_Cohorts_Utxo_Sth,
pub lth: ActivityCostInvestedOutputsRealizedSupplyUnrealizedPattern2,
pub lth: SeriesTree_Cohorts_Utxo_Lth,
pub age_range: SeriesTree_Cohorts_Utxo_AgeRange,
pub under_age: SeriesTree_Cohorts_Utxo_UnderAge,
pub over_age: SeriesTree_Cohorts_Utxo_OverAge,
@@ -8239,7 +8199,7 @@ impl SeriesTree_Cohorts_Utxo {
Self {
all: SeriesTree_Cohorts_Utxo_All::new(client.clone(), format!("{base_path}_all")),
sth: SeriesTree_Cohorts_Utxo_Sth::new(client.clone(), format!("{base_path}_sth")),
lth: ActivityCostInvestedOutputsRealizedSupplyUnrealizedPattern2::new(client.clone(), "lth".to_string()),
lth: SeriesTree_Cohorts_Utxo_Lth::new(client.clone(), format!("{base_path}_lth")),
age_range: SeriesTree_Cohorts_Utxo_AgeRange::new(client.clone(), format!("{base_path}_age_range")),
under_age: SeriesTree_Cohorts_Utxo_UnderAge::new(client.clone(), format!("{base_path}_under_age")),
over_age: SeriesTree_Cohorts_Utxo_OverAge::new(client.clone(), format!("{base_path}_over_age")),
@@ -8537,6 +8497,81 @@ impl SeriesTree_Cohorts_Utxo_Sth {
}
}
/// Series tree node.
pub struct SeriesTree_Cohorts_Utxo_Lth {
pub supply: DeltaDominanceHalfInTotalPattern2,
pub outputs: SpentUnspentPattern,
pub activity: CoindaysCoinyearsDormancyTransferPattern,
pub realized: SeriesTree_Cohorts_Utxo_Lth_Realized,
pub cost_basis: InMaxMinPerSupplyPattern,
pub unrealized: CapitalizedGrossInvestedLossNetNuplProfitSentimentPattern2,
pub invested_capital: InPattern,
}
impl SeriesTree_Cohorts_Utxo_Lth {
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
Self {
supply: DeltaDominanceHalfInTotalPattern2::new(client.clone(), "lth_supply".to_string()),
outputs: SpentUnspentPattern::new(client.clone(), "lth".to_string()),
activity: CoindaysCoinyearsDormancyTransferPattern::new(client.clone(), "lth".to_string()),
realized: SeriesTree_Cohorts_Utxo_Lth_Realized::new(client.clone(), format!("{base_path}_realized")),
cost_basis: InMaxMinPerSupplyPattern::new(client.clone(), "lth".to_string()),
unrealized: CapitalizedGrossInvestedLossNetNuplProfitSentimentPattern2::new(client.clone(), "lth".to_string()),
invested_capital: InPattern::new(client.clone(), "lth_invested_capital_in".to_string()),
}
}
}
/// Series tree node.
pub struct SeriesTree_Cohorts_Utxo_Lth_Realized {
pub cap: CentsDeltaToUsdPattern,
pub profit: BlockCumulativeSumPattern,
pub loss: BlockCumulativeNegativeSumPattern,
pub price: CentsPpmRatioSatsUsdPattern,
pub mvrv: SeriesPattern1<StoredF32>,
pub net_pnl: BlockChangeCumulativeDeltaSumPattern,
pub sopr: SeriesTree_Cohorts_Utxo_Lth_Realized_Sopr,
pub gross_pnl: BlockCumulativeSumPattern,
pub sell_side_risk_ratio: _1m1w1y24hPattern8,
pub peak_regret: BlockCumulativeSumPattern,
pub capitalized: PricePattern,
pub profit_to_loss_ratio: _1m1w1y24hPattern<StoredF64>,
}
impl SeriesTree_Cohorts_Utxo_Lth_Realized {
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
Self {
cap: CentsDeltaToUsdPattern::new(client.clone(), "lth_realized_cap".to_string()),
profit: BlockCumulativeSumPattern::new(client.clone(), "lth_realized_profit".to_string()),
loss: BlockCumulativeNegativeSumPattern::new(client.clone(), "lth_realized_loss".to_string()),
price: CentsPpmRatioSatsUsdPattern::new(client.clone(), "lth_realized_price".to_string()),
mvrv: SeriesPattern1::new(client.clone(), "lth_mvrv".to_string()),
net_pnl: BlockChangeCumulativeDeltaSumPattern::new(client.clone(), "lth_net".to_string()),
sopr: SeriesTree_Cohorts_Utxo_Lth_Realized_Sopr::new(client.clone(), format!("{base_path}_sopr")),
gross_pnl: BlockCumulativeSumPattern::new(client.clone(), "lth_realized_gross_pnl".to_string()),
sell_side_risk_ratio: _1m1w1y24hPattern8::new(client.clone(), "lth_sell_side_risk_ratio".to_string()),
peak_regret: BlockCumulativeSumPattern::new(client.clone(), "lth_realized_peak_regret".to_string()),
capitalized: PricePattern::new(client.clone(), "lth_capitalized_price".to_string()),
profit_to_loss_ratio: _1m1w1y24hPattern::new(client.clone(), "lth_realized_profit_to_loss_ratio".to_string()),
}
}
}
/// Series tree node.
pub struct SeriesTree_Cohorts_Utxo_Lth_Realized_Sopr {
pub value_destroyed: AverageBlockCumulativeSumPattern<Cents>,
pub ratio: _1m1w1y24hPattern<StoredF64>,
}
impl SeriesTree_Cohorts_Utxo_Lth_Realized_Sopr {
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
Self {
value_destroyed: AverageBlockCumulativeSumPattern::new(client.clone(), "lth_value_destroyed".to_string()),
ratio: _1m1w1y24hPattern::new(client.clone(), "lth_sopr".to_string()),
}
}
}
/// Series tree node.
pub struct SeriesTree_Cohorts_Utxo_AgeRange {
pub under_1h: ActivityOutputsRealizedSupplyUnrealizedPattern,
@@ -8548,8 +8583,10 @@ pub struct SeriesTree_Cohorts_Utxo_AgeRange {
pub _3m_to_4m: ActivityOutputsRealizedSupplyUnrealizedPattern,
pub _4m_to_5m: ActivityOutputsRealizedSupplyUnrealizedPattern,
pub _5m_to_6m: ActivityOutputsRealizedSupplyUnrealizedPattern,
pub _6m_to_1y: ActivityOutputsRealizedSupplyUnrealizedPattern,
pub _1y_to_2y: ActivityOutputsRealizedSupplyUnrealizedPattern,
pub _6m_to_9m: ActivityOutputsRealizedSupplyUnrealizedPattern,
pub _9m_to_1y: ActivityOutputsRealizedSupplyUnrealizedPattern,
pub _1y_to_18m: ActivityOutputsRealizedSupplyUnrealizedPattern,
pub _18m_to_2y: ActivityOutputsRealizedSupplyUnrealizedPattern,
pub _2y_to_3y: ActivityOutputsRealizedSupplyUnrealizedPattern,
pub _3y_to_4y: ActivityOutputsRealizedSupplyUnrealizedPattern,
pub _4y_to_5y: ActivityOutputsRealizedSupplyUnrealizedPattern,
@@ -8574,8 +8611,10 @@ impl SeriesTree_Cohorts_Utxo_AgeRange {
_3m_to_4m: ActivityOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "utxos_3m_to_4m_old".to_string()),
_4m_to_5m: ActivityOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "utxos_4m_to_5m_old".to_string()),
_5m_to_6m: ActivityOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "utxos_5m_to_6m_old".to_string()),
_6m_to_1y: ActivityOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "utxos_6m_to_1y_old".to_string()),
_1y_to_2y: ActivityOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "utxos_1y_to_2y_old".to_string()),
_6m_to_9m: ActivityOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "utxos_6m_to_9m_old".to_string()),
_9m_to_1y: ActivityOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "utxos_9m_to_1y_old".to_string()),
_1y_to_18m: ActivityOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "utxos_1y_to_18m_old".to_string()),
_18m_to_2y: ActivityOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "utxos_18m_to_2y_old".to_string()),
_2y_to_3y: ActivityOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "utxos_2y_to_3y_old".to_string()),
_3y_to_4y: ActivityOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "utxos_3y_to_4y_old".to_string()),
_4y_to_5y: ActivityOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "utxos_4y_to_5y_old".to_string()),
@@ -8599,7 +8638,9 @@ pub struct SeriesTree_Cohorts_Utxo_UnderAge {
pub _4m: ActivityOutputsRealizedSupplyUnrealizedPattern,
pub _5m: ActivityOutputsRealizedSupplyUnrealizedPattern,
pub _6m: ActivityOutputsRealizedSupplyUnrealizedPattern,
pub _9m: ActivityOutputsRealizedSupplyUnrealizedPattern,
pub _1y: ActivityOutputsRealizedSupplyUnrealizedPattern,
pub _18m: ActivityOutputsRealizedSupplyUnrealizedPattern,
pub _2y: ActivityOutputsRealizedSupplyUnrealizedPattern,
pub _3y: ActivityOutputsRealizedSupplyUnrealizedPattern,
pub _4y: ActivityOutputsRealizedSupplyUnrealizedPattern,
@@ -8622,7 +8663,9 @@ impl SeriesTree_Cohorts_Utxo_UnderAge {
_4m: ActivityOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "utxos_under_4m_old".to_string()),
_5m: ActivityOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "utxos_under_5m_old".to_string()),
_6m: ActivityOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "utxos_under_6m_old".to_string()),
_9m: ActivityOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "utxos_under_9m_old".to_string()),
_1y: ActivityOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "utxos_under_1y_old".to_string()),
_18m: ActivityOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "utxos_under_18m_old".to_string()),
_2y: ActivityOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "utxos_under_2y_old".to_string()),
_3y: ActivityOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "utxos_under_3y_old".to_string()),
_4y: ActivityOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "utxos_under_4y_old".to_string()),
@@ -8647,7 +8690,9 @@ pub struct SeriesTree_Cohorts_Utxo_OverAge {
pub _4m: ActivityOutputsRealizedSupplyUnrealizedPattern,
pub _5m: ActivityOutputsRealizedSupplyUnrealizedPattern,
pub _6m: ActivityOutputsRealizedSupplyUnrealizedPattern,
pub _9m: ActivityOutputsRealizedSupplyUnrealizedPattern,
pub _1y: ActivityOutputsRealizedSupplyUnrealizedPattern,
pub _18m: ActivityOutputsRealizedSupplyUnrealizedPattern,
pub _2y: ActivityOutputsRealizedSupplyUnrealizedPattern,
pub _3y: ActivityOutputsRealizedSupplyUnrealizedPattern,
pub _4y: ActivityOutputsRealizedSupplyUnrealizedPattern,
@@ -8670,7 +8715,9 @@ impl SeriesTree_Cohorts_Utxo_OverAge {
_4m: ActivityOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "utxos_over_4m_old".to_string()),
_5m: ActivityOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "utxos_over_5m_old".to_string()),
_6m: ActivityOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "utxos_over_6m_old".to_string()),
_9m: ActivityOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "utxos_over_9m_old".to_string()),
_1y: ActivityOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "utxos_over_1y_old".to_string()),
_18m: ActivityOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "utxos_over_18m_old".to_string()),
_2y: ActivityOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "utxos_over_2y_old".to_string()),
_3y: ActivityOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "utxos_over_3y_old".to_string()),
_4y: ActivityOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "utxos_over_4y_old".to_string()),
@@ -8754,15 +8801,15 @@ impl SeriesTree_Cohorts_Utxo_Class {
/// Series tree node.
pub struct SeriesTree_Cohorts_Utxo_Entry {
pub discount: ActivityCostInvestedOutputsRealizedSupplyUnrealizedPattern2,
pub premium: ActivityCostInvestedOutputsRealizedSupplyUnrealizedPattern2,
pub discount: ActivityOutputsRealizedSupplyUnrealizedPattern,
pub premium: ActivityOutputsRealizedSupplyUnrealizedPattern,
}
impl SeriesTree_Cohorts_Utxo_Entry {
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
Self {
discount: ActivityCostInvestedOutputsRealizedSupplyUnrealizedPattern2::new(client.clone(), "veteran".to_string()),
premium: ActivityCostInvestedOutputsRealizedSupplyUnrealizedPattern2::new(client.clone(), "rookie".to_string()),
discount: ActivityOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "veteran".to_string()),
premium: ActivityOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "rookie".to_string()),
}
}
}
@@ -9072,8 +9119,10 @@ pub struct SeriesTree_Cohorts_Utxo_Matured {
pub _3m_to_4m: AverageBlockCumulativeSumPattern2,
pub _4m_to_5m: AverageBlockCumulativeSumPattern2,
pub _5m_to_6m: AverageBlockCumulativeSumPattern2,
pub _6m_to_1y: AverageBlockCumulativeSumPattern2,
pub _1y_to_2y: AverageBlockCumulativeSumPattern2,
pub _6m_to_9m: AverageBlockCumulativeSumPattern2,
pub _9m_to_1y: AverageBlockCumulativeSumPattern2,
pub _1y_to_18m: AverageBlockCumulativeSumPattern2,
pub _18m_to_2y: AverageBlockCumulativeSumPattern2,
pub _2y_to_3y: AverageBlockCumulativeSumPattern2,
pub _3y_to_4y: AverageBlockCumulativeSumPattern2,
pub _4y_to_5y: AverageBlockCumulativeSumPattern2,
@@ -9098,8 +9147,10 @@ impl SeriesTree_Cohorts_Utxo_Matured {
_3m_to_4m: AverageBlockCumulativeSumPattern2::new(client.clone(), "utxos_3m_to_4m_old_matured_supply".to_string()),
_4m_to_5m: AverageBlockCumulativeSumPattern2::new(client.clone(), "utxos_4m_to_5m_old_matured_supply".to_string()),
_5m_to_6m: AverageBlockCumulativeSumPattern2::new(client.clone(), "utxos_5m_to_6m_old_matured_supply".to_string()),
_6m_to_1y: AverageBlockCumulativeSumPattern2::new(client.clone(), "utxos_6m_to_1y_old_matured_supply".to_string()),
_1y_to_2y: AverageBlockCumulativeSumPattern2::new(client.clone(), "utxos_1y_to_2y_old_matured_supply".to_string()),
_6m_to_9m: AverageBlockCumulativeSumPattern2::new(client.clone(), "utxos_6m_to_9m_old_matured_supply".to_string()),
_9m_to_1y: AverageBlockCumulativeSumPattern2::new(client.clone(), "utxos_9m_to_1y_old_matured_supply".to_string()),
_1y_to_18m: AverageBlockCumulativeSumPattern2::new(client.clone(), "utxos_1y_to_18m_old_matured_supply".to_string()),
_18m_to_2y: AverageBlockCumulativeSumPattern2::new(client.clone(), "utxos_18m_to_2y_old_matured_supply".to_string()),
_2y_to_3y: AverageBlockCumulativeSumPattern2::new(client.clone(), "utxos_2y_to_3y_old_matured_supply".to_string()),
_3y_to_4y: AverageBlockCumulativeSumPattern2::new(client.clone(), "utxos_3y_to_4y_old_matured_supply".to_string()),
_4y_to_5y: AverageBlockCumulativeSumPattern2::new(client.clone(), "utxos_4y_to_5y_old_matured_supply".to_string()),
@@ -9133,115 +9184,115 @@ impl SeriesTree_Cohorts_Addr {
/// Series tree node.
pub struct SeriesTree_Cohorts_Addr_OverAmount {
pub _1sat: ActivityAddrOutputsRealizedSupplyUnrealizedPattern,
pub _10sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern,
pub _100sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern,
pub _1k_sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern,
pub _10k_sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern,
pub _100k_sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern,
pub _1m_sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern,
pub _10m_sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern,
pub _1btc: ActivityAddrOutputsRealizedSupplyUnrealizedPattern,
pub _10btc: ActivityAddrOutputsRealizedSupplyUnrealizedPattern,
pub _100btc: ActivityAddrOutputsRealizedSupplyUnrealizedPattern,
pub _1k_btc: ActivityAddrOutputsRealizedSupplyUnrealizedPattern,
pub _10k_btc: ActivityAddrOutputsRealizedSupplyUnrealizedPattern,
pub _1sat: ActivityAddrOutputsRealizedSupplyPattern,
pub _10sats: ActivityAddrOutputsRealizedSupplyPattern,
pub _100sats: ActivityAddrOutputsRealizedSupplyPattern,
pub _1k_sats: ActivityAddrOutputsRealizedSupplyPattern,
pub _10k_sats: ActivityAddrOutputsRealizedSupplyPattern,
pub _100k_sats: ActivityAddrOutputsRealizedSupplyPattern,
pub _1m_sats: ActivityAddrOutputsRealizedSupplyPattern,
pub _10m_sats: ActivityAddrOutputsRealizedSupplyPattern,
pub _1btc: ActivityAddrOutputsRealizedSupplyPattern,
pub _10btc: ActivityAddrOutputsRealizedSupplyPattern,
pub _100btc: ActivityAddrOutputsRealizedSupplyPattern,
pub _1k_btc: ActivityAddrOutputsRealizedSupplyPattern,
pub _10k_btc: ActivityAddrOutputsRealizedSupplyPattern,
}
impl SeriesTree_Cohorts_Addr_OverAmount {
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
Self {
_1sat: ActivityAddrOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "addrs_over_1sat".to_string()),
_10sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "addrs_over_10sats".to_string()),
_100sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "addrs_over_100sats".to_string()),
_1k_sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "addrs_over_1k_sats".to_string()),
_10k_sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "addrs_over_10k_sats".to_string()),
_100k_sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "addrs_over_100k_sats".to_string()),
_1m_sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "addrs_over_1m_sats".to_string()),
_10m_sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "addrs_over_10m_sats".to_string()),
_1btc: ActivityAddrOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "addrs_over_1btc".to_string()),
_10btc: ActivityAddrOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "addrs_over_10btc".to_string()),
_100btc: ActivityAddrOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "addrs_over_100btc".to_string()),
_1k_btc: ActivityAddrOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "addrs_over_1k_btc".to_string()),
_10k_btc: ActivityAddrOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "addrs_over_10k_btc".to_string()),
_1sat: ActivityAddrOutputsRealizedSupplyPattern::new(client.clone(), "addrs_over_1sat".to_string()),
_10sats: ActivityAddrOutputsRealizedSupplyPattern::new(client.clone(), "addrs_over_10sats".to_string()),
_100sats: ActivityAddrOutputsRealizedSupplyPattern::new(client.clone(), "addrs_over_100sats".to_string()),
_1k_sats: ActivityAddrOutputsRealizedSupplyPattern::new(client.clone(), "addrs_over_1k_sats".to_string()),
_10k_sats: ActivityAddrOutputsRealizedSupplyPattern::new(client.clone(), "addrs_over_10k_sats".to_string()),
_100k_sats: ActivityAddrOutputsRealizedSupplyPattern::new(client.clone(), "addrs_over_100k_sats".to_string()),
_1m_sats: ActivityAddrOutputsRealizedSupplyPattern::new(client.clone(), "addrs_over_1m_sats".to_string()),
_10m_sats: ActivityAddrOutputsRealizedSupplyPattern::new(client.clone(), "addrs_over_10m_sats".to_string()),
_1btc: ActivityAddrOutputsRealizedSupplyPattern::new(client.clone(), "addrs_over_1btc".to_string()),
_10btc: ActivityAddrOutputsRealizedSupplyPattern::new(client.clone(), "addrs_over_10btc".to_string()),
_100btc: ActivityAddrOutputsRealizedSupplyPattern::new(client.clone(), "addrs_over_100btc".to_string()),
_1k_btc: ActivityAddrOutputsRealizedSupplyPattern::new(client.clone(), "addrs_over_1k_btc".to_string()),
_10k_btc: ActivityAddrOutputsRealizedSupplyPattern::new(client.clone(), "addrs_over_10k_btc".to_string()),
}
}
}
/// Series tree node.
pub struct SeriesTree_Cohorts_Addr_AmountRange {
pub _0sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern,
pub _1sat_to_10sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern,
pub _10sats_to_100sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern,
pub _100sats_to_1k_sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern,
pub _1k_sats_to_10k_sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern,
pub _10k_sats_to_100k_sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern,
pub _100k_sats_to_1m_sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern,
pub _1m_sats_to_10m_sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern,
pub _10m_sats_to_1btc: ActivityAddrOutputsRealizedSupplyUnrealizedPattern,
pub _1btc_to_10btc: ActivityAddrOutputsRealizedSupplyUnrealizedPattern,
pub _10btc_to_100btc: ActivityAddrOutputsRealizedSupplyUnrealizedPattern,
pub _100btc_to_1k_btc: ActivityAddrOutputsRealizedSupplyUnrealizedPattern,
pub _1k_btc_to_10k_btc: ActivityAddrOutputsRealizedSupplyUnrealizedPattern,
pub _10k_btc_to_100k_btc: ActivityAddrOutputsRealizedSupplyUnrealizedPattern,
pub over_100k_btc: ActivityAddrOutputsRealizedSupplyUnrealizedPattern,
pub _0sats: ActivityAddrOutputsRealizedSupplyPattern,
pub _1sat_to_10sats: ActivityAddrOutputsRealizedSupplyPattern,
pub _10sats_to_100sats: ActivityAddrOutputsRealizedSupplyPattern,
pub _100sats_to_1k_sats: ActivityAddrOutputsRealizedSupplyPattern,
pub _1k_sats_to_10k_sats: ActivityAddrOutputsRealizedSupplyPattern,
pub _10k_sats_to_100k_sats: ActivityAddrOutputsRealizedSupplyPattern,
pub _100k_sats_to_1m_sats: ActivityAddrOutputsRealizedSupplyPattern,
pub _1m_sats_to_10m_sats: ActivityAddrOutputsRealizedSupplyPattern,
pub _10m_sats_to_1btc: ActivityAddrOutputsRealizedSupplyPattern,
pub _1btc_to_10btc: ActivityAddrOutputsRealizedSupplyPattern,
pub _10btc_to_100btc: ActivityAddrOutputsRealizedSupplyPattern,
pub _100btc_to_1k_btc: ActivityAddrOutputsRealizedSupplyPattern,
pub _1k_btc_to_10k_btc: ActivityAddrOutputsRealizedSupplyPattern,
pub _10k_btc_to_100k_btc: ActivityAddrOutputsRealizedSupplyPattern,
pub over_100k_btc: ActivityAddrOutputsRealizedSupplyPattern,
}
impl SeriesTree_Cohorts_Addr_AmountRange {
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
Self {
_0sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "addrs_0sats".to_string()),
_1sat_to_10sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "addrs_1sat_to_10sats".to_string()),
_10sats_to_100sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "addrs_10sats_to_100sats".to_string()),
_100sats_to_1k_sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "addrs_100sats_to_1k_sats".to_string()),
_1k_sats_to_10k_sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "addrs_1k_sats_to_10k_sats".to_string()),
_10k_sats_to_100k_sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "addrs_10k_sats_to_100k_sats".to_string()),
_100k_sats_to_1m_sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "addrs_100k_sats_to_1m_sats".to_string()),
_1m_sats_to_10m_sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "addrs_1m_sats_to_10m_sats".to_string()),
_10m_sats_to_1btc: ActivityAddrOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "addrs_10m_sats_to_1btc".to_string()),
_1btc_to_10btc: ActivityAddrOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "addrs_1btc_to_10btc".to_string()),
_10btc_to_100btc: ActivityAddrOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "addrs_10btc_to_100btc".to_string()),
_100btc_to_1k_btc: ActivityAddrOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "addrs_100btc_to_1k_btc".to_string()),
_1k_btc_to_10k_btc: ActivityAddrOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "addrs_1k_btc_to_10k_btc".to_string()),
_10k_btc_to_100k_btc: ActivityAddrOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "addrs_10k_btc_to_100k_btc".to_string()),
over_100k_btc: ActivityAddrOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "addrs_over_100k_btc".to_string()),
_0sats: ActivityAddrOutputsRealizedSupplyPattern::new(client.clone(), "addrs_0sats".to_string()),
_1sat_to_10sats: ActivityAddrOutputsRealizedSupplyPattern::new(client.clone(), "addrs_1sat_to_10sats".to_string()),
_10sats_to_100sats: ActivityAddrOutputsRealizedSupplyPattern::new(client.clone(), "addrs_10sats_to_100sats".to_string()),
_100sats_to_1k_sats: ActivityAddrOutputsRealizedSupplyPattern::new(client.clone(), "addrs_100sats_to_1k_sats".to_string()),
_1k_sats_to_10k_sats: ActivityAddrOutputsRealizedSupplyPattern::new(client.clone(), "addrs_1k_sats_to_10k_sats".to_string()),
_10k_sats_to_100k_sats: ActivityAddrOutputsRealizedSupplyPattern::new(client.clone(), "addrs_10k_sats_to_100k_sats".to_string()),
_100k_sats_to_1m_sats: ActivityAddrOutputsRealizedSupplyPattern::new(client.clone(), "addrs_100k_sats_to_1m_sats".to_string()),
_1m_sats_to_10m_sats: ActivityAddrOutputsRealizedSupplyPattern::new(client.clone(), "addrs_1m_sats_to_10m_sats".to_string()),
_10m_sats_to_1btc: ActivityAddrOutputsRealizedSupplyPattern::new(client.clone(), "addrs_10m_sats_to_1btc".to_string()),
_1btc_to_10btc: ActivityAddrOutputsRealizedSupplyPattern::new(client.clone(), "addrs_1btc_to_10btc".to_string()),
_10btc_to_100btc: ActivityAddrOutputsRealizedSupplyPattern::new(client.clone(), "addrs_10btc_to_100btc".to_string()),
_100btc_to_1k_btc: ActivityAddrOutputsRealizedSupplyPattern::new(client.clone(), "addrs_100btc_to_1k_btc".to_string()),
_1k_btc_to_10k_btc: ActivityAddrOutputsRealizedSupplyPattern::new(client.clone(), "addrs_1k_btc_to_10k_btc".to_string()),
_10k_btc_to_100k_btc: ActivityAddrOutputsRealizedSupplyPattern::new(client.clone(), "addrs_10k_btc_to_100k_btc".to_string()),
over_100k_btc: ActivityAddrOutputsRealizedSupplyPattern::new(client.clone(), "addrs_over_100k_btc".to_string()),
}
}
}
/// Series tree node.
pub struct SeriesTree_Cohorts_Addr_UnderAmount {
pub _10sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern,
pub _100sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern,
pub _1k_sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern,
pub _10k_sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern,
pub _100k_sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern,
pub _1m_sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern,
pub _10m_sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern,
pub _1btc: ActivityAddrOutputsRealizedSupplyUnrealizedPattern,
pub _10btc: ActivityAddrOutputsRealizedSupplyUnrealizedPattern,
pub _100btc: ActivityAddrOutputsRealizedSupplyUnrealizedPattern,
pub _1k_btc: ActivityAddrOutputsRealizedSupplyUnrealizedPattern,
pub _10k_btc: ActivityAddrOutputsRealizedSupplyUnrealizedPattern,
pub _100k_btc: ActivityAddrOutputsRealizedSupplyUnrealizedPattern,
pub _10sats: ActivityAddrOutputsRealizedSupplyPattern,
pub _100sats: ActivityAddrOutputsRealizedSupplyPattern,
pub _1k_sats: ActivityAddrOutputsRealizedSupplyPattern,
pub _10k_sats: ActivityAddrOutputsRealizedSupplyPattern,
pub _100k_sats: ActivityAddrOutputsRealizedSupplyPattern,
pub _1m_sats: ActivityAddrOutputsRealizedSupplyPattern,
pub _10m_sats: ActivityAddrOutputsRealizedSupplyPattern,
pub _1btc: ActivityAddrOutputsRealizedSupplyPattern,
pub _10btc: ActivityAddrOutputsRealizedSupplyPattern,
pub _100btc: ActivityAddrOutputsRealizedSupplyPattern,
pub _1k_btc: ActivityAddrOutputsRealizedSupplyPattern,
pub _10k_btc: ActivityAddrOutputsRealizedSupplyPattern,
pub _100k_btc: ActivityAddrOutputsRealizedSupplyPattern,
}
impl SeriesTree_Cohorts_Addr_UnderAmount {
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
Self {
_10sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "addrs_under_10sats".to_string()),
_100sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "addrs_under_100sats".to_string()),
_1k_sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "addrs_under_1k_sats".to_string()),
_10k_sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "addrs_under_10k_sats".to_string()),
_100k_sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "addrs_under_100k_sats".to_string()),
_1m_sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "addrs_under_1m_sats".to_string()),
_10m_sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "addrs_under_10m_sats".to_string()),
_1btc: ActivityAddrOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "addrs_under_1btc".to_string()),
_10btc: ActivityAddrOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "addrs_under_10btc".to_string()),
_100btc: ActivityAddrOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "addrs_under_100btc".to_string()),
_1k_btc: ActivityAddrOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "addrs_under_1k_btc".to_string()),
_10k_btc: ActivityAddrOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "addrs_under_10k_btc".to_string()),
_100k_btc: ActivityAddrOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "addrs_under_100k_btc".to_string()),
_10sats: ActivityAddrOutputsRealizedSupplyPattern::new(client.clone(), "addrs_under_10sats".to_string()),
_100sats: ActivityAddrOutputsRealizedSupplyPattern::new(client.clone(), "addrs_under_100sats".to_string()),
_1k_sats: ActivityAddrOutputsRealizedSupplyPattern::new(client.clone(), "addrs_under_1k_sats".to_string()),
_10k_sats: ActivityAddrOutputsRealizedSupplyPattern::new(client.clone(), "addrs_under_10k_sats".to_string()),
_100k_sats: ActivityAddrOutputsRealizedSupplyPattern::new(client.clone(), "addrs_under_100k_sats".to_string()),
_1m_sats: ActivityAddrOutputsRealizedSupplyPattern::new(client.clone(), "addrs_under_1m_sats".to_string()),
_10m_sats: ActivityAddrOutputsRealizedSupplyPattern::new(client.clone(), "addrs_under_10m_sats".to_string()),
_1btc: ActivityAddrOutputsRealizedSupplyPattern::new(client.clone(), "addrs_under_1btc".to_string()),
_10btc: ActivityAddrOutputsRealizedSupplyPattern::new(client.clone(), "addrs_under_10btc".to_string()),
_100btc: ActivityAddrOutputsRealizedSupplyPattern::new(client.clone(), "addrs_under_100btc".to_string()),
_1k_btc: ActivityAddrOutputsRealizedSupplyPattern::new(client.clone(), "addrs_under_1k_btc".to_string()),
_10k_btc: ActivityAddrOutputsRealizedSupplyPattern::new(client.clone(), "addrs_under_10k_btc".to_string()),
_100k_btc: ActivityAddrOutputsRealizedSupplyPattern::new(client.clone(), "addrs_under_100k_btc".to_string()),
}
}
}
+112 -38
View File
@@ -17,7 +17,10 @@ pub const HOURS_3M: usize = 24 * 3 * 30;
pub const HOURS_4M: usize = 24 * 4 * 30;
pub const HOURS_5M: usize = 24 * 5 * 30; // STH/LTH threshold
pub const HOURS_6M: usize = 24 * 6 * 30;
pub const HOURS_9M: usize = HOURS_6M + HOURS_3M;
pub const HOURS_1Y: usize = 24 * 365;
// Keep year-based ranges anchored to the existing 365-day year convention.
pub const HOURS_18M: usize = HOURS_1Y + HOURS_6M;
pub const HOURS_2Y: usize = 24 * 2 * 365;
pub const HOURS_3Y: usize = 24 * 3 * 365;
pub const HOURS_4Y: usize = 24 * 4 * 365;
@@ -29,12 +32,14 @@ pub const HOURS_10Y: usize = 24 * 10 * 365;
pub const HOURS_12Y: usize = 24 * 12 * 365;
pub const HOURS_15Y: usize = 24 * 15 * 365;
pub const AGE_RANGE_COUNT: usize = 23;
/// Age boundaries in hours. Defines the cohort ranges:
/// [0, 1h), [1h, 1d), [1d, 1w), [1w, 1m), ..., [15y, ∞)
pub const AGE_BOUNDARIES: [usize; 20] = [
pub const AGE_BOUNDARIES: [usize; AGE_RANGE_COUNT - 1] = [
HOURS_1H, HOURS_1D, HOURS_1W, HOURS_1M, HOURS_2M, HOURS_3M, HOURS_4M, HOURS_5M, HOURS_6M,
HOURS_1Y, HOURS_2Y, HOURS_3Y, HOURS_4Y, HOURS_5Y, HOURS_6Y, HOURS_7Y, HOURS_8Y, HOURS_10Y,
HOURS_12Y, HOURS_15Y,
HOURS_9M, HOURS_1Y, HOURS_18M, HOURS_2Y, HOURS_3Y, HOURS_4Y, HOURS_5Y, HOURS_6Y, HOURS_7Y,
HOURS_8Y, HOURS_10Y, HOURS_12Y, HOURS_15Y,
];
/// Age range bounds (end = usize::MAX means unbounded)
@@ -48,8 +53,10 @@ pub const AGE_RANGE_BOUNDS: AgeRange<Range<usize>> = AgeRange {
_3m_to_4m: HOURS_3M..HOURS_4M,
_4m_to_5m: HOURS_4M..HOURS_5M,
_5m_to_6m: HOURS_5M..HOURS_6M,
_6m_to_1y: HOURS_6M..HOURS_1Y,
_1y_to_2y: HOURS_1Y..HOURS_2Y,
_6m_to_9m: HOURS_6M..HOURS_9M,
_9m_to_1y: HOURS_9M..HOURS_1Y,
_1y_to_18m: HOURS_1Y..HOURS_18M,
_18m_to_2y: HOURS_18M..HOURS_2Y,
_2y_to_3y: HOURS_2Y..HOURS_3Y,
_3y_to_4y: HOURS_3Y..HOURS_4Y,
_4y_to_5y: HOURS_4Y..HOURS_5Y,
@@ -73,8 +80,10 @@ pub const AGE_RANGE_FILTERS: AgeRange<Filter> = AgeRange {
_3m_to_4m: Filter::Time(TimeFilter::Range(AGE_RANGE_BOUNDS._3m_to_4m)),
_4m_to_5m: Filter::Time(TimeFilter::Range(AGE_RANGE_BOUNDS._4m_to_5m)),
_5m_to_6m: Filter::Time(TimeFilter::Range(AGE_RANGE_BOUNDS._5m_to_6m)),
_6m_to_1y: Filter::Time(TimeFilter::Range(AGE_RANGE_BOUNDS._6m_to_1y)),
_1y_to_2y: Filter::Time(TimeFilter::Range(AGE_RANGE_BOUNDS._1y_to_2y)),
_6m_to_9m: Filter::Time(TimeFilter::Range(AGE_RANGE_BOUNDS._6m_to_9m)),
_9m_to_1y: Filter::Time(TimeFilter::Range(AGE_RANGE_BOUNDS._9m_to_1y)),
_1y_to_18m: Filter::Time(TimeFilter::Range(AGE_RANGE_BOUNDS._1y_to_18m)),
_18m_to_2y: Filter::Time(TimeFilter::Range(AGE_RANGE_BOUNDS._18m_to_2y)),
_2y_to_3y: Filter::Time(TimeFilter::Range(AGE_RANGE_BOUNDS._2y_to_3y)),
_3y_to_4y: Filter::Time(TimeFilter::Range(AGE_RANGE_BOUNDS._3y_to_4y)),
_4y_to_5y: Filter::Time(TimeFilter::Range(AGE_RANGE_BOUNDS._4y_to_5y)),
@@ -98,8 +107,10 @@ pub const AGE_RANGE_NAMES: AgeRange<CohortName> = AgeRange {
_3m_to_4m: CohortName::new("3m_to_4m_old", "3m-4m", "3 to 4 Months Old"),
_4m_to_5m: CohortName::new("4m_to_5m_old", "4m-5m", "4 to 5 Months Old"),
_5m_to_6m: CohortName::new("5m_to_6m_old", "5m-6m", "5 to 6 Months Old"),
_6m_to_1y: CohortName::new("6m_to_1y_old", "6m-1y", "6 Months to 1 Year Old"),
_1y_to_2y: CohortName::new("1y_to_2y_old", "1y-2y", "1 to 2 Years Old"),
_6m_to_9m: CohortName::new("6m_to_9m_old", "6m-9m", "6 to 9 Months Old"),
_9m_to_1y: CohortName::new("9m_to_1y_old", "9m-1y", "9 Months to 1 Year Old"),
_1y_to_18m: CohortName::new("1y_to_18m_old", "1y-18m", "1 Year to 18 Months Old"),
_18m_to_2y: CohortName::new("18m_to_2y_old", "18m-2y", "18 Months to 2 Years Old"),
_2y_to_3y: CohortName::new("2y_to_3y_old", "2y-3y", "2 to 3 Years Old"),
_3y_to_4y: CohortName::new("3y_to_4y_old", "3y-4y", "3 to 4 Years Old"),
_4y_to_5y: CohortName::new("4y_to_5y_old", "4y-5y", "4 to 5 Years Old"),
@@ -129,8 +140,10 @@ pub struct AgeRange<T> {
pub _3m_to_4m: T,
pub _4m_to_5m: T,
pub _5m_to_6m: T,
pub _6m_to_1y: T,
pub _1y_to_2y: T,
pub _6m_to_9m: T,
pub _9m_to_1y: T,
pub _1y_to_18m: T,
pub _18m_to_2y: T,
pub _2y_to_3y: T,
pub _3y_to_4y: T,
pub _4y_to_5y: T,
@@ -157,8 +170,10 @@ impl<T> AgeRange<T> {
HOURS_3M..HOURS_4M => &mut self._3m_to_4m,
HOURS_4M..HOURS_5M => &mut self._4m_to_5m,
HOURS_5M..HOURS_6M => &mut self._5m_to_6m,
HOURS_6M..HOURS_1Y => &mut self._6m_to_1y,
HOURS_1Y..HOURS_2Y => &mut self._1y_to_2y,
HOURS_6M..HOURS_9M => &mut self._6m_to_9m,
HOURS_9M..HOURS_1Y => &mut self._9m_to_1y,
HOURS_1Y..HOURS_18M => &mut self._1y_to_18m,
HOURS_18M..HOURS_2Y => &mut self._18m_to_2y,
HOURS_2Y..HOURS_3Y => &mut self._2y_to_3y,
HOURS_3Y..HOURS_4Y => &mut self._3y_to_4y,
HOURS_4Y..HOURS_5Y => &mut self._4y_to_5y,
@@ -185,8 +200,10 @@ impl<T> AgeRange<T> {
HOURS_3M..HOURS_4M => &self._3m_to_4m,
HOURS_4M..HOURS_5M => &self._4m_to_5m,
HOURS_5M..HOURS_6M => &self._5m_to_6m,
HOURS_6M..HOURS_1Y => &self._6m_to_1y,
HOURS_1Y..HOURS_2Y => &self._1y_to_2y,
HOURS_6M..HOURS_9M => &self._6m_to_9m,
HOURS_9M..HOURS_1Y => &self._9m_to_1y,
HOURS_1Y..HOURS_18M => &self._1y_to_18m,
HOURS_18M..HOURS_2Y => &self._18m_to_2y,
HOURS_2Y..HOURS_3Y => &self._2y_to_3y,
HOURS_3Y..HOURS_4Y => &self._3y_to_4y,
HOURS_4Y..HOURS_5Y => &self._4y_to_5y,
@@ -200,7 +217,7 @@ impl<T> AgeRange<T> {
}
}
pub fn from_array(arr: [T; 21]) -> Self {
pub fn from_array(arr: [T; AGE_RANGE_COUNT]) -> Self {
let [
a0,
a1,
@@ -223,6 +240,8 @@ impl<T> AgeRange<T> {
a18,
a19,
a20,
a21,
a22,
] = arr;
Self {
under_1h: a0,
@@ -234,18 +253,20 @@ impl<T> AgeRange<T> {
_3m_to_4m: a6,
_4m_to_5m: a7,
_5m_to_6m: a8,
_6m_to_1y: a9,
_1y_to_2y: a10,
_2y_to_3y: a11,
_3y_to_4y: a12,
_4y_to_5y: a13,
_5y_to_6y: a14,
_6y_to_7y: a15,
_7y_to_8y: a16,
_8y_to_10y: a17,
_10y_to_12y: a18,
_12y_to_15y: a19,
over_15y: a20,
_6m_to_9m: a9,
_9m_to_1y: a10,
_1y_to_18m: a11,
_18m_to_2y: a12,
_2y_to_3y: a13,
_3y_to_4y: a14,
_4y_to_5y: a15,
_5y_to_6y: a16,
_6y_to_7y: a17,
_7y_to_8y: a18,
_8y_to_10y: a19,
_10y_to_12y: a20,
_12y_to_15y: a21,
over_15y: a22,
}
}
@@ -265,8 +286,10 @@ impl<T> AgeRange<T> {
_3m_to_4m: create(f._3m_to_4m.clone(), n._3m_to_4m.id),
_4m_to_5m: create(f._4m_to_5m.clone(), n._4m_to_5m.id),
_5m_to_6m: create(f._5m_to_6m.clone(), n._5m_to_6m.id),
_6m_to_1y: create(f._6m_to_1y.clone(), n._6m_to_1y.id),
_1y_to_2y: create(f._1y_to_2y.clone(), n._1y_to_2y.id),
_6m_to_9m: create(f._6m_to_9m.clone(), n._6m_to_9m.id),
_9m_to_1y: create(f._9m_to_1y.clone(), n._9m_to_1y.id),
_1y_to_18m: create(f._1y_to_18m.clone(), n._1y_to_18m.id),
_18m_to_2y: create(f._18m_to_2y.clone(), n._18m_to_2y.id),
_2y_to_3y: create(f._2y_to_3y.clone(), n._2y_to_3y.id),
_3y_to_4y: create(f._3y_to_4y.clone(), n._3y_to_4y.id),
_4y_to_5y: create(f._4y_to_5y.clone(), n._4y_to_5y.id),
@@ -296,8 +319,10 @@ impl<T> AgeRange<T> {
_3m_to_4m: create(f._3m_to_4m.clone(), n._3m_to_4m.id)?,
_4m_to_5m: create(f._4m_to_5m.clone(), n._4m_to_5m.id)?,
_5m_to_6m: create(f._5m_to_6m.clone(), n._5m_to_6m.id)?,
_6m_to_1y: create(f._6m_to_1y.clone(), n._6m_to_1y.id)?,
_1y_to_2y: create(f._1y_to_2y.clone(), n._1y_to_2y.id)?,
_6m_to_9m: create(f._6m_to_9m.clone(), n._6m_to_9m.id)?,
_9m_to_1y: create(f._9m_to_1y.clone(), n._9m_to_1y.id)?,
_1y_to_18m: create(f._1y_to_18m.clone(), n._1y_to_18m.id)?,
_18m_to_2y: create(f._18m_to_2y.clone(), n._18m_to_2y.id)?,
_2y_to_3y: create(f._2y_to_3y.clone(), n._2y_to_3y.id)?,
_3y_to_4y: create(f._3y_to_4y.clone(), n._3y_to_4y.id)?,
_4y_to_5y: create(f._4y_to_5y.clone(), n._4y_to_5y.id)?,
@@ -322,8 +347,10 @@ impl<T> AgeRange<T> {
&self._3m_to_4m,
&self._4m_to_5m,
&self._5m_to_6m,
&self._6m_to_1y,
&self._1y_to_2y,
&self._6m_to_9m,
&self._9m_to_1y,
&self._1y_to_18m,
&self._18m_to_2y,
&self._2y_to_3y,
&self._3y_to_4y,
&self._4y_to_5y,
@@ -349,8 +376,10 @@ impl<T> AgeRange<T> {
&mut self._3m_to_4m,
&mut self._4m_to_5m,
&mut self._5m_to_6m,
&mut self._6m_to_1y,
&mut self._1y_to_2y,
&mut self._6m_to_9m,
&mut self._9m_to_1y,
&mut self._1y_to_18m,
&mut self._18m_to_2y,
&mut self._2y_to_3y,
&mut self._3y_to_4y,
&mut self._4y_to_5y,
@@ -379,8 +408,10 @@ impl<T> AgeRange<T> {
&mut self._3m_to_4m,
&mut self._4m_to_5m,
&mut self._5m_to_6m,
&mut self._6m_to_1y,
&mut self._1y_to_2y,
&mut self._6m_to_9m,
&mut self._9m_to_1y,
&mut self._1y_to_18m,
&mut self._18m_to_2y,
&mut self._2y_to_3y,
&mut self._3y_to_4y,
&mut self._4y_to_5y,
@@ -395,3 +426,46 @@ impl<T> AgeRange<T> {
.into_par_iter()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ranges_are_contiguous_and_match_the_declared_count() {
let ranges: Vec<_> = AGE_RANGE_BOUNDS.iter().collect();
assert_eq!(ranges.len(), AGE_RANGE_COUNT);
assert_eq!(ranges.first().unwrap().start, 0);
assert_eq!(ranges.last().unwrap().end, usize::MAX);
for adjacent in ranges.windows(2) {
assert_eq!(adjacent[0].end, adjacent[1].start);
}
}
#[test]
fn split_range_names_and_boundaries_match() {
assert_eq!(HOURS_9M, HOURS_6M + HOURS_3M);
assert_eq!(HOURS_18M, HOURS_1Y + HOURS_6M);
assert_eq!(AGE_RANGE_NAMES._6m_to_9m.id, "6m_to_9m_old");
assert_eq!(AGE_RANGE_NAMES._9m_to_1y.id, "9m_to_1y_old");
assert_eq!(AGE_RANGE_NAMES._1y_to_18m.id, "1y_to_18m_old");
assert_eq!(AGE_RANGE_NAMES._18m_to_2y.id, "18m_to_2y_old");
}
#[test]
fn eighteen_month_classifier_stays_anchored_to_one_year_plus_six_months() {
let age_at_540_days = Age::new(
brk_types::Timestamp::new((HOURS_6M * 3 * 60 * 60) as u32),
brk_types::Timestamp::ZERO,
);
let age_at_18m = Age::new(
brk_types::Timestamp::new((HOURS_18M * 60 * 60) as u32),
brk_types::Timestamp::ZERO,
);
assert_eq!(AGE_RANGE_NAMES.get(age_at_540_days).id, "1y_to_18m_old");
assert_eq!(AGE_RANGE_NAMES.get(age_at_18m).id, "18m_to_2y_old");
}
}
+2 -2
View File
@@ -6,8 +6,8 @@ mod amount_filter;
mod amount_range;
mod by_addr_type;
mod by_any_addr;
mod by_epoch;
mod by_entry;
mod by_epoch;
mod by_term;
mod by_type;
mod class;
@@ -36,8 +36,8 @@ pub use amount_filter::*;
pub use amount_range::*;
pub use by_addr_type::*;
pub use by_any_addr::*;
pub use by_epoch::*;
pub use by_entry::*;
pub use by_epoch::*;
pub use by_term::*;
pub use by_type::*;
pub use class::*;
+41 -3
View File
@@ -5,7 +5,7 @@ use serde::Serialize;
use super::{
CohortName, Filter, HOURS_1D, HOURS_1M, HOURS_1W, HOURS_1Y, HOURS_2M, HOURS_2Y, HOURS_3M,
HOURS_3Y, HOURS_4M, HOURS_4Y, HOURS_5M, HOURS_5Y, HOURS_6M, HOURS_6Y, HOURS_7Y, HOURS_8Y,
HOURS_10Y, HOURS_12Y, TimeFilter,
HOURS_9M, HOURS_10Y, HOURS_12Y, HOURS_18M, TimeFilter,
};
/// Over-age thresholds in hours
@@ -18,7 +18,9 @@ pub const OVER_AGE_HOURS: OverAge<usize> = OverAge {
_4m: HOURS_4M,
_5m: HOURS_5M,
_6m: HOURS_6M,
_9m: HOURS_9M,
_1y: HOURS_1Y,
_18m: HOURS_18M,
_2y: HOURS_2Y,
_3y: HOURS_3Y,
_4y: HOURS_4Y,
@@ -40,7 +42,9 @@ pub const OVER_AGE_FILTERS: OverAge<Filter> = OverAge {
_4m: Filter::Time(TimeFilter::GreaterOrEqual(OVER_AGE_HOURS._4m)),
_5m: Filter::Time(TimeFilter::GreaterOrEqual(OVER_AGE_HOURS._5m)),
_6m: Filter::Time(TimeFilter::GreaterOrEqual(OVER_AGE_HOURS._6m)),
_9m: Filter::Time(TimeFilter::GreaterOrEqual(OVER_AGE_HOURS._9m)),
_1y: Filter::Time(TimeFilter::GreaterOrEqual(OVER_AGE_HOURS._1y)),
_18m: Filter::Time(TimeFilter::GreaterOrEqual(OVER_AGE_HOURS._18m)),
_2y: Filter::Time(TimeFilter::GreaterOrEqual(OVER_AGE_HOURS._2y)),
_3y: Filter::Time(TimeFilter::GreaterOrEqual(OVER_AGE_HOURS._3y)),
_4y: Filter::Time(TimeFilter::GreaterOrEqual(OVER_AGE_HOURS._4y)),
@@ -62,7 +66,9 @@ pub const OVER_AGE_NAMES: OverAge<CohortName> = OverAge {
_4m: CohortName::new("over_4m_old", "4m+", "Over 4 Months Old"),
_5m: CohortName::new("over_5m_old", "5m+", "Over 5 Months Old"),
_6m: CohortName::new("over_6m_old", "6m+", "Over 6 Months Old"),
_9m: CohortName::new("over_9m_old", "9m+", "Over 9 Months Old"),
_1y: CohortName::new("over_1y_old", "1y+", "Over 1 Year Old"),
_18m: CohortName::new("over_18m_old", "18m+", "Over 18 Months Old"),
_2y: CohortName::new("over_2y_old", "2y+", "Over 2 Years Old"),
_3y: CohortName::new("over_3y_old", "3y+", "Over 3 Years Old"),
_4y: CohortName::new("over_4y_old", "4y+", "Over 4 Years Old"),
@@ -84,7 +90,9 @@ pub struct OverAge<T> {
pub _4m: T,
pub _5m: T,
pub _6m: T,
pub _9m: T,
pub _1y: T,
pub _18m: T,
pub _2y: T,
pub _3y: T,
pub _4y: T,
@@ -118,7 +126,9 @@ impl<T> OverAge<T> {
_4m: create(f._4m.clone(), n._4m.id),
_5m: create(f._5m.clone(), n._5m.id),
_6m: create(f._6m.clone(), n._6m.id),
_9m: create(f._9m.clone(), n._9m.id),
_1y: create(f._1y.clone(), n._1y.id),
_18m: create(f._18m.clone(), n._18m.id),
_2y: create(f._2y.clone(), n._2y.id),
_3y: create(f._3y.clone(), n._3y.id),
_4y: create(f._4y.clone(), n._4y.id),
@@ -146,7 +156,9 @@ impl<T> OverAge<T> {
_4m: create(f._4m.clone(), n._4m.id)?,
_5m: create(f._5m.clone(), n._5m.id)?,
_6m: create(f._6m.clone(), n._6m.id)?,
_9m: create(f._9m.clone(), n._9m.id)?,
_1y: create(f._1y.clone(), n._1y.id)?,
_18m: create(f._18m.clone(), n._18m.id)?,
_2y: create(f._2y.clone(), n._2y.id)?,
_3y: create(f._3y.clone(), n._3y.id)?,
_4y: create(f._4y.clone(), n._4y.id)?,
@@ -162,8 +174,8 @@ impl<T> OverAge<T> {
pub fn iter(&self) -> impl Iterator<Item = &T> {
[
&self._1d, &self._1w, &self._1m, &self._2m, &self._3m, &self._4m, &self._5m, &self._6m,
&self._1y, &self._2y, &self._3y, &self._4y, &self._5y, &self._6y, &self._7y, &self._8y,
&self._10y, &self._12y,
&self._9m, &self._1y, &self._18m, &self._2y, &self._3y, &self._4y, &self._5y,
&self._6y, &self._7y, &self._8y, &self._10y, &self._12y,
]
.into_iter()
}
@@ -178,7 +190,9 @@ impl<T> OverAge<T> {
&mut self._4m,
&mut self._5m,
&mut self._6m,
&mut self._9m,
&mut self._1y,
&mut self._18m,
&mut self._2y,
&mut self._3y,
&mut self._4y,
@@ -205,7 +219,9 @@ impl<T> OverAge<T> {
&mut self._4m,
&mut self._5m,
&mut self._6m,
&mut self._9m,
&mut self._1y,
&mut self._18m,
&mut self._2y,
&mut self._3y,
&mut self._4y,
@@ -219,3 +235,25 @@ impl<T> OverAge<T> {
.into_par_iter()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::AGE_RANGE_FILTERS;
#[test]
fn new_thresholds_include_only_older_ranges() {
assert!(!OVER_AGE_FILTERS._9m.includes(&AGE_RANGE_FILTERS._6m_to_9m));
assert!(OVER_AGE_FILTERS._9m.includes(&AGE_RANGE_FILTERS._9m_to_1y));
assert!(
!OVER_AGE_FILTERS
._18m
.includes(&AGE_RANGE_FILTERS._1y_to_18m)
);
assert!(
OVER_AGE_FILTERS
._18m
.includes(&AGE_RANGE_FILTERS._18m_to_2y)
);
}
}
+43 -5
View File
@@ -4,8 +4,8 @@ use serde::Serialize;
use super::{
CohortName, Filter, HOURS_1M, HOURS_1W, HOURS_1Y, HOURS_2M, HOURS_2Y, HOURS_3M, HOURS_3Y,
HOURS_4M, HOURS_4Y, HOURS_5M, HOURS_5Y, HOURS_6M, HOURS_6Y, HOURS_7Y, HOURS_8Y, HOURS_10Y,
HOURS_12Y, HOURS_15Y, TimeFilter,
HOURS_4M, HOURS_4Y, HOURS_5M, HOURS_5Y, HOURS_6M, HOURS_6Y, HOURS_7Y, HOURS_8Y, HOURS_9M,
HOURS_10Y, HOURS_12Y, HOURS_15Y, HOURS_18M, TimeFilter,
};
/// Under-age thresholds in hours
@@ -17,7 +17,9 @@ pub const UNDER_AGE_HOURS: UnderAge<usize> = UnderAge {
_4m: HOURS_4M,
_5m: HOURS_5M,
_6m: HOURS_6M,
_9m: HOURS_9M,
_1y: HOURS_1Y,
_18m: HOURS_18M,
_2y: HOURS_2Y,
_3y: HOURS_3Y,
_4y: HOURS_4Y,
@@ -39,7 +41,9 @@ pub const UNDER_AGE_FILTERS: UnderAge<Filter> = UnderAge {
_4m: Filter::Time(TimeFilter::LowerThan(UNDER_AGE_HOURS._4m)),
_5m: Filter::Time(TimeFilter::LowerThan(UNDER_AGE_HOURS._5m)),
_6m: Filter::Time(TimeFilter::LowerThan(UNDER_AGE_HOURS._6m)),
_9m: Filter::Time(TimeFilter::LowerThan(UNDER_AGE_HOURS._9m)),
_1y: Filter::Time(TimeFilter::LowerThan(UNDER_AGE_HOURS._1y)),
_18m: Filter::Time(TimeFilter::LowerThan(UNDER_AGE_HOURS._18m)),
_2y: Filter::Time(TimeFilter::LowerThan(UNDER_AGE_HOURS._2y)),
_3y: Filter::Time(TimeFilter::LowerThan(UNDER_AGE_HOURS._3y)),
_4y: Filter::Time(TimeFilter::LowerThan(UNDER_AGE_HOURS._4y)),
@@ -61,7 +65,9 @@ pub const UNDER_AGE_NAMES: UnderAge<CohortName> = UnderAge {
_4m: CohortName::new("under_4m_old", "<4m", "Under 4 Months Old"),
_5m: CohortName::new("under_5m_old", "<5m", "Under 5 Months Old"),
_6m: CohortName::new("under_6m_old", "<6m", "Under 6 Months Old"),
_9m: CohortName::new("under_9m_old", "<9m", "Under 9 Months Old"),
_1y: CohortName::new("under_1y_old", "<1y", "Under 1 Year Old"),
_18m: CohortName::new("under_18m_old", "<18m", "Under 18 Months Old"),
_2y: CohortName::new("under_2y_old", "<2y", "Under 2 Years Old"),
_3y: CohortName::new("under_3y_old", "<3y", "Under 3 Years Old"),
_4y: CohortName::new("under_4y_old", "<4y", "Under 4 Years Old"),
@@ -83,7 +89,9 @@ pub struct UnderAge<T> {
pub _4m: T,
pub _5m: T,
pub _6m: T,
pub _9m: T,
pub _1y: T,
pub _18m: T,
pub _2y: T,
pub _3y: T,
pub _4y: T,
@@ -117,7 +125,9 @@ impl<T> UnderAge<T> {
_4m: create(f._4m.clone(), n._4m.id),
_5m: create(f._5m.clone(), n._5m.id),
_6m: create(f._6m.clone(), n._6m.id),
_9m: create(f._9m.clone(), n._9m.id),
_1y: create(f._1y.clone(), n._1y.id),
_18m: create(f._18m.clone(), n._18m.id),
_2y: create(f._2y.clone(), n._2y.id),
_3y: create(f._3y.clone(), n._3y.id),
_4y: create(f._4y.clone(), n._4y.id),
@@ -145,7 +155,9 @@ impl<T> UnderAge<T> {
_4m: create(f._4m.clone(), n._4m.id)?,
_5m: create(f._5m.clone(), n._5m.id)?,
_6m: create(f._6m.clone(), n._6m.id)?,
_9m: create(f._9m.clone(), n._9m.id)?,
_1y: create(f._1y.clone(), n._1y.id)?,
_18m: create(f._18m.clone(), n._18m.id)?,
_2y: create(f._2y.clone(), n._2y.id)?,
_3y: create(f._3y.clone(), n._3y.id)?,
_4y: create(f._4y.clone(), n._4y.id)?,
@@ -161,9 +173,9 @@ impl<T> UnderAge<T> {
pub fn iter(&self) -> impl Iterator<Item = &T> {
[
&self._1w, &self._1m, &self._2m, &self._3m, &self._4m, &self._5m, &self._6m, &self._1y,
&self._2y, &self._3y, &self._4y, &self._5y, &self._6y, &self._7y, &self._8y,
&self._10y, &self._12y, &self._15y,
&self._1w, &self._1m, &self._2m, &self._3m, &self._4m, &self._5m, &self._6m, &self._9m,
&self._1y, &self._18m, &self._2y, &self._3y, &self._4y, &self._5y, &self._6y,
&self._7y, &self._8y, &self._10y, &self._12y, &self._15y,
]
.into_iter()
}
@@ -177,7 +189,9 @@ impl<T> UnderAge<T> {
&mut self._4m,
&mut self._5m,
&mut self._6m,
&mut self._9m,
&mut self._1y,
&mut self._18m,
&mut self._2y,
&mut self._3y,
&mut self._4y,
@@ -204,7 +218,9 @@ impl<T> UnderAge<T> {
&mut self._4m,
&mut self._5m,
&mut self._6m,
&mut self._9m,
&mut self._1y,
&mut self._18m,
&mut self._2y,
&mut self._3y,
&mut self._4y,
@@ -219,3 +235,25 @@ impl<T> UnderAge<T> {
.into_par_iter()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::AGE_RANGE_FILTERS;
#[test]
fn new_thresholds_include_only_younger_ranges() {
assert!(UNDER_AGE_FILTERS._9m.includes(&AGE_RANGE_FILTERS._6m_to_9m));
assert!(!UNDER_AGE_FILTERS._9m.includes(&AGE_RANGE_FILTERS._9m_to_1y));
assert!(
UNDER_AGE_FILTERS
._18m
.includes(&AGE_RANGE_FILTERS._1y_to_18m)
);
assert!(
!UNDER_AGE_FILTERS
._18m
.includes(&AGE_RANGE_FILTERS._18m_to_2y)
);
}
}
+1 -1
View File
@@ -45,7 +45,7 @@ let realized_cap = computer.distribution.utxo_cohorts.all.metrics.realized.unwra
## Cohort System
UTXO and address cohorts support filtering by:
- **Age**: STH (<150d), LTH (≥150d), 21 age bands (<1h, 1h-1d, 1d-1w, 1w-1m, 1m-2m, ..., 6m-1y, 1y-2y, ..., 12y-15y, 15y+)
- **Age**: STH (<150d), LTH (≥150d), 23 age bands (<1h, 1h-1d, 1d-1w, 1w-1m, 1m-2m, ..., 6m-9m, 9m-1y, 1y-18m, 18m-2y, ..., 12y-15y, 15y+)
- **Amount**: 0-0.001 BTC, 0.001-0.01, ..., 10k+ BTC
- **Type**: P2PK, P2PKH, P2MS, P2SH, P2WPKH, P2WSH, P2TR, P2A
- **Epoch**: By halving epoch
@@ -37,15 +37,15 @@ use brk_cohort::ByAddrType;
use brk_error::Result;
use brk_indexer::Lengths;
use brk_traversable::Traversable;
use brk_types::{Height, Sats, Version};
use brk_types::{Cents, Height, Sats, Version};
use rayon::prelude::*;
use vecdb::{AnyStoredVec, Database, Exit, ReadableVec, Rw, StorageMode};
use vecdb::{AnyStoredVec, CachedBoxedVec, Database, Exit, ReadableVec, Rw, StorageMode};
use super::{
count::AddrCountFundedTotalVecs,
supply::{AddrSupplyShareVecs, AddrSupplyVecs},
};
use crate::{indexes, price};
use crate::indexes;
mod state;
@@ -66,10 +66,11 @@ impl ExposedAddrVecs {
db: &Database,
version: Version,
indexes: &indexes::Vecs,
spot_price: &CachedBoxedVec<Height, Cents>,
) -> Result<Self> {
Ok(Self {
count: AddrCountFundedTotalVecs::forced_import(db, "exposed", version, indexes)?,
supply: AddrSupplyVecs::forced_import(db, "exposed", version, indexes)?,
supply: AddrSupplyVecs::forced_import(db, "exposed", version, indexes, spot_price)?,
supply_share: AddrSupplyShareVecs::forced_import(db, "exposed", version, indexes)?,
})
}
@@ -104,14 +105,11 @@ impl ExposedAddrVecs {
pub(crate) fn compute_rest(
&mut self,
starting_lengths: &Lengths,
prices: &price::Vecs,
all_supply_sats: &impl ReadableVec<Height, Sats>,
type_supply_sats: &ByAddrType<&impl ReadableVec<Height, Sats>>,
exit: &Exit,
) -> Result<()> {
self.count.compute_rest(starting_lengths, exit)?;
self.supply
.compute_rest(starting_lengths.height, prices, exit)?;
self.supply_share.compute_rest(
starting_lengths.height,
&self.supply,
@@ -24,9 +24,9 @@ use brk_cohort::ByAddrType;
use brk_error::Result;
use brk_indexer::Lengths;
use brk_traversable::Traversable;
use brk_types::{Height, Sats, Version};
use brk_types::{Cents, Height, Sats, Version};
use rayon::prelude::*;
use vecdb::{AnyStoredVec, Database, Exit, ReadableVec, Rw, StorageMode};
use vecdb::{AnyStoredVec, CachedBoxedVec, Database, Exit, ReadableVec, Rw, StorageMode};
use super::{
count::AddrCountFundedTotalVecs,
@@ -35,7 +35,7 @@ use super::{
use crate::{
indexes, inputs,
internal::{WindowStartVec, Windows},
outputs, price,
outputs,
};
mod state;
@@ -61,11 +61,12 @@ impl ReusedAddrVecs {
version: Version,
indexes: &indexes::Vecs,
cached_starts: &Windows<&WindowStartVec>,
spot_price: &CachedBoxedVec<Height, Cents>,
) -> Result<Self> {
Ok(Self {
count: AddrCountFundedTotalVecs::forced_import(db, name, version, indexes)?,
events: AddrEventsVecs::forced_import(db, name, version, indexes, cached_starts)?,
supply: AddrSupplyVecs::forced_import(db, name, version, indexes)?,
supply: AddrSupplyVecs::forced_import(db, name, version, indexes, spot_price)?,
supply_share: AddrSupplyShareVecs::forced_import(db, name, version, indexes)?,
})
}
@@ -112,7 +113,6 @@ impl ReusedAddrVecs {
starting_lengths: &Lengths,
outputs_by_type: &outputs::ByTypeVecs,
inputs_by_type: &inputs::ByTypeVecs,
prices: &price::Vecs,
all_supply_sats: &impl ReadableVec<Height, Sats>,
type_supply_sats: &ByAddrType<&impl ReadableVec<Height, Sats>>,
exit: &Exit,
@@ -120,8 +120,6 @@ impl ReusedAddrVecs {
self.count.compute_rest(starting_lengths, exit)?;
self.events
.compute_rest(starting_lengths, outputs_by_type, inputs_by_type, exit)?;
self.supply
.compute_rest(starting_lengths.height, prices, exit)?;
self.supply_share.compute_rest(
starting_lengths.height,
&self.supply,
@@ -1,12 +1,12 @@
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::Version;
use brk_types::{Cents, Height, Version};
use derive_more::{Deref, DerefMut};
use vecdb::{Database, Rw, StorageMode};
use vecdb::{CachedBoxedVec, Database, Rw, StorageMode};
use crate::{
indexes,
internal::{ValuePerBlock, WithAddrTypes},
internal::{SpotValuePerBlock, WithAddrTypes},
};
use super::AddrTypeToSupply;
@@ -17,7 +17,7 @@ use super::AddrTypeToSupply;
/// sats × spot price.
#[derive(Deref, DerefMut, Traversable)]
pub struct AddrSupplyVecs<M: StorageMode = Rw>(
#[traversable(flatten)] pub WithAddrTypes<ValuePerBlock<M>>,
#[traversable(flatten)] pub WithAddrTypes<SpotValuePerBlock<M>>,
);
impl AddrSupplyVecs {
@@ -26,12 +26,14 @@ impl AddrSupplyVecs {
name: &str,
version: Version,
indexes: &indexes::Vecs,
spot_price: &CachedBoxedVec<Height, Cents>,
) -> Result<Self> {
Ok(Self(WithAddrTypes::<ValuePerBlock>::forced_import(
Ok(Self(WithAddrTypes::<SpotValuePerBlock>::forced_import(
db,
&format!("{name}_addr_supply"),
version,
indexes,
spot_price,
)?))
}
@@ -1,46 +1,31 @@
use brk_cohort::{AmountBucket, ByAddrType};
use brk_error::Result;
use brk_types::{Age, Cents, CheckedSub, Height, Sats, Timestamp, TypeIndex};
use brk_types::{Cents, Sats, TypeIndex};
use rustc_hash::FxHashSet;
use vecdb::VecIndex;
use crate::distribution::{
addr::{AddrMetricsState, AddrSendPreState, HeightToAddrTypeToVec},
cohorts::AddrCohorts,
compute::PriceRangeMax,
};
use super::super::cache::AddrLookup;
/// Process sent UTXOs for address cohorts: age metrics, cohort membership,
/// and empty-address transitions.
///
/// Takes separate price/timestamp slices rather than `chain_state` so it can
/// run in parallel with UTXO cohort processing (which mutates `chain_state`).
/// `price_range_max` feeds peak-regret computation via max price during
/// each UTXO's holding period.
#[allow(clippy::too_many_arguments)]
/// Process sent UTXOs for address cohort membership and empty-address transitions.
pub(crate) fn process_sent(
sent_data: HeightToAddrTypeToVec<(TypeIndex, Sats)>,
cohorts: &mut AddrCohorts,
lookup: &mut AddrLookup<'_>,
current_price: Cents,
price_range_max: &PriceRangeMax,
state: &mut AddrMetricsState,
received_addrs: &ByAddrType<FxHashSet<TypeIndex>>,
height_to_price: &[Cents],
height_to_timestamp: &[Timestamp],
current_height: Height,
current_timestamp: Timestamp,
seen_senders: &mut ByAddrType<FxHashSet<TypeIndex>>,
) -> Result<()> {
seen_senders.values_mut().for_each(|set| set.clear());
for (receive_height, by_type) in sent_data.into_iter() {
let prev_price = height_to_price[receive_height.to_usize()];
let prev_timestamp = height_to_timestamp[receive_height.to_usize()];
let age = Age::new(current_timestamp, prev_timestamp);
let peak_price = price_range_max.max_between(receive_height, current_height);
for (output_type, vec) in by_type.unwrap().into_iter() {
let type_received = received_addrs.get(output_type);
@@ -51,15 +36,11 @@ pub(crate) fn process_sent(
let pre = AddrSendPreState::capture(addr_data, output_type);
let prev_balance = addr_data.balance();
let new_balance = prev_balance.checked_sub(value).unwrap();
let is_first_encounter = type_seen.insert(type_index);
let also_received = type_received.is_some_and(|s| s.contains(&type_index));
let will_be_empty = addr_data.has_1_utxos();
let prev_bucket = AmountBucket::from(prev_balance);
let new_bucket = AmountBucket::from(new_balance);
let crossing_boundary = prev_bucket != new_bucket;
let cohort_state = cohorts
.amount_range
.get_mut_by_bucket(prev_bucket)
@@ -68,7 +49,9 @@ pub(crate) fn process_sent(
.unwrap();
// Mutates addr_data.spent_txo_count (+= 1). on_send_applied reads the post-spend view.
cohort_state.send(addr_data, value, current_price, prev_price, peak_price, age)?;
cohort_state.send(addr_data, value, current_price, prev_price)?;
let new_bucket = AmountBucket::from(addr_data.balance());
let crossing_boundary = prev_bucket != new_bucket;
state.on_send_applied(
output_type,
addr_data,
@@ -4,10 +4,10 @@ use brk_cohort::{AddrGroups, AmountRange, Filter, Filtered, OverAmount, UnderAmo
use brk_error::Result;
use brk_indexer::Lengths;
use brk_traversable::Traversable;
use brk_types::{Height, Sats, Version};
use brk_types::{Cents, Height, Sats, Version};
use derive_more::{Deref, DerefMut};
use rayon::prelude::*;
use vecdb::{AnyStoredVec, Database, Exit, ReadableVec, Rw, StorageMode};
use vecdb::{AnyStoredVec, CachedBoxedVec, Database, Exit, ReadableVec, Rw, StorageMode};
use crate::{
distribution::DynCohortVecs,
@@ -32,6 +32,7 @@ impl AddrCohorts {
indexes: &indexes::Vecs,
states_path: &Path,
cached_starts: &Windows<&WindowStartVec>,
spot_price: &CachedBoxedVec<Height, Cents>,
) -> Result<Self> {
let v = version + VERSION;
@@ -39,7 +40,16 @@ impl AddrCohorts {
let create =
|filter: Filter, name: &'static str, has_state: bool| -> Result<AddrCohortVecs> {
let sp = if has_state { Some(states_path) } else { None };
AddrCohortVecs::forced_import(db, filter, name, v, indexes, sp, cached_starts)
AddrCohortVecs::forced_import(
db,
filter,
name,
v,
indexes,
sp,
cached_starts,
spot_price,
)
};
let full = |f: Filter, name: &'static str| create(f, name, true);
@@ -108,14 +118,13 @@ impl AddrCohorts {
/// Second phase of post-processing: compute relative metrics.
pub(crate) fn compute_rest_part2(
&mut self,
prices: &price::Vecs,
starting_lengths: &Lengths,
all_supply_sats: &impl ReadableVec<Height, Sats>,
exit: &Exit,
) -> Result<()> {
self.0
.par_iter_mut()
.try_for_each(|v| v.compute_rest_part2(prices, starting_lengths, all_supply_sats, exit))
.try_for_each(|v| v.compute_rest_part2(starting_lengths, all_supply_sats, exit))
}
/// Returns a parallel iterator over all vecs for parallel writing.
@@ -0,0 +1,114 @@
use brk_cohort::Filter;
use brk_error::Result;
use brk_indexer::Lengths;
use brk_traversable::Traversable;
use brk_types::{Height, Sats};
use vecdb::{AnyStoredVec, Exit, ReadableVec, Rw, StorageMode};
use crate::{
distribution::metrics::{
ActivityMinimal, ImportConfig, OutputsUnspent, RealizedBase, SupplyBase,
},
price,
};
/// Address-balance metrics: holdings plus economically meaningful flows.
///
/// Address cohorts intentionally omit spent-output counts, realized price,
/// MVRV, and NUPL.
#[derive(Traversable)]
pub struct AddrCohortMetrics<M: StorageMode = Rw> {
#[traversable(skip)]
pub filter: Filter,
pub supply: Box<SupplyBase<M>>,
pub outputs: Box<OutputsUnspent<M>>,
pub activity: Box<ActivityMinimal<M>>,
pub realized: Box<RealizedBase<M>>,
}
impl AddrCohortMetrics {
pub(super) fn forced_import(cfg: &ImportConfig) -> Result<Self> {
Ok(Self {
filter: cfg.filter.clone(),
supply: Box::new(SupplyBase::forced_import(cfg)?),
outputs: Box::new(OutputsUnspent::forced_import(cfg)?),
activity: Box::new(ActivityMinimal::forced_import(cfg)?),
realized: Box::new(RealizedBase::forced_import(cfg)?),
})
}
pub(super) fn min_stateful_len(&self) -> usize {
self.supply
.min_len()
.min(self.outputs.min_len())
.min(self.activity.min_len())
.min(self.realized.min_stateful_len())
}
pub(super) fn collect_all_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
let mut vecs = Vec::new();
vecs.extend(self.supply.collect_vecs_mut());
vecs.extend(self.outputs.collect_vecs_mut());
vecs.extend(self.activity.collect_vecs_mut());
vecs.extend(self.realized.collect_vecs_mut());
vecs
}
pub(super) fn compute_from_sources(
&mut self,
starting_lengths: &Lengths,
others: &[&Self],
exit: &Exit,
) -> Result<()> {
self.supply.compute_from_stateful(
starting_lengths,
&others.iter().map(|v| v.supply.as_ref()).collect::<Vec<_>>(),
exit,
)?;
self.outputs.compute_from_stateful(
starting_lengths,
&others
.iter()
.map(|v| v.outputs.as_ref())
.collect::<Vec<_>>(),
exit,
)?;
self.activity.compute_from_stateful(
starting_lengths,
&others
.iter()
.map(|v| v.activity.as_ref())
.collect::<Vec<_>>(),
exit,
)?;
self.realized.compute_from_stateful(
starting_lengths,
&others
.iter()
.map(|v| v.realized.as_ref())
.collect::<Vec<_>>(),
exit,
)?;
Ok(())
}
pub(super) fn compute_rest_part1(
&mut self,
prices: &price::Vecs,
starting_lengths: &Lengths,
exit: &Exit,
) -> Result<()> {
self.activity
.compute_rest_part1(prices, starting_lengths, exit)
}
pub(super) fn compute_rest_part2(
&mut self,
starting_lengths: &Lengths,
all_supply_sats: &impl ReadableVec<Height, Sats>,
exit: &Exit,
) -> Result<()> {
self.supply
.compute_dominance(starting_lengths.height, all_supply_sats, exit)
}
}
@@ -1,4 +1,5 @@
mod groups;
mod metrics;
mod vecs;
pub use groups::*;
@@ -6,27 +6,31 @@ use brk_indexer::Lengths;
use brk_traversable::Traversable;
use brk_types::{Cents, Height, PartsPerMillionSigned64, Sats, StoredI64, StoredU64, Version};
use rayon::prelude::*;
use vecdb::{AnyStoredVec, AnyVec, Database, Exit, ReadableVec, Rw, StorageMode, WritableVec};
use vecdb::{
AnyStoredVec, AnyVec, CachedBoxedVec, Database, Exit, ReadableVec, Rw, StorageMode, WritableVec,
};
use crate::{
distribution::state::{AddrCohortState, MinimalRealizedState},
distribution::state::AddrCohortState,
indexes,
internal::{PerBlockWithDeltas, WindowStartVec, Windows},
price,
};
use crate::distribution::metrics::{ImportConfig, MinimalCohortMetrics};
use crate::distribution::metrics::ImportConfig;
use super::super::traits::{CohortVecs, DynCohortVecs};
use super::metrics::AddrCohortMetrics;
#[derive(Traversable)]
pub struct AddrCohortVecs<M: StorageMode = Rw> {
starting_height: Option<Height>,
#[traversable(skip)]
pub state: Option<Box<AddrCohortState<MinimalRealizedState>>>,
pub state: Option<Box<AddrCohortState>>,
#[traversable(flatten)]
pub metrics: MinimalCohortMetrics<M>,
pub metrics: AddrCohortMetrics<M>,
pub addr_count: PerBlockWithDeltas<StoredU64, StoredI64, PartsPerMillionSigned64, M>,
}
@@ -40,6 +44,7 @@ impl AddrCohortVecs {
indexes: &indexes::Vecs,
states_path: Option<&Path>,
cached_starts: &Windows<&WindowStartVec>,
spot_price: &CachedBoxedVec<Height, Cents>,
) -> Result<Self> {
let full_name = CohortContext::Addr.full_name(&filter, name);
@@ -50,6 +55,7 @@ impl AddrCohortVecs {
version,
indexes,
cached_starts,
spot_price,
};
let addr_count = PerBlockWithDeltas::forced_import(
@@ -64,7 +70,7 @@ impl AddrCohortVecs {
Ok(Self {
starting_height: None,
state: states_path.map(|path| Box::new(AddrCohortState::new(path, &full_name))),
metrics: MinimalCohortMetrics::forced_import(&cfg)?,
metrics: AddrCohortMetrics::forced_import(&cfg)?,
addr_count,
})
}
@@ -229,12 +235,11 @@ impl CohortVecs for AddrCohortVecs {
fn compute_rest_part2(
&mut self,
prices: &price::Vecs,
starting_lengths: &Lengths,
all_supply_sats: &impl ReadableVec<Height, Sats>,
exit: &Exit,
) -> Result<()> {
self.metrics
.compute_rest_part2(prices, starting_lengths, all_supply_sats, exit)
.compute_rest_part2(starting_lengths, all_supply_sats, exit)
}
}
@@ -61,7 +61,6 @@ pub trait CohortVecs: DynCohortVecs {
/// Second phase of post-processing computations.
fn compute_rest_part2(
&mut self,
prices: &price::Vecs,
starting_lengths: &Lengths,
all_supply_sats: &impl ReadableVec<Height, Sats>,
exit: &Exit,
@@ -1,4 +1,6 @@
use brk_cohort::{Filter, PROFITABILITY_RANGE_COUNT, compute_profitability_boundaries};
use brk_cohort::{
AGE_RANGE_COUNT, Filter, PROFITABILITY_RANGE_COUNT, compute_profitability_boundaries,
};
use brk_types::{Cents, CentsCompact, PartsPerMillion32, Sats};
use crate::{
@@ -11,9 +13,6 @@ use crate::{
use super::COST_BASIS_PRICE_DIGITS;
/// Number of age range cohorts (21: 20 boundaries + 1 unbounded).
const AGE_RANGE_COUNT: usize = 21;
// Tier boundaries for 5-significant-digit dollar bucketing.
// Matches the rounding used by `Cents::round_to_dollar(5)`.
const TIER0_COUNT: usize = 100_000; // $0-$99,999 exact dollars
@@ -25,15 +24,13 @@ const TIER1_START: usize = TIER0_COUNT;
/// Total number of buckets.
const TREE_SIZE: usize = TIER0_COUNT + TIER1_COUNT + OVERFLOW; // 190,001
/// 4-field Fenwick tree node for combined cost basis tracking.
/// Fenwick tree node for combined cost basis tracking.
#[derive(Clone, Copy, Default)]
pub(super) struct CostBasisNode {
all_sats: i64,
sth_sats: i64,
discount_sats: i64,
all_usd: i128,
sth_usd: i128,
discount_usd: i128,
}
impl CostBasisNode {
@@ -42,22 +39,8 @@ impl CostBasisNode {
Self {
all_sats: sats,
sth_sats: if is_sth { sats } else { 0 },
discount_sats: 0,
all_usd: usd,
sth_usd: if is_sth { usd } else { 0 },
discount_usd: 0,
}
}
#[inline(always)]
fn new_discount(sats: i64, usd: i128) -> Self {
Self {
all_sats: 0,
sth_sats: 0,
discount_sats: sats,
all_usd: 0,
sth_usd: 0,
discount_usd: usd,
}
}
}
@@ -67,10 +50,8 @@ impl FenwickNode for CostBasisNode {
fn add_assign(&mut self, other: &Self) {
self.all_sats += other.all_sats;
self.sth_sats += other.sth_sats;
self.discount_sats += other.discount_sats;
self.all_usd += other.all_usd;
self.sth_usd += other.sth_usd;
self.discount_usd += other.discount_usd;
}
}
@@ -174,29 +155,10 @@ impl CostBasisFenwick {
self.totals.add_assign(&delta);
}
/// Apply a net delta from the discount-entry cohort.
///
/// Supply totals are maintained from the age-range cohorts; this updates
/// only the discount-entry partition so premium can be derived as all - discount.
pub(super) fn apply_discount_delta(&mut self, price: CentsCompact, pending: &PendingDelta) {
let net_sats = u64::from(pending.inc) as i64 - u64::from(pending.dec) as i64;
if net_sats == 0 {
return;
}
let bucket = price_to_bucket(price);
let delta =
CostBasisNode::new_discount(net_sats, price.as_u128() as i128 * net_sats as i128);
self.tree.add(bucket, &delta);
self.totals.add_assign(&delta);
}
/// Bulk-initialize from age-range maps plus the discount-entry map.
/// Age-range maps maintain all/STH/LTH totals; the discount-entry map
/// maintains only the discount partition used to derive premium.
pub(super) fn bulk_init_with_discount<'a>(
/// Bulk-initialize from age-range maps.
pub(super) fn bulk_init<'a>(
&mut self,
maps: impl Iterator<Item = (&'a std::collections::BTreeMap<CentsCompact, Sats>, bool)>,
discount_maps: impl Iterator<Item = &'a std::collections::BTreeMap<CentsCompact, Sats>>,
) {
self.tree.reset();
self.totals = CostBasisNode::default();
@@ -212,15 +174,6 @@ impl CostBasisFenwick {
}
}
for map in discount_maps {
for (&price, &sats) in map.iter() {
let bucket = price_to_bucket(price);
let s = u64::from(sats) as i64;
let node = CostBasisNode::new_discount(s, price.as_u128() as i128 * s as i128);
self.tree.add_raw(bucket, &node);
self.totals.add_assign(&node);
}
}
self.tree.build_in_place();
self.initialized = true;
}
@@ -259,26 +212,6 @@ impl CostBasisFenwick {
)
}
/// Compute percentile prices for discount-entry cohort.
pub(super) fn percentiles_discount_entry(&self) -> PercentileResult {
self.compute_percentiles(
self.totals.discount_sats,
self.totals.discount_usd,
|n| n.discount_sats,
|n| n.discount_usd,
)
}
/// Compute percentile prices for premium-entry cohort (all - discount).
pub(super) fn percentiles_premium_entry(&self) -> PercentileResult {
self.compute_percentiles(
self.totals.all_sats - self.totals.discount_sats,
self.totals.all_usd - self.totals.discount_usd,
|n| n.all_sats - n.discount_sats,
|n| n.all_usd - n.discount_usd,
)
}
fn compute_percentiles(
&self,
total_sats: i64,
@@ -357,26 +290,6 @@ impl CostBasisFenwick {
)
}
/// Compute supply density for entry cohorts: (discount, premium).
pub(super) fn entry_density(
&self,
spot_price: Cents,
) -> (PartsPerMillion32, PartsPerMillion32) {
if self.totals.all_sats <= 0 {
return (PartsPerMillion32::ZERO, PartsPerMillion32::ZERO);
}
let range = self.density_range(spot_price);
let discount_range = range.discount_sats.max(0);
let premium_range = range.all_sats.max(0) - discount_range;
let premium_total = self.totals.all_sats - self.totals.discount_sats;
(
Self::to_ppm(discount_range, self.totals.discount_sats),
Self::to_ppm(premium_range, premium_total),
)
}
fn density_range(&self, spot_price: Cents) -> CostBasisNode {
let spot_f64 = u64::from(spot_price) as f64;
let low = Cents::from((spot_f64 * 0.95) as u64);
@@ -395,10 +308,8 @@ impl CostBasisFenwick {
CostBasisNode {
all_sats: cum_high.all_sats - cum_low.all_sats,
sth_sats: cum_high.sth_sats - cum_low.sth_sats,
discount_sats: cum_high.discount_sats - cum_low.discount_sats,
all_usd: cum_high.all_usd - cum_low.all_usd,
sth_usd: cum_high.sth_usd - cum_low.sth_usd,
discount_usd: cum_high.discount_usd - cum_low.discount_usd,
}
}
@@ -1,8 +1,8 @@
use std::path::Path;
use brk_cohort::{
AgeRange, AmountRange, ByEntry, ByEpoch, Class, CohortContext, Filter, Filtered, OverAge,
OverAmount, SpendableType, Term, UnderAge, UnderAmount,
AGE_RANGE_COUNT, AgeRange, AmountRange, ByEntry, ByEpoch, Class, CohortContext, Filter,
Filtered, OverAge, OverAmount, SpendableType, Term, UnderAge, UnderAmount,
};
use brk_error::Result;
use brk_indexer::Lengths;
@@ -10,7 +10,8 @@ use brk_traversable::Traversable;
use brk_types::{Cents, CentsSquaredSats, Dollars, Height, Sats, Version};
use rayon::prelude::*;
use vecdb::{
AnyStoredVec, AnyVec, Database, Exit, ReadOnlyClone, ReadableVec, Rw, StorageMode, WritableVec,
AnyStoredVec, AnyVec, CachedBoxedVec, Database, Exit, ReadOnlyClone, ReadableVec, Rw,
StorageMode, WritableVec,
};
use crate::{
@@ -44,7 +45,7 @@ pub struct UTXOCohorts<M: StorageMode = Rw> {
pub over_age: OverAge<UTXOCohortVecs<CoreCohortMetrics<M>>>,
pub epoch: ByEpoch<UTXOCohortVecs<CoreCohortMetrics<M>>>,
pub class: Class<UTXOCohortVecs<CoreCohortMetrics<M>>>,
pub entry: ByEntry<UTXOCohortVecs<ExtendedCohortMetrics<M>>>,
pub entry: ByEntry<UTXOCohortVecs<CoreCohortMetrics<M>>>,
pub over_amount: OverAmount<UTXOCohortVecs<MinimalCohortMetrics<M>>>,
pub amount_range: AmountRange<UTXOCohortVecs<MinimalCohortMetrics<M>>>,
pub under_amount: UnderAmount<UTXOCohortVecs<MinimalCohortMetrics<M>>>,
@@ -63,14 +64,14 @@ pub(crate) struct UTXOCohortsTransientState {
/// Cached partition_point positions for tick_tock boundary searches.
/// Avoids O(log n) binary search per boundary per block; scans forward
/// from last known position (typically O(1) per boundary).
pub(super) tick_tock_cached_positions: [usize; 20],
pub(super) tick_tock_cached_positions: [usize; AGE_RANGE_COUNT - 1],
}
impl UTXOCohorts<Rw> {
/// Separate cohorts currently total 72:
/// 21 age + 5 epoch + 18 class + 2 entry + 15 amount + 11 spendable type.
/// Separate cohorts currently total 74:
/// 23 age + 5 epoch + 18 class + 2 entry + 15 amount + 11 spendable type.
/// Keep small headroom because this is only Vec allocation capacity.
const SEPARATE_COHORT_CAPACITY: usize = 82;
const SEPARATE_COHORT_CAPACITY: usize = 84;
/// Import all UTXO cohorts from database.
pub(crate) fn forced_import(
@@ -79,6 +80,7 @@ impl UTXOCohorts<Rw> {
indexes: &indexes::Vecs,
states_path: &Path,
cached_starts: &Windows<&WindowStartVec>,
spot_price: &CachedBoxedVec<Height, Cents>,
) -> Result<Self> {
let v = version + VERSION;
@@ -91,6 +93,7 @@ impl UTXOCohorts<Rw> {
version: v + Version::ONE,
indexes,
cached_starts,
spot_price,
};
let all_supply = SupplyCore::forced_import(&all_cfg)?;
@@ -107,6 +110,7 @@ impl UTXOCohorts<Rw> {
version: v,
indexes,
cached_starts,
spot_price,
};
let state = Some(Box::new(UTXOCohortState::new(states_path, &full_name)));
Ok(UTXOCohortVecs::new(
@@ -127,6 +131,7 @@ impl UTXOCohorts<Rw> {
version: v,
indexes,
cached_starts,
spot_price,
};
let state = Some(Box::new(UTXOCohortState::new(states_path, &full_name)));
Ok(UTXOCohortVecs::new(
@@ -138,25 +143,7 @@ impl UTXOCohorts<Rw> {
let epoch = ByEpoch::try_new(&core_separate)?;
let class = Class::try_new(&core_separate)?;
let extended_separate =
|f: Filter, name: &'static str| -> Result<UTXOCohortVecs<ExtendedCohortMetrics>> {
let full_name = CohortContext::Utxo.full_name(&f, name);
let cfg = ImportConfig {
db,
filter: &f,
full_name: &full_name,
version: v,
indexes,
cached_starts,
};
let state = Some(Box::new(UTXOCohortState::new(states_path, &full_name)));
Ok(UTXOCohortVecs::new(
state,
ExtendedCohortMetrics::forced_import(&cfg)?,
))
};
let entry = ByEntry::try_new(&extended_separate)?;
let entry = ByEntry::try_new(&core_separate)?;
// Helper for separate cohorts with MinimalCohortMetrics + MinimalRealizedState
let minimal_separate =
@@ -169,6 +156,7 @@ impl UTXOCohorts<Rw> {
version: v,
indexes,
cached_starts,
spot_price,
};
let state = Some(Box::new(UTXOCohortState::new(states_path, &full_name)));
Ok(UTXOCohortVecs::new(
@@ -189,6 +177,7 @@ impl UTXOCohorts<Rw> {
version: v,
indexes,
cached_starts,
spot_price,
};
let state = Some(Box::new(UTXOCohortState::new(states_path, &full_name)));
Ok(UTXOCohortVecs::new(
@@ -206,7 +195,8 @@ impl UTXOCohorts<Rw> {
);
// Phase 3b: Import profitability metrics (derived from "all" during k-way merge).
let profitability = ProfitabilityMetrics::forced_import(db, v, indexes, cached_starts)?;
let profitability =
ProfitabilityMetrics::forced_import(db, v, indexes, cached_starts, spot_price)?;
// Phase 4: Import aggregate cohorts.
@@ -221,6 +211,7 @@ impl UTXOCohorts<Rw> {
version: v,
indexes,
cached_starts,
spot_price,
};
UTXOCohortVecs::new(None, ExtendedAdjustedCohortMetrics::forced_import(&cfg)?)
};
@@ -236,6 +227,7 @@ impl UTXOCohorts<Rw> {
version: v,
indexes,
cached_starts,
spot_price,
};
UTXOCohortVecs::new(None, ExtendedCohortMetrics::forced_import(&cfg)?)
};
@@ -251,6 +243,7 @@ impl UTXOCohorts<Rw> {
version: v,
indexes,
cached_starts,
spot_price,
};
Ok(UTXOCohortVecs::new(
None,
@@ -274,6 +267,7 @@ impl UTXOCohorts<Rw> {
version: v,
indexes,
cached_starts,
spot_price,
};
Ok(UTXOCohortVecs::new(
None,
@@ -332,7 +326,6 @@ impl UTXOCohorts<Rw> {
sth,
caches,
age_range,
entry,
..
} = self;
caches
@@ -351,15 +344,7 @@ impl UTXOCohorts<Rw> {
Some((map, caches.fenwick.is_sth_at(i)))
})
.collect();
let discount_maps = entry
.discount
.state
.as_ref()
.map(|state| state.cost_basis_map())
.into_iter();
caches
.fenwick
.bulk_init_with_discount(maps.into_iter(), discount_maps);
caches.fenwick.bulk_init(maps.into_iter());
}
/// Apply pending deltas from all age-range cohorts to the Fenwick tree.
@@ -370,10 +355,7 @@ impl UTXOCohorts<Rw> {
}
// Destructure to get separate borrows on caches and age_range
let Self {
caches,
age_range,
entry,
..
caches, age_range, ..
} = self;
for (i, sub) in age_range.iter().enumerate() {
if let Some(state) = sub.state.as_ref() {
@@ -383,11 +365,6 @@ impl UTXOCohorts<Rw> {
});
}
}
if let Some(state) = entry.discount.state.as_ref() {
state.for_each_cost_basis_pending(|&price, delta| {
caches.fenwick.apply_discount_delta(price, delta);
});
}
}
/// Push maturation sats to the matured vecs for the given height.
@@ -712,13 +689,8 @@ impl UTXOCohorts<Rw> {
}),
Box::new(|| {
entry.par_iter_mut().try_for_each(|v| {
v.metrics.compute_rest_part2(
prices,
starting_lengths,
height_to_market_cap,
ss,
exit,
)
v.metrics
.compute_rest_part2(prices, starting_lengths, ss, exit)
})
}),
Box::new(|| {
@@ -50,22 +50,6 @@ impl UTXOCohorts {
let lth = self.caches.fenwick.percentiles_lth();
push_cost_basis(&lth, lth_d, &mut self.lth.metrics.cost_basis);
let (discount_d, premium_d) = self.caches.fenwick.entry_density(spot_price);
let discount = self.caches.fenwick.percentiles_discount_entry();
push_cost_basis(
&discount,
discount_d,
&mut self.entry.discount.metrics.cost_basis,
);
let premium = self.caches.fenwick.percentiles_premium_entry();
push_cost_basis(
&premium,
premium_d,
&mut self.entry.premium.metrics.cost_basis,
);
let prof = self.caches.fenwick.profitability(spot_price);
push_profitability(&prof, &mut self.profitability);
}
@@ -1,4 +1,4 @@
use brk_cohort::{AGE_BOUNDARIES, AgeRange};
use brk_cohort::{AGE_BOUNDARIES, AGE_RANGE_COUNT, AgeRange};
use brk_types::{CostBasisSnapshot, ONE_HOUR_IN_SEC, Sats, Timestamp};
use vecdb::{Rw, unlikely};
@@ -14,7 +14,7 @@ impl UTXOCohorts<Rw> {
///
/// Uses cached positions per boundary to avoid binary search.
/// Since timestamps are monotonic, positions only advance forward.
/// Complexity: O(k * c) where k = 20 boundaries, c = ~1 (forward scan steps).
/// Complexity: O(k * c), where k is the boundary count and c is ~1 forward scan step.
///
/// Returns how many sats matured OUT OF each cohort into the older adjacent one.
/// `over_15y` is always zero since nothing ages out of the oldest cohort.
@@ -35,12 +35,12 @@ impl UTXOCohorts<Rw> {
return AgeRange::default();
}
let mut matured = [Sats::ZERO; 21];
let mut matured = [Sats::ZERO; AGE_RANGE_COUNT];
// Get age_range cohort states (indexed 0..21)
// Get age_range cohort states (indexed 0..AGE_RANGE_COUNT)
// Cohort i covers hours [BOUNDARIES[i-1], BOUNDARIES[i])
// Cohort 0 covers [0, 1) hours
// Cohort 20 covers [15*365*24, infinity) hours
// The final cohort covers [15*365*24, infinity) hours
let mut age_cohorts: Vec<_> = self.age_range.iter_mut().map(|v| &mut v.state).collect();
let cached = &mut self.caches.tick_tock_cached_positions;
@@ -74,7 +74,6 @@ pub(crate) fn process_blocks(
let tx_index_to_input_count = &indexes.tx_index.input_count;
let height_to_price_vec = cached_prices;
let height_to_timestamp_vec = cached_timestamps;
let start_usize = starting_height.to_usize();
let end_usize = last_height.to_usize() + 1;
@@ -440,13 +439,9 @@ pub(crate) fn process_blocks(
&mut vecs.addr_cohorts,
&mut lookup,
block_price,
ctx.price_range_max,
&mut state,
&received_addrs,
height_to_price_vec,
height_to_timestamp_vec,
height,
timestamp,
&mut seen_senders,
)
},
@@ -109,8 +109,6 @@ impl CoreCohortMetrics {
starting_lengths: &Lengths,
exit: &Exit,
) -> Result<()> {
self.supply.compute(prices, starting_lengths.height, exit)?;
self.activity
.compute_rest_part1(prices, starting_lengths, exit)?;
@@ -104,7 +104,6 @@ impl MinimalCohortMetrics {
starting_lengths: &Lengths,
exit: &Exit,
) -> Result<()> {
self.supply.compute(prices, starting_lengths.height, exit)?;
self.activity
.compute_rest_part1(prices, starting_lengths, exit)?;
Ok(())
@@ -66,7 +66,6 @@ impl TypeCohortMetrics {
starting_lengths: &Lengths,
exit: &Exit,
) -> Result<()> {
self.supply.compute(prices, starting_lengths.height, exit)?;
self.activity
.compute_rest_part1(prices, starting_lengths, exit)?;
Ok(())
@@ -4,7 +4,7 @@ use brk_types::{
Cents, Height, PartsPerMillion32, PartsPerMillionSigned32, PartsPerMillionSigned64, Version,
};
use schemars::JsonSchema;
use vecdb::{BytesVec, BytesVecValue, Database, ImportableVec};
use vecdb::{BytesVec, BytesVecValue, CachedBoxedVec, Database, ImportableVec};
use crate::{
indexes,
@@ -12,7 +12,7 @@ use crate::{
FiatPerBlock, FiatPerBlockCumulativeWithSums, FiatType, NumericValue, PerBlock,
PerBlockCumulativeRolling, PercentPerBlock, PercentRollingWindows, Price,
PriceWithRatioPerBlock, RatioPerBlock, RollingWindow24hPerBlock, RollingWindows,
RollingWindowsFrom1w, ValuePerBlock, ValuePerBlockCumulative,
RollingWindowsFrom1w, SpotValuePerBlock, ValuePerBlock, ValuePerBlockCumulative,
ValuePerBlockCumulativeRolling, WindowStartVec, Windows,
},
};
@@ -39,7 +39,6 @@ macro_rules! impl_config_import {
impl_config_import!(
ValuePerBlock,
ValuePerBlockCumulative,
PriceWithRatioPerBlock,
RatioPerBlock<PartsPerMillionSigned32>,
PercentPerBlock<PartsPerMillion32>,
PercentPerBlock<PartsPerMillionSigned32>,
@@ -48,6 +47,30 @@ impl_config_import!(
Price<PerBlock<Cents>>,
);
impl ConfigImport for PriceWithRatioPerBlock {
fn config_import(cfg: &ImportConfig, suffix: &str, offset: Version) -> Result<Self> {
Self::forced_import(
cfg.db,
&cfg.name(suffix),
cfg.version + offset,
cfg.indexes,
cfg.spot_price,
)
}
}
impl ConfigImport for SpotValuePerBlock {
fn config_import(cfg: &ImportConfig, suffix: &str, offset: Version) -> Result<Self> {
Self::forced_import(
cfg.db,
&cfg.name(suffix),
cfg.version + offset,
cfg.indexes,
cfg.spot_price,
)
}
}
// Generic types (macro_rules can't parse generic bounds, so written out)
impl<T: NumericValue + JsonSchema> ConfigImport for PerBlock<T> {
fn config_import(cfg: &ImportConfig, suffix: &str, offset: Version) -> Result<Self> {
@@ -128,6 +151,7 @@ pub struct ImportConfig<'a> {
pub version: Version,
pub indexes: &'a indexes::Vecs,
pub cached_starts: &'a Windows<&'a WindowStartVec>,
pub spot_price: &'a CachedBoxedVec<Height, Cents>,
}
impl<'a> ImportConfig<'a> {
@@ -127,10 +127,11 @@ pub use cohort::{
};
pub use config::ImportConfig;
pub use cost_basis::CostBasis;
pub use outputs::OutputsBase;
pub use outputs::{OutputsBase, OutputsUnspent};
pub use profitability::ProfitabilityMetrics;
pub use realized::{
AdjustedSopr, RealizedCore, RealizedFull, RealizedFullAccum, RealizedLike, RealizedMinimal,
AdjustedSopr, RealizedBase, RealizedCore, RealizedFull, RealizedFullAccum, RealizedLike,
RealizedMinimal,
};
pub use relative::{RelativeForAll, RelativeWithExtended};
pub use supply::{AvgAmountMetrics, SupplyBase, SupplyCore};
@@ -274,8 +275,6 @@ pub trait CohortMetricsBase:
starting_lengths: &Lengths,
exit: &Exit,
) -> Result<()> {
self.supply_mut()
.compute(prices, starting_lengths.height, exit)?;
self.activity_mut()
.compute_rest_part1(prices, starting_lengths, exit)?;
@@ -2,6 +2,7 @@ use brk_error::Result;
use brk_indexer::Lengths;
use brk_traversable::Traversable;
use brk_types::{PartsPerMillionSigned64, StoredI64, StoredU64, Version};
use derive_more::{Deref, DerefMut};
use vecdb::{AnyStoredVec, AnyVec, Exit, Rw, StorageMode, WritableVec};
use crate::{
@@ -12,16 +13,14 @@ use crate::{
internal::{PerBlockCumulativeRolling, PerBlockWithDeltas},
};
/// Base output metrics: utxo_count + delta.
/// Unspent output metrics.
#[derive(Traversable)]
pub struct OutputsBase<M: StorageMode = Rw> {
pub struct OutputsUnspent<M: StorageMode = Rw> {
pub unspent_count: PerBlockWithDeltas<StoredU64, StoredI64, PartsPerMillionSigned64, M>,
pub spent_count: PerBlockCumulativeRolling<StoredU64, M>,
}
impl OutputsBase {
impl OutputsUnspent {
pub(crate) fn forced_import(cfg: &ImportConfig) -> Result<Self> {
let v1 = Version::ONE;
Ok(Self {
unspent_count: PerBlockWithDeltas::forced_import(
cfg.db,
@@ -31,15 +30,11 @@ impl OutputsBase {
cfg.indexes,
cfg.cached_starts,
)?,
spent_count: cfg.import("spent_utxo_count", v1)?,
})
}
pub(crate) fn min_len(&self) -> usize {
self.unspent_count
.height
.len()
.min(self.spent_count.block.len())
self.unspent_count.height.len()
}
#[inline(always)]
@@ -47,15 +42,10 @@ impl OutputsBase {
self.unspent_count
.height
.push(StoredU64::from(state.supply.utxo_count));
self.spent_count
.push_block(StoredU64::from(state.spent_utxo_count));
}
pub(crate) fn collect_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
vec![
&mut self.unspent_count.height as &mut dyn AnyStoredVec,
self.spent_count.stored_mut(),
]
vec![&mut self.unspent_count.height as &mut dyn AnyStoredVec]
}
pub(crate) fn compute_from_stateful(
@@ -72,6 +62,55 @@ impl OutputsBase {
.collect::<Vec<_>>(),
exit,
)?;
Ok(())
}
}
/// Base output metrics: unspent and spent output counts.
#[derive(Deref, DerefMut, Traversable)]
pub struct OutputsBase<M: StorageMode = Rw> {
#[deref]
#[deref_mut]
#[traversable(flatten)]
pub unspent: OutputsUnspent<M>,
pub spent_count: PerBlockCumulativeRolling<StoredU64, M>,
}
impl OutputsBase {
pub(crate) fn forced_import(cfg: &ImportConfig) -> Result<Self> {
let v1 = Version::ONE;
Ok(Self {
unspent: OutputsUnspent::forced_import(cfg)?,
spent_count: cfg.import("spent_utxo_count", v1)?,
})
}
pub(crate) fn min_len(&self) -> usize {
self.unspent.min_len().min(self.spent_count.block.len())
}
#[inline(always)]
pub(crate) fn push_state(&mut self, state: &CohortState<impl RealizedOps, impl CostBasisOps>) {
self.unspent.push_state(state);
self.spent_count
.push_block(StoredU64::from(state.spent_utxo_count));
}
pub(crate) fn collect_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
let mut vecs = self.unspent.collect_vecs_mut();
vecs.push(self.spent_count.stored_mut());
vecs
}
pub(crate) fn compute_from_stateful(
&mut self,
starting_lengths: &Lengths,
others: &[&Self],
exit: &Exit,
) -> Result<()> {
let unspent: Vec<_> = others.iter().map(|v| &v.unspent).collect();
self.unspent
.compute_from_stateful(starting_lengths, &unspent, exit)?;
sum_others!(self, starting_lengths, others, exit; spent_count.cumulative.height);
Ok(())
}
@@ -1,3 +1,3 @@
mod base;
pub use base::OutputsBase;
pub use base::{OutputsBase, OutputsUnspent};
@@ -2,13 +2,14 @@ use brk_cohort::{Loss, Profit, ProfitabilityRange};
use brk_error::Result;
use brk_indexer::Lengths;
use brk_traversable::Traversable;
use brk_types::{Bitcoin, Cents, Dollars, PartsPerMillionSigned32, Sats, Version};
use vecdb::{AnyStoredVec, AnyVec, Database, Exit, Rw, StorageMode, WritableVec};
use brk_types::{Bitcoin, Cents, Dollars, Height, PartsPerMillionSigned32, Sats, Version};
use vecdb::{AnyStoredVec, AnyVec, CachedBoxedVec, Database, Exit, Rw, StorageMode, WritableVec};
use crate::{
indexes,
internal::{
PerBlock, RatioPerBlock, ValuePerBlock, ValuePerBlockWithDeltas, WindowStartVec, Windows,
PerBlock, RatioPerBlock, SpotValuePerBlock, SpotValuePerBlockWithDeltas, WindowStartVec,
Windows,
},
price,
};
@@ -21,7 +22,7 @@ pub struct WithSth<All, Sth = All> {
#[derive(Traversable)]
pub struct ProfitabilityBucket<M: StorageMode = Rw> {
pub supply: WithSth<ValuePerBlockWithDeltas<M>, ValuePerBlock<M>>,
pub supply: WithSth<SpotValuePerBlockWithDeltas<M>, SpotValuePerBlock<M>>,
pub realized_cap: WithSth<PerBlock<Dollars, M>>,
pub unrealized_pnl: WithSth<PerBlock<Dollars, M>>,
pub nupl: RatioPerBlock<PartsPerMillionSigned32, M>,
@@ -45,21 +46,24 @@ impl ProfitabilityBucket {
version: Version,
indexes: &indexes::Vecs,
cached_starts: &Windows<&WindowStartVec>,
spot_price: &CachedBoxedVec<Height, Cents>,
) -> Result<Self> {
Ok(Self {
supply: WithSth {
all: ValuePerBlockWithDeltas::forced_import(
all: SpotValuePerBlockWithDeltas::forced_import(
db,
&format!("{name}_supply"),
version,
indexes,
cached_starts,
spot_price,
)?,
sth: ValuePerBlock::forced_import(
sth: SpotValuePerBlock::forced_import(
db,
&format!("{name}_sth_supply"),
version,
indexes,
spot_price,
)?,
},
realized_cap: WithSth {
@@ -122,9 +126,6 @@ impl ProfitabilityBucket {
) -> Result<()> {
let max_from = starting_lengths.height;
self.supply.all.compute(prices, max_from, exit)?;
self.supply.sth.compute(prices, max_from, exit)?;
self.unrealized_pnl.all.height.compute_transform3(
max_from,
&prices.spot.cents.height,
@@ -223,9 +224,7 @@ impl ProfitabilityBucket {
pub(crate) fn collect_all_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
vec![
&mut self.supply.all.inner.sats.height as &mut dyn AnyStoredVec,
&mut self.supply.all.inner.cents.height,
&mut self.supply.sth.sats.height,
&mut self.supply.sth.cents.height,
&mut self.realized_cap.all.height,
&mut self.realized_cap.sth.height,
&mut self.unrealized_pnl.all.height,
@@ -269,19 +268,41 @@ impl ProfitabilityMetrics {
version: Version,
indexes: &indexes::Vecs,
cached_starts: &Windows<&WindowStartVec>,
spot_price: &CachedBoxedVec<Height, Cents>,
) -> Result<Self> {
let range = ProfitabilityRange::try_new(|name| {
ProfitabilityBucket::forced_import(db, name, version, indexes, cached_starts)
ProfitabilityBucket::forced_import(
db,
name,
version,
indexes,
cached_starts,
spot_price,
)
})?;
let aggregate_version = version + Version::TWO;
let profit = Profit::try_new(|name| {
ProfitabilityBucket::forced_import(db, name, aggregate_version, indexes, cached_starts)
ProfitabilityBucket::forced_import(
db,
name,
aggregate_version,
indexes,
cached_starts,
spot_price,
)
})?;
let loss = Loss::try_new(|name| {
ProfitabilityBucket::forced_import(db, name, aggregate_version, indexes, cached_starts)
ProfitabilityBucket::forced_import(
db,
name,
aggregate_version,
indexes,
cached_starts,
spot_price,
)
})?;
Ok(Self {
@@ -271,11 +271,6 @@ impl RealizedFull {
exit,
)?;
// Capitalized price ratio
self.capitalized
.price
.compute_ratio(starting_lengths, &prices.spot.cents.height, exit)?;
// Sell-side risk ratios
for (ssrr, rv) in self
.sell_side_risk_ratio
@@ -5,6 +5,7 @@ use brk_types::{
Bitcoin, Cents, CentsSigned, Height, PartsPerMillion64, PartsPerMillionSigned64, Sats,
StoredF32, Version,
};
use derive_more::{Deref, DerefMut};
use vecdb::{AnyStoredVec, AnyVec, Exit, ReadableVec, Rw, StorageMode, WritableVec};
use crate::{
@@ -19,40 +20,27 @@ use crate::{
use crate::distribution::metrics::ImportConfig;
#[derive(Traversable)]
pub struct RealizedMinimal<M: StorageMode = Rw> {
pub struct RealizedBase<M: StorageMode = Rw> {
pub cap: FiatPerBlockWithDeltas<Cents, CentsSigned, PartsPerMillionSigned64, M>,
pub profit: FiatPerBlockCumulativeWithSums<Cents, M>,
pub loss: FiatPerBlockCumulativeWithSums<Cents, M>,
pub price: PriceWithRatioPerBlock<M>,
pub mvrv: LazyPerBlock<StoredF32>,
}
impl RealizedMinimal {
impl RealizedBase {
pub(crate) fn forced_import(cfg: &ImportConfig) -> Result<Self> {
let v1 = Version::ONE;
let cap = FiatPerBlockWithDeltas::forced_import(
cfg.db,
&cfg.name("realized_cap"),
cfg.version,
Version::TWO,
cfg.indexes,
cfg.cached_starts,
)?;
let price: PriceWithRatioPerBlock = cfg.import("realized_price", v1)?;
let mvrv = LazyPerBlock::from_lazy::<Identity<StoredF32>, PartsPerMillion64>(
&cfg.name("mvrv"),
cfg.version,
&price.ratio,
);
Ok(Self {
cap,
cap: FiatPerBlockWithDeltas::forced_import(
cfg.db,
&cfg.name("realized_cap"),
cfg.version,
Version::TWO,
cfg.indexes,
cfg.cached_starts,
)?,
profit: cfg.import("realized_profit", v1)?,
loss: cfg.import("realized_loss", v1)?,
price,
mvrv,
})
}
@@ -99,33 +87,80 @@ impl RealizedMinimal {
)?;
Ok(())
}
}
#[derive(Deref, DerefMut, Traversable)]
pub struct RealizedMinimal<M: StorageMode = Rw> {
#[deref]
#[deref_mut]
#[traversable(flatten)]
pub base: RealizedBase<M>,
pub price: PriceWithRatioPerBlock<M>,
pub mvrv: LazyPerBlock<StoredF32>,
}
impl RealizedMinimal {
pub(crate) fn forced_import(cfg: &ImportConfig) -> Result<Self> {
let v1 = Version::ONE;
let base = RealizedBase::forced_import(cfg)?;
let price: PriceWithRatioPerBlock = cfg.import("realized_price", v1)?;
let mvrv = LazyPerBlock::from_lazy::<Identity<StoredF32>, PartsPerMillion64>(
&cfg.name("mvrv"),
cfg.version,
&price.ratio,
);
Ok(Self { base, price, mvrv })
}
pub(crate) fn min_stateful_len(&self) -> usize {
self.base.min_stateful_len()
}
#[inline(always)]
pub(crate) fn push_state(&mut self, state: &CohortState<impl RealizedOps, impl CostBasisOps>) {
self.base.push_state(state);
}
pub(crate) fn collect_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
self.base.collect_vecs_mut()
}
pub(crate) fn compute_from_stateful(
&mut self,
starting_lengths: &Lengths,
others: &[&Self],
exit: &Exit,
) -> Result<()> {
let bases: Vec<_> = others.iter().map(|v| &v.base).collect();
self.base
.compute_from_stateful(starting_lengths, &bases, exit)
}
pub(crate) fn compute_rest_part2(
&mut self,
prices: &price::Vecs,
_prices: &price::Vecs,
starting_lengths: &Lengths,
height_to_supply: &impl ReadableVec<Height, Bitcoin>,
exit: &Exit,
) -> Result<()> {
let cap = &self.cap.cents.height;
self.price
.compute_all(prices, starting_lengths, exit, |v| {
Ok(v.compute_transform2(
starting_lengths.height,
cap,
height_to_supply,
|(i, cap_cents, supply, ..)| {
let cap = cap_cents.as_u128();
let supply_sats = Sats::from(supply).as_u128();
let cents = (cap * Sats::ONE_BTC_U128)
.checked_div(supply_sats)
.map(Cents::from)
.unwrap_or(Cents::ZERO);
(i, cents)
},
exit,
)?)
})?;
let cap = &self.base.cap.cents.height;
self.price.cents.height.compute_transform2(
starting_lengths.height,
cap,
height_to_supply,
|(i, cap_cents, supply, ..)| {
let cap = cap_cents.as_u128();
let supply_sats = Sats::from(supply).as_u128();
let cents = (cap * Sats::ONE_BTC_U128)
.checked_div(supply_sats)
.map(Cents::from)
.unwrap_or(Cents::ZERO);
(i, cents)
},
exit,
)?;
Ok(())
}
@@ -6,7 +6,7 @@ mod minimal;
pub use self::core::RealizedCore;
pub use adjusted::AdjustedSopr;
pub use full::{RealizedFull, RealizedFullAccum};
pub use minimal::RealizedMinimal;
pub use minimal::{RealizedBase, RealizedMinimal};
use brk_error::Result;
use brk_indexer::Lengths;
@@ -1,17 +1,19 @@
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Height, Sats, StoredU64, Version};
use vecdb::{AnyStoredVec, Database, Exit, ReadableVec, Rw, StorageMode, WritableVec};
use brk_types::{Cents, Height, Sats, StoredU64, Version};
use vecdb::{
AnyStoredVec, CachedBoxedVec, Database, Exit, ReadableVec, Rw, StorageMode, WritableVec,
};
use crate::{indexes, internal::ValuePerBlock, price};
use crate::{indexes, internal::SpotValuePerBlock};
/// Average amount held per UTXO and per funded address.
///
/// `utxo = supply / utxo_count`, `addr = supply / funded_addr_count`.
#[derive(Traversable)]
pub struct AvgAmountMetrics<M: StorageMode = Rw> {
pub utxo: ValuePerBlock<M>,
pub addr: ValuePerBlock<M>,
pub utxo: SpotValuePerBlock<M>,
pub addr: SpotValuePerBlock<M>,
}
impl AvgAmountMetrics {
@@ -20,6 +22,7 @@ impl AvgAmountMetrics {
prefix: &str,
version: Version,
indexes: &indexes::Vecs,
spot_price: &CachedBoxedVec<Height, Cents>,
) -> Result<Self> {
let name = |suffix: &str| {
if prefix.is_empty() {
@@ -29,31 +32,38 @@ impl AvgAmountMetrics {
}
};
Ok(Self {
utxo: ValuePerBlock::forced_import(db, &name("avg_utxo_amount"), version, indexes)?,
addr: ValuePerBlock::forced_import(db, &name("avg_addr_amount"), version, indexes)?,
utxo: SpotValuePerBlock::forced_import(
db,
&name("avg_utxo_amount"),
version,
indexes,
spot_price,
)?,
addr: SpotValuePerBlock::forced_import(
db,
&name("avg_addr_amount"),
version,
indexes,
spot_price,
)?,
})
}
pub(crate) fn collect_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
vec![
&mut self.utxo.sats.height as &mut dyn AnyStoredVec,
&mut self.utxo.cents.height,
&mut self.addr.sats.height,
&mut self.addr.cents.height,
]
}
pub(crate) fn reset_height(&mut self) -> Result<()> {
self.utxo.sats.height.reset()?;
self.utxo.cents.height.reset()?;
self.addr.sats.height.reset()?;
self.addr.cents.height.reset()?;
Ok(())
}
pub(crate) fn compute(
&mut self,
prices: &price::Vecs,
supply_sats: &impl ReadableVec<Height, Sats>,
utxo_count: &impl ReadableVec<Height, StoredU64>,
funded_addr_count: &impl ReadableVec<Height, StoredU64>,
@@ -64,13 +74,11 @@ impl AvgAmountMetrics {
.sats
.height
.compute_divide(max_from, supply_sats, utxo_count, exit)?;
self.utxo.compute(prices, max_from, exit)?;
self.addr
.sats
.height
.compute_divide(max_from, supply_sats, funded_addr_count, exit)?;
self.addr.compute(prices, max_from, exit)?;
Ok(())
}
@@ -4,13 +4,10 @@ use brk_traversable::Traversable;
use brk_types::{Height, PartsPerMillion32, PartsPerMillionSigned64, Sats, SatsSigned, Version};
use vecdb::{AnyStoredVec, AnyVec, Exit, ReadableVec, Rw, StorageMode, WritableVec};
use crate::{
distribution::state::{CohortState, CostBasisOps, RealizedOps},
price,
};
use crate::distribution::state::{CohortState, CostBasisOps, RealizedOps};
use crate::internal::{
LazyRollingDeltasAmountFromHeight, PercentPerBlock, RatioSats, ValuePerBlock,
LazyRollingDeltasAmountFromHeight, PercentPerBlock, RatioSats, SpotValuePerBlock,
};
use crate::distribution::metrics::ImportConfig;
@@ -18,7 +15,7 @@ use crate::distribution::metrics::ImportConfig;
/// Base supply metrics: total supply + dominance (share of circulating).
#[derive(Traversable)]
pub struct SupplyBase<M: StorageMode = Rw> {
pub total: ValuePerBlock<M>,
pub total: SpotValuePerBlock<M>,
pub delta: LazyRollingDeltasAmountFromHeight<Sats, SatsSigned, PartsPerMillionSigned64>,
#[traversable(rename = "dominance")]
pub dominance: PercentPerBlock<PartsPerMillion32, M>,
@@ -26,7 +23,7 @@ pub struct SupplyBase<M: StorageMode = Rw> {
impl SupplyBase {
pub(crate) fn forced_import(cfg: &ImportConfig) -> Result<Self> {
let supply: ValuePerBlock = cfg.import("supply", Version::ZERO)?;
let supply: SpotValuePerBlock = cfg.import("supply", Version::ZERO)?;
let delta = LazyRollingDeltasAmountFromHeight::new(
&cfg.name("supply_delta"),
@@ -57,20 +54,10 @@ impl SupplyBase {
pub(crate) fn collect_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
vec![
&mut self.total.sats.height as &mut dyn AnyStoredVec,
&mut self.total.cents.height,
&mut self.dominance.ppm.height,
]
}
pub(crate) fn compute(
&mut self,
prices: &price::Vecs,
max_from: Height,
exit: &Exit,
) -> Result<()> {
self.total.compute(prices, max_from, exit)
}
pub(crate) fn compute_dominance(
&mut self,
max_from: Height,
@@ -1,14 +1,14 @@
use brk_error::Result;
use brk_indexer::Lengths;
use brk_traversable::Traversable;
use brk_types::{Height, Version};
use brk_types::Version;
use derive_more::{Deref, DerefMut};
use vecdb::{AnyStoredVec, AnyVec, Exit, Rw, StorageMode, WritableVec};
use crate::{distribution::state::UnrealizedState, price};
use crate::distribution::state::UnrealizedState;
use crate::internal::{
HalveCents, HalveDollars, HalveSats, HalveSatsToBitcoin, LazyValuePerBlock, ValuePerBlock,
HalveCents, HalveDollars, HalveSats, HalveSatsToBitcoin, LazyValuePerBlock, SpotValuePerBlock,
};
use crate::distribution::metrics::ImportConfig;
@@ -24,8 +24,8 @@ pub struct SupplyCore<M: StorageMode = Rw> {
pub base: SupplyBase<M>,
pub half: LazyValuePerBlock,
pub in_profit: ValuePerBlock<M>,
pub in_loss: ValuePerBlock<M>,
pub in_profit: SpotValuePerBlock<M>,
pub in_loss: SpotValuePerBlock<M>,
}
impl SupplyCore {
@@ -33,7 +33,7 @@ impl SupplyCore {
let v0 = Version::ZERO;
let base = SupplyBase::forced_import(cfg)?;
let half = LazyValuePerBlock::from_block_source::<
let half = LazyValuePerBlock::from_spot_block_source::<
HalveSats,
HalveSatsToBitcoin,
HalveCents,
@@ -64,24 +64,10 @@ impl SupplyCore {
pub(crate) fn collect_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
let mut vecs = self.base.collect_vecs_mut();
vecs.push(&mut self.in_profit.sats.height as &mut dyn AnyStoredVec);
vecs.push(&mut self.in_profit.cents.height);
vecs.push(&mut self.in_loss.sats.height);
vecs.push(&mut self.in_loss.cents.height);
vecs
}
pub(crate) fn compute(
&mut self,
prices: &price::Vecs,
max_from: Height,
exit: &Exit,
) -> Result<()> {
self.base.compute(prices, max_from, exit)?;
self.in_profit.compute(prices, max_from, exit)?;
self.in_loss.compute(prices, max_from, exit)?;
Ok(())
}
pub(crate) fn validate_computed_versions(&mut self, _base_version: Version) -> Result<()> {
Ok(())
}
@@ -7,7 +7,7 @@ use vecdb::{AnyStoredVec, AnyVec, Exit, ReadableCloneableVec, Rw, StorageMode, W
use crate::{
distribution::{metrics::ImportConfig, state::UnrealizedState},
internal::{FiatPerBlock, LazyPerBlock, NegCentsUnsignedToDollars, PerBlock},
internal::{FiatPerBlock, LazyPerBlock, NegCentsUnsignedToDollars},
};
use super::UnrealizedMinimal;
@@ -27,7 +27,7 @@ pub struct UnrealizedBasic<M: StorageMode = Rw> {
impl UnrealizedBasic {
pub(crate) fn forced_import(
cfg: &ImportConfig,
mvrv: &PerBlock<PartsPerMillion64>,
mvrv: &LazyPerBlock<PartsPerMillion64>,
) -> Result<Self> {
let v1 = Version::ONE;
@@ -7,7 +7,7 @@ use vecdb::{AnyStoredVec, Exit, Rw, StorageMode};
use crate::{
distribution::{metrics::ImportConfig, state::UnrealizedState},
internal::{CentsSubtractToCentsSigned, FiatPerBlock, PerBlock},
internal::{CentsSubtractToCentsSigned, FiatPerBlock, LazyPerBlock},
};
use super::UnrealizedBasic;
@@ -25,7 +25,7 @@ pub struct UnrealizedCore<M: StorageMode = Rw> {
impl UnrealizedCore {
pub(crate) fn forced_import(
cfg: &ImportConfig,
mvrv: &PerBlock<PartsPerMillion64>,
mvrv: &LazyPerBlock<PartsPerMillion64>,
) -> Result<Self> {
let basic = UnrealizedBasic::forced_import(cfg, mvrv)?;
let net_unrealized_pnl = cfg.import("net_unrealized_pnl", Version::ZERO)?;
@@ -6,7 +6,7 @@ use derive_more::{Deref, DerefMut};
use vecdb::{AnyStoredVec, AnyVec, BytesVec, Exit, ReadableVec, Rw, StorageMode, WritableVec};
use crate::distribution::state::UnrealizedState;
use crate::internal::{CentsSubtractToCentsSigned, FiatPerBlock, PerBlock};
use crate::internal::{CentsSubtractToCentsSigned, FiatPerBlock, LazyPerBlock};
use crate::{distribution::metrics::ImportConfig, price};
use super::UnrealizedCore;
@@ -43,7 +43,7 @@ pub struct UnrealizedFull<M: StorageMode = Rw> {
impl UnrealizedFull {
pub(crate) fn forced_import(
cfg: &ImportConfig,
mvrv: &PerBlock<PartsPerMillion64>,
mvrv: &LazyPerBlock<PartsPerMillion64>,
) -> Result<Self> {
let v0 = Version::ZERO;
let inner = UnrealizedCore::forced_import(cfg, mvrv)?;
@@ -2,7 +2,7 @@ use brk_traversable::Traversable;
use brk_types::{PartsPerMillion64, PartsPerMillionSigned32, Version};
use vecdb::UnaryTransform;
use crate::internal::{LazyRatioPerBlock, PerBlock};
use crate::internal::{LazyPerBlock, LazyRatioPerBlock};
use crate::distribution::metrics::ImportConfig;
@@ -14,9 +14,9 @@ pub struct UnrealizedMinimal {
}
impl UnrealizedMinimal {
pub(crate) fn new(cfg: &ImportConfig, mvrv: &PerBlock<PartsPerMillion64>) -> Self {
pub(crate) fn new(cfg: &ImportConfig, mvrv: &LazyPerBlock<PartsPerMillion64>) -> Self {
Self {
nupl: LazyRatioPerBlock::from_source::<MvrvToNupl>(
nupl: LazyRatioPerBlock::from_lazy_source::<MvrvToNupl, PartsPerMillion64>(
&cfg.name("nupl"),
cfg.version + VERSION,
mvrv,
@@ -1,25 +1,22 @@
use std::path::Path;
use brk_error::Result;
use brk_types::{Age, Cents, FundedAddrData, Sats, SupplyState};
use brk_types::{Cents, FundedAddrData, Sats, SupplyState};
use vecdb::unlikely;
use super::super::cost_basis::{CostBasisRaw, RealizedOps};
use super::super::cost_basis::{CostBasisRaw, MinimalRealizedState};
use super::base::CohortState;
/// Significant digits for address cost basis prices (after rounding to dollars).
const COST_BASIS_PRICE_DIGITS: i32 = 4;
pub struct AddrCohortState<R: RealizedOps> {
pub struct AddrCohortState {
pub addr_count: u64,
pub inner: CohortState<R, CostBasisRaw>,
pub inner: CohortState<MinimalRealizedState, CostBasisRaw>,
}
impl<R: RealizedOps> AddrCohortState<R> {
impl AddrCohortState {
pub(crate) fn new(path: &Path, name: &str) -> Self {
Self {
addr_count: 0,
inner: CohortState::new(path, name).with_price_rounding(COST_BASIS_PRICE_DIGITS),
inner: CohortState::new(path, name),
}
}
@@ -30,7 +27,7 @@ impl<R: RealizedOps> AddrCohortState<R> {
self.inner.sent = Sats::ZERO;
self.inner.spent_utxo_count = 0;
self.inner.satdays_destroyed = Sats::ZERO;
self.inner.realized = R::default();
self.inner.realized = MinimalRealizedState::default();
}
pub(crate) fn send(
@@ -39,12 +36,8 @@ impl<R: RealizedOps> AddrCohortState<R> {
value: Sats,
current_price: Cents,
prev_price: Cents,
ath: Cents,
age: Age,
) -> Result<()> {
let prev = addr_data.cost_basis_snapshot();
addr_data.send(value, prev_price)?;
let current = addr_data.cost_basis_snapshot();
let prev_ps = addr_data.send(value, prev_price)?;
self.inner.send_addr(
&SupplyState {
@@ -52,11 +45,7 @@ impl<R: RealizedOps> AddrCohortState<R> {
value,
},
current_price,
prev_price,
ath,
age,
&current,
&prev,
prev_ps,
);
Ok(())
@@ -69,32 +58,29 @@ impl<R: RealizedOps> AddrCohortState<R> {
price: Cents,
output_count: u32,
) {
let prev = addr_data.cost_basis_snapshot();
addr_data.receive_outputs(value, price, output_count);
let current = addr_data.cost_basis_snapshot();
let cap = addr_data.receive_outputs(value, price, output_count);
self.inner.receive_addr(
self.inner.increment_addr(
&SupplyState {
utxo_count: output_count as u64,
value,
},
price,
&current,
&prev,
cap,
);
}
pub(crate) fn add(&mut self, addr_data: &FundedAddrData) {
self.addr_count += 1;
let supply = SupplyState::from(addr_data);
self.inner
.increment_snapshot(&addr_data.cost_basis_snapshot());
.increment_addr(&supply, addr_data.realized_cap_raw);
}
pub(crate) fn subtract(&mut self, addr_data: &FundedAddrData) {
let snapshot = addr_data.cost_basis_snapshot();
let supply = SupplyState::from(addr_data);
// Check for potential underflow before it happens
if unlikely(self.inner.supply.utxo_count < snapshot.supply_state.utxo_count) {
if unlikely(self.inner.supply.utxo_count < supply.utxo_count) {
panic!(
"AddrCohortState::subtract underflow!\n\
Cohort state: addr_count={}, supply={}\n\
@@ -105,11 +91,11 @@ impl<R: RealizedOps> AddrCohortState<R> {
self.addr_count,
self.inner.supply,
addr_data,
snapshot.supply_state,
snapshot.realized_price
supply,
addr_data.realized_price()
);
}
if unlikely(self.inner.supply.value < snapshot.supply_state.value) {
if unlikely(self.inner.supply.value < supply.value) {
panic!(
"AddrCohortState::subtract value underflow!\n\
Cohort state: addr_count={}, supply={}\n\
@@ -120,8 +106,8 @@ impl<R: RealizedOps> AddrCohortState<R> {
self.addr_count,
self.inner.supply,
addr_data,
snapshot.supply_state,
snapshot.realized_price
supply,
addr_data.realized_price()
);
}
@@ -130,10 +116,52 @@ impl<R: RealizedOps> AddrCohortState<R> {
"AddrCohortState::subtract addr_count underflow! addr_count=0\n\
Addr being subtracted: {}\n\
Realized price: {}",
addr_data, snapshot.realized_price
addr_data,
addr_data.realized_price()
)
});
self.inner.decrement_snapshot(&snapshot);
self.inner
.decrement_addr(&supply, addr_data.realized_cap_raw);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::distribution::state::cost_basis::RealizedOps;
#[test]
fn address_updates_preserve_supply_cap_and_transfer_state() {
let mut cohort = AddrCohortState::new(Path::new(""), "test");
let mut addr = FundedAddrData::default();
addr.receive(Sats::ONE_BTC, Cents::new(10_000));
cohort.add(&addr);
cohort.receive_outputs(&mut addr, Sats::new(50_000_000), Cents::new(20_000), 1);
cohort
.send(
&mut addr,
Sats::new(25_000_000),
Cents::new(15_000),
Cents::new(10_000),
)
.unwrap();
assert_eq!(
cohort.inner.supply.utxo_count,
SupplyState::from(&addr).utxo_count
);
assert_eq!(cohort.inner.supply.value, SupplyState::from(&addr).value);
assert_eq!(cohort.inner.realized.cap(), Cents::new(17_500));
assert_eq!(cohort.inner.realized.profit(), Cents::new(1_250));
assert_eq!(cohort.inner.sent, Sats::new(25_000_000));
assert_eq!(cohort.inner.realized.value_destroyed(), Cents::ZERO);
cohort.subtract(&addr);
assert_eq!(cohort.addr_count, 0);
assert_eq!(cohort.inner.supply.utxo_count, 0);
assert_eq!(cohort.inner.supply.value, Sats::ZERO);
assert_eq!(cohort.inner.realized.cap(), Cents::ZERO);
}
}
@@ -7,7 +7,10 @@ use brk_types::{
};
use super::super::{
cost_basis::{Accumulate, CostBasisData, CostBasisOps, RealizedOps, UnrealizedState},
cost_basis::{
Accumulate, CostBasisData, CostBasisOps, CostBasisRaw, MinimalRealizedState, RealizedOps,
UnrealizedState,
},
pending::PendingDelta,
};
@@ -76,12 +79,6 @@ impl<R: RealizedOps, C: CostBasisOps> CohortState<R, C> {
}
}
/// Enable price rounding for cost basis data.
pub(crate) fn with_price_rounding(mut self, digits: i32) -> Self {
self.cost_basis = self.cost_basis.with_price_rounding(digits);
self
}
pub(crate) fn import_at_or_before(&mut self, height: Height) -> Result<Height> {
self.cost_basis.import_at_or_before(height)
}
@@ -167,38 +164,6 @@ impl<R: RealizedOps, C: CostBasisOps> CohortState<R, C> {
}
}
pub(crate) fn receive_addr(
&mut self,
supply: &SupplyState,
price: Cents,
current: &CostBasisSnapshot,
prev: &CostBasisSnapshot,
) {
self.supply += supply;
if supply.value > Sats::ZERO {
self.realized.receive(price, supply.value);
if current.supply_state.value.is_not_zero() {
self.cost_basis.increment(
current.realized_price,
current.supply_state.value,
current.price_sats,
current.capitalized_cap_raw,
);
}
if prev.supply_state.value.is_not_zero() {
self.cost_basis.decrement(
prev.realized_price,
prev.supply_state.value,
prev.price_sats,
prev.capitalized_cap_raw,
);
}
}
}
pub(crate) fn send_utxo_precomputed(&mut self, supply: &SupplyState, pre: &SendPrecomputed) {
self.supply -= supply;
self.sent += pre.sats;
@@ -239,63 +204,52 @@ impl<R: RealizedOps, C: CostBasisOps> CohortState<R, C> {
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn write(&mut self, height: Height, cleanup: bool) -> Result<()> {
self.cost_basis.write(height, cleanup)
}
}
impl CohortState<MinimalRealizedState, CostBasisRaw> {
pub(crate) fn increment_addr(&mut self, supply: &SupplyState, cap: CentsSats) {
self.supply += supply;
if supply.value.is_not_zero() {
self.realized.increment_cap(cap);
self.cost_basis.increment_cap(cap);
}
}
pub(crate) fn decrement_addr(&mut self, supply: &SupplyState, cap: CentsSats) {
self.supply -= supply;
if supply.value.is_not_zero() {
self.realized.decrement_cap(cap);
self.cost_basis.decrement_cap(cap);
}
}
pub(crate) fn send_addr(
&mut self,
supply: &SupplyState,
current_price: Cents,
prev_price: Cents,
ath: Cents,
age: Age,
current: &CostBasisSnapshot,
prev: &CostBasisSnapshot,
prev_ps: CentsSats,
) {
if supply.utxo_count == 0 {
return;
}
self.supply -= supply;
self.spent_utxo_count += supply.utxo_count;
if supply.value > Sats::ZERO {
self.sent += supply.value;
if R::TRACK_ACTIVITY {
self.satdays_destroyed += age.satdays_destroyed(supply.value);
}
let sats = supply.value;
// Compute once for realized.send using typed values
let current_ps = CentsSats::from_price_sats(current_price, sats);
let prev_ps = CentsSats::from_price_sats(prev_price, sats);
let ath_ps = CentsSats::from_price_sats(ath, sats);
let prev_capitalized_cap = prev_ps.to_capitalized_cap(prev_price);
self.realized
.send(sats, current_ps, prev_ps, ath_ps, prev_capitalized_cap);
if current.supply_state.value.is_not_zero() {
self.cost_basis.increment(
current.realized_price,
current.supply_state.value,
current.price_sats,
current.capitalized_cap_raw,
);
}
if prev.supply_state.value.is_not_zero() {
self.cost_basis.decrement(
prev.realized_price,
prev.supply_state.value,
prev.price_sats,
prev.capitalized_cap_raw,
);
}
if supply.value == Sats::ZERO {
return;
}
}
pub(crate) fn write(&mut self, height: Height, cleanup: bool) -> Result<()> {
self.cost_basis.write(height, cleanup)
self.sent += supply.value;
let sats = supply.value;
let current_ps = CentsSats::from_price_sats(current_price, sats);
self.realized.realize_spend(current_ps, prev_ps);
self.cost_basis.decrement_cap(prev_ps);
}
}
@@ -24,7 +24,6 @@ const STATE_TO_KEEP: usize = 10;
/// Implemented by `CostBasisRaw` (scalars only) and `CostBasisData` (full map + scalars).
pub trait CostBasisOps: Send + Sync + 'static {
fn create(path: &Path, name: &str) -> Self;
fn with_price_rounding(self, digits: i32) -> Self;
fn import_at_or_before(&mut self, height: Height) -> Result<Height>;
fn cap_raw(&self) -> CentsSats;
fn capitalized_cap_raw(&self) -> CentsSquaredSats;
@@ -78,6 +77,16 @@ pub struct CostBasisRaw {
}
impl CostBasisRaw {
#[inline]
pub(crate) fn increment_cap(&mut self, value: CentsSats) {
self.pending_cap.inc += value;
}
#[inline]
pub(crate) fn decrement_cap(&mut self, value: CentsSats) {
self.pending_cap.dec += value;
}
pub(super) fn path_state(&self, height: Height) -> PathBuf {
self.pathbuf.join(height.to_string())
}
@@ -151,10 +160,6 @@ impl CostBasisOps for CostBasisRaw {
}
}
fn with_price_rounding(self, _digits: i32) -> Self {
self
}
fn import_at_or_before(&mut self, height: Height) -> Result<Height> {
let files = self.read_dir(None)?;
let (&height, path) = files.range(..=height).next_back().ok_or(Error::NotFound(
@@ -189,7 +194,7 @@ impl CostBasisOps for CostBasisRaw {
price_sats: CentsSats,
_capitalized_cap: CentsSquaredSats,
) {
self.pending_cap.inc += price_sats;
self.increment_cap(price_sats);
}
#[inline]
@@ -200,7 +205,7 @@ impl CostBasisOps for CostBasisRaw {
price_sats: CentsSats,
_capitalized_cap: CentsSquaredSats,
) {
self.pending_cap.dec += price_sats;
self.decrement_cap(price_sats);
}
fn apply_pending(&mut self) {
@@ -237,26 +242,18 @@ impl CostBasisOps for CostBasisRaw {
/// Generic over the accumulator `S`:
/// - `WithCapital`: tracks all fields including invested capital + capitalized cap (128 bytes)
/// - `WithoutCapital`: tracks only supply + unrealized profit/loss (64 bytes, 1 cache line)
/// and writes a compact checkpoint without a capitalized-cap slot
#[derive(Clone, Debug)]
pub struct CostBasisData<S: Accumulate> {
raw: CostBasisRaw,
map: Option<UrpdRaw>,
pending: FxHashMap<CentsCompact, PendingDelta>,
cache: Option<CachedUnrealizedState<S>>,
rounding_digits: Option<i32>,
capitalized_cap_raw: CentsSquaredSats,
pending_capitalized_cap: PendingCapitalizedCapRawDelta,
}
impl<S: Accumulate> CostBasisData<S> {
#[inline]
fn round_price(&self, price: Cents) -> Cents {
match self.rounding_digits {
Some(digits) => price.round_to_dollar(digits),
None => price,
}
}
pub(crate) fn map(&self) -> &CostBasisMap {
debug_assert!(self.pending.is_empty() && self.raw.pending_cap.is_zero());
&self.map.as_ref().unwrap().map
@@ -345,17 +342,11 @@ impl<S: Accumulate> CostBasisOps for CostBasisData<S> {
map: None,
pending: FxHashMap::default(),
cache: None,
rounding_digits: None,
capitalized_cap_raw: CentsSquaredSats::ZERO,
pending_capitalized_cap: PendingCapitalizedCapRawDelta::default(),
}
}
fn with_price_rounding(mut self, digits: i32) -> Self {
self.rounding_digits = Some(digits);
self
}
fn import_at_or_before(&mut self, height: Height) -> Result<Height> {
let files = self.raw.read_dir(None)?;
let (&height, path) = files.range(..=height).next_back().ok_or(Error::NotFound(
@@ -363,14 +354,18 @@ impl<S: Accumulate> CostBasisOps for CostBasisData<S> {
))?;
let data = fs::read(path)?;
let (base, rest) = UrpdRaw::deserialize_with_rest(&data)?;
self.map = Some(base);
self.raw.state = Some(RawState::deserialize(rest)?);
debug_assert!(
rest.len() >= 32,
rest.len() >= if S::TRACK_CAPITAL { 32 } else { 16 },
"CostBasisData state too short: {} bytes",
rest.len()
);
self.capitalized_cap_raw = CentsSquaredSats::from_bytes(&rest[16..32])?;
self.map = Some(base);
self.raw.state = Some(RawState::deserialize(rest)?);
self.capitalized_cap_raw = if S::TRACK_CAPITAL {
CentsSquaredSats::from_bytes(&rest[16..32])?
} else {
CentsSquaredSats::ZERO
};
self.pending.clear();
self.raw.pending_cap = PendingCapDelta::default();
self.pending_capitalized_cap = PendingCapitalizedCapRawDelta::default();
@@ -394,10 +389,9 @@ impl<S: Accumulate> CostBasisOps for CostBasisData<S> {
price_sats: CentsSats,
capitalized_cap: CentsSquaredSats,
) {
let price = self.round_price(price);
self.pending.entry(price.into()).or_default().inc += sats;
self.raw.pending_cap.inc += price_sats;
if capitalized_cap != CentsSquaredSats::ZERO {
self.raw.increment_cap(price_sats);
if S::TRACK_CAPITAL && capitalized_cap != CentsSquaredSats::ZERO {
self.pending_capitalized_cap.inc += capitalized_cap;
}
if let Some(cache) = self.cache.as_mut() {
@@ -413,10 +407,9 @@ impl<S: Accumulate> CostBasisOps for CostBasisData<S> {
price_sats: CentsSats,
capitalized_cap: CentsSquaredSats,
) {
let price = self.round_price(price);
self.pending.entry(price.into()).or_default().dec += sats;
self.raw.pending_cap.dec += price_sats;
if capitalized_cap != CentsSquaredSats::ZERO {
self.raw.decrement_cap(price_sats);
if S::TRACK_CAPITAL && capitalized_cap != CentsSquaredSats::ZERO {
self.pending_capitalized_cap.dec += capitalized_cap;
}
if let Some(cache) = self.cache.as_mut() {
@@ -427,19 +420,21 @@ impl<S: Accumulate> CostBasisOps for CostBasisData<S> {
fn apply_pending(&mut self) {
self.apply_map_pending();
self.raw.apply_pending_cap();
self.capitalized_cap_raw += self.pending_capitalized_cap.inc;
debug_assert!(
self.capitalized_cap_raw >= self.pending_capitalized_cap.dec,
"CostBasis capitalized_cap_raw underflow!\n\
Path: {:?}\n\
Current (after increments): {:?}\n\
Trying to decrement by: {:?}",
self.raw.pathbuf,
self.capitalized_cap_raw,
self.pending_capitalized_cap.dec
);
self.capitalized_cap_raw -= self.pending_capitalized_cap.dec;
self.pending_capitalized_cap = PendingCapitalizedCapRawDelta::default();
if S::TRACK_CAPITAL {
self.capitalized_cap_raw += self.pending_capitalized_cap.inc;
debug_assert!(
self.capitalized_cap_raw >= self.pending_capitalized_cap.dec,
"CostBasis capitalized_cap_raw underflow!\n\
Path: {:?}\n\
Current (after increments): {:?}\n\
Trying to decrement by: {:?}",
self.raw.pathbuf,
self.capitalized_cap_raw,
self.pending_capitalized_cap.dec
);
self.capitalized_cap_raw -= self.pending_capitalized_cap.dec;
self.pending_capitalized_cap = PendingCapitalizedCapRawDelta::default();
}
}
fn init(&mut self) {
@@ -464,9 +459,70 @@ impl<S: Accumulate> CostBasisOps for CostBasisData<S> {
let raw_state = self.raw.state.as_ref().unwrap();
let mut buffer = self.map.as_ref().unwrap().serialize()?;
buffer.extend(raw_state.cap_raw.to_bytes());
buffer.extend(self.capitalized_cap_raw.to_bytes());
if S::TRACK_CAPITAL {
buffer.extend(self.capitalized_cap_raw.to_bytes());
}
fs::write(self.raw.path_state(height), buffer)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use std::{
fs,
sync::atomic::{AtomicU64, Ordering},
};
use super::*;
use crate::distribution::state::{WithCapital, WithoutCapital};
static NEXT_PATH_ID: AtomicU64 = AtomicU64::new(0);
fn test_path() -> PathBuf {
std::env::temp_dir().join(format!(
"brk-cost-basis-data-{}-{}",
std::process::id(),
NEXT_PATH_ID.fetch_add(1, Ordering::Relaxed)
))
}
#[test]
fn checkpoint_layout_tracks_capital_capability() {
let root = test_path();
let mut compact = CostBasisData::<WithoutCapital>::create(&root, "compact");
compact.clean().unwrap();
compact.init();
compact.write(Height::ZERO, false).unwrap();
let mut full = CostBasisData::<WithCapital>::create(&root, "full");
full.clean().unwrap();
full.init();
full.write(Height::ZERO, false).unwrap();
let compact_len = fs::metadata(compact.raw.path_state(Height::ZERO))
.unwrap()
.len();
let full_len = fs::metadata(full.raw.path_state(Height::ZERO))
.unwrap()
.len();
assert_eq!(full_len, compact_len + 16);
let mut compact_reader = CostBasisData::<WithoutCapital>::create(&root, "compact");
assert_eq!(
compact_reader.import_at_or_before(Height::ZERO).unwrap(),
Height::ZERO
);
let mut legacy_reader = CostBasisData::<WithoutCapital>::create(&root, "full");
assert_eq!(
legacy_reader.import_at_or_before(Height::ZERO).unwrap(),
Height::ZERO
);
assert_eq!(legacy_reader.capitalized_cap_raw(), CentsSquaredSats::ZERO);
fs::remove_dir_all(root).unwrap();
}
}
@@ -1,6 +1,7 @@
use std::cmp::Ordering;
use brk_types::{Cents, CentsSats, CentsSquaredSats, Sats};
use vecdb::unlikely;
/// Trait for realized state operations, implemented by Minimal, Core, and Full variants.
pub trait RealizedOps: Default + Clone + Send + Sync + 'static {
@@ -36,14 +37,39 @@ pub trait RealizedOps: Default + Clone + Send + Sync + 'static {
);
}
/// Minimal realized state: cap, profit, loss, value_created/destroyed.
/// Used by MinimalCohortMetrics cohorts (amount_range, type_, address — ~135 separate cohorts).
/// Minimal realized state: cap, profit, and loss.
/// Used by MinimalCohortMetrics cohorts (amount_range and type) and address cohorts.
#[derive(Debug, Default, Clone)]
pub struct MinimalRealizedState {
cap_raw: u128,
profit_raw: u128,
loss_raw: u128,
value_destroyed_raw: u128,
}
impl MinimalRealizedState {
#[inline]
pub(crate) fn increment_cap(&mut self, cap: CentsSats) {
self.cap_raw += cap.as_u128();
}
#[inline]
pub(crate) fn decrement_cap(&mut self, cap: CentsSats) {
self.cap_raw -= cap.as_u128();
}
#[inline]
pub(crate) fn realize_spend(&mut self, current: CentsSats, previous: CentsSats) {
match current.cmp(&previous) {
Ordering::Greater => {
self.profit_raw += (current - previous).as_u128();
}
Ordering::Less => {
self.loss_raw += (previous - current).as_u128();
}
Ordering::Equal => {}
}
self.decrement_cap(previous);
}
}
impl RealizedOps for MinimalRealizedState {
@@ -71,14 +97,6 @@ impl RealizedOps for MinimalRealizedState {
Cents::new((self.loss_raw / Sats::ONE_BTC_U128) as u64)
}
#[inline]
fn value_destroyed(&self) -> Cents {
if self.value_destroyed_raw == 0 {
return Cents::ZERO;
}
Cents::new((self.value_destroyed_raw / Sats::ONE_BTC_U128) as u64)
}
#[inline]
fn set_cap_raw(&mut self, cap_raw: CentsSats) {
self.cap_raw = cap_raw.inner();
@@ -91,7 +109,6 @@ impl RealizedOps for MinimalRealizedState {
fn reset_single_iteration_values(&mut self) {
self.profit_raw = 0;
self.loss_raw = 0;
self.value_destroyed_raw = 0;
}
#[inline]
@@ -99,18 +116,17 @@ impl RealizedOps for MinimalRealizedState {
if sats.is_zero() {
return;
}
let price_sats = CentsSats::from_price_sats(price, sats);
self.cap_raw += price_sats.as_u128();
self.increment_cap(CentsSats::from_price_sats(price, sats));
}
#[inline]
fn increment_snapshot(&mut self, price_sats: CentsSats, _capitalized_cap: CentsSquaredSats) {
self.cap_raw += price_sats.as_u128();
self.increment_cap(price_sats);
}
#[inline]
fn decrement_snapshot(&mut self, price_sats: CentsSats, _capitalized_cap: CentsSquaredSats) {
self.cap_raw -= price_sats.as_u128();
self.decrement_cap(price_sats);
}
#[inline]
@@ -122,25 +138,16 @@ impl RealizedOps for MinimalRealizedState {
_ath_ps: CentsSats,
_prev_capitalized_cap: CentsSquaredSats,
) {
match current_ps.cmp(&prev_ps) {
Ordering::Greater => {
self.profit_raw += (current_ps - prev_ps).as_u128();
}
Ordering::Less => {
self.loss_raw += (prev_ps - current_ps).as_u128();
}
Ordering::Equal => {}
}
self.cap_raw -= prev_ps.as_u128();
self.value_destroyed_raw += prev_ps.as_u128();
self.realize_spend(current_ps, prev_ps);
}
}
/// Core realized state: extends Minimal with sent_in_profit/loss tracking.
/// Used by CoreCohortMetrics cohorts (epoch, class, under_age, over_age — ~59 separate cohorts).
/// Core realized state: extends Minimal with activity tracking.
/// Used by CoreCohortMetrics cohorts (entry, epoch, class, under_age, over_age).
#[derive(Debug, Default, Clone)]
pub struct CoreRealizedState {
minimal: MinimalRealizedState,
value_destroyed_raw: u128,
sent_in_profit: Sats,
sent_in_loss: Sats,
}
@@ -165,7 +172,10 @@ impl RealizedOps for CoreRealizedState {
#[inline]
fn value_destroyed(&self) -> Cents {
self.minimal.value_destroyed()
if unlikely(self.value_destroyed_raw == 0) {
return Cents::ZERO;
}
Cents::new((self.value_destroyed_raw / Sats::ONE_BTC_U128) as u64)
}
#[inline]
@@ -189,6 +199,7 @@ impl RealizedOps for CoreRealizedState {
#[inline]
fn reset_single_iteration_values(&mut self) {
self.minimal.reset_single_iteration_values();
self.value_destroyed_raw = 0;
self.sent_in_profit = Sats::ZERO;
self.sent_in_loss = Sats::ZERO;
}
@@ -221,6 +232,7 @@ impl RealizedOps for CoreRealizedState {
) {
self.minimal
.send(sats, current_ps, prev_ps, ath_ps, prev_capitalized_cap);
self.value_destroyed_raw += prev_ps.as_u128();
match current_ps.cmp(&prev_ps) {
Ordering::Greater | Ordering::Equal => {
self.sent_in_profit += sats;
@@ -240,7 +252,7 @@ impl CoreRealizedState {
}
/// Full realized state (~160 bytes).
/// Used by BasicCohortMetrics cohorts (age_range — 21 separate cohorts).
/// Used by BasicCohortMetrics cohorts (age_range — 23 separate cohorts).
#[derive(Debug, Default, Clone)]
pub struct RealizedState {
core: CoreRealizedState,
@@ -376,3 +388,52 @@ impl RealizedState {
self.peak_regret_raw
}
}
#[cfg(test)]
mod tests {
use super::*;
fn price_sats(price: u64) -> CentsSats {
CentsSats::from_price_sats(Cents::new(price), Sats::ONE_BTC)
}
#[test]
fn minimal_tracks_only_cap_profit_and_loss() {
let mut state = MinimalRealizedState::default();
state.increment(Cents::new(10_000), Sats::ONE_BTC);
state.send(
Sats::ONE_BTC,
price_sats(15_000),
price_sats(10_000),
CentsSats::ZERO,
CentsSquaredSats::ZERO,
);
assert_eq!(state.cap(), Cents::ZERO);
assert_eq!(state.profit(), Cents::new(5_000));
assert_eq!(state.loss(), Cents::ZERO);
assert_eq!(state.value_destroyed(), Cents::ZERO);
assert_eq!(state.sent_in_profit(), Sats::ZERO);
assert_eq!(state.sent_in_loss(), Sats::ZERO);
}
#[test]
fn core_adds_activity_to_minimal_realized_state() {
let mut state = CoreRealizedState::default();
state.increment(Cents::new(10_000), Sats::ONE_BTC);
state.send(
Sats::ONE_BTC,
price_sats(15_000),
price_sats(10_000),
CentsSats::ZERO,
CentsSquaredSats::ZERO,
);
assert_eq!(state.cap(), Cents::ZERO);
assert_eq!(state.profit(), Cents::new(5_000));
assert_eq!(state.loss(), Cents::ZERO);
assert_eq!(state.value_destroyed(), Cents::new(10_000));
assert_eq!(state.sent_in_profit(), Sats::ONE_BTC);
assert_eq!(state.sent_in_loss(), Sats::ZERO);
}
}
@@ -54,6 +54,8 @@ fn div_btc(raw: u128) -> Cents {
/// Trait for accumulating profit/loss across BTreeMap entries.
/// `WithoutCapital` skips capital tracking; `WithCapital` tracks all fields.
pub trait Accumulate: Default + Clone + Send + Sync + 'static {
const TRACK_CAPITAL: bool;
fn to_output(&self) -> UnrealizedState;
fn core(&self) -> &WithoutCapital;
fn core_mut(&mut self) -> &mut WithoutCapital;
@@ -78,6 +80,8 @@ pub trait Accumulate: Default + Clone + Send + Sync + 'static {
}
impl Accumulate for WithoutCapital {
const TRACK_CAPITAL: bool = false;
fn to_output(&self) -> UnrealizedState {
UnrealizedState {
supply_in_profit: self.supply_in_profit,
@@ -114,6 +118,8 @@ impl Accumulate for WithoutCapital {
}
impl Accumulate for WithCapital {
const TRACK_CAPITAL: bool = true;
fn to_output(&self) -> UnrealizedState {
UnrealizedState {
capitalized_cap_in_profit_raw: self.capitalized_cap_in_profit,
+44 -19
View File
@@ -40,7 +40,7 @@ use super::{
metrics::AvgAmountMetrics,
};
const VERSION: Version = Version::new(24 + brk_oracle::VERSION);
const VERSION: Version = Version::new(30 + brk_oracle::VERSION);
#[derive(Traversable)]
pub struct AddrMetricsVecs<M: StorageMode = Rw> {
@@ -170,6 +170,8 @@ struct DistributionTransientState {
}
const SAVED_STAMPED_CHANGES: u16 = 10;
/// Version of the fixed-width `FundedAddrData` record layout.
const FUNDED_ADDR_DATA_VERSION: Version = Version::ONE;
impl Vecs {
pub(crate) fn forced_import(
@@ -177,6 +179,7 @@ impl Vecs {
parent_version: Version,
indexes: &indexes::Vecs,
cached_starts: &Windows<&WindowStartVec>,
prices: &price::Vecs,
) -> Result<Self> {
let db_path = parent.join(super::DB_NAME);
let states_path = db_path.join("states");
@@ -185,16 +188,30 @@ impl Vecs {
db.set_min_regions(50_000)?;
let version = parent_version + VERSION;
let spot_price = prices.spot.cents.height.read_only_cached_boxed_clone();
let utxo_cohorts =
UTXOCohorts::forced_import(&db, version, indexes, &states_path, cached_starts)?;
let utxo_cohorts = UTXOCohorts::forced_import(
&db,
version,
indexes,
&states_path,
cached_starts,
&spot_price,
)?;
let addr_cohorts =
AddrCohorts::forced_import(&db, version, indexes, &states_path, cached_starts)?;
let addr_cohorts = AddrCohorts::forced_import(
&db,
version,
indexes,
&states_path,
cached_starts,
&spot_price,
)?;
// Create address data BytesVecs first so we can also use them for identity mappings
let funded_addr_data_version = version + FUNDED_ADDR_DATA_VERSION;
let funded_addr_index_to_funded_addr_data = BytesVec::forced_import_with(
vecdb::ImportOptions::new(&db, "funded_addr_data", version)
vecdb::ImportOptions::new(&db, "funded_addr_data", funded_addr_data_version)
.with_saved_stamped_changes(SAVED_STAMPED_CHANGES),
)?;
let empty_addr_index_to_empty_addr_data = BytesVec::forced_import_with(
@@ -205,7 +222,7 @@ impl Vecs {
// Identity mappings for traversable
let funded_addr_index = LazyVecFrom1::init(
"funded_addr_index",
version,
funded_addr_data_version,
funded_addr_index_to_funded_addr_data.read_only_boxed_clone(),
|index, _| index,
);
@@ -232,19 +249,32 @@ impl Vecs {
// `reused_*` uses the receive-side predicate (funded_txo_count > 1,
// industry standard). `respent_*` uses the spend-side counterpart
// (spent_txo_count > 1, strictly more restrictive).
let reused_addr_count =
ReusedAddrVecs::forced_import(&db, "reused", version, indexes, cached_starts)?;
let respent_addr_count =
ReusedAddrVecs::forced_import(&db, "respent", version, indexes, cached_starts)?;
let reused_addr_count = ReusedAddrVecs::forced_import(
&db,
"reused",
version,
indexes,
cached_starts,
&spot_price,
)?;
let respent_addr_count = ReusedAddrVecs::forced_import(
&db,
"respent",
version,
indexes,
cached_starts,
&spot_price,
)?;
// Exposed address tracking (counts + supply) - quantum / pubkey-exposure sense
let exposed_addr_vecs = ExposedAddrVecs::forced_import(&db, version, indexes)?;
let exposed_addr_vecs = ExposedAddrVecs::forced_import(&db, version, indexes, &spot_price)?;
// Growth rate: delta change + rate (global + per-type)
let delta = DeltaVecs::new(version, &addr_count, cached_starts, indexes);
// Average amount (supply / utxo_count, supply / funded_addr_count) for `all` and per addr type.
let avg_amount = WithAddrTypes::<AvgAmountMetrics>::forced_import(&db, version, indexes)?;
let avg_amount =
WithAddrTypes::<AvgAmountMetrics>::forced_import(&db, version, indexes, &spot_price)?;
let this = Self {
supply_state: BytesVec::forced_import_with(
@@ -585,7 +615,6 @@ impl Vecs {
&starting_lengths,
&outputs.by_type,
&inputs.by_type,
prices,
all_supply_sats,
&type_supply_sats,
exit,
@@ -594,14 +623,12 @@ impl Vecs {
&starting_lengths,
&outputs.by_type,
&inputs.by_type,
prices,
all_supply_sats,
&type_supply_sats,
exit,
)?;
self.addrs.exposed.compute_rest(
&starting_lengths,
prices,
all_supply_sats,
&type_supply_sats,
exit,
@@ -610,7 +637,6 @@ impl Vecs {
// Average amount (supply / utxo_count, supply / funded_addr_count) for `all` and per addr type.
let all_m = &self.utxo_cohorts.all.metrics;
self.addrs.avg_amount.all.compute(
prices,
&all_m.supply.total.sats.height,
&all_m.outputs.unspent_count.height,
&self.addrs.funded.all.height,
@@ -626,7 +652,6 @@ impl Vecs {
{
let type_m = &t.get(ot).metrics;
avg.compute(
prices,
&type_m.supply.total.sats.height,
&type_m.outputs.unspent_count.height,
&funded.height,
@@ -672,7 +697,7 @@ impl Vecs {
.height
.read_only_clone();
self.addr_cohorts
.compute_rest_part2(prices, &starting_lengths, &all_supply_sats, exit)?;
.compute_rest_part2(&starting_lengths, &all_supply_sats, exit)?;
let exit = exit.clone();
self.db.run_bg(move |db| {
@@ -28,7 +28,7 @@ impl Vecs {
&mut self,
indexer: &Indexer,
indexes: &indexes::Vecs,
prices: &price::Vecs,
_prices: &price::Vecs,
distribution: &distribution::Vecs,
cointime: &cointime::Vecs,
exit: &Exit,
@@ -67,7 +67,7 @@ impl Vecs {
.map(|cohort| &cohort.coindays_created.cumulative.height)
.collect();
let rest_start = self.compute_primary(
self.compute_primary(
&starting_lengths,
&indexes.timestamp.monotonic,
&transfer_volumes,
@@ -77,9 +77,6 @@ impl Vecs {
&realized_caps,
exit,
)?;
let mut rest_lengths = starting_lengths.clone();
rest_lengths.height = rest_start;
self.compute_rest(prices, &rest_lengths, exit)?;
let exit = exit.clone();
self.db.run_bg(move |db| {
@@ -271,34 +268,6 @@ impl Vecs {
Ok(Height::from(start))
}
fn compute_rest(
&mut self,
prices: &price::Vecs,
starting_lengths: &Lengths,
exit: &Exit,
) -> Result<()> {
for cohort in self.age_range.iter_mut() {
cohort
.supply
.mobile
.compute(prices, starting_lengths.height, exit)?;
cohort
.supply
.immobile
.compute(prices, starting_lengths.height, exit)?;
}
self.supply
.mobile
.compute(prices, starting_lengths.height, exit)?;
self.supply
.immobile
.compute(prices, starting_lengths.height, exit)?;
self.price
.compute_ratio(starting_lengths, &prices.spot.cents.height, exit)?;
Ok(())
}
fn primary_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
let mut vecs = Vec::with_capacity(AGE_COHORT_COUNT * 4 + 5 + HORIZON_COUNT);
@@ -2,14 +2,14 @@ use std::path::Path;
use brk_cohort::{AgeRange, CohortContext};
use brk_error::Result;
use brk_types::{StoredF64, Version};
use vecdb::{ReadableCloneableVec, UnaryTransform};
use brk_types::{Cents, Height, StoredF64, Version};
use vecdb::{CachedBoxedVec, ReadableCloneableVec, UnaryTransform};
use super::{CohortVecs, DB_NAME, HorizonVecs, Horizons, Split, Vecs, mobility};
use crate::{
indexes,
internal::{
FiatPerBlock, LazyPerBlock, PerBlock, PriceWithRatioPerBlock, ValuePerBlock,
FiatPerBlock, LazyPerBlock, PerBlock, PriceWithRatioPerBlock, SpotValuePerBlock,
db_utils::{finalize_db, open_db},
},
};
@@ -38,6 +38,7 @@ impl CohortVecs {
name: &str,
version: Version,
indexes: &indexes::Vecs,
spot_price: &CachedBoxedVec<Height, Cents>,
) -> Result<Self> {
let spending_rate =
PerBlock::forced_import(db, &format!("{name}_spending_rate"), version, indexes)?;
@@ -55,7 +56,13 @@ impl CohortVecs {
spending_exposure,
mobility,
supply: import_split(|side| {
ValuePerBlock::forced_import(db, &format!("{name}_{side}_supply"), version, indexes)
SpotValuePerBlock::forced_import(
db,
&format!("{name}_{side}_supply"),
version,
indexes,
spot_price,
)
})?,
})
}
@@ -66,17 +73,25 @@ impl Vecs {
parent_path: &Path,
parent_version: Version,
indexes: &indexes::Vecs,
prices: &crate::price::Vecs,
) -> Result<Self> {
let db = open_db(parent_path, DB_NAME, 250_000)?;
let version = parent_version + VERSION;
let spot_price = prices.spot.cents.height.read_only_cached_boxed_clone();
let this = Self {
age_range: AgeRange::try_new(|_, name| {
let name = CohortContext::Utxo.prefixed(name);
CohortVecs::forced_import(&db, &name, version, indexes)
CohortVecs::forced_import(&db, &name, version, indexes, &spot_price)
})?,
supply: import_split(|side| {
ValuePerBlock::forced_import(&db, &format!("{side}_supply"), version, indexes)
SpotValuePerBlock::forced_import(
&db,
&format!("{side}_supply"),
version,
indexes,
&spot_price,
)
})?,
supply_in_loss_share: PerBlock::forced_import(
&db,
@@ -95,7 +110,13 @@ impl Vecs {
})
})?,
cap: FiatPerBlock::forced_import(&db, "coinflow_cap", version, indexes)?,
price: PriceWithRatioPerBlock::forced_import(&db, "coinflow_price", version, indexes)?,
price: PriceWithRatioPerBlock::forced_import(
&db,
"coinflow_price",
version,
indexes,
&spot_price,
)?,
db,
};
@@ -4,12 +4,12 @@ mod vecs;
use brk_cohort::AGE_RANGE_BOUNDS;
pub(crate) use brk_cohort::AGE_RANGE_COUNT as AGE_COHORT_COUNT;
pub(crate) use vecs::HORIZON_DAYS;
pub use vecs::{CohortVecs, HorizonVecs, Horizons, Split, Vecs};
pub const DB_NAME: &str = "coinflow";
pub(crate) const AGE_COHORT_COUNT: usize = 21;
pub(crate) const HORIZON_COUNT: usize = 7;
pub(crate) const HOURS_PER_DAY: f64 = 24.0;
pub(crate) const MINIMUM_DURATION_DAYS: f64 = 1.0 / HOURS_PER_DAY;
@@ -4,7 +4,7 @@ use brk_types::{Cents, StoredF64};
use vecdb::{Database, Rw, StorageMode};
use crate::internal::{
FiatPerBlock, LazyPerBlock, PerBlock, PriceWithRatioPerBlock, ValuePerBlock,
FiatPerBlock, LazyPerBlock, PerBlock, PriceWithRatioPerBlock, SpotValuePerBlock,
};
#[derive(Clone, Copy, Traversable)]
@@ -113,7 +113,7 @@ pub struct CohortVecs<M: StorageMode = Rw> {
pub spending_rate: PerBlock<StoredF64, M>,
pub spending_exposure: PerBlock<StoredF64, M>,
pub mobility: LazyPerBlock<StoredF64>,
pub supply: Split<ValuePerBlock<M>>,
pub supply: Split<SpotValuePerBlock<M>>,
}
#[derive(Traversable)]
@@ -122,7 +122,7 @@ pub struct Vecs<M: StorageMode = Rw> {
pub(crate) db: Database,
pub age_range: AgeRange<CohortVecs<M>>,
pub supply: Split<ValuePerBlock<M>>,
pub supply: Split<SpotValuePerBlock<M>>,
#[traversable(wrap = "supply/mobile/in_loss", rename = "share")]
pub supply_in_loss_share: PerBlock<StoredF64, M>,
pub horizon: Horizons<HorizonVecs<M>>,
@@ -1,4 +1,4 @@
use brk_cohort::AGE_RANGE_BOUNDS;
use brk_cohort::{AGE_RANGE_BOUNDS, AGE_RANGE_COUNT};
use brk_error::Result;
use brk_indexer::Indexer;
use brk_types::{Bitcoin, Height, ONE_DAY_IN_SEC_F64, Sats, StoredF64, Timestamp, Version};
@@ -6,9 +6,8 @@ use vecdb::{AnyVec, Exit, ReadableVec};
use super::super::activity;
use super::{CohortVecs, Vecs};
use crate::{distribution, indexes, price};
use crate::{distribution, indexes};
const AGE_COHORT_COUNT: usize = 21;
const HOURS_PER_DAY: f64 = 24.0;
const WRITE_INTERVAL: usize = 10_000;
@@ -17,7 +16,6 @@ impl Vecs {
&mut self,
indexer: &Indexer,
indexes: &indexes::Vecs,
prices: &price::Vecs,
distribution: &distribution::Vecs,
exit: &Exit,
) -> Result<()> {
@@ -48,7 +46,7 @@ impl Vecs {
&coindays_destroyed,
exit,
)?;
self.compute_rest(starting_height, prices, &supplies, exit)
self.compute_rest(starting_height, &supplies, exit)
}
fn compute_created<T, S>(
@@ -62,7 +60,7 @@ impl Vecs {
T: ReadableVec<Height, Timestamp>,
S: ReadableVec<Height, Sats>,
{
debug_assert_eq!(supplies.len(), AGE_COHORT_COUNT);
debug_assert_eq!(supplies.len(), AGE_RANGE_COUNT);
let created_version: Version = std::iter::once(timestamps.version())
.chain(supplies.iter().map(|vec| vec.version()))
@@ -138,8 +136,8 @@ impl Vecs {
V: ReadableVec<Height, Sats>,
D: ReadableVec<Height, StoredF64>,
{
debug_assert_eq!(transfer_volumes.len(), AGE_COHORT_COUNT);
debug_assert_eq!(source_coindays_destroyed.len(), AGE_COHORT_COUNT);
debug_assert_eq!(transfer_volumes.len(), AGE_RANGE_COUNT);
debug_assert_eq!(source_coindays_destroyed.len(), AGE_RANGE_COUNT);
let destroyed_version: Version = transfer_volumes
.iter()
@@ -186,10 +184,10 @@ impl Vecs {
.collect();
for offset in 0..(chunk_end - chunk_start) {
let volumes_btc: [f64; AGE_COHORT_COUNT] = std::array::from_fn(|index| {
let volumes_btc: [f64; AGE_RANGE_COUNT] = std::array::from_fn(|index| {
f64::from(Bitcoin::from(transfer_batches[index][offset]))
});
let cdd: [f64; AGE_COHORT_COUNT] =
let cdd: [f64; AGE_RANGE_COUNT] =
std::array::from_fn(|index| f64::from(destroyed_batches[index][offset]));
let consumed = allocate_consumed_coindays(volumes_btc, cdd, &bounds);
@@ -215,14 +213,13 @@ impl Vecs {
fn compute_rest<S>(
&mut self,
starting_height: Height,
prices: &price::Vecs,
supplies: &[&S],
exit: &Exit,
) -> Result<()>
where
S: ReadableVec<Height, Sats>,
{
debug_assert_eq!(supplies.len(), AGE_COHORT_COUNT);
debug_assert_eq!(supplies.len(), AGE_RANGE_COUNT);
for (cohort, &total_supply) in self.iter_mut().zip(supplies) {
let CohortVecs {
@@ -244,7 +241,6 @@ impl Vecs {
supply.compute_from(
starting_height,
prices,
total_supply,
&activity_vecs.liveliness.height,
&activity_vecs.vaultedness.height,
@@ -275,12 +271,12 @@ fn coindays_created(supply: Sats, interval_seconds: u32) -> StoredF64 {
StoredF64::from(f64::from(Bitcoin::from(supply)) * interval_seconds as f64 / ONE_DAY_IN_SEC_F64)
}
fn age_bounds_days() -> [(f64, f64); AGE_COHORT_COUNT] {
fn age_bounds_days() -> [(f64, f64); AGE_RANGE_COUNT] {
let mut bounds = AGE_RANGE_BOUNDS.iter();
std::array::from_fn(|index| {
let bound = bounds.next().unwrap();
let lower = bound.start as f64 / HOURS_PER_DAY;
let width = if index + 1 < AGE_COHORT_COUNT {
let width = if index + 1 < AGE_RANGE_COUNT {
(bound.end - bound.start) as f64 / HOURS_PER_DAY
} else {
0.0
@@ -290,14 +286,14 @@ fn age_bounds_days() -> [(f64, f64); AGE_COHORT_COUNT] {
}
fn allocate_consumed_coindays(
transfer_volume_btc: [f64; AGE_COHORT_COUNT],
coindays_destroyed: [f64; AGE_COHORT_COUNT],
bounds: &[(f64, f64); AGE_COHORT_COUNT],
) -> [f64; AGE_COHORT_COUNT] {
let mut result = [0.0; AGE_COHORT_COUNT];
transfer_volume_btc: [f64; AGE_RANGE_COUNT],
coindays_destroyed: [f64; AGE_RANGE_COUNT],
bounds: &[(f64, f64); AGE_RANGE_COUNT],
) -> [f64; AGE_RANGE_COUNT] {
let mut result = [0.0; AGE_RANGE_COUNT];
let mut older_transfer_volume = 0.0;
for index in (0..AGE_COHORT_COUNT).rev() {
for index in (0..AGE_RANGE_COUNT).rev() {
let (lower_days, width_days) = bounds[index];
let within_cohort =
(coindays_destroyed[index] - transfer_volume_btc[index] * lower_days).max(0.0);
@@ -335,8 +331,8 @@ mod tests {
#[test]
fn destruction_at_a_boundary_stays_in_the_ranges_already_traversed() {
let mut volumes = [0.0; AGE_COHORT_COUNT];
let mut cdd = [0.0; AGE_COHORT_COUNT];
let mut volumes = [0.0; AGE_RANGE_COUNT];
let mut cdd = [0.0; AGE_RANGE_COUNT];
volumes[2] = 1.0;
cdd[2] = 1.0;
@@ -350,8 +346,8 @@ mod tests {
#[test]
fn consumed_coindays_cover_every_traversed_cohort() {
let mut volumes = [0.0; AGE_COHORT_COUNT];
let mut cdd = [0.0; AGE_COHORT_COUNT];
let mut volumes = [0.0; AGE_RANGE_COUNT];
let mut cdd = [0.0; AGE_RANGE_COUNT];
volumes[2] = 2.0;
cdd[2] = 20.0;
@@ -366,10 +362,10 @@ mod tests {
#[test]
fn allocated_coindays_conserve_mixed_cohort_destruction() {
let bounds = age_bounds_days();
let mut volumes = [0.0; AGE_COHORT_COUNT];
let mut cdd = [0.0; AGE_COHORT_COUNT];
let mut volumes = [0.0; AGE_RANGE_COUNT];
let mut cdd = [0.0; AGE_RANGE_COUNT];
for index in [0, 1, 2, 10, 20] {
for index in [0, 1, 2, 10, 20, AGE_RANGE_COUNT - 2, AGE_RANGE_COUNT - 1] {
let (lower, width) = bounds[index];
let volume = index as f64 + 1.0;
let age = lower + if width > 0.0 { width / 2.0 } else { 30.0 };
@@ -381,4 +377,21 @@ mod tests {
assert!((allocated.iter().sum::<f64>() - cdd.iter().sum::<f64>()).abs() < 1e-9);
}
#[test]
fn bounds_and_allocation_cover_every_canonical_age_range() {
let bounds = age_bounds_days();
let mut volumes = [0.0; AGE_RANGE_COUNT];
let mut cdd = [0.0; AGE_RANGE_COUNT];
let last = AGE_RANGE_COUNT - 1;
volumes[last] = 1.0;
cdd[last] = bounds[last].0 + 30.0;
let allocated = allocate_consumed_coindays(volumes, cdd, &bounds);
assert_eq!(bounds.len(), AGE_RANGE_BOUNDS.iter().count());
assert!(allocated[last] > 0.0);
assert!((allocated.iter().sum::<f64>() - cdd.iter().sum::<f64>()).abs() < 1e-9);
}
}
@@ -1,7 +1,7 @@
use brk_cohort::{AgeRange, CohortContext};
use brk_error::Result;
use brk_types::Version;
use vecdb::Database;
use brk_types::{Cents, Height, Version};
use vecdb::{CachedBoxedVec, Database};
use super::{CohortVecs, Vecs};
use crate::{
@@ -20,6 +20,7 @@ impl CohortVecs {
version: Version,
indexes: &indexes::Vecs,
cached_starts: &Windows<&WindowStartVec>,
spot_price: &CachedBoxedVec<Height, Cents>,
) -> Result<Self> {
Ok(Self {
coindays_created: PerBlockCumulativeRolling::forced_import(
@@ -44,7 +45,9 @@ impl CohortVecs {
cached_starts,
)?,
activity: ActivityDerivedVecs::forced_import_with_prefix(db, name, version, indexes)?,
supply: SupplyBaseVecs::forced_import_with_prefix(db, name, version, indexes)?,
supply: SupplyBaseVecs::forced_import_with_prefix(
db, name, version, indexes, spot_price,
)?,
})
}
}
@@ -55,12 +58,13 @@ impl Vecs {
parent_version: Version,
indexes: &indexes::Vecs,
cached_starts: &Windows<&WindowStartVec>,
spot_price: &CachedBoxedVec<Height, Cents>,
) -> Result<Self> {
let version = parent_version + VERSION;
Ok(Self(AgeRange::try_new(|_, name| {
let name = CohortContext::Utxo.prefixed(name);
CohortVecs::forced_import(db, &name, version, indexes, cached_starts)
CohortVecs::forced_import(db, &name, version, indexes, cached_starts, spot_price)
})?))
}
}
@@ -5,14 +5,12 @@ use vecdb::Exit;
use super::super::{activity, value};
use super::Vecs;
use crate::{distribution, mining};
use crate::distribution;
impl Vecs {
#[allow(clippy::too_many_arguments)]
pub(crate) fn compute(
&mut self,
indexer: &Indexer,
mining: &mining::Vecs,
distribution: &distribution::Vecs,
activity: &activity::Vecs,
value: &value::Vecs,
@@ -23,13 +21,6 @@ impl Vecs {
let realized_cap_cents = &all_metrics.realized.cap.cents.height;
let circulating_supply = &all_metrics.supply.total.btc.height;
self.thermo.cents.height.compute_transform(
starting_lengths.height,
&mining.rewards.subsidy.cumulative.cents.height,
|(i, v, ..)| (i, v),
exit,
)?;
self.investor.cents.height.compute_subtract(
starting_lengths.height,
realized_cap_cents,
@@ -1,11 +1,14 @@
use brk_error::Result;
use brk_types::Version;
use vecdb::Database;
use brk_types::{Cents, Version};
use vecdb::{Database, ReadableCloneableVec};
use super::Vecs;
use crate::{
indexes,
internal::{FiatPerBlock, RatioPerBlock},
internal::{
CentsUnsignedToDollars, FiatPerBlock, Identity, LazyFiatPerBlock, LazyPerBlock, PerBlock,
RatioPerBlock,
},
};
impl Vecs {
@@ -13,9 +16,24 @@ impl Vecs {
db: &Database,
version: Version,
indexes: &indexes::Vecs,
subsidy_cents: &PerBlock<Cents>,
) -> Result<Self> {
let thermo_cents = LazyPerBlock::from_computed::<Identity<Cents>>(
"thermo_cap_cents",
version,
subsidy_cents.height.read_only_boxed_clone(),
subsidy_cents,
);
let thermo_usd = LazyPerBlock::from_lazy::<CentsUnsignedToDollars, Cents>(
"thermo_cap",
version,
&thermo_cents,
);
Ok(Self {
thermo: FiatPerBlock::forced_import(db, "thermo_cap", version, indexes)?,
thermo: LazyFiatPerBlock {
usd: thermo_usd,
cents: thermo_cents,
},
investor: FiatPerBlock::forced_import(db, "investor_cap", version, indexes)?,
vaulted: FiatPerBlock::forced_import(db, "vaulted_cap", version, indexes)?,
active: FiatPerBlock::forced_import(db, "active_cap", version, indexes)?,
@@ -2,11 +2,11 @@ use brk_traversable::Traversable;
use brk_types::{Cents, PartsPerMillion32};
use vecdb::{Rw, StorageMode};
use crate::internal::{FiatPerBlock, RatioPerBlock};
use crate::internal::{FiatPerBlock, LazyFiatPerBlock, RatioPerBlock};
#[derive(Traversable)]
pub struct Vecs<M: StorageMode = Rw> {
pub thermo: FiatPerBlock<Cents, M>,
pub thermo: LazyFiatPerBlock<Cents>,
pub investor: FiatPerBlock<Cents, M>,
pub vaulted: FiatPerBlock<Cents, M>,
pub active: FiatPerBlock<Cents, M>,
@@ -3,7 +3,7 @@ use brk_indexer::Indexer;
use vecdb::Exit;
use super::Vecs;
use crate::{blocks, distribution, indexes, mining, price, supply};
use crate::{blocks, distribution, indexes, price, supply};
impl Vecs {
#[allow(clippy::too_many_arguments)]
@@ -13,7 +13,6 @@ impl Vecs {
indexes: &indexes::Vecs,
prices: &price::Vecs,
blocks: &blocks::Vecs,
mining: &mining::Vecs,
supply_vecs: &supply::Vecs,
distribution: &distribution::Vecs,
exit: &Exit,
@@ -23,19 +22,13 @@ impl Vecs {
// Activity computes first (liveliness, vaultedness, etc.)
self.activity.compute(indexer, distribution, exit)?;
self.age_range
.compute(indexer, indexes, prices, distribution, exit)?;
.compute(indexer, indexes, distribution, exit)?;
// Phase 2: supply, adjusted, and value are independent.
let (r1, r2) = rayon::join(
|| {
self.supply.compute(
indexer,
prices,
distribution,
&self.activity,
&self.age_range,
exit,
)
self.supply
.compute(indexer, distribution, &self.activity, &self.age_range, exit)
},
|| {
rayon::join(
@@ -55,14 +48,8 @@ impl Vecs {
r2.1?;
// Cap depends on activity + value
self.cap.compute(
indexer,
mining,
distribution,
&self.activity,
&self.value,
exit,
)?;
self.cap
.compute(indexer, distribution, &self.activity, &self.value, exit)?;
// Phase 4: pricing and reserve_risk are independent
let (r3, r4) = rayon::join(
@@ -1,11 +1,15 @@
use std::path::Path;
use brk_error::Result;
use brk_types::Version;
use brk_types::{Cents, Version};
use crate::{
indexes,
internal::db_utils::{finalize_db, open_db},
internal::{
PerBlock,
db_utils::{finalize_db, open_db},
},
price,
};
use super::{
@@ -21,18 +25,23 @@ impl Vecs {
parent_version: Version,
indexes: &indexes::Vecs,
cached_starts: &Windows<&WindowStartVec>,
prices: &price::Vecs,
subsidy_cents: &PerBlock<Cents>,
) -> Result<Self> {
let db = open_db(parent_path, DB_NAME, 250_000)?;
let version = parent_version;
let v1 = version + Version::ONE;
let spot_price = prices.spot.cents.height.read_only_cached_boxed_clone();
let activity = ActivityVecs::forced_import(&db, version, indexes, cached_starts)?;
let age_range = AgeRangeVecs::forced_import(&db, version, indexes, cached_starts)?;
let supply = SupplyVecs::forced_import(&db, v1, indexes)?;
let age_range =
AgeRangeVecs::forced_import(&db, version, indexes, cached_starts, &spot_price)?;
let supply = SupplyVecs::forced_import(&db, v1, indexes, &spot_price)?;
let value = ValueVecs::forced_import(&db, v1, indexes, cached_starts)?;
let cap = CapVecs::forced_import(&db, version + Version::TWO, indexes)?;
let prices = PricesVecs::forced_import(&db, version + Version::new(3), indexes)?;
let cap = CapVecs::forced_import(&db, version + Version::TWO, indexes, subsidy_cents)?;
let prices =
PricesVecs::forced_import(&db, version + Version::new(3), indexes, &spot_price)?;
let adjusted = AdjustedVecs::forced_import(&db, version, indexes)?;
let reserve_risk = ReserveRiskVecs::forced_import(&db, v1, indexes)?;
let reserve_risk = ReserveRiskVecs::forced_import(&db, v1, indexes, &spot_price)?;
let this = Self {
db,
@@ -12,7 +12,7 @@ impl Vecs {
pub(crate) fn compute(
&mut self,
indexer: &Indexer,
prices: &price::Vecs,
_prices: &price::Vecs,
distribution: &distribution::Vecs,
activity: &activity::Vecs,
supply: &supply::Vecs,
@@ -24,57 +24,43 @@ impl Vecs {
let circulating_supply = &all_metrics.supply.total.btc.height;
let realized_price = &all_metrics.realized.price.cents.height;
self.vaulted
.compute_all(prices, &starting_lengths, exit, |v| {
Ok(v.compute_transform2(
starting_lengths.height,
realized_price,
&activity.vaultedness.height,
|(i, price, vaultedness, ..)| {
(i, Cents::from(f64::from(price) / f64::from(vaultedness)))
},
exit,
)?)
})?;
self.vaulted.cents.height.compute_transform2(
starting_lengths.height,
realized_price,
&activity.vaultedness.height,
|(i, price, vaultedness, ..)| {
(i, Cents::from(f64::from(price) / f64::from(vaultedness)))
},
exit,
)?;
self.active
.compute_all(prices, &starting_lengths, exit, |v| {
Ok(v.compute_transform2(
starting_lengths.height,
realized_price,
&activity.liveliness.height,
|(i, price, liveliness, ..)| {
(i, Cents::from(f64::from(price) / f64::from(liveliness)))
},
exit,
)?)
})?;
self.active.cents.height.compute_transform2(
starting_lengths.height,
realized_price,
&activity.liveliness.height,
|(i, price, liveliness, ..)| (i, Cents::from(f64::from(price) / f64::from(liveliness))),
exit,
)?;
self.true_market_mean
.compute_all(prices, &starting_lengths, exit, |v| {
Ok(v.compute_transform2(
starting_lengths.height,
&cap.investor.cents.height,
&supply.active.btc.height,
|(i, cap_cents, supply_btc, ..)| {
(i, Cents::from(f64::from(cap_cents) / f64::from(supply_btc)))
},
exit,
)?)
})?;
self.true_market_mean.cents.height.compute_transform2(
starting_lengths.height,
&cap.investor.cents.height,
&supply.active.btc.height,
|(i, cap_cents, supply_btc, ..)| {
(i, Cents::from(f64::from(cap_cents) / f64::from(supply_btc)))
},
exit,
)?;
self.cointime
.compute_all(prices, &starting_lengths, exit, |v| {
Ok(v.compute_transform2(
starting_lengths.height,
&cap.cointime.cents.height,
circulating_supply,
|(i, cap_cents, supply_btc, ..)| {
(i, Cents::from(f64::from(cap_cents) / f64::from(supply_btc)))
},
exit,
)?)
})?;
self.cointime.cents.height.compute_transform2(
starting_lengths.height,
&cap.cointime.cents.height,
circulating_supply,
|(i, cap_cents, supply_btc, ..)| {
(i, Cents::from(f64::from(cap_cents) / f64::from(supply_btc)))
},
exit,
)?;
Ok(())
}
@@ -1,6 +1,6 @@
use brk_error::Result;
use brk_types::Version;
use vecdb::Database;
use brk_types::{Cents, Height, Version};
use vecdb::{CachedBoxedVec, Database};
use super::Vecs;
use crate::{indexes, internal::PriceWithRatioPerBlock};
@@ -10,10 +10,11 @@ impl Vecs {
db: &Database,
version: Version,
indexes: &indexes::Vecs,
spot_price: &CachedBoxedVec<Height, Cents>,
) -> Result<Self> {
macro_rules! import {
($name:expr) => {
PriceWithRatioPerBlock::forced_import(db, $name, version, indexes)?
PriceWithRatioPerBlock::forced_import(db, $name, version, indexes, spot_price)?
};
}
@@ -32,13 +32,6 @@ impl Vecs {
exit,
)?;
self.value.height.compute_divide(
starting_height,
&prices.spot.usd.height,
&self.hodl_bank,
exit,
)?;
Ok(())
}
}
@@ -1,21 +1,38 @@
use brk_error::Result;
use brk_types::Version;
use vecdb::{Database, EagerVec, ImportableVec};
use brk_types::{Cents, Dollars, Height, StoredF64, Version};
use vecdb::{CachedBoxedVec, Database, EagerVec, ImportableVec, ReadableCloneableVec};
use super::Vecs;
use crate::{indexes, internal::PerBlock};
use crate::{
indexes,
internal::{Identity, LazyIndexedVec, LazyPerBlock},
};
impl Vecs {
pub(crate) fn forced_import(
db: &Database,
version: Version,
indexes: &indexes::Vecs,
spot_price: &CachedBoxedVec<Height, Cents>,
) -> Result<Self> {
let v1 = version + Version::ONE;
let hodl_bank = EagerVec::forced_import(db, "hodl_bank", v1)?;
let value_source = LazyIndexedVec::new(
"reserve_risk_source",
v1,
hodl_bank.read_only_boxed_clone(),
spot_price.clone(),
|_, hodl_bank, spot| StoredF64::from(Dollars::from(spot)) / hodl_bank,
);
Ok(Self {
vocdd_median_1y: EagerVec::forced_import(db, "vocdd_median_1y", v1)?,
hodl_bank: EagerVec::forced_import(db, "hodl_bank", v1)?,
value: PerBlock::forced_import(db, "reserve_risk", v1, indexes)?,
hodl_bank,
value: LazyPerBlock::from_height_source::<Identity<StoredF64>, _>(
"reserve_risk",
v1,
value_source,
indexes,
),
})
}
}
@@ -2,11 +2,11 @@ use brk_traversable::Traversable;
use brk_types::{Height, StoredF64};
use vecdb::{EagerVec, PcoVec, Rw, StorageMode};
use crate::internal::PerBlock;
use crate::internal::LazyPerBlock;
#[derive(Traversable)]
pub struct Vecs<M: StorageMode = Rw> {
pub value: PerBlock<StoredF64, M>,
pub value: LazyPerBlock<StoredF64>,
pub vocdd_median_1y: M::Stored<EagerVec<PcoVec<Height, StoredF64>>>,
pub hodl_bank: M::Stored<EagerVec<PcoVec<Height, StoredF64>>>,
}
@@ -5,7 +5,7 @@ use vecdb::{AnyStoredVec, AnyVec, Exit, ReadableVec, WritableVec};
use super::super::{activity, age_range};
use super::{BaseVecs, Vecs};
use crate::{distribution, frameworks::WeightedRatio, price};
use crate::{distribution, frameworks::WeightedRatio};
const WRITE_INTERVAL: usize = 10_000;
@@ -14,7 +14,6 @@ impl BaseVecs {
pub(crate) fn compute_from(
&mut self,
starting_height: Height,
prices: &price::Vecs,
total_supply: &impl ReadableVec<Height, Sats>,
liveliness: &impl ReadableVec<Height, StoredF64>,
vaultedness: &impl ReadableVec<Height, StoredF64>,
@@ -34,9 +33,6 @@ impl BaseVecs {
exit,
)?;
self.vaulted.compute(prices, starting_height, exit)?;
self.active.compute(prices, starting_height, exit)?;
Ok(())
}
}
@@ -45,7 +41,6 @@ impl Vecs {
pub(crate) fn compute(
&mut self,
indexer: &Indexer,
prices: &price::Vecs,
distribution: &distribution::Vecs,
activity: &activity::Vecs,
age_range: &age_range::Vecs,
@@ -63,7 +58,6 @@ impl Vecs {
self.base.compute_from(
starting_height,
prices,
circulating_supply,
&activity.liveliness.height,
&activity.vaultedness.height,
@@ -1,11 +1,11 @@
use brk_error::Result;
use brk_types::Version;
use vecdb::Database;
use brk_types::{Cents, Height, Version};
use vecdb::{CachedBoxedVec, Database};
use super::{BaseVecs, Vecs};
use crate::{
indexes,
internal::{PerBlock, ValuePerBlock},
internal::{PerBlock, SpotValuePerBlock},
};
impl BaseVecs {
@@ -14,6 +14,7 @@ impl BaseVecs {
prefix: &str,
version: Version,
indexes: &indexes::Vecs,
spot_price: &CachedBoxedVec<Height, Cents>,
) -> Result<Self> {
let name = |metric: &str| {
if prefix.is_empty() {
@@ -24,8 +25,20 @@ impl BaseVecs {
};
Ok(Self {
vaulted: ValuePerBlock::forced_import(db, &name("vaulted_supply"), version, indexes)?,
active: ValuePerBlock::forced_import(db, &name("active_supply"), version, indexes)?,
vaulted: SpotValuePerBlock::forced_import(
db,
&name("vaulted_supply"),
version,
indexes,
spot_price,
)?,
active: SpotValuePerBlock::forced_import(
db,
&name("active_supply"),
version,
indexes,
spot_price,
)?,
})
}
}
@@ -35,9 +48,10 @@ impl Vecs {
db: &Database,
version: Version,
indexes: &indexes::Vecs,
spot_price: &CachedBoxedVec<Height, Cents>,
) -> Result<Self> {
Ok(Self {
base: BaseVecs::forced_import_with_prefix(db, "", version, indexes)?,
base: BaseVecs::forced_import_with_prefix(db, "", version, indexes, spot_price)?,
active_supply_in_loss_share: PerBlock::forced_import(
db,
"cointime_supply_in_loss_share",
@@ -3,12 +3,12 @@ use brk_types::StoredF64;
use derive_more::{Deref, DerefMut};
use vecdb::{Rw, StorageMode};
use crate::internal::{PerBlock, ValuePerBlock};
use crate::internal::{PerBlock, SpotValuePerBlock};
#[derive(Traversable)]
pub struct BaseVecs<M: StorageMode = Rw> {
pub vaulted: ValuePerBlock<M>,
pub active: ValuePerBlock<M>,
pub vaulted: SpotValuePerBlock<M>,
pub active: SpotValuePerBlock<M>,
}
#[derive(Deref, DerefMut, Traversable)]
+43 -24
View File
@@ -35,30 +35,49 @@ impl Vecs {
gini::compute(&mut self.gini, distribution, indexer, exit)?;
// RHODL Ratio: 1d-1w realized cap / 1y-2y realized cap
self.rhodl_ratio
.ppm
.compute_binary::<Dollars, Dollars, RatioDollars<PartsPerMillion64>>(
starting_lengths.height,
&distribution
.utxo_cohorts
.age_range
._1d_to_1w
.metrics
.realized
.cap
.usd
.height,
&distribution
.utxo_cohorts
.age_range
._1y_to_2y
.metrics
.realized
.cap
.usd
.height,
exit,
)?;
self.rhodl_ratio.ppm.height.compute_transform3(
starting_lengths.height,
&distribution
.utxo_cohorts
.age_range
._1d_to_1w
.metrics
.realized
.cap
.usd
.height,
&distribution
.utxo_cohorts
.age_range
._1y_to_18m
.metrics
.realized
.cap
.usd
.height,
&distribution
.utxo_cohorts
.age_range
._18m_to_2y
.metrics
.realized
.cap
.usd
.height,
|(i, young_cap, year1_cap, month18_cap, ..)| {
let denominator = year1_cap + month18_cap;
let ratio = f64::from(young_cap) / f64::from(denominator);
(
i,
if ratio.is_finite() {
PartsPerMillion64::from(ratio)
} else {
PartsPerMillion64::default()
},
)
},
exit,
)?;
// NVT: market_cap / tx_volume_24h
let market_cap = &distribution
@@ -1,8 +1,7 @@
use brk_traversable::Traversable;
use brk_types::{Dollars, Version};
use vecdb::ReadableCloneableVec;
use crate::internal::{FiatType, Identity, LazyPerBlock, NumericValue, PerBlock};
use crate::internal::{FiatType, Identity, LazyPerBlock, NumericValue};
/// Lazy fiat: both cents and usd are lazy views of a stored source.
/// Zero extra stored vecs.
@@ -13,22 +12,13 @@ pub struct LazyFiatPerBlock<C: FiatType> {
}
impl<C: FiatType> LazyFiatPerBlock<C> {
pub(crate) fn from_computed(name: &str, version: Version, source: &PerBlock<C>) -> Self
pub(crate) fn from_lazy(name: &str, version: Version, source: &LazyPerBlock<C>) -> Self
where
C: NumericValue,
{
let cents = LazyPerBlock::from_computed::<Identity<C>>(
&format!("{name}_cents"),
version,
source.height.read_only_boxed_clone(),
source,
);
let usd = LazyPerBlock::from_computed::<C::ToDollars>(
name,
version,
source.height.read_only_boxed_clone(),
source,
);
let cents =
LazyPerBlock::from_lazy::<Identity<C>, C>(&format!("{name}_cents"), version, source);
let usd = LazyPerBlock::from_lazy::<C::ToDollars, C>(name, version, source);
Self { usd, cents }
}
}
@@ -119,24 +119,6 @@ impl<B: FixedRatio> LazyPercentPerBlock<B> {
}
impl LazyPercentPerBlock<brk_types::PartsPerMillionSigned64> {
pub(crate) fn from_cagr(
name: &str,
version: Version,
years: u8,
source: &PercentPerBlock<brk_types::PartsPerMillionSigned64>,
) -> Self {
match years {
2 => Self::from_percent::<Cagr<2>>(name, version, source),
3 => Self::from_percent::<Cagr<3>>(name, version, source),
4 => Self::from_percent::<Cagr<4>>(name, version, source),
5 => Self::from_percent::<Cagr<5>>(name, version, source),
6 => Self::from_percent::<Cagr<6>>(name, version, source),
8 => Self::from_percent::<Cagr<8>>(name, version, source),
10 => Self::from_percent::<Cagr<10>>(name, version, source),
_ => unreachable!("unsupported DCA CAGR period: {years} years"),
}
}
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),
@@ -8,9 +8,7 @@ use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Cents, Dollars, SatsFract, Version};
use schemars::JsonSchema;
use vecdb::{
Database, ReadableCloneableVec, ReadableVec, TypedVec, UnaryTransform,
};
use vecdb::{Database, ReadableCloneableVec, ReadableVec, TypedVec, UnaryTransform};
use super::{LazyPerBlock, PerBlock};
use crate::{
@@ -95,8 +93,7 @@ impl Price<LazyPerBlock<Cents>> {
source,
indexes,
);
let usd =
LazyPerBlock::from_lazy::<CentsUnsignedToDollars, Cents>(name, version, &cents);
let usd = LazyPerBlock::from_lazy::<CentsUnsignedToDollars, Cents>(name, version, &cents);
let sats = LazyPerBlock::from_lazy::<DollarsToSatsFract, Cents>(
&format!("{name}_sats"),
version,
@@ -1,9 +1,12 @@
use brk_traversable::Traversable;
use brk_types::{StoredF32, Version};
use brk_types::{Height, StoredF32, Version};
use schemars::JsonSchema;
use vecdb::{ReadableCloneableVec, UnaryTransform};
use vecdb::{ReadableVec, TypedVec, UnaryTransform};
use crate::internal::{FixedRatio, LazyPerBlock, NumericValue, PerBlock};
use crate::{
indexes,
internal::{ComputedVecValue, FixedRatio, Identity, LazyPerBlock, NumericValue},
};
/// Fully lazy variant of `RatioPerBlock` derived from one per-block source.
#[derive(Clone, Traversable)]
@@ -21,18 +24,44 @@ where
R: FixedRatio,
S: NumericValue + JsonSchema,
{
pub(crate) fn from_source<F>(name: &str, version: Version, source: &PerBlock<S>) -> Self
pub(crate) fn from_lazy_source<F, S2T>(
name: &str,
version: Version,
source: &LazyPerBlock<S, S2T>,
) -> Self
where
F: UnaryTransform<S, R>,
S2T: ComputedVecValue + JsonSchema,
{
let ppm = LazyPerBlock::from_computed::<F>(
&format!("{name}_{}", R::SUFFIX),
version,
source.height.read_only_boxed_clone(),
source,
);
let ppm =
LazyPerBlock::from_lazy::<F, S2T>(&format!("{name}_{}", R::SUFFIX), version, source);
let ratio = LazyPerBlock::from_lazy::<R::ToRatio, S>(name, version, &ppm);
Self { ppm, ratio }
}
}
impl<R> LazyRatioPerBlock<R>
where
R: FixedRatio,
{
pub(crate) fn from_height_source<V>(
name: &str,
version: Version,
source: V,
indexes: &indexes::Vecs,
) -> Self
where
V: TypedVec<I = Height, T = R> + ReadableVec<Height, R> + Clone + 'static,
{
let ppm = LazyPerBlock::from_height_source::<Identity<R>, _>(
&format!("{name}_{}", R::SUFFIX),
version,
source,
indexes,
);
let ratio = LazyPerBlock::from_lazy::<R::ToRatio, R>(name, version, &ppm);
Self { ppm, ratio }
}
}
@@ -1,9 +1,7 @@
mod base;
mod lazy;
mod price;
mod windows;
pub use base::*;
pub use lazy::*;
pub use price::*;
pub use windows::*;
@@ -1,20 +1,19 @@
use brk_error::Result;
use brk_indexer::Lengths;
use brk_traversable::Traversable;
use brk_types::{Cents, Dollars, Height, PartsPerMillion64, SatsFract, StoredF32, Version};
use vecdb::{Database, EagerVec, Exit, PcoVec, ReadableVec, Rw, StorageMode, unlikely};
use vecdb::{CachedBoxedVec, Database, ReadableCloneableVec, Rw, StorageMode, unlikely};
use crate::internal::{LazyPerBlock, PerBlock, Price};
use crate::{indexes, price};
use super::RatioPerBlock;
use crate::{
indexes,
internal::{LazyIndexedVec, LazyPerBlock, LazyRatioPerBlock, PerBlock, Price},
};
#[derive(Traversable)]
pub struct PriceWithRatioPerBlock<M: StorageMode = Rw> {
pub usd: LazyPerBlock<Dollars, Cents>,
pub cents: PerBlock<Cents, M>,
pub sats: LazyPerBlock<SatsFract, Dollars>,
pub ppm: PerBlock<PartsPerMillion64, M>,
pub ppm: LazyPerBlock<PartsPerMillion64>,
pub ratio: LazyPerBlock<StoredF32, PartsPerMillion64>,
}
@@ -24,9 +23,23 @@ impl PriceWithRatioPerBlock {
name: &str,
version: Version,
indexes: &indexes::Vecs,
spot_price: &CachedBoxedVec<Height, Cents>,
) -> Result<Self> {
let price = Price::forced_import(db, name, version, indexes)?;
let ratio = RatioPerBlock::forced_import(db, name, version + Version::ONE, indexes)?;
let ratio_version = version + Version::new(4);
let ppm_source = LazyIndexedVec::new(
&format!("{name}_ratio_ppm_source"),
ratio_version,
price.cents.height.read_only_boxed_clone(),
spot_price.clone(),
|_, price, spot| price_ratio(spot, price),
);
let ratio = LazyRatioPerBlock::from_height_source(
&format!("{name}_ratio"),
ratio_version,
ppm_source,
indexes,
);
Ok(Self {
usd: price.usd,
cents: price.cents,
@@ -35,38 +48,6 @@ impl PriceWithRatioPerBlock {
ratio: ratio.ratio,
})
}
/// Compute ratio from close price and this metric's price.
pub(crate) fn compute_ratio(
&mut self,
starting_lengths: &Lengths,
close_price: &impl ReadableVec<Height, Cents>,
exit: &Exit,
) -> Result<()> {
self.ppm.height.compute_transform2(
starting_lengths.height,
close_price,
&self.cents.height,
|(i, close, price, ..)| (i, price_ratio(close, price)),
exit,
)?;
Ok(())
}
/// Compute price via closure (in cents), then compute ratio.
pub(crate) fn compute_all<F>(
&mut self,
prices: &price::Vecs,
starting_lengths: &Lengths,
exit: &Exit,
mut compute_price: F,
) -> Result<()>
where
F: FnMut(&mut EagerVec<PcoVec<Height, Cents>>) -> Result<()>,
{
compute_price(&mut self.cents.height)?;
self.compute_ratio(starting_lengths, &prices.spot.cents.height, exit)
}
}
fn price_ratio(close: Cents, price: Cents) -> PartsPerMillion64 {
@@ -1,58 +0,0 @@
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Height, Version};
use derive_more::{Deref, DerefMut};
use vecdb::{BinaryTransform, Database, Exit, ReadableVec, Rw, StorageMode, VecValue};
use crate::{
indexes,
internal::{FixedRatio, RatioPerBlock, Windows},
};
/// Four rolling-window ratios with fixed-point storage and lazy float views.
#[derive(Deref, DerefMut, Traversable)]
#[traversable(transparent)]
pub struct RatioRollingWindows<B: FixedRatio, M: StorageMode = Rw>(
pub Windows<RatioPerBlock<B, M>>,
);
impl<B: FixedRatio> RatioRollingWindows<B> {
pub(crate) fn forced_import(
db: &Database,
name: &str,
version: Version,
indexes: &indexes::Vecs,
) -> Result<Self> {
Ok(Self(Windows::try_from_fn(|suffix| {
RatioPerBlock::forced_import_ppm(db, &format!("{name}_{suffix}"), version, indexes)
})?))
}
pub(crate) fn compute_binary<S1T, S2T, F, R1, R2>(
&mut self,
max_from: Height,
sources1: [&R1; 4],
sources2: [&R2; 4],
exit: &Exit,
) -> Result<()>
where
S1T: VecValue,
S2T: VecValue,
R1: ReadableVec<Height, S1T>,
R2: ReadableVec<Height, S2T>,
F: BinaryTransform<S1T, S2T, B>,
{
for ((target, s1), s2) in self
.0
.as_mut_array()
.into_iter()
.zip(sources1)
.zip(sources2)
{
target
.ppm
.compute_binary::<S1T, S2T, F>(max_from, s1, s2, exit)?;
}
Ok(())
}
}
@@ -1,16 +1,15 @@
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Bitcoin, Cents, Dollars, Height, Sats, SatsSigned, Version};
use brk_types::{Bitcoin, Cents, Dollars, Sats, SatsSigned, Version};
use schemars::JsonSchema;
use vecdb::{Database, Exit, ReadableCloneableVec, Rw, StorageMode, UnaryTransform};
use vecdb::{Database, ReadableCloneableVec, Rw, StorageMode, UnaryTransform};
use crate::{
indexes,
internal::{
CentsUnsignedToDollars, LazyPerBlock, NumericValue, PerBlock, SatsSignedToBitcoin,
SatsToBitcoin, SatsToCents,
SatsToBitcoin,
},
price,
};
/// Trait that associates a sats type with its transform to Bitcoin.
@@ -66,19 +65,4 @@ impl ValuePerBlock {
cents,
})
}
pub(crate) fn compute(
&mut self,
prices: &price::Vecs,
max_from: Height,
exit: &Exit,
) -> Result<()> {
self.cents.compute_binary::<Sats, Cents, SatsToCents>(
max_from,
&self.sats.height,
&prices.spot.cents.height,
exit,
)?;
Ok(())
}
}
@@ -1,4 +1,4 @@
//! Lazy value wrapper for ValuePerBlock - all transforms are lazy.
//! Lazy value wrapper for point-in-time value sources.
use brk_traversable::Traversable;
use brk_types::{Bitcoin, Cents, Dollars, Height, Sats, Version};
@@ -6,10 +6,10 @@ use derive_more::{Deref, DerefMut};
use vecdb::UnaryTransform;
use crate::internal::{
Identity, LazyValue, LazyValueDerivedResolutions, SatsToBitcoin, ValuePerBlock,
Identity, LazyValue, LazyValueDerivedResolutions, SatsToBitcoin, SpotValuePerBlock,
};
/// Lazy value wrapper with height + all derived last transforms from ValuePerBlock.
/// Lazy value wrapper with height + all derived last transforms.
#[derive(Clone, Deref, DerefMut, Traversable)]
#[traversable(merge)]
pub struct LazyValuePerBlock {
@@ -22,14 +22,23 @@ pub struct LazyValuePerBlock {
}
impl LazyValuePerBlock {
pub(crate) fn from_block_source<
pub(crate) fn spot_identity(name: &str, source: &SpotValuePerBlock, version: Version) -> Self {
Self::from_spot_block_source::<
Identity<Sats>,
SatsToBitcoin,
Identity<Cents>,
Identity<Dollars>,
>(name, source, version)
}
pub(crate) fn from_spot_block_source<
SatsTransform,
BitcoinTransform,
CentsTransform,
DollarsTransform,
>(
name: &str,
source: &ValuePerBlock,
source: &SpotValuePerBlock,
version: Version,
) -> Self
where
@@ -38,14 +47,14 @@ impl LazyValuePerBlock {
CentsTransform: UnaryTransform<Cents, Cents>,
DollarsTransform: UnaryTransform<Dollars, Dollars>,
{
let height = LazyValue::from_block_source::<
let height = LazyValue::from_spot_block_source::<
SatsTransform,
BitcoinTransform,
CentsTransform,
DollarsTransform,
>(name, source, version);
let resolutions = LazyValueDerivedResolutions::from_block_source::<
let resolutions = LazyValueDerivedResolutions::from_spot_block_source::<
SatsTransform,
BitcoinTransform,
CentsTransform,
@@ -57,10 +66,4 @@ impl LazyValuePerBlock {
resolutions: Box::new(resolutions),
}
}
pub(crate) fn identity(name: &str, source: &ValuePerBlock, version: Version) -> Self {
Self::from_block_source::<Identity<Sats>, SatsToBitcoin, Identity<Cents>, Identity<Dollars>>(
name, source, version,
)
}
}
@@ -2,7 +2,7 @@ use brk_traversable::Traversable;
use brk_types::{Bitcoin, Cents, Dollars, Sats, Version};
use vecdb::UnaryTransform;
use crate::internal::{DerivedResolutions, ValuePerBlock};
use crate::internal::{DerivedResolutions, SpotValuePerBlock};
#[derive(Clone, Traversable)]
pub struct LazyValueDerivedResolutions {
@@ -13,14 +13,14 @@ pub struct LazyValueDerivedResolutions {
}
impl LazyValueDerivedResolutions {
pub(crate) fn from_block_source<
pub(crate) fn from_spot_block_source<
SatsTransform,
BitcoinTransform,
CentsTransform,
DollarsTransform,
>(
name: &str,
source: &ValuePerBlock,
source: &SpotValuePerBlock,
version: Version,
) -> Self
where
@@ -41,7 +41,7 @@ impl LazyValueDerivedResolutions {
&source.sats.resolutions,
);
let cents = DerivedResolutions::from_derived_computed::<CentsTransform>(
let cents = DerivedResolutions::from_lazy::<CentsTransform, Cents>(
&format!("{name}_cents"),
version,
&source.cents.resolutions,
@@ -8,7 +8,8 @@ mod lazy_derived_resolutions;
mod lazy_rolling_avg;
mod lazy_rolling_sum;
mod rolling_distribution;
mod with_deltas;
mod spot;
mod spot_with_deltas;
pub use base::*;
pub use cumulative::*;
@@ -20,4 +21,5 @@ pub use lazy_derived_resolutions::*;
pub use lazy_rolling_avg::*;
pub use lazy_rolling_sum::*;
pub use rolling_distribution::*;
pub use with_deltas::*;
pub use spot::*;
pub use spot_with_deltas::*;
@@ -0,0 +1,67 @@
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Bitcoin, Cents, Dollars, Height, Sats, Version};
use vecdb::{BinaryTransform, CachedBoxedVec, Database, ReadableCloneableVec, Rw, StorageMode};
use crate::{
indexes,
internal::{
CentsUnsignedToDollars, Identity, LazyIndexedVec, LazyPerBlock, PerBlock, SatsToBitcoin,
SatsToCents,
},
};
/// A point-in-time value whose fiat amount is derived from sats and same-height spot price.
#[derive(Traversable)]
pub struct SpotValuePerBlock<M: StorageMode = Rw> {
pub btc: LazyPerBlock<Bitcoin, Sats>,
pub sats: PerBlock<Sats, M>,
pub usd: LazyPerBlock<Dollars, Cents>,
pub cents: LazyPerBlock<Cents>,
}
impl SpotValuePerBlock {
pub(crate) fn forced_import(
db: &Database,
name: &str,
version: Version,
indexes: &indexes::Vecs,
spot_price: &CachedBoxedVec<Height, Cents>,
) -> Result<Self> {
let sats = PerBlock::forced_import(db, &format!("{name}_sats"), version, indexes)?;
let btc = LazyPerBlock::from_computed::<SatsToBitcoin>(
name,
version,
sats.height.read_only_boxed_clone(),
&sats,
);
let cents_source = LazyIndexedVec::new(
&format!("{name}_cents_source"),
version,
sats.height.read_only_boxed_clone(),
spot_price.clone(),
|_, sats, spot| SatsToCents::apply(sats, spot),
);
let cents = LazyPerBlock::from_height_source::<Identity<Cents>, _>(
&format!("{name}_cents"),
version,
cents_source,
indexes,
);
let usd = LazyPerBlock::from_lazy::<CentsUnsignedToDollars, Cents>(
&format!("{name}_usd"),
version,
&cents,
);
Ok(Self {
btc,
sats,
usd,
cents,
})
}
}
@@ -1,32 +1,33 @@
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{PartsPerMillionSigned64, Sats, SatsSigned, Version};
use brk_types::{Cents, Height, PartsPerMillionSigned64, Sats, SatsSigned, Version};
use derive_more::{Deref, DerefMut};
use vecdb::{Database, Rw, StorageMode};
use vecdb::{CachedBoxedVec, Database, Rw, StorageMode};
use crate::{
indexes,
internal::{LazyRollingDeltasAmountFromHeight, ValuePerBlock, WindowStartVec, Windows},
internal::{LazyRollingDeltasAmountFromHeight, SpotValuePerBlock, WindowStartVec, Windows},
};
#[derive(Deref, DerefMut, Traversable)]
pub struct ValuePerBlockWithDeltas<M: StorageMode = Rw> {
pub struct SpotValuePerBlockWithDeltas<M: StorageMode = Rw> {
#[deref]
#[deref_mut]
#[traversable(flatten)]
pub inner: ValuePerBlock<M>,
pub inner: SpotValuePerBlock<M>,
pub delta: LazyRollingDeltasAmountFromHeight<Sats, SatsSigned, PartsPerMillionSigned64>,
}
impl ValuePerBlockWithDeltas {
impl SpotValuePerBlockWithDeltas {
pub(crate) fn forced_import(
db: &Database,
name: &str,
version: Version,
indexes: &indexes::Vecs,
cached_starts: &Windows<&WindowStartVec>,
spot_price: &CachedBoxedVec<Height, Cents>,
) -> Result<Self> {
let inner = ValuePerBlock::forced_import(db, name, version, indexes)?;
let inner = SpotValuePerBlock::forced_import(db, name, version, indexes, spot_price)?;
let delta = LazyRollingDeltasAmountFromHeight::new(
&format!("{name}_delta"),
+3 -3
View File
@@ -2,7 +2,7 @@ use brk_traversable::Traversable;
use brk_types::{Bitcoin, Cents, Dollars, Height, Sats, Version};
use vecdb::{LazyVecFrom1, ReadableCloneableVec, UnaryTransform, VecIndex};
use crate::internal::ValuePerBlock;
use crate::internal::SpotValuePerBlock;
/// Fully lazy value type at height level.
///
@@ -16,14 +16,14 @@ pub struct LazyValue<I: VecIndex> {
}
impl LazyValue<Height> {
pub(crate) fn from_block_source<
pub(crate) fn from_spot_block_source<
SatsTransform,
BitcoinTransform,
CentsTransform,
DollarsTransform,
>(
name: &str,
source: &ValuePerBlock,
source: &SpotValuePerBlock,
version: Version,
) -> Self
where
@@ -6,16 +6,16 @@ use brk_cohort::ByAddrType;
use brk_error::Result;
use brk_indexer::Lengths;
use brk_traversable::Traversable;
use brk_types::{Height, Sats, Version};
use brk_types::{Cents, Height, Sats, Version};
use rayon::prelude::*;
use schemars::JsonSchema;
use vecdb::{AnyStoredVec, AnyVec, Database, EagerVec, Exit, PcoVec, WritableVec};
use vecdb::{AnyStoredVec, AnyVec, CachedBoxedVec, Database, EagerVec, Exit, PcoVec, WritableVec};
use crate::{indexes, price};
use crate::indexes;
use super::{
FixedRatio, NumericValue, PerBlock, PerBlockCumulativeRolling, PercentPerBlock, ValuePerBlock,
WindowStartVec, Windows,
FixedRatio, NumericValue, PerBlock, PerBlockCumulativeRolling, PercentPerBlock,
SpotValuePerBlock, WindowStartVec, Windows,
};
use crate::distribution::metrics::AvgAmountMetrics;
@@ -158,16 +158,23 @@ where
}
}
impl WithAddrTypes<ValuePerBlock> {
impl WithAddrTypes<SpotValuePerBlock> {
pub(crate) fn forced_import(
db: &Database,
name: &str,
version: Version,
indexes: &indexes::Vecs,
spot_price: &CachedBoxedVec<Height, Cents>,
) -> Result<Self> {
let all = ValuePerBlock::forced_import(db, name, version, indexes)?;
let all = SpotValuePerBlock::forced_import(db, name, version, indexes, spot_price)?;
let by_addr_type = ByAddrType::new_with_name(|type_name| {
ValuePerBlock::forced_import(db, &format!("{type_name}_{name}"), version, indexes)
SpotValuePerBlock::forced_import(
db,
&format!("{type_name}_{name}"),
version,
indexes,
spot_price,
)
})?;
Ok(Self { all, by_addr_type })
}
@@ -193,16 +200,13 @@ impl WithAddrTypes<ValuePerBlock> {
pub(crate) fn reset_height(&mut self) -> Result<()> {
self.all.sats.height.reset()?;
self.all.cents.height.reset()?;
for v in self.by_addr_type.values_mut() {
v.sats.height.reset()?;
v.cents.height.reset()?;
}
Ok(())
}
/// Push the stateful sats value for `all` and each per-type. Cents are
/// derived post-hoc from sats × price in [`Self::compute_rest`].
/// Push the stateful sats value for `all` and each per-type.
#[inline(always)]
pub(crate) fn push_height<U>(&mut self, total: U, per_type: impl IntoIterator<Item = U>)
where
@@ -213,21 +217,6 @@ impl WithAddrTypes<ValuePerBlock> {
v.sats.height.push(value.into());
}
}
/// Derive cents (and thus lazy btc/usd) for `all` and every per-type vec
/// from the stateful sats values × spot price.
pub(crate) fn compute_rest(
&mut self,
max_from: Height,
prices: &price::Vecs,
exit: &Exit,
) -> Result<()> {
self.all.compute(prices, max_from, exit)?;
for v in self.by_addr_type.values_mut() {
v.compute(prices, max_from, exit)?;
}
Ok(())
}
}
impl WithAddrTypes<AvgAmountMetrics> {
@@ -235,10 +224,11 @@ impl WithAddrTypes<AvgAmountMetrics> {
db: &Database,
version: Version,
indexes: &indexes::Vecs,
spot_price: &CachedBoxedVec<Height, Cents>,
) -> Result<Self> {
let all = AvgAmountMetrics::forced_import(db, "", version, indexes)?;
let all = AvgAmountMetrics::forced_import(db, "", version, indexes, spot_price)?;
let by_addr_type = ByAddrType::new_with_name(|type_name| {
AvgAmountMetrics::forced_import(db, type_name, version, indexes)
AvgAmountMetrics::forced_import(db, type_name, version, indexes, spot_price)
})?;
Ok(Self { all, by_addr_type })
}
+1 -41
View File
@@ -73,10 +73,7 @@ impl<T> ByDcaClass<T> {
})
}
pub(crate) fn try_from_class<U, F, E>(
class: &ByDcaClass<U>,
mut create: F,
) -> Result<Self, E>
pub(crate) fn try_from_class<U, F, E>(class: &ByDcaClass<U>, mut create: F) -> Result<Self, E>
where
F: FnMut(&'static str, u16, Day1, &U) -> Result<T, E>,
{
@@ -161,41 +158,4 @@ impl<T> ByDcaClass<T> {
pub(crate) fn day1(year: u16) -> Day1 {
Day1::try_from(Date::new(year, 1, 1)).unwrap()
}
pub(crate) fn iter(&self) -> impl Iterator<Item = &T> {
[
&self.from_2015,
&self.from_2016,
&self.from_2017,
&self.from_2018,
&self.from_2019,
&self.from_2020,
&self.from_2021,
&self.from_2022,
&self.from_2023,
&self.from_2024,
&self.from_2025,
&self.from_2026,
]
.into_iter()
}
pub(crate) fn iter_mut(&mut self) -> impl Iterator<Item = &mut T> {
[
&mut self.from_2015,
&mut self.from_2016,
&mut self.from_2017,
&mut self.from_2018,
&mut self.from_2019,
&mut self.from_2020,
&mut self.from_2021,
&mut self.from_2022,
&mut self.from_2023,
&mut self.from_2024,
&mut self.from_2025,
&mut self.from_2026,
]
.into_iter()
}
}
+1 -43
View File
@@ -120,44 +120,6 @@ impl<T> ByDcaPeriod<T> {
_10y: create(n._10y, d._10y, &period._10y)?,
})
}
pub(crate) fn iter_mut(&mut self) -> impl Iterator<Item = &mut T> {
[
&mut self._1w,
&mut self._1m,
&mut self._3m,
&mut self._6m,
&mut self._1y,
&mut self._2y,
&mut self._3y,
&mut self._4y,
&mut self._5y,
&mut self._6y,
&mut self._8y,
&mut self._10y,
]
.into_iter()
}
pub(crate) fn iter_with_days(&self) -> impl Iterator<Item = (&T, u32)> {
let d = DCA_PERIOD_DAYS;
[
(&self._1w, d._1w),
(&self._1m, d._1m),
(&self._3m, d._3m),
(&self._6m, d._6m),
(&self._1y, d._1y),
(&self._2y, d._2y),
(&self._3y, d._3y),
(&self._4y, d._4y),
(&self._5y, d._5y),
(&self._6y, d._6y),
(&self._8y, d._8y),
(&self._10y, d._10y),
]
.into_iter()
}
}
impl<T> ByDcaPeriod<&T> {
@@ -193,10 +155,7 @@ pub struct ByDcaCagr<T> {
}
impl<T> ByDcaCagr<T> {
pub(crate) fn try_new<U, F, E>(
period: &ByDcaPeriod<U>,
mut create: F,
) -> Result<Self, E>
pub(crate) fn try_new<U, F, E>(period: &ByDcaPeriod<U>, mut create: F) -> Result<Self, E>
where
F: FnMut(&'static str, u32, &U) -> Result<T, E>,
{
@@ -212,5 +171,4 @@ impl<T> ByDcaCagr<T> {
_10y: create(n._10y, d._10y, &period._10y)?,
})
}
}
+3 -58
View File
@@ -1,11 +1,10 @@
use brk_error::Result;
use brk_indexer::Indexer;
use brk_types::{Bitcoin, Cents, PartsPerMillionSigned64, Sats};
use brk_types::{Bitcoin, Sats};
use vecdb::{AnyVec, Exit, ReadableOptionVec, ReadableVec, VecIndex};
use super::vecs::DcaStack;
use super::{DCA_AMOUNT, Vecs};
use crate::{indexes, internal::{RatioDiffCents, SatsToCents}, price};
use crate::{indexes, price};
impl Vecs {
pub(crate) fn compute(
@@ -21,10 +20,7 @@ impl Vecs {
let h2d = &indexes.height.day1;
let close = &prices.split.close.usd.day1;
let start = self
.sats_cumulative
.len()
.min(starting_height.to_usize());
let start = self.sats_cumulative.len().min(starting_height.to_usize());
let mut cumulative = start
.checked_sub(1)
.and_then(|height| self.sats_cumulative.collect_one_at(height))
@@ -49,41 +45,6 @@ impl Vecs {
exit,
)?;
for stack in self.period.dca_stack.iter_mut() {
stack.compute_cents(prices, starting_height, exit)?;
}
for stack in self.class.dca_stack.iter_mut() {
stack.compute_cents(prices, starting_height, exit)?;
}
for (returns, (cost_basis, _)) in self
.period
.dca_return
.iter_mut()
.zip(self.period.dca_cost_basis.iter_with_days())
{
returns.compute_binary::<Cents, Cents, RatioDiffCents<PartsPerMillionSigned64>>(
starting_height,
&prices.spot.cents.height,
&cost_basis.cents.height,
exit,
)?;
}
for (returns, cost_basis) in self
.class
.dca_return
.iter_mut()
.zip(self.class.dca_cost_basis.iter())
{
returns.compute_binary::<Cents, Cents, RatioDiffCents<PartsPerMillionSigned64>>(
starting_height,
&prices.spot.cents.height,
&cost_basis.cents.height,
exit,
)?;
}
let exit = exit.clone();
self.db.run_bg(move |db| {
let _lock = exit.lock();
@@ -93,22 +54,6 @@ impl Vecs {
}
}
impl DcaStack {
fn compute_cents(
&mut self,
prices: &price::Vecs,
starting_height: brk_types::Height,
exit: &Exit,
) -> Result<()> {
self.cents.compute_binary::<Sats, Cents, SatsToCents>(
starting_height,
&self.sats.height,
&prices.spot.cents.height,
exit,
)
}
}
fn sats_from_dca(price: brk_types::Dollars) -> Sats {
if price == brk_types::Dollars::ZERO {
Sats::ZERO
+125 -92
View File
@@ -15,8 +15,8 @@ use crate::{
blocks, indexes,
internal::{
CentsUnsignedToDollars, Identity, LazyIndexedVec, LazyPerBlock, LazyPercentPerBlock,
LazyPreviousDeltaVec, LazySinceDayVec, LazyWindowVec, PerBlock, PercentPerBlock, Price,
RatioDiffCents, SatsToBitcoin, SatsToCents,
LazyPreviousDeltaVec, LazySinceDayVec, LazyWindowVec, Price, RatioDiffCents, SatsToBitcoin,
SatsToCents,
db_utils::{finalize_db, open_db},
},
price,
@@ -50,39 +50,60 @@ impl Vecs {
)
})?;
let cached_days = indexes.height.day1.read_only_cached_boxed_clone();
let spot_price = prices.spot.cents.height.read_only_cached_boxed_clone();
let dca_stack = ByDcaPeriod::try_from_period(&cached_starts, |name, _days, window_starts| {
let metric_name = format!("dca_stack_{name}");
let source = LazyWindowVec::<Height, Sats, Sats>::new(
&format!("{metric_name}_sats_source"),
version,
sats_cumulative.read_only_boxed_clone(),
window_starts.clone(),
true,
|current, before, _| current.checked_sub(before).unwrap_or_default(),
);
dca_stack_from_source(&db, &metric_name, version, indexes, source)
})?;
let dca_stack =
ByDcaPeriod::try_from_period(&cached_starts, |name, _days, window_starts| {
let metric_name = format!("dca_stack_{name}");
let source = LazyWindowVec::<Height, Sats, Sats>::new(
&format!("{metric_name}_sats_source"),
version,
sats_cumulative.read_only_boxed_clone(),
window_starts.clone(),
true,
|current, before, _| current.checked_sub(before).unwrap_or_default(),
);
dca_stack_from_source(&metric_name, version, indexes, source, &spot_price)
})?;
let first_price_day = Day1::try_from(Date::new(2010, 7, 12)).unwrap();
let dca_cost_basis =
ByDcaPeriod::try_from_period(&dca_stack, |name, days, stack| {
let metric_name = format!("dca_cost_basis_{name}");
let dca_cost_basis = ByDcaPeriod::try_from_period(&dca_stack, |name, days, stack| {
let metric_name = format!("dca_cost_basis_{name}");
let source = LazyIndexedVec::new(
&format!("{metric_name}_cents_source"),
version,
stack.sats.height.read_only_boxed_clone(),
cached_days.clone(),
move |_, stack_sats, day| {
if day <= first_price_day {
return Cents::ZERO;
}
let num_days =
(days as usize).min(day.to_usize() + 1 - first_price_day.to_usize());
Cents::from(DCA_AMOUNT * num_days / Bitcoin::from(stack_sats))
},
);
Ok::<_, Error>(Price::from_height_source(
&metric_name,
version,
source,
indexes,
))
})?;
let dca_return =
ByDcaPeriod::try_from_period(&dca_cost_basis, |name, _days, cost_basis| {
let metric_name = format!("dca_return_{name}");
let source = LazyIndexedVec::new(
&format!("{metric_name}_cents_source"),
&format!("{metric_name}_ppm_source"),
version,
stack.sats.height.read_only_boxed_clone(),
cached_days.clone(),
move |_, stack_sats, day| {
if day <= first_price_day {
return Cents::ZERO;
}
let num_days =
(days as usize).min(day.to_usize() + 1 - first_price_day.to_usize());
Cents::from(DCA_AMOUNT * num_days / Bitcoin::from(stack_sats))
cost_basis.cents.height.read_only_boxed_clone(),
spot_price.clone(),
|_, cost_basis, spot| {
RatioDiffCents::<PartsPerMillionSigned64>::apply(spot, cost_basis)
},
);
Ok::<_, Error>(Price::from_height_source(
Ok::<_, Error>(LazyPercentPerBlock::from_height_source(
&metric_name,
version,
source,
@@ -90,57 +111,47 @@ impl Vecs {
))
})?;
let dca_return = ByDcaPeriod::try_new(|name, _days| {
PercentPerBlock::forced_import(
&db,
&format!("dca_return_{name}"),
let dca_cagr = ByDcaCagr::try_new(&dca_return, |name, days, source| {
Ok::<_, Error>(LazyPercentPerBlock::from_lazy_cagr(
&format!("dca_cagr_{name}"),
version,
indexes,
)
(days / 365) as u8,
source,
))
})?;
let dca_cagr =
ByDcaCagr::try_new(&dca_return, |name, days, source| {
Ok::<_, Error>(LazyPercentPerBlock::from_cagr(
&format!("dca_cagr_{name}"),
version,
(days / 365) as u8,
source,
))
})?;
let lump_sum_stack =
ByDcaPeriod::try_from_period(&cached_starts, |name, days, window_starts| {
lump_sum_stack(
&format!("lump_sum_stack_{name}"),
days,
version,
indexes,
window_starts,
prices,
)
})?;
lump_sum_stack(
&format!("lump_sum_stack_{name}"),
days,
version,
indexes,
window_starts,
prices,
)
})?;
let lump_sum_return =
ByDcaPeriod::try_from_period(&cached_starts, |name, _days, window_starts| {
let metric_name = format!("lump_sum_return_{name}");
let source = LazyWindowVec::<Height, Cents, PartsPerMillionSigned64>::new(
&format!("{metric_name}_ppm_source"),
version,
prices.spot.cents.height.read_only_boxed_clone(),
window_starts.clone(),
false,
|current, past, _| {
RatioDiffCents::<PartsPerMillionSigned64>::apply(current, past)
},
);
Ok::<_, Error>(LazyPercentPerBlock::from_height_source(
&metric_name,
version,
source,
indexes,
))
})?;
let metric_name = format!("lump_sum_return_{name}");
let source = LazyWindowVec::<Height, Cents, PartsPerMillionSigned64>::new(
&format!("{metric_name}_ppm_source"),
version,
prices.spot.cents.height.read_only_boxed_clone(),
window_starts.clone(),
false,
|current, past, _| {
RatioDiffCents::<PartsPerMillionSigned64>::apply(current, past)
},
);
Ok::<_, Error>(LazyPercentPerBlock::from_height_source(
&metric_name,
version,
source,
indexes,
))
})?;
let class_stack = ByDcaClass::try_new(|name, _year, day| {
let metric_name = format!("dca_stack_{name}");
@@ -152,7 +163,7 @@ impl Vecs {
day,
|current, before| current.checked_sub(before).unwrap_or_default(),
);
dca_stack_from_source(&db, &metric_name, version, indexes, source)
dca_stack_from_source(&metric_name, version, indexes, source, &spot_price)
})?;
let class_cost_basis =
@@ -179,14 +190,25 @@ impl Vecs {
))
})?;
let class_return = ByDcaClass::try_new(|name, _year, _day| {
PercentPerBlock::forced_import(
&db,
&format!("dca_return_{name}"),
version,
indexes,
)
})?;
let class_return =
ByDcaClass::try_from_class(&class_cost_basis, |name, _year, _from, cost_basis| {
let metric_name = format!("dca_return_{name}");
let source = LazyIndexedVec::new(
&format!("{metric_name}_ppm_source"),
version,
cost_basis.cents.height.read_only_boxed_clone(),
spot_price.clone(),
|_, cost_basis, spot| {
RatioDiffCents::<PartsPerMillionSigned64>::apply(spot, cost_basis)
},
);
Ok::<_, Error>(LazyPercentPerBlock::from_height_source(
&metric_name,
version,
source,
indexes,
))
})?;
let this = Self {
sats_per_day,
@@ -212,23 +234,38 @@ impl Vecs {
}
fn dca_stack_from_source<V>(
db: &vecdb::Database,
name: &str,
version: Version,
indexes: &indexes::Vecs,
source: V,
spot_price: &CachedBoxedVec<Height, Cents>,
) -> Result<DcaStack>
where
V: TypedVec<I = Height, T = Sats> + ReadableVec<Height, Sats> + Clone + 'static,
{
let sats =
LazyPerBlock::from_height_source::<Identity<Sats>, _>(&format!("{name}_sats"), version, source, indexes);
let sats = LazyPerBlock::from_height_source::<Identity<Sats>, _>(
&format!("{name}_sats"),
version,
source,
indexes,
);
let btc = LazyPerBlock::from_lazy::<SatsToBitcoin, Sats>(name, version, &sats);
let cents = PerBlock::forced_import(db, &format!("{name}_cents"), version, indexes)?;
let usd = LazyPerBlock::from_computed::<CentsUnsignedToDollars>(
let cents_source = LazyIndexedVec::new(
&format!("{name}_cents_source"),
version,
sats.height.read_only_boxed_clone(),
spot_price.clone(),
|_, sats, spot| SatsToCents::apply(sats, spot),
);
let cents = LazyPerBlock::from_height_source::<Identity<Cents>, _>(
&format!("{name}_cents"),
version,
cents_source,
indexes,
);
let usd = LazyPerBlock::from_lazy::<CentsUnsignedToDollars, Cents>(
&format!("{name}_usd"),
version,
cents.height.read_only_boxed_clone(),
&cents,
);
Ok(DcaStack {
@@ -271,9 +308,7 @@ fn lump_sum_stack(
prices.spot.cents.height.read_only_boxed_clone(),
window_starts.clone(),
false,
move |current, past, _| {
SatsToCents::apply(lump_sum_sats(total_invested, past), current)
},
move |current, past, _| SatsToCents::apply(lump_sum_sats(total_invested, past), current),
);
let cents = LazyPerBlock::from_height_source::<Identity<Cents>, _>(
&format!("{name}_cents"),
@@ -299,8 +334,6 @@ fn lump_sum_sats(total_invested: Dollars, past_price: Cents) -> Sats {
if past_price == Cents::ZERO {
Sats::ZERO
} else {
Sats::from(Bitcoin::from(
total_invested / Dollars::from(past_price),
))
Sats::from(Bitcoin::from(total_invested / Dollars::from(past_price)))
}
}
+14 -16
View File
@@ -3,16 +3,14 @@ use brk_types::{Bitcoin, Cents, Dollars, Height, PartsPerMillionSigned64, Sats};
use vecdb::{Database, EagerVec, PcoVec, Rw, StorageMode};
use super::{ByDcaCagr, ByDcaClass, ByDcaPeriod};
use crate::internal::{
LazyPerBlock, LazyPercentPerBlock, LazyPreviousDeltaVec, PerBlock, PercentPerBlock, Price,
};
use crate::internal::{LazyPerBlock, LazyPercentPerBlock, LazyPreviousDeltaVec, Price};
#[derive(Traversable)]
pub struct DcaStack<M: StorageMode = Rw> {
#[derive(Clone, Traversable)]
pub struct DcaStack {
pub btc: LazyPerBlock<Bitcoin, Sats>,
pub sats: LazyPerBlock<Sats>,
pub usd: LazyPerBlock<Dollars, Cents>,
pub cents: PerBlock<Cents, M>,
pub cents: LazyPerBlock<Cents>,
}
#[derive(Clone, Traversable)]
@@ -23,21 +21,21 @@ pub struct LumpSumStack {
pub cents: LazyPerBlock<Cents>,
}
#[derive(Traversable)]
pub struct PeriodVecs<M: StorageMode = Rw> {
pub dca_stack: ByDcaPeriod<DcaStack<M>>,
#[derive(Clone, Traversable)]
pub struct PeriodVecs {
pub dca_stack: ByDcaPeriod<DcaStack>,
pub dca_cost_basis: ByDcaPeriod<Price<LazyPerBlock<Cents>>>,
pub dca_return: ByDcaPeriod<PercentPerBlock<PartsPerMillionSigned64, M>>,
pub dca_return: ByDcaPeriod<LazyPercentPerBlock<PartsPerMillionSigned64>>,
pub dca_cagr: ByDcaCagr<LazyPercentPerBlock<PartsPerMillionSigned64>>,
pub lump_sum_stack: ByDcaPeriod<LumpSumStack>,
pub lump_sum_return: ByDcaPeriod<LazyPercentPerBlock<PartsPerMillionSigned64>>,
}
#[derive(Traversable)]
pub struct ClassVecs<M: StorageMode = Rw> {
pub dca_stack: ByDcaClass<DcaStack<M>>,
#[derive(Clone, Traversable)]
pub struct ClassVecs {
pub dca_stack: ByDcaClass<DcaStack>,
pub dca_cost_basis: ByDcaClass<Price<LazyPerBlock<Cents>>>,
pub dca_return: ByDcaClass<PercentPerBlock<PartsPerMillionSigned64, M>>,
pub dca_return: ByDcaClass<LazyPercentPerBlock<PartsPerMillionSigned64>>,
}
#[derive(Traversable)]
@@ -47,6 +45,6 @@ pub struct Vecs<M: StorageMode = Rw> {
pub sats_per_day: LazyPreviousDeltaVec<Height, Sats>,
#[traversable(hidden)]
pub sats_cumulative: M::Stored<EagerVec<PcoVec<Height, Sats>>>,
pub period: PeriodVecs<M>,
pub class: ClassVecs<M>,
pub period: PeriodVecs,
pub class: ClassVecs,
}
+16 -17
View File
@@ -118,12 +118,21 @@ impl Computer {
})?;
let mining_handle = big_thread().spawn_scoped(s, || -> Result<_> {
Ok(Box::new(mining::Vecs::forced_import(
let mining = Box::new(mining::Vecs::forced_import(
&computed_path,
VERSION,
&indexes,
&cached_starts,
)?))
)?);
let cointime = Box::new(cointime::Vecs::forced_import(
&computed_path,
VERSION,
&indexes,
&cached_starts,
&price,
&mining.rewards.subsidy.cumulative.cents,
)?);
Ok((mining, cointime))
})?;
let transactions_handle = big_thread().spawn_scoped(s, || -> Result<_> {
@@ -145,17 +154,11 @@ impl Computer {
)?))
})?;
let cointime = Box::new(cointime::Vecs::forced_import(
&computed_path,
VERSION,
&indexes,
&cached_starts,
)?);
let coinflow = Box::new(coinflow::Vecs::forced_import(
&computed_path,
VERSION,
&indexes,
&price,
)?);
let op_return_handle = big_thread().spawn_scoped(s, || -> Result<_> {
@@ -169,7 +172,7 @@ impl Computer {
let inputs = inputs_handle.join().unwrap()?;
let outputs = outputs_handle.join().unwrap()?;
let mining = mining_handle.join().unwrap()?;
let (mining, cointime) = mining_handle.join().unwrap()?;
let transactions = transactions_handle.join().unwrap()?;
let pools = pools_handle.join().unwrap()?;
let op_return = op_return_handle.join().unwrap()?;
@@ -226,6 +229,7 @@ impl Computer {
VERSION,
&indexes,
&cached_starts,
&price,
)?);
let market = market_handle.join().unwrap()?;
@@ -433,12 +437,8 @@ impl Computer {
let investing = scope.spawn(|| {
timed("Computed investing", || {
self.investing.compute(
indexer,
&self.indexes,
&self.price,
exit,
)
self.investing
.compute(indexer, &self.indexes, &self.price, exit)
})
});
@@ -494,7 +494,6 @@ impl Computer {
&self.indexes,
&self.price,
&self.blocks,
&self.mining,
&self.supply,
&self.distribution,
exit,
@@ -69,13 +69,6 @@ impl Vecs {
exit,
)?;
self.drawdown.compute_drawdown(
starting_height,
&prices.spot.cents.height,
&self.high.cents.height,
exit,
)?;
Ok(())
}
}
+16 -4
View File
@@ -1,11 +1,14 @@
use brk_error::Result;
use brk_types::Version;
use vecdb::{Database, ReadableCloneableVec};
use brk_types::{Cents, Height, PartsPerMillionSigned32, Version};
use vecdb::{BinaryTransform, CachedBoxedVec, Database, ReadableCloneableVec};
use super::Vecs;
use crate::{
indexes,
internal::{DaysToYears, LazyPerBlock, PerBlock, PercentPerBlock, Price},
internal::{
DaysToYears, LazyIndexedVec, LazyPerBlock, LazyPercentPerBlock, PerBlock, Price,
RatioDiffCents,
},
};
const VERSION: Version = Version::ONE;
@@ -15,6 +18,7 @@ impl Vecs {
db: &Database,
version: Version,
indexes: &indexes::Vecs,
spot_price: &CachedBoxedVec<Height, Cents>,
) -> Result<Self> {
let v = version + VERSION;
@@ -39,7 +43,15 @@ impl Vecs {
&days_since,
);
let drawdown = PercentPerBlock::forced_import(db, "price_drawdown", v, indexes)?;
let drawdown_source = LazyIndexedVec::new(
"price_drawdown_ppm_source",
v,
high.cents.height.read_only_boxed_clone(),
spot_price.clone(),
|_, high, spot| RatioDiffCents::<PartsPerMillionSigned32>::apply(spot, high),
);
let drawdown =
LazyPercentPerBlock::from_height_source("price_drawdown", v, drawdown_source, indexes);
Ok(Self {
high,
+2 -2
View File
@@ -2,12 +2,12 @@ use brk_traversable::Traversable;
use brk_types::{Cents, PartsPerMillionSigned32, StoredF32};
use vecdb::{Rw, StorageMode};
use crate::internal::{LazyPerBlock, PerBlock, PercentPerBlock, Price};
use crate::internal::{LazyPerBlock, LazyPercentPerBlock, PerBlock, Price};
#[derive(Traversable)]
pub struct Vecs<M: StorageMode = Rw> {
pub high: Price<PerBlock<Cents, M>>,
pub drawdown: PercentPerBlock<PartsPerMillionSigned32, M>,
pub drawdown: LazyPercentPerBlock<PartsPerMillionSigned32>,
pub days_since: PerBlock<StoredF32, M>,
pub years_since: LazyPerBlock<StoredF32>,
pub max_days_between: PerBlock<StoredF32, M>,
+3 -9
View File
@@ -34,15 +34,9 @@ impl Vecs {
// Phase 2: Stored volatility inputs derived from lazy 24h returns.
self.returns.compute(indexer, blocks, exit)?;
// Phase 3: Depends on returns, moving_average
self.technical.compute(
indexer,
&self.returns,
prices,
blocks,
&self.moving_average,
exit,
)?;
// Phase 3: Depends on moving_average
self.technical
.compute(indexer, prices, blocks, &self.moving_average, exit)?;
let exit = exit.clone();
self.db.run_bg(move |db| {
+9 -6
View File
@@ -2,6 +2,7 @@ use std::path::Path;
use brk_error::{Error, Result};
use brk_types::Version;
use vecdb::ReadOnlyClone;
use crate::{
blocks, indexes,
@@ -25,7 +26,8 @@ impl Vecs {
let db = open_db(parent_path, super::DB_NAME, 250_000)?;
let version = parent_version;
let ath = AthVecs::forced_import(&db, version, indexes)?;
let spot_price = prices.spot.cents.height.read_only_cached_boxed_clone();
let ath = AthVecs::forced_import(&db, version, indexes, &spot_price)?;
let cached_starts = ByLookbackPeriod::try_new(|_, days| {
Ok::<_, Error>(
blocks
@@ -35,12 +37,13 @@ impl Vecs {
)
})?;
let lookback = LookbackVecs::forced_import(version, indexes, &cached_starts, prices)?;
let returns =
ReturnsVecs::forced_import(&db, version, indexes, &cached_starts, prices)?;
let returns = ReturnsVecs::forced_import(&db, version, indexes, &cached_starts, prices)?;
let volatility = VolatilityVecs::forced_import(version, &returns)?;
let range = RangeVecs::forced_import(&db, version, indexes)?;
let moving_average = MovingAverageVecs::forced_import(&db, version, indexes)?;
let technical = TechnicalVecs::forced_import(&db, version, indexes)?;
let cached_spot_price = prices.spot.cents.height.read_only_clone();
let range = RangeVecs::forced_import(&db, version, indexes, &cached_spot_price)?;
let moving_average = MovingAverageVecs::forced_import(&db, version, indexes, &spot_price)?;
let technical =
TechnicalVecs::forced_import(&db, version, indexes, &returns.periods._24h.ratio)?;
let this = Self {
db,
@@ -101,5 +101,4 @@ impl<T> ByLookbackPeriod<T> {
_10y: create(n._10y, d._10y, &period._10y)?,
})
}
}
@@ -1,10 +1,14 @@
use brk_error::{Error, Result};
use brk_types::{Cents, Height};
use brk_types::Version;
use brk_types::{Cents, Height};
use vecdb::CachedBoxedVec;
use super::{ByLookbackPeriod, Vecs};
use crate::{indexes, internal::{LazyWindowVec, Price}, price};
use crate::{
indexes,
internal::{LazyWindowVec, Price},
price,
};
impl Vecs {
pub(crate) fn forced_import(
@@ -13,25 +17,24 @@ impl Vecs {
cached_starts: &ByLookbackPeriod<CachedBoxedVec<Height, Height>>,
prices: &price::Vecs,
) -> Result<Self> {
let price_past = ByLookbackPeriod::try_from_period(
cached_starts,
|name, _days, window_starts| {
let metric_name = format!("price_past_{name}");
let source = LazyWindowVec::<Height, Cents, Cents>::new(
&format!("{metric_name}_cents_source"),
version,
prices.spot.cents.height.read_only_boxed_clone(),
window_starts.clone(),
false,
|_, past, _| past,
);
Ok::<_, Error>(Price::from_height_source(
&metric_name,
version,
source,
indexes,
))
})?;
let price_past =
ByLookbackPeriod::try_from_period(cached_starts, |name, _days, window_starts| {
let metric_name = format!("price_past_{name}");
let source = LazyWindowVec::<Height, Cents, Cents>::new(
&format!("{metric_name}_cents_source"),
version,
prices.spot.cents.height.read_only_boxed_clone(),
window_starts.clone(),
false,
|_, past, _| past,
);
Ok::<_, Error>(Price::from_height_source(
&metric_name,
version,
source,
indexes,
))
})?;
Ok(Self { price_past })
}
@@ -35,10 +35,12 @@ impl Vecs {
(&mut self.sma._4y, 4 * 365),
] {
let window_starts = blocks.lookback.start_vec(period);
sma.compute_all(prices, &starting_lengths, exit, |v| {
v.compute_rolling_average(starting_lengths.height, window_starts, close, exit)?;
Ok(())
})?;
sma.cents.height.compute_rolling_average(
starting_lengths.height,
window_starts,
close,
exit,
)?;
}
for (ema, period) in [
@@ -60,10 +62,12 @@ impl Vecs {
(&mut self.ema._4y, 4 * 365),
] {
let window_starts = blocks.lookback.start_vec(period);
ema.compute_all(prices, &starting_lengths, exit, |v| {
v.compute_rolling_ema(starting_lengths.height, window_starts, close, exit)?;
Ok(())
})?;
ema.cents.height.compute_rolling_ema(
starting_lengths.height,
window_starts,
close,
exit,
)?;
}
Ok(())
@@ -1,6 +1,6 @@
use brk_error::Result;
use brk_types::Version;
use vecdb::Database;
use brk_types::{Cents, Height, Version};
use vecdb::{CachedBoxedVec, Database};
use super::{
Vecs,
@@ -16,10 +16,11 @@ impl Vecs {
db: &Database,
version: Version,
indexes: &indexes::Vecs,
spot_price: &CachedBoxedVec<Height, Cents>,
) -> Result<Self> {
macro_rules! import {
($name:expr) => {
PriceWithRatioPerBlock::forced_import(db, $name, version, indexes)?
PriceWithRatioPerBlock::forced_import(db, $name, version, indexes, spot_price)?
};
}

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