From cc5b6da341b469859cb457c9c0c93a547eb6ee00 Mon Sep 17 00:00:00 2001 From: nym21 Date: Thu, 30 Jul 2026 14:11:40 +0200 Subject: [PATCH] brk: metric trimming part X + 3 --- Cargo.lock | 6 - Cargo.toml | 4 +- crates/brk_client/src/lib.rs | 236 ++++++++------ crates/brk_computer/src/blocks/compute.rs | 18 +- .../brk_computer/src/blocks/count/compute.rs | 19 -- .../brk_computer/src/blocks/count/import.rs | 30 +- crates/brk_computer/src/blocks/count/mod.rs | 1 - crates/brk_computer/src/blocks/count/vecs.rs | 9 +- .../src/blocks/difficulty/compute.rs | 40 --- .../src/blocks/difficulty/import.rs | 65 ++-- .../brk_computer/src/blocks/difficulty/mod.rs | 1 - .../src/blocks/difficulty/vecs.rs | 14 +- .../src/blocks/halving/compute.rs | 33 -- .../brk_computer/src/blocks/halving/import.rs | 41 ++- crates/brk_computer/src/blocks/halving/mod.rs | 1 - .../brk_computer/src/blocks/halving/vecs.rs | 12 +- crates/brk_computer/src/blocks/import.rs | 10 +- .../src/blocks/interval/compute.rs | 44 +-- .../src/blocks/interval/import.rs | 4 +- .../brk_computer/src/blocks/interval/vecs.rs | 4 +- crates/brk_computer/src/blocks/lookback.rs | 95 +++--- crates/brk_computer/src/blocks/mod.rs | 8 +- .../brk_computer/src/blocks/size/compute.rs | 11 +- crates/brk_computer/src/blocks/size/import.rs | 10 +- crates/brk_computer/src/blocks/size/vecs.rs | 4 +- .../brk_computer/src/blocks/weight/compute.rs | 20 -- .../brk_computer/src/blocks/weight/import.rs | 31 +- crates/brk_computer/src/blocks/weight/mod.rs | 1 - crates/brk_computer/src/blocks/weight/vecs.rs | 9 +- .../src/distribution/addr/activity.rs | 98 +++--- .../src/distribution/addr/new_addr_count.rs | 69 ++-- .../distribution/addr/reused/events/vecs.rs | 27 +- .../src/distribution/cohorts/addr/groups.rs | 15 +- .../src/distribution/cohorts/addr/vecs.rs | 10 +- .../src/distribution/cohorts/traits.rs | 3 +- .../src/distribution/cohorts/utxo/groups.rs | 31 +- .../src/distribution/metrics/cohort/all.rs | 13 +- .../src/distribution/metrics/cohort/basic.rs | 15 +- .../src/distribution/metrics/cohort/core.rs | 22 +- .../distribution/metrics/cohort/extended.rs | 17 +- .../metrics/cohort/extended_adjusted.rs | 4 +- .../distribution/metrics/cohort/minimal.rs | 23 +- .../src/distribution/metrics/cohort/type.rs | 21 +- .../src/distribution/metrics/mod.rs | 2 - .../src/distribution/metrics/outputs/base.rs | 27 +- .../src/distribution/metrics/realized/full.rs | 54 +++- .../relative/extended_own_market_cap.rs | 23 +- .../metrics/relative/extended_own_pnl.rs | 7 +- .../metrics/relative/with_extended.rs | 7 +- .../distribution/metrics/unrealized/basic.rs | 13 +- .../distribution/metrics/unrealized/core.rs | 11 +- .../distribution/metrics/unrealized/full.rs | 11 +- .../metrics/unrealized/minimal.rs | 84 ++--- crates/brk_computer/src/distribution/vecs.rs | 29 +- .../src/frameworks/coinflow/compute.rs | 6 +- .../src/frameworks/coinflow/import.rs | 43 ++- .../src/frameworks/coinflow/vecs.rs | 6 +- .../frameworks/cointime/activity/compute.rs | 7 - .../frameworks/cointime/activity/import.rs | 11 +- .../src/frameworks/cointime/activity/vecs.rs | 2 +- crates/brk_computer/src/indexes/height.rs | 6 +- crates/brk_computer/src/indexes/mod.rs | 2 +- crates/brk_computer/src/inputs/compute.rs | 3 - crates/brk_computer/src/inputs/import.rs | 6 +- crates/brk_computer/src/inputs/mod.rs | 6 +- .../src/inputs/per_sec/compute.rs | 29 -- .../brk_computer/src/inputs/per_sec/import.rs | 21 -- crates/brk_computer/src/inputs/per_sec/mod.rs | 5 - .../brk_computer/src/inputs/per_sec/vecs.rs | 8 - .../per_block/computed/cumulative_rolling.rs | 46 +-- .../src/internal/per_block/computed/full.rs | 56 +++- .../computed/lazy_cumulative_rolling.rs | 120 +++++++ .../per_block/computed/lazy_rolling.rs | 18 +- .../src/internal/per_block/computed/mod.rs | 2 + .../per_block/computed/rolling_average.rs | 151 +++++++-- .../src/internal/per_block/lazy/base.rs | 30 +- .../src/internal/per_block/lazy/indexed.rs | 195 ++++++++++++ .../src/internal/per_block/lazy/lookback.rs | 203 ++++++++++++ .../src/internal/per_block/lazy/mod.rs | 10 + .../src/internal/per_block/lazy/per_second.rs | 40 +++ .../internal/per_block/lazy/previous_delta.rs | 70 +++-- .../src/internal/per_block/lazy/since_day.rs | 221 +++++++++++++ .../src/internal/per_block/lazy/window.rs | 241 ++++++++++++++ .../src/internal/per_block/percent/lazy.rs | 123 +++++++- .../internal/per_block/percent/lazy_vec.rs | 43 +++ .../per_block/percent/lazy_windows.rs | 47 ++- .../src/internal/per_block/percent/mod.rs | 4 +- .../src/internal/per_block/percent/vec.rs | 43 --- .../src/internal/per_block/price.rs | 34 +- .../src/internal/per_block/ratio/lazy.rs | 38 +++ .../src/internal/per_block/ratio/mod.rs | 2 + .../src/internal/per_block/ratio/price.rs | 37 ++- .../src/internal/transform/arithmetic.rs | 43 +++ .../src/internal/transform/currency.rs | 48 ++- .../src/internal/transform/derived.rs | 32 +- .../src/internal/transform/mod.rs | 8 +- .../src/internal/transform/ratio.rs | 42 +-- crates/brk_computer/src/investing/by_class.rs | 102 +++++- .../brk_computer/src/investing/by_period.rs | 103 +++--- crates/brk_computer/src/investing/compute.rs | 292 ++++------------- crates/brk_computer/src/investing/import.rs | 294 +++++++++++++++--- crates/brk_computer/src/investing/mod.rs | 3 + crates/brk_computer/src/investing/vecs.rs | 40 ++- crates/brk_computer/src/lib.rs | 16 +- crates/brk_computer/src/market/compute.rs | 15 +- crates/brk_computer/src/market/import.rs | 22 +- .../src/market/lookback/by_period.rs | 59 ++-- .../src/market/lookback/compute.rs | 31 -- .../src/market/lookback/import.rs | 30 +- .../brk_computer/src/market/lookback/mod.rs | 1 - .../brk_computer/src/market/lookback/vecs.rs | 9 +- crates/brk_computer/src/market/mod.rs | 2 +- .../src/market/returns/compute.rs | 35 +-- .../brk_computer/src/market/returns/import.rs | 51 ++- .../brk_computer/src/market/returns/vecs.rs | 6 +- .../src/mining/rewards/compute.rs | 16 +- .../brk_computer/src/mining/rewards/import.rs | 8 +- .../brk_computer/src/mining/rewards/vecs.rs | 6 +- .../src/models/bedrock/compute.rs | 54 ++-- .../brk_computer/src/models/bedrock/import.rs | 9 +- crates/brk_computer/src/models/bedrock/mod.rs | 1 + .../brk_computer/src/models/bedrock/price.rs | 87 ++++++ .../src/models/bedrock/urpd_metric.rs | 2 +- .../brk_computer/src/models/bedrock/vecs.rs | 8 +- crates/brk_computer/src/outputs/compute.rs | 1 - crates/brk_computer/src/outputs/import.rs | 7 +- crates/brk_computer/src/outputs/mod.rs | 6 +- .../src/outputs/per_sec/compute.rs | 29 -- .../src/outputs/per_sec/import.rs | 21 -- .../brk_computer/src/outputs/per_sec/mod.rs | 5 - .../brk_computer/src/outputs/per_sec/vecs.rs | 8 - crates/brk_computer/src/pools/major.rs | 36 +-- crates/brk_computer/src/pools/minor.rs | 88 +++++- crates/brk_computer/src/pools/mod.rs | 11 +- crates/brk_computer/src/supply/compute.rs | 29 +- crates/brk_computer/src/supply/import.rs | 31 +- crates/brk_computer/src/supply/vecs.rs | 4 +- .../brk_computer/src/transactions/compute.rs | 4 +- .../src/transactions/count/compute.rs | 13 +- .../src/transactions/count/import.rs | 16 +- .../src/transactions/count/vecs.rs | 2 +- .../brk_computer/src/transactions/import.rs | 8 +- .../src/transactions/volume/compute.rs | 17 +- .../src/transactions/volume/import.rs | 12 +- .../src/transactions/volume/vecs.rs | 5 +- crates/brk_types/src/cents.rs | 230 +++++++++++--- modules/brk-client/index.js | 258 +++++++++------ packages/brk_client/brk_client/__init__.py | 120 ++++--- website/llms-full.txt | 2 +- website/llms.txt | 2 +- .../scripts/options/distribution/holdings.js | 24 -- website/scripts/options/mining.js | 13 +- website/scripts/options/models/bedrock.js | 19 +- website_next/llms-full.txt | 2 +- website_next/llms.txt | 2 +- 155 files changed, 3774 insertions(+), 2078 deletions(-) delete mode 100644 crates/brk_computer/src/blocks/count/compute.rs delete mode 100644 crates/brk_computer/src/blocks/difficulty/compute.rs delete mode 100644 crates/brk_computer/src/blocks/halving/compute.rs delete mode 100644 crates/brk_computer/src/blocks/weight/compute.rs delete mode 100644 crates/brk_computer/src/inputs/per_sec/compute.rs delete mode 100644 crates/brk_computer/src/inputs/per_sec/import.rs delete mode 100644 crates/brk_computer/src/inputs/per_sec/mod.rs delete mode 100644 crates/brk_computer/src/inputs/per_sec/vecs.rs create mode 100644 crates/brk_computer/src/internal/per_block/computed/lazy_cumulative_rolling.rs create mode 100644 crates/brk_computer/src/internal/per_block/lazy/indexed.rs create mode 100644 crates/brk_computer/src/internal/per_block/lazy/lookback.rs create mode 100644 crates/brk_computer/src/internal/per_block/lazy/per_second.rs create mode 100644 crates/brk_computer/src/internal/per_block/lazy/since_day.rs create mode 100644 crates/brk_computer/src/internal/per_block/lazy/window.rs create mode 100644 crates/brk_computer/src/internal/per_block/percent/lazy_vec.rs delete mode 100644 crates/brk_computer/src/internal/per_block/percent/vec.rs create mode 100644 crates/brk_computer/src/internal/per_block/ratio/lazy.rs delete mode 100644 crates/brk_computer/src/market/lookback/compute.rs create mode 100644 crates/brk_computer/src/models/bedrock/price.rs delete mode 100644 crates/brk_computer/src/outputs/per_sec/compute.rs delete mode 100644 crates/brk_computer/src/outputs/per_sec/import.rs delete mode 100644 crates/brk_computer/src/outputs/per_sec/mod.rs delete mode 100644 crates/brk_computer/src/outputs/per_sec/vecs.rs diff --git a/Cargo.lock b/Cargo.lock index 7b912d607..3b299ead5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2438,8 +2438,6 @@ dependencies = [ [[package]] name = "rawdb" version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7df8d0fa0319c87c369464ffe5ee0baa6659ed37d3426a9ae021fb463d95ad52" dependencies = [ "libc", "log", @@ -3367,8 +3365,6 @@ checksum = "bfa6c38708f6257f1ec2ca7e5a11f9bbf58a27d7060078b6b333624968183d96" [[package]] name = "vecdb" version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "294fcad993a26e2668dca931c610b8461e66837b5f359a0c1a89bdf357cacf4b" dependencies = [ "itoa", "libc", @@ -3390,8 +3386,6 @@ dependencies = [ [[package]] name = "vecdb_derive" version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87e6ddc694828bd6d372573727e02ea07047f4b65f78aaf859dae2fbf2dbbcfe" dependencies = [ "quote", "syn 2.0.119", diff --git a/Cargo.toml b/Cargo.toml index 590ef2de5..82634d666 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -84,8 +84,8 @@ tower-http = { version = "0.7.0", features = ["catch-panic", "compression-br", " tower-layer = "0.3" tracing = { version = "0.1", default-features = false, features = ["std"] } ureq = { version = "3.3.0", features = ["json"] } -vecdb = { version = "0.10.4", features = ["derive", "serde_json", "pco", "schemars"] } -# vecdb = { path = "../anydb/crates/vecdb", features = ["derive", "serde_json", "pco", "schemars"] } +# vecdb = { version = "0.10.4", features = ["derive", "serde_json", "pco", "schemars"] } +vecdb = { path = "../anydb/crates/vecdb", features = ["derive", "serde_json", "pco", "schemars"] } [workspace.metadata.release] diff --git a/crates/brk_client/src/lib.rs b/crates/brk_client/src/lib.rs index 3ba01d13a..bbf3ecb15 100644 --- a/crates/brk_client/src/lib.rs +++ b/crates/brk_client/src/lib.rs @@ -1758,30 +1758,30 @@ impl CapitalizedGrossInvestedLossNetNuplProfitSentimentPattern2 { /// Pattern struct for repeated tree structure. pub struct Pct10Pct20Pct30Pct40Pct50Pct60Pct70Pct80Pct90Pattern { - pub pct10: SeriesPattern1, - pub pct20: SeriesPattern1, - pub pct30: SeriesPattern1, - pub pct40: SeriesPattern1, - pub pct50: SeriesPattern1, - pub pct60: SeriesPattern1, - pub pct70: SeriesPattern1, - pub pct80: SeriesPattern1, - pub pct90: SeriesPattern1, + pub pct10: CentsSatsUsdPattern, + pub pct20: CentsSatsUsdPattern, + pub pct30: CentsSatsUsdPattern, + pub pct40: CentsSatsUsdPattern, + pub pct50: CentsSatsUsdPattern, + pub pct60: CentsSatsUsdPattern, + pub pct70: CentsSatsUsdPattern, + pub pct80: CentsSatsUsdPattern, + pub pct90: CentsSatsUsdPattern, } impl Pct10Pct20Pct30Pct40Pct50Pct60Pct70Pct80Pct90Pattern { /// Create a new pattern node with accumulated series name. pub fn new(client: Arc, acc: String) -> Self { Self { - pct10: SeriesPattern1::new(client.clone(), _m(&acc, "pct10")), - pct20: SeriesPattern1::new(client.clone(), _m(&acc, "pct20")), - pct30: SeriesPattern1::new(client.clone(), _m(&acc, "pct30")), - pct40: SeriesPattern1::new(client.clone(), _m(&acc, "pct40")), - pct50: SeriesPattern1::new(client.clone(), _m(&acc, "pct50")), - pct60: SeriesPattern1::new(client.clone(), _m(&acc, "pct60")), - pct70: SeriesPattern1::new(client.clone(), _m(&acc, "pct70")), - pct80: SeriesPattern1::new(client.clone(), _m(&acc, "pct80")), - pct90: SeriesPattern1::new(client.clone(), _m(&acc, "pct90")), + pct10: CentsSatsUsdPattern::new(client.clone(), _m(&acc, "pct10")), + pct20: CentsSatsUsdPattern::new(client.clone(), _m(&acc, "pct20")), + pct30: CentsSatsUsdPattern::new(client.clone(), _m(&acc, "pct30")), + pct40: CentsSatsUsdPattern::new(client.clone(), _m(&acc, "pct40")), + pct50: CentsSatsUsdPattern::new(client.clone(), _m(&acc, "pct50")), + pct60: CentsSatsUsdPattern::new(client.clone(), _m(&acc, "pct60")), + pct70: CentsSatsUsdPattern::new(client.clone(), _m(&acc, "pct70")), + pct80: CentsSatsUsdPattern::new(client.clone(), _m(&acc, "pct80")), + pct90: CentsSatsUsdPattern::new(client.clone(), _m(&acc, "pct90")), } } } @@ -1882,7 +1882,7 @@ pub struct ActivityCostInvestedOutputsRealizedSupplyUnrealizedPattern2 { pub activity: CoindaysCoinyearsDormancyTransferPattern, pub cost_basis: InMaxMinPerSupplyPattern, pub invested_capital: InPattern, - pub outputs: SpentUnspentUtxoPattern, + pub outputs: SpentUnspentPattern, pub realized: CapCapitalizedGrossLossMvrvNetPeakPriceProfitSellSoprPattern2, pub supply: DeltaDominanceHalfInTotalPattern2, pub unrealized: CapitalizedGrossInvestedLossNetNuplProfitSentimentPattern2, @@ -1895,7 +1895,7 @@ impl ActivityCostInvestedOutputsRealizedSupplyUnrealizedPattern2 { 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: SpentUnspentUtxoPattern::new(client.clone(), acc.clone()), + 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()), @@ -2037,7 +2037,7 @@ impl MaxMedianMinPct10Pct25Pct75Pct90Pattern { pub struct ActivityAddrOutputsRealizedSupplyUnrealizedPattern { pub activity: TransferPattern, pub addr_count: BaseDeltaPattern, - pub outputs: SpentUnspentUtxoPattern, + pub outputs: SpentUnspentPattern, pub realized: CapLossMvrvPriceProfitPattern, pub supply: DeltaDominanceTotalPattern, pub unrealized: NuplPattern, @@ -2049,7 +2049,7 @@ impl ActivityAddrOutputsRealizedSupplyUnrealizedPattern { Self { activity: TransferPattern::new(client.clone(), _m(&acc, "transfer_volume")), addr_count: BaseDeltaPattern::new(client.clone(), _m(&acc, "addr_count")), - outputs: SpentUnspentUtxoPattern::new(client.clone(), acc.clone()), + 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")), @@ -2222,7 +2222,7 @@ impl ActiveBidirectionalReactivatedReceivingSendingPattern { /// Pattern struct for repeated tree structure. pub struct ActivityOutputsRealizedSupplyUnrealizedPattern { pub activity: CoindaysTransferPattern, - pub outputs: SpentUnspentUtxoPattern, + pub outputs: SpentUnspentPattern, pub realized: CapLossMvrvNetPriceProfitSoprPattern, pub supply: DeltaDominanceHalfInTotalPattern, pub unrealized: LossNetNuplProfitPattern, @@ -2233,7 +2233,7 @@ impl ActivityOutputsRealizedSupplyUnrealizedPattern { pub fn new(client: Arc, acc: String) -> Self { Self { activity: CoindaysTransferPattern::new(client.clone(), acc.clone()), - outputs: SpentUnspentUtxoPattern::new(client.clone(), acc.clone()), + outputs: SpentUnspentPattern::new(client.clone(), acc.clone()), realized: CapLossMvrvNetPriceProfitSoprPattern::new(client.clone(), acc.clone()), supply: DeltaDominanceHalfInTotalPattern::new(client.clone(), _m(&acc, "supply")), unrealized: LossNetNuplProfitPattern::new(client.clone(), acc.clone()), @@ -2244,7 +2244,7 @@ impl ActivityOutputsRealizedSupplyUnrealizedPattern { /// Pattern struct for repeated tree structure. pub struct ActivityOutputsRealizedSupplyUnrealizedPattern3 { pub activity: TransferPattern, - pub outputs: SpentUnspentUtxoPattern, + pub outputs: SpentUnspentPattern, pub realized: CapLossMvrvPriceProfitPattern, pub supply: DeltaDominanceHalfInTotalPattern, pub unrealized: LossNuplProfitPattern, @@ -2255,7 +2255,7 @@ impl ActivityOutputsRealizedSupplyUnrealizedPattern3 { pub fn new(client: Arc, acc: String) -> Self { Self { activity: TransferPattern::new(client.clone(), _m(&acc, "transfer_volume")), - outputs: SpentUnspentUtxoPattern::new(client.clone(), acc.clone()), + outputs: SpentUnspentPattern::new(client.clone(), acc.clone()), realized: CapLossMvrvPriceProfitPattern::new(client.clone(), acc.clone()), supply: DeltaDominanceHalfInTotalPattern::new(client.clone(), _m(&acc, "supply")), unrealized: LossNuplProfitPattern::new(client.clone(), acc.clone()), @@ -2266,7 +2266,7 @@ impl ActivityOutputsRealizedSupplyUnrealizedPattern3 { /// Pattern struct for repeated tree structure. pub struct ActivityOutputsRealizedSupplyUnrealizedPattern2 { pub activity: TransferPattern, - pub outputs: SpentUnspentUtxoPattern, + pub outputs: SpentUnspentPattern, pub realized: CapLossMvrvPriceProfitPattern, pub supply: DeltaDominanceTotalPattern, pub unrealized: NuplPattern, @@ -2277,7 +2277,7 @@ impl ActivityOutputsRealizedSupplyUnrealizedPattern2 { pub fn new(client: Arc, acc: String) -> Self { Self { activity: TransferPattern::new(client.clone(), _m(&acc, "transfer_volume")), - outputs: SpentUnspentUtxoPattern::new(client.clone(), acc.clone()), + 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")), @@ -2435,6 +2435,50 @@ pub struct EmaHistogramLineSignalPattern { pub signal: SeriesPattern1, } +/// Pattern struct for repeated tree structure. +pub struct Pct95Pct98Pct99Pattern { + pub pct95: CentsSatsUsdPattern, + pub pct98: CentsSatsUsdPattern, + pub pct99: CentsSatsUsdPattern, + pub pct99_5: CentsSatsUsdPattern, + pub pct99_9: CentsSatsUsdPattern, +} + +impl Pct95Pct98Pct99Pattern { + /// Create a new pattern node with accumulated series name. + pub fn new(client: Arc, acc: String) -> Self { + Self { + pct95: CentsSatsUsdPattern::new(client.clone(), _m(&acc, "pct95")), + pct98: CentsSatsUsdPattern::new(client.clone(), _m(&acc, "pct98")), + pct99: CentsSatsUsdPattern::new(client.clone(), _m(&acc, "pct99")), + pct99_5: CentsSatsUsdPattern::new(client.clone(), _m(&acc, "pct99_5")), + pct99_9: CentsSatsUsdPattern::new(client.clone(), _m(&acc, "pct99_9")), + } + } +} + +/// Pattern struct for repeated tree structure. +pub struct Pct95Pct98Pct99Pattern2 { + pub pct95: SeriesPattern1, + pub pct98: SeriesPattern1, + pub pct99: SeriesPattern1, + pub pct99_5: SeriesPattern1, + pub pct99_9: SeriesPattern1, +} + +impl Pct95Pct98Pct99Pattern2 { + /// Create a new pattern node with accumulated series name. + pub fn new(client: Arc, acc: String) -> Self { + Self { + pct95: SeriesPattern1::new(client.clone(), _m(&acc, "pct95")), + pct98: SeriesPattern1::new(client.clone(), _m(&acc, "pct98")), + pct99: SeriesPattern1::new(client.clone(), _m(&acc, "pct99")), + pct99_5: SeriesPattern1::new(client.clone(), _m(&acc, "pct99_5")), + pct99_9: SeriesPattern1::new(client.clone(), _m(&acc, "pct99_9")), + } + } +} + /// Pattern struct for repeated tree structure. pub struct PhsReboundThsPattern { pub phs: SeriesPattern1, @@ -2457,28 +2501,6 @@ impl PhsReboundThsPattern { } } -/// Pattern struct for repeated tree structure. -pub struct Pct95Pct98Pct99Pattern { - pub pct95: SeriesPattern1, - pub pct98: SeriesPattern1, - pub pct99: SeriesPattern1, - pub pct99_5: SeriesPattern1, - pub pct99_9: SeriesPattern1, -} - -impl Pct95Pct98Pct99Pattern { - /// Create a new pattern node with accumulated series name. - pub fn new(client: Arc, acc: String) -> Self { - Self { - pct95: SeriesPattern1::new(client.clone(), _m(&acc, "pct95")), - pct98: SeriesPattern1::new(client.clone(), _m(&acc, "pct98")), - pct99: SeriesPattern1::new(client.clone(), _m(&acc, "pct99")), - pct99_5: SeriesPattern1::new(client.clone(), _m(&acc, "pct99_5")), - pct99_9: SeriesPattern1::new(client.clone(), _m(&acc, "pct99_9")), - } - } -} - /// Pattern struct for repeated tree structure. pub struct _1m1w1y24hPattern4 { pub _1m: BtcCentsSatsUsdPattern, @@ -2955,9 +2977,27 @@ impl BlockCumulativeSumPattern { } } +/// Pattern struct for repeated tree structure. +pub struct BlockCumulativeSumPattern2 { + pub block: SeriesPattern18, + pub cumulative: SeriesPattern1, + pub sum: _1m1w1y24hPattern, +} + +impl BlockCumulativeSumPattern2 { + /// Create a new pattern node with accumulated series name. + pub fn new(client: Arc, acc: String) -> Self { + Self { + block: SeriesPattern18::new(client.clone(), acc.clone()), + cumulative: SeriesPattern1::new(client.clone(), _m(&acc, "cumulative")), + sum: _1m1w1y24hPattern::new(client.clone(), _m(&acc, "sum")), + } + } +} + /// Pattern struct for repeated tree structure. pub struct BlocksDominanceRewardsPattern { - pub blocks_mined: AverageBlockCumulativeSumPattern, + pub blocks_mined: BlockCumulativeSumPattern2, pub dominance: _1m1w1y24hPercentPpmRatioPattern, pub rewards: AverageBlockCumulativeSumPattern2, } @@ -2966,7 +3006,7 @@ impl BlocksDominanceRewardsPattern { /// Create a new pattern node with accumulated series name. pub fn new(client: Arc, acc: String) -> Self { Self { - blocks_mined: AverageBlockCumulativeSumPattern::new(client.clone(), _m(&acc, "blocks_mined")), + blocks_mined: BlockCumulativeSumPattern2::new(client.clone(), _m(&acc, "blocks_mined")), dominance: _1m1w1y24hPercentPpmRatioPattern::new(client.clone(), _m(&acc, "dominance")), rewards: AverageBlockCumulativeSumPattern2::new(client.clone(), _m(&acc, "rewards")), } @@ -3090,9 +3130,9 @@ impl DeltaDominanceTotalPattern { /// Pattern struct for repeated tree structure. pub struct FloorLevelLossPattern { - pub floor: Pct95Pct98Pct99Pattern, + pub floor: Pct95Pct98Pct99Pattern, pub level: Pct10Pct20Pct30Pct40Pct50Pct60Pct70Pct80Pct90Pattern, - pub loss_threshold: Pct95Pct98Pct99Pattern, + pub loss_threshold: Pct95Pct98Pct99Pattern2, } impl FloorLevelLossPattern { @@ -3101,7 +3141,7 @@ impl FloorLevelLossPattern { Self { floor: Pct95Pct98Pct99Pattern::new(client.clone(), _m(&acc, "floor")), level: Pct10Pct20Pct30Pct40Pct50Pct60Pct70Pct80Pct90Pattern::new(client.clone(), _m(&acc, "level")), - loss_threshold: Pct95Pct98Pct99Pattern::new(client.clone(), _m(&acc, "loss_threshold")), + loss_threshold: Pct95Pct98Pct99Pattern2::new(client.clone(), _m(&acc, "loss_threshold")), } } } @@ -3160,6 +3200,24 @@ impl PercentPpmRatioPattern2 { } } +/// Pattern struct for repeated tree structure. +pub struct PercentPpmRatioPattern5 { + pub percent: SeriesPattern1, + pub ppm: SeriesPattern1, + pub ratio: SeriesPattern1, +} + +impl PercentPpmRatioPattern5 { + /// Create a new pattern node with accumulated series name. + pub fn new(client: Arc, acc: String) -> Self { + Self { + percent: SeriesPattern1::new(client.clone(), acc.clone()), + ppm: SeriesPattern1::new(client.clone(), _m(&acc, "ppm")), + ratio: SeriesPattern1::new(client.clone(), _m(&acc, "ratio")), + } + } +} + /// Pattern struct for repeated tree structure. pub struct PercentPpmRatioPattern3 { pub percent: SeriesPattern1, @@ -3250,24 +3308,6 @@ impl RsiStochPattern { } } -/// Pattern struct for repeated tree structure. -pub struct SpentUnspentUtxoPattern { - pub spent_count: AverageBlockCumulativeSumPattern, - pub unspent_count: BaseDeltaPattern, - pub utxo_turnover_1y: SeriesPattern1, -} - -impl SpentUnspentUtxoPattern { - /// Create a new pattern node with accumulated series name. - pub fn new(client: Arc, acc: String) -> Self { - Self { - spent_count: AverageBlockCumulativeSumPattern::new(client.clone(), _m(&acc, "spent_utxo_count")), - unspent_count: BaseDeltaPattern::new(client.clone(), _m(&acc, "utxo_count")), - utxo_turnover_1y: SeriesPattern1::new(client.clone(), _m(&acc, "utxo_turnover_1y")), - } - } -} - /// Pattern struct for repeated tree structure. pub struct _6bBlockTxPattern { pub _6b: MaxMedianMinPct10Pct25Pct75Pct90Pattern, @@ -3448,7 +3488,7 @@ impl BlockCumulativePattern { /// Pattern struct for repeated tree structure. pub struct BlocksDominancePattern { - pub blocks_mined: AverageBlockCumulativeSumPattern, + pub blocks_mined: BlockCumulativeSumPattern2, pub dominance: PercentPpmRatioPattern2, } @@ -3456,7 +3496,7 @@ impl BlocksDominancePattern { /// Create a new pattern node with accumulated series name. pub fn new(client: Arc, acc: String) -> Self { Self { - blocks_mined: AverageBlockCumulativeSumPattern::new(client.clone(), _m(&acc, "blocks_mined")), + blocks_mined: BlockCumulativeSumPattern2::new(client.clone(), _m(&acc, "blocks_mined")), dominance: PercentPpmRatioPattern2::new(client.clone(), _m(&acc, "dominance")), } } @@ -3724,6 +3764,22 @@ pub struct SdSmaPattern { pub sma: SeriesPattern1, } +/// Pattern struct for repeated tree structure. +pub struct SpentUnspentPattern { + pub spent_count: AverageBlockCumulativeSumPattern, + pub unspent_count: BaseDeltaPattern, +} + +impl SpentUnspentPattern { + /// Create a new pattern node with accumulated series name. + pub fn new(client: Arc, acc: String) -> Self { + Self { + spent_count: AverageBlockCumulativeSumPattern::new(client.clone(), _m(&acc, "spent_utxo_count")), + unspent_count: BaseDeltaPattern::new(client.clone(), _m(&acc, "utxo_count")), + } + } +} + /// Pattern struct for repeated tree structure. pub struct ToPattern { pub to_mcap: PercentPpmRatioPattern, @@ -5738,7 +5794,7 @@ pub struct SeriesTree_Mining_Rewards_Fees { pub pct75: _1m1w1y24hPattern4, pub pct90: _1m1w1y24hPattern4, pub dominance: _1m1w1y24hPercentPpmRatioPattern, - pub to_subsidy_ratio: SeriesTree_Mining_Rewards_Fees_ToSubsidyRatio, + pub to_subsidy: SeriesTree_Mining_Rewards_Fees_ToSubsidy, } impl SeriesTree_Mining_Rewards_Fees { @@ -5756,26 +5812,26 @@ impl SeriesTree_Mining_Rewards_Fees { pct75: _1m1w1y24hPattern4::new(client.clone(), "fees_pct75".to_string()), pct90: _1m1w1y24hPattern4::new(client.clone(), "fees_pct90".to_string()), dominance: _1m1w1y24hPercentPpmRatioPattern::new(client.clone(), "fee_dominance".to_string()), - to_subsidy_ratio: SeriesTree_Mining_Rewards_Fees_ToSubsidyRatio::new(client.clone(), format!("{base_path}_to_subsidy_ratio")), + to_subsidy: SeriesTree_Mining_Rewards_Fees_ToSubsidy::new(client.clone(), format!("{base_path}_to_subsidy")), } } } /// Series tree node. -pub struct SeriesTree_Mining_Rewards_Fees_ToSubsidyRatio { - pub _24h: PpmRatioPattern3, - pub _1w: PpmRatioPattern3, - pub _1m: PpmRatioPattern3, - pub _1y: PpmRatioPattern3, +pub struct SeriesTree_Mining_Rewards_Fees_ToSubsidy { + pub _24h: PercentPpmRatioPattern5, + pub _1w: PercentPpmRatioPattern5, + pub _1m: PercentPpmRatioPattern5, + pub _1y: PercentPpmRatioPattern5, } -impl SeriesTree_Mining_Rewards_Fees_ToSubsidyRatio { +impl SeriesTree_Mining_Rewards_Fees_ToSubsidy { pub fn new(client: Arc, base_path: String) -> Self { Self { - _24h: PpmRatioPattern3::new(client.clone(), "fee_to_subsidy_ratio_24h".to_string()), - _1w: PpmRatioPattern3::new(client.clone(), "fee_to_subsidy_ratio_1w".to_string()), - _1m: PpmRatioPattern3::new(client.clone(), "fee_to_subsidy_ratio_1m".to_string()), - _1y: PpmRatioPattern3::new(client.clone(), "fee_to_subsidy_ratio_1y".to_string()), + _24h: PercentPpmRatioPattern5::new(client.clone(), "fee_to_subsidy_24h".to_string()), + _1w: PercentPpmRatioPattern5::new(client.clone(), "fee_to_subsidy_1w".to_string()), + _1m: PercentPpmRatioPattern5::new(client.clone(), "fee_to_subsidy_1m".to_string()), + _1y: PercentPpmRatioPattern5::new(client.clone(), "fee_to_subsidy_1y".to_string()), } } } @@ -8229,7 +8285,6 @@ impl SeriesTree_Cohorts_Utxo_All { pub struct SeriesTree_Cohorts_Utxo_All_Outputs { pub unspent_count: BaseDeltaPattern, pub spent_count: AverageBlockCumulativeSumPattern, - pub utxo_turnover_1y: SeriesPattern1, } impl SeriesTree_Cohorts_Utxo_All_Outputs { @@ -8237,7 +8292,6 @@ impl SeriesTree_Cohorts_Utxo_All_Outputs { Self { unspent_count: BaseDeltaPattern::new(client.clone(), "utxo_count".to_string()), spent_count: AverageBlockCumulativeSumPattern::new(client.clone(), "spent_utxo_count".to_string()), - utxo_turnover_1y: SeriesPattern1::new(client.clone(), "utxo_turnover_1y".to_string()), } } } @@ -8461,7 +8515,7 @@ impl SeriesTree_Cohorts_Utxo_All_Unrealized_Sentiment { /// Series tree node. pub struct SeriesTree_Cohorts_Utxo_Sth { pub supply: DeltaDominanceHalfInTotalPattern2, - pub outputs: SpentUnspentUtxoPattern, + pub outputs: SpentUnspentPattern, pub activity: CoindaysCoinyearsDormancyTransferPattern, pub realized: CapCapitalizedGrossLossMvrvNetPeakPriceProfitSellSoprPattern, pub cost_basis: InMaxMinPerSupplyPattern, @@ -8473,7 +8527,7 @@ impl SeriesTree_Cohorts_Utxo_Sth { pub fn new(client: Arc, base_path: String) -> Self { Self { supply: DeltaDominanceHalfInTotalPattern2::new(client.clone(), "sth_supply".to_string()), - outputs: SpentUnspentUtxoPattern::new(client.clone(), "sth".to_string()), + outputs: SpentUnspentPattern::new(client.clone(), "sth".to_string()), activity: CoindaysCoinyearsDormancyTransferPattern::new(client.clone(), "sth".to_string()), realized: CapCapitalizedGrossLossMvrvNetPeakPriceProfitSellSoprPattern::new(client.clone(), "sth".to_string()), cost_basis: InMaxMinPerSupplyPattern::new(client.clone(), "sth".to_string()), diff --git a/crates/brk_computer/src/blocks/compute.rs b/crates/brk_computer/src/blocks/compute.rs index f7af05dab..de0d4a385 100644 --- a/crates/brk_computer/src/blocks/compute.rs +++ b/crates/brk_computer/src/blocks/compute.rs @@ -20,30 +20,18 @@ impl Vecs { // lookback depends on indexes.timestamp.monotonic self.lookback.compute(indexer, indexes, exit)?; - // Parallel: remaining sub-modules are independent of each other. - // size depends on lookback (already computed above). + // Interval and size are independent. Size depends on lookback, which + // was already computed above. let Vecs { lookback, - count, interval, size, - weight, - difficulty, - halving, .. } = self; thread::scope(|s| -> Result<()> { - let r1 = s.spawn(|| count.compute(indexer, exit)); - let r2 = s.spawn(|| interval.compute(indexer, exit)); - let r3 = s.spawn(|| weight.compute(indexer, exit)); - let r4 = s.spawn(|| difficulty.compute(indexer, indexes, exit)); - let r5 = s.spawn(|| halving.compute(indexer, indexes, exit)); + let r1 = s.spawn(|| interval.compute(indexer, exit)); size.compute(indexer, &*lookback, exit)?; r1.join().unwrap()?; - r2.join().unwrap()?; - r3.join().unwrap()?; - r4.join().unwrap()?; - r5.join().unwrap()?; Ok(()) })?; diff --git a/crates/brk_computer/src/blocks/count/compute.rs b/crates/brk_computer/src/blocks/count/compute.rs deleted file mode 100644 index b13542128..000000000 --- a/crates/brk_computer/src/blocks/count/compute.rs +++ /dev/null @@ -1,19 +0,0 @@ -use brk_error::Result; -use brk_indexer::Indexer; -use vecdb::Exit; - -use super::Vecs; - -impl Vecs { - pub(crate) fn compute(&mut self, indexer: &Indexer, exit: &Exit) -> Result<()> { - let starting_height = indexer.safe_lengths().height; - self.total.cumulative.height.compute_cumulative_count( - starting_height, - &indexer.vecs.blocks.weight, - |_| true, - exit, - )?; - - Ok(()) - } -} diff --git a/crates/brk_computer/src/blocks/count/import.rs b/crates/brk_computer/src/blocks/count/import.rs index 31a772ecc..71b890c22 100644 --- a/crates/brk_computer/src/blocks/count/import.rs +++ b/crates/brk_computer/src/blocks/count/import.rs @@ -1,24 +1,27 @@ -use brk_error::Result; -use brk_types::Version; -use vecdb::Database; +use brk_indexer::Indexer; +use brk_types::{Height, StoredU64, Version, Weight}; use super::Vecs; use crate::{ indexes, internal::{ BlockCountTarget1m, BlockCountTarget1w, BlockCountTarget1y, BlockCountTarget24h, - ConstantVecs, PerBlockCumulativeRolling, WindowStartVec, Windows, + ConstantVecs, LazyPerBlockCumulativeRolling, WindowStartVec, Windows, }, }; +fn cumulative_block_count(height: Height, _: Weight) -> StoredU64 { + StoredU64::from(u64::from(height) + 1) +} + impl Vecs { - pub(crate) fn forced_import( - db: &Database, + pub(crate) fn new( version: Version, + indexer: &Indexer, indexes: &indexes::Vecs, cached_starts: &Windows<&WindowStartVec>, - ) -> Result { - Ok(Self { + ) -> Self { + Self { target: Windows { _24h: ConstantVecs::new::( "block_count_target_24h", @@ -41,13 +44,14 @@ impl Vecs { indexes, ), }, - total: PerBlockCumulativeRolling::forced_import( - db, + total: LazyPerBlockCumulativeRolling::from_indexed_source( "block_count", version + Version::ONE, - indexes, + &indexer.vecs.blocks.weight, + cumulative_block_count, cached_starts, - )?, - }) + indexes, + ), + } } } diff --git a/crates/brk_computer/src/blocks/count/mod.rs b/crates/brk_computer/src/blocks/count/mod.rs index 1136f9ebd..f8623047a 100644 --- a/crates/brk_computer/src/blocks/count/mod.rs +++ b/crates/brk_computer/src/blocks/count/mod.rs @@ -1,4 +1,3 @@ -mod compute; mod import; mod vecs; diff --git a/crates/brk_computer/src/blocks/count/vecs.rs b/crates/brk_computer/src/blocks/count/vecs.rs index deeaaaa84..bfed15ef5 100644 --- a/crates/brk_computer/src/blocks/count/vecs.rs +++ b/crates/brk_computer/src/blocks/count/vecs.rs @@ -1,11 +1,10 @@ use brk_traversable::Traversable; use brk_types::StoredU64; -use vecdb::{Rw, StorageMode}; -use crate::internal::{ConstantVecs, PerBlockCumulativeRolling, Windows}; +use crate::internal::{ConstantVecs, LazyPerBlockCumulativeRolling, Windows}; -#[derive(Traversable)] -pub struct Vecs { +#[derive(Clone, Traversable)] +pub struct Vecs { pub target: Windows>, - pub total: PerBlockCumulativeRolling, + pub total: LazyPerBlockCumulativeRolling, } diff --git a/crates/brk_computer/src/blocks/difficulty/compute.rs b/crates/brk_computer/src/blocks/difficulty/compute.rs deleted file mode 100644 index 133a9df43..000000000 --- a/crates/brk_computer/src/blocks/difficulty/compute.rs +++ /dev/null @@ -1,40 +0,0 @@ -use brk_error::Result; -use brk_indexer::Indexer; -use brk_types::StoredU32; -use vecdb::Exit; - -use super::Vecs; -use crate::indexes; - -impl Vecs { - pub(crate) fn compute( - &mut self, - indexer: &Indexer, - indexes: &indexes::Vecs, - exit: &Exit, - ) -> Result<()> { - let starting_height = indexer.safe_lengths().height; - self.adjustment.ppm.height.compute_ratio_change( - starting_height, - &indexer.vecs.blocks.difficulty, - 2016, - exit, - )?; - - self.epoch.height.compute_transform( - starting_height, - &indexes.height.epoch, - |(h, epoch, ..)| (h, epoch), - exit, - )?; - - self.blocks_to_retarget.height.compute_transform( - starting_height, - &indexes.height.epoch, - |(h, ..)| (h, StoredU32::from(h.left_before_next_diff_adj())), - exit, - )?; - - Ok(()) - } -} diff --git a/crates/brk_computer/src/blocks/difficulty/import.rs b/crates/brk_computer/src/blocks/difficulty/import.rs index 1c78cc9dd..e3c00ac3b 100644 --- a/crates/brk_computer/src/blocks/difficulty/import.rs +++ b/crates/brk_computer/src/blocks/difficulty/import.rs @@ -1,23 +1,36 @@ -use brk_error::Result; use brk_indexer::Indexer; -use brk_types::Version; -use vecdb::{Database, ReadableCloneableVec}; +use brk_types::{Epoch, Height, PartsPerMillionSigned32, StoredF64, StoredU32, Version}; +use vecdb::ReadOnlyClone; use super::Vecs; use crate::{ indexes, internal::{ - BlocksToDaysF32, DifficultyToHashF64, LazyPerBlock, PerBlock, PercentPerBlock, Resolutions, + BlocksToDaysF32, DifficultyToHashF64, Identity, LazyPerBlock, LazyPercentPerBlock, + Resolutions, }, }; +const DIFFICULTY_ADJUSTMENT_LOOKBACK: usize = 2016; + +fn blocks_left_to_retarget(height: Height, _: Epoch) -> StoredU32 { + StoredU32::from(height.left_before_next_diff_adj()) +} + +fn difficulty_adjustment( + current: StoredF64, + previous: Option, +) -> PartsPerMillionSigned32 { + match previous { + Some(previous) => { + PartsPerMillionSigned32::from((f32::from(current) / f32::from(previous)) - 1.0) + } + None => PartsPerMillionSigned32::from(f32::NAN), + } +} + impl Vecs { - pub(crate) fn forced_import( - db: &Database, - version: Version, - indexer: &Indexer, - indexes: &indexes::Vecs, - ) -> Result { + pub(crate) fn new(version: Version, indexer: &Indexer, indexes: &indexes::Vecs) -> Self { let v2 = Version::TWO; let hashrate = LazyPerBlock::from_height_source::( @@ -27,17 +40,27 @@ impl Vecs { indexes, ); - let blocks_to_retarget = - PerBlock::forced_import(db, "blocks_to_retarget", version + v2, indexes)?; + let epoch = LazyPerBlock::from_height_source::, _>( + "difficulty_epoch", + version, + indexes.height.epoch.read_only_clone(), + indexes, + ); + let blocks_to_retarget = LazyPerBlock::from_indexed_source( + "blocks_to_retarget", + version + v2, + &indexes.height.epoch, + blocks_left_to_retarget, + indexes, + ); - let days_to_retarget = LazyPerBlock::from_computed::( + let days_to_retarget = LazyPerBlock::from_lazy::( "days_to_retarget", version + v2, - blocks_to_retarget.height.read_only_boxed_clone(), &blocks_to_retarget, ); - Ok(Self { + Self { value: Resolutions::forced_import( "difficulty", indexer.vecs.blocks.difficulty.read_only_clone(), @@ -45,15 +68,17 @@ impl Vecs { indexes, ), hashrate, - adjustment: PercentPerBlock::forced_import( - db, + adjustment: LazyPercentPerBlock::from_lookback_source( "difficulty_adjustment", version + Version::ONE, + &indexer.vecs.blocks.difficulty, + DIFFICULTY_ADJUSTMENT_LOOKBACK, + difficulty_adjustment, indexes, - )?, - epoch: PerBlock::forced_import(db, "difficulty_epoch", version, indexes)?, + ), + epoch, blocks_to_retarget, days_to_retarget, - }) + } } } diff --git a/crates/brk_computer/src/blocks/difficulty/mod.rs b/crates/brk_computer/src/blocks/difficulty/mod.rs index 1136f9ebd..f8623047a 100644 --- a/crates/brk_computer/src/blocks/difficulty/mod.rs +++ b/crates/brk_computer/src/blocks/difficulty/mod.rs @@ -1,4 +1,3 @@ -mod compute; mod import; mod vecs; diff --git a/crates/brk_computer/src/blocks/difficulty/vecs.rs b/crates/brk_computer/src/blocks/difficulty/vecs.rs index 0da25a7c6..810801def 100644 --- a/crates/brk_computer/src/blocks/difficulty/vecs.rs +++ b/crates/brk_computer/src/blocks/difficulty/vecs.rs @@ -1,14 +1,14 @@ use brk_traversable::Traversable; use brk_types::{Epoch, PartsPerMillionSigned32, StoredF32, StoredF64, StoredU32}; -use vecdb::{Rw, StorageMode}; -use crate::internal::{LazyPerBlock, PerBlock, PercentPerBlock, Resolutions}; -#[derive(Traversable)] -pub struct Vecs { +use crate::internal::{LazyPerBlock, LazyPercentPerBlock, Resolutions}; + +#[derive(Clone, Traversable)] +pub struct Vecs { pub value: Resolutions, pub hashrate: LazyPerBlock, - pub adjustment: PercentPerBlock, - pub epoch: PerBlock, - pub blocks_to_retarget: PerBlock, + pub adjustment: LazyPercentPerBlock, + pub epoch: LazyPerBlock, + pub blocks_to_retarget: LazyPerBlock, pub days_to_retarget: LazyPerBlock, } diff --git a/crates/brk_computer/src/blocks/halving/compute.rs b/crates/brk_computer/src/blocks/halving/compute.rs deleted file mode 100644 index 41c957a45..000000000 --- a/crates/brk_computer/src/blocks/halving/compute.rs +++ /dev/null @@ -1,33 +0,0 @@ -use brk_error::Result; -use brk_indexer::Indexer; -use brk_types::StoredU32; -use vecdb::Exit; - -use super::Vecs; -use crate::indexes; - -impl Vecs { - pub(crate) fn compute( - &mut self, - indexer: &Indexer, - indexes: &indexes::Vecs, - exit: &Exit, - ) -> Result<()> { - let starting_height = indexer.safe_lengths().height; - self.epoch.height.compute_transform( - starting_height, - &indexes.height.halving, - |(h, epoch, ..)| (h, epoch), - exit, - )?; - - self.blocks_to_halving.height.compute_transform( - starting_height, - &indexes.height.halving, - |(h, ..)| (h, StoredU32::from(h.left_before_next_halving())), - exit, - )?; - - Ok(()) - } -} diff --git a/crates/brk_computer/src/blocks/halving/import.rs b/crates/brk_computer/src/blocks/halving/import.rs index b54aecc25..b94323228 100644 --- a/crates/brk_computer/src/blocks/halving/import.rs +++ b/crates/brk_computer/src/blocks/halving/import.rs @@ -1,35 +1,44 @@ -use brk_error::Result; -use brk_types::Version; -use vecdb::{Database, ReadableCloneableVec}; +use brk_types::{Halving, Height, StoredU32, Version}; +use vecdb::ReadOnlyClone; use super::Vecs; use crate::{ indexes, - internal::{BlocksToDaysF32, LazyPerBlock, PerBlock}, + internal::{BlocksToDaysF32, Identity, LazyPerBlock}, }; +fn blocks_left_to_halving(height: Height, _: Halving) -> StoredU32 { + StoredU32::from(height.left_before_next_halving()) +} + impl Vecs { - pub(crate) fn forced_import( - db: &Database, - version: Version, - indexes: &indexes::Vecs, - ) -> Result { + pub(crate) fn new(version: Version, indexes: &indexes::Vecs) -> Self { let v2 = Version::TWO; - let blocks_to_halving = - PerBlock::forced_import(db, "blocks_to_halving", version + v2, indexes)?; + let epoch = LazyPerBlock::from_height_source::, _>( + "halving_epoch", + version, + indexes.height.halving.read_only_clone(), + indexes, + ); + let blocks_to_halving = LazyPerBlock::from_indexed_source( + "blocks_to_halving", + version + v2, + &indexes.height.halving, + blocks_left_to_halving, + indexes, + ); - let days_to_halving = LazyPerBlock::from_computed::( + let days_to_halving = LazyPerBlock::from_lazy::( "days_to_halving", version + v2, - blocks_to_halving.height.read_only_boxed_clone(), &blocks_to_halving, ); - Ok(Self { - epoch: PerBlock::forced_import(db, "halving_epoch", version, indexes)?, + Self { + epoch, blocks_to_halving, days_to_halving, - }) + } } } diff --git a/crates/brk_computer/src/blocks/halving/mod.rs b/crates/brk_computer/src/blocks/halving/mod.rs index 1136f9ebd..f8623047a 100644 --- a/crates/brk_computer/src/blocks/halving/mod.rs +++ b/crates/brk_computer/src/blocks/halving/mod.rs @@ -1,4 +1,3 @@ -mod compute; mod import; mod vecs; diff --git a/crates/brk_computer/src/blocks/halving/vecs.rs b/crates/brk_computer/src/blocks/halving/vecs.rs index a7eb8e2fa..0331d47b7 100644 --- a/crates/brk_computer/src/blocks/halving/vecs.rs +++ b/crates/brk_computer/src/blocks/halving/vecs.rs @@ -1,11 +1,11 @@ use brk_traversable::Traversable; use brk_types::{Halving, StoredF32, StoredU32}; -use vecdb::{Rw, StorageMode}; -use crate::internal::{LazyPerBlock, PerBlock}; -#[derive(Traversable)] -pub struct Vecs { - pub epoch: PerBlock, - pub blocks_to_halving: PerBlock, +use crate::internal::LazyPerBlock; + +#[derive(Clone, Traversable)] +pub struct Vecs { + pub epoch: LazyPerBlock, + pub blocks_to_halving: LazyPerBlock, pub days_to_halving: LazyPerBlock, } diff --git a/crates/brk_computer/src/blocks/import.rs b/crates/brk_computer/src/blocks/import.rs index d541a0b05..b7d2066ae 100644 --- a/crates/brk_computer/src/blocks/import.rs +++ b/crates/brk_computer/src/blocks/import.rs @@ -25,12 +25,12 @@ impl Vecs { let lookback = LookbackVecs::forced_import(&db, version)?; let cached_starts = lookback.cached_window_starts(); - let count = CountVecs::forced_import(&db, version, indexes, &cached_starts)?; + let count = CountVecs::new(version, indexer, indexes, &cached_starts); let interval = IntervalVecs::forced_import(&db, version, indexes, &cached_starts)?; - let size = SizeVecs::forced_import(&db, version, indexes, &cached_starts)?; - let weight = WeightVecs::forced_import(&db, version, indexes, &cached_starts, &size)?; - let difficulty = DifficultyVecs::forced_import(&db, version, indexer, indexes)?; - let halving = HalvingVecs::forced_import(&db, version, indexes)?; + let size = SizeVecs::forced_import(&db, version, indexer, indexes, &cached_starts)?; + let weight = WeightVecs::new(version, indexer, indexes, &cached_starts, &size); + let difficulty = DifficultyVecs::new(version, indexer, indexes); + let halving = HalvingVecs::new(version, indexes); let this = Self { db, diff --git a/crates/brk_computer/src/blocks/interval/compute.rs b/crates/brk_computer/src/blocks/interval/compute.rs index 664daaaec..e3123c208 100644 --- a/crates/brk_computer/src/blocks/interval/compute.rs +++ b/crates/brk_computer/src/blocks/interval/compute.rs @@ -9,27 +9,27 @@ impl Vecs { pub(crate) fn compute(&mut self, indexer: &Indexer, exit: &Exit) -> Result<()> { let starting_height = indexer.safe_lengths().height; let mut prev_timestamp = None; - self.0.compute(starting_height, exit, |vec| { - vec.compute_transform( - starting_height, - &indexer.vecs.blocks.timestamp, - |(h, timestamp, ..)| { - let interval = if let Some(prev_h) = h.decremented() { - let prev = prev_timestamp.unwrap_or_else(|| { - indexer.vecs.blocks.timestamp.collect_one(prev_h).unwrap() - }); - timestamp.checked_sub(prev).unwrap_or(Timestamp::ZERO) - } else { - Timestamp::ZERO - }; - prev_timestamp = Some(timestamp); - (h, interval) - }, - exit, - )?; - Ok(()) - })?; - - Ok(()) + self.0.compute_from( + starting_height, + &indexer.vecs.blocks.timestamp, + |height, timestamp| { + let interval = if let Some(previous_height) = height.decremented() { + let previous = prev_timestamp.unwrap_or_else(|| { + indexer + .vecs + .blocks + .timestamp + .collect_one(previous_height) + .unwrap() + }); + timestamp.checked_sub(previous).unwrap_or(Timestamp::ZERO) + } else { + Timestamp::ZERO + }; + prev_timestamp = Some(timestamp); + interval + }, + exit, + ) } } diff --git a/crates/brk_computer/src/blocks/interval/import.rs b/crates/brk_computer/src/blocks/interval/import.rs index 3da683394..8e843efa0 100644 --- a/crates/brk_computer/src/blocks/interval/import.rs +++ b/crates/brk_computer/src/blocks/interval/import.rs @@ -5,7 +5,7 @@ use vecdb::Database; use super::Vecs; use crate::{ indexes, - internal::{PerBlockRollingAverage, WindowStartVec, Windows}, + internal::{PerBlockCumulativeAverage, WindowStartVec, Windows}, }; impl Vecs { @@ -15,7 +15,7 @@ impl Vecs { indexes: &indexes::Vecs, cached_starts: &Windows<&WindowStartVec>, ) -> Result { - let interval = PerBlockRollingAverage::forced_import( + let interval = PerBlockCumulativeAverage::forced_import( db, "block_interval", version, diff --git a/crates/brk_computer/src/blocks/interval/vecs.rs b/crates/brk_computer/src/blocks/interval/vecs.rs index c4615a776..124979098 100644 --- a/crates/brk_computer/src/blocks/interval/vecs.rs +++ b/crates/brk_computer/src/blocks/interval/vecs.rs @@ -4,9 +4,9 @@ use brk_traversable::Traversable; use brk_types::Timestamp; use vecdb::{Rw, StorageMode}; -use crate::internal::PerBlockRollingAverage; +use crate::internal::PerBlockCumulativeAverage; #[derive(Deref, DerefMut, Traversable)] pub struct Vecs( - #[traversable(flatten)] pub PerBlockRollingAverage, + #[traversable(flatten)] pub PerBlockCumulativeAverage, ); diff --git a/crates/brk_computer/src/blocks/lookback.rs b/crates/brk_computer/src/blocks/lookback.rs index 01cf254d0..265b9aea5 100644 --- a/crates/brk_computer/src/blocks/lookback.rs +++ b/crates/brk_computer/src/blocks/lookback.rs @@ -32,28 +32,28 @@ pub struct Vecs { pub _9w: M::Stored>>, // 63d pub _12w: M::Stored>>, // 84d pub _89d: M::Stored>>, - pub _3m: M::Stored>>, // 90d - pub _14w: M::Stored>>, // 98d + pub _3m: CachedVec>>>, // 90d + pub _14w: M::Stored>>, // 98d pub _111d: M::Stored>>, pub _144d: M::Stored>>, - pub _6m: M::Stored>>, // 180d - pub _26w: M::Stored>>, // 182d + pub _6m: CachedVec>>>, // 180d + pub _26w: M::Stored>>, // 182d pub _200d: M::Stored>>, pub _9m: M::Stored>>, // 270d pub _350d: M::Stored>>, pub _12m: M::Stored>>, // 360d pub _1y: CachedVec>>>, // 365d pub _14m: M::Stored>>, // 420d - pub _2y: M::Stored>>, // 730d + pub _2y: CachedVec>>>, // 730d pub _26m: M::Stored>>, // 780d - pub _3y: M::Stored>>, // 1095d + pub _3y: CachedVec>>>, // 1095d pub _200w: M::Stored>>, // 1400d - pub _4y: M::Stored>>, // 1460d - pub _5y: M::Stored>>, // 1825d - pub _6y: M::Stored>>, // 2190d - pub _8y: M::Stored>>, // 2920d + pub _4y: CachedVec>>>, // 1460d + pub _5y: CachedVec>>>, // 1825d + pub _6y: CachedVec>>>, // 2190d + pub _8y: CachedVec>>>, // 2920d pub _9y: M::Stored>>, // 3285d - pub _10y: M::Stored>>, // 3650d + pub _10y: CachedVec>>>, // 3650d pub _12y: M::Stored>>, // 4380d pub _14y: M::Stored>>, // 5110d pub _26y: M::Stored>>, // 9490d @@ -124,11 +124,11 @@ impl Vecs { _9w, _12w, _89d, - _3m, + _3m: CachedVec::wrap(_3m), _14w, _111d, _144d, - _6m, + _6m: CachedVec::wrap(_6m), _26w, _200d, _9m, @@ -136,16 +136,16 @@ impl Vecs { _12m, _1y: CachedVec::wrap(_1y), _14m, - _2y, + _2y: CachedVec::wrap(_2y), _26m, - _3y, + _3y: CachedVec::wrap(_3y), _200w, - _4y, - _5y, - _6y, - _8y, + _4y: CachedVec::wrap(_4y), + _5y: CachedVec::wrap(_5y), + _6y: CachedVec::wrap(_6y), + _8y: CachedVec::wrap(_8y), _9y, - _10y, + _10y: CachedVec::wrap(_10y), _12y, _14y, _26y, @@ -189,11 +189,11 @@ impl Vecs { 63 => &self._9w, 84 => &self._12w, 89 => &self._89d, - 90 => &self._3m, + 90 => &self._3m.inner, 98 => &self._14w, 111 => &self._111d, 144 => &self._144d, - 180 => &self._6m, + 180 => &self._6m.inner, 182 => &self._26w, 200 => &self._200d, 270 => &self._9m, @@ -201,16 +201,16 @@ impl Vecs { 360 => &self._12m, 365 => &self._1y.inner, 420 => &self._14m, - 730 => &self._2y, + 730 => &self._2y.inner, 780 => &self._26m, - 1095 => &self._3y, + 1095 => &self._3y.inner, 1400 => &self._200w, - 1460 => &self._4y, - 1825 => &self._5y, - 2190 => &self._6y, - 2920 => &self._8y, + 1460 => &self._4y.inner, + 1825 => &self._5y.inner, + 2190 => &self._6y.inner, + 2920 => &self._8y.inner, 3285 => &self._9y, - 3650 => &self._10y, + 3650 => &self._10y.inner, 4380 => &self._12y, 5110 => &self._14y, 9490 => &self._26y, @@ -218,6 +218,25 @@ impl Vecs { } } + pub fn cached_start_vec(&self, days: usize) -> &WindowStartVec { + match days { + 1 => &self._24h, + 7 => &self._1w, + 30 => &self._1m, + 90 => &self._3m, + 180 => &self._6m, + 365 => &self._1y, + 730 => &self._2y, + 1095 => &self._3y, + 1460 => &self._4y, + 1825 => &self._5y, + 2190 => &self._6y, + 2920 => &self._8y, + 3650 => &self._10y, + _ => panic!("No cached start vec for {days} days"), + } + } + pub(crate) fn compute( &mut self, indexer: &Indexer, @@ -243,11 +262,11 @@ impl Vecs { self.compute_rolling_start(indexes, starting_height, exit, 63, |s| &mut s._9w)?; self.compute_rolling_start(indexes, starting_height, exit, 84, |s| &mut s._12w)?; self.compute_rolling_start(indexes, starting_height, exit, 89, |s| &mut s._89d)?; - self.compute_rolling_start(indexes, starting_height, exit, 90, |s| &mut s._3m)?; + self.compute_rolling_start(indexes, starting_height, exit, 90, |s| &mut s._3m.inner)?; self.compute_rolling_start(indexes, starting_height, exit, 98, |s| &mut s._14w)?; self.compute_rolling_start(indexes, starting_height, exit, 111, |s| &mut s._111d)?; self.compute_rolling_start(indexes, starting_height, exit, 144, |s| &mut s._144d)?; - self.compute_rolling_start(indexes, starting_height, exit, 180, |s| &mut s._6m)?; + self.compute_rolling_start(indexes, starting_height, exit, 180, |s| &mut s._6m.inner)?; self.compute_rolling_start(indexes, starting_height, exit, 182, |s| &mut s._26w)?; self.compute_rolling_start(indexes, starting_height, exit, 200, |s| &mut s._200d)?; self.compute_rolling_start(indexes, starting_height, exit, 270, |s| &mut s._9m)?; @@ -255,16 +274,16 @@ impl Vecs { self.compute_rolling_start(indexes, starting_height, exit, 360, |s| &mut s._12m)?; self.compute_rolling_start(indexes, starting_height, exit, 365, |s| &mut s._1y.inner)?; self.compute_rolling_start(indexes, starting_height, exit, 420, |s| &mut s._14m)?; - self.compute_rolling_start(indexes, starting_height, exit, 730, |s| &mut s._2y)?; + self.compute_rolling_start(indexes, starting_height, exit, 730, |s| &mut s._2y.inner)?; self.compute_rolling_start(indexes, starting_height, exit, 780, |s| &mut s._26m)?; - self.compute_rolling_start(indexes, starting_height, exit, 1095, |s| &mut s._3y)?; + self.compute_rolling_start(indexes, starting_height, exit, 1095, |s| &mut s._3y.inner)?; self.compute_rolling_start(indexes, starting_height, exit, 1400, |s| &mut s._200w)?; - self.compute_rolling_start(indexes, starting_height, exit, 1460, |s| &mut s._4y)?; - self.compute_rolling_start(indexes, starting_height, exit, 1825, |s| &mut s._5y)?; - self.compute_rolling_start(indexes, starting_height, exit, 2190, |s| &mut s._6y)?; - self.compute_rolling_start(indexes, starting_height, exit, 2920, |s| &mut s._8y)?; + self.compute_rolling_start(indexes, starting_height, exit, 1460, |s| &mut s._4y.inner)?; + self.compute_rolling_start(indexes, starting_height, exit, 1825, |s| &mut s._5y.inner)?; + self.compute_rolling_start(indexes, starting_height, exit, 2190, |s| &mut s._6y.inner)?; + self.compute_rolling_start(indexes, starting_height, exit, 2920, |s| &mut s._8y.inner)?; self.compute_rolling_start(indexes, starting_height, exit, 3285, |s| &mut s._9y)?; - self.compute_rolling_start(indexes, starting_height, exit, 3650, |s| &mut s._10y)?; + self.compute_rolling_start(indexes, starting_height, exit, 3650, |s| &mut s._10y.inner)?; self.compute_rolling_start(indexes, starting_height, exit, 4380, |s| &mut s._12y)?; self.compute_rolling_start(indexes, starting_height, exit, 5110, |s| &mut s._14y)?; self.compute_rolling_start(indexes, starting_height, exit, 9490, |s| &mut s._26y)?; diff --git a/crates/brk_computer/src/blocks/mod.rs b/crates/brk_computer/src/blocks/mod.rs index 1e796b038..89d156e7f 100644 --- a/crates/brk_computer/src/blocks/mod.rs +++ b/crates/brk_computer/src/blocks/mod.rs @@ -37,13 +37,13 @@ pub struct Vecs { #[traversable(skip)] pub db: Database, - pub count: CountVecs, + pub count: CountVecs, pub lookback: LookbackVecs, pub interval: IntervalVecs, #[traversable(flatten)] pub size: SizeVecs, #[traversable(flatten)] - pub weight: WeightVecs, - pub difficulty: DifficultyVecs, - pub halving: HalvingVecs, + pub weight: WeightVecs, + pub difficulty: DifficultyVecs, + pub halving: HalvingVecs, } diff --git a/crates/brk_computer/src/blocks/size/compute.rs b/crates/brk_computer/src/blocks/size/compute.rs index 17934b94b..d16029164 100644 --- a/crates/brk_computer/src/blocks/size/compute.rs +++ b/crates/brk_computer/src/blocks/size/compute.rs @@ -1,6 +1,5 @@ use brk_error::Result; use brk_indexer::Indexer; -use brk_types::StoredU64; use vecdb::Exit; use super::Vecs; @@ -16,15 +15,7 @@ impl Vecs { let starting_height = indexer.safe_lengths().height; let window_starts = lookback.window_starts(); - self.vbytes - .compute(starting_height, &window_starts, exit, |height| { - Ok(height.compute_transform( - starting_height, - &indexer.vecs.blocks.weight, - |(h, weight, ..)| (h, StoredU64::from(weight.to_vbytes_floor())), - exit, - )?) - })?; + self.vbytes.compute(starting_height, &window_starts, exit)?; self.size.compute( starting_height, diff --git a/crates/brk_computer/src/blocks/size/import.rs b/crates/brk_computer/src/blocks/size/import.rs index 49de02015..e757287b1 100644 --- a/crates/brk_computer/src/blocks/size/import.rs +++ b/crates/brk_computer/src/blocks/size/import.rs @@ -1,5 +1,6 @@ use brk_error::Result; -use brk_types::Version; +use brk_indexer::Indexer; +use brk_types::{Height, StoredU64, Version, Weight}; use vecdb::Database; use super::Vecs; @@ -8,10 +9,15 @@ use crate::{ internal::{PerBlockFull, PerBlockRolling, WindowStartVec, Windows}, }; +fn block_vbytes(_: Height, weight: Weight) -> StoredU64 { + StoredU64::from(weight.to_vbytes_floor()) +} + impl Vecs { pub(crate) fn forced_import( db: &Database, version: Version, + indexer: &Indexer, indexes: &indexes::Vecs, cached_starts: &Windows<&WindowStartVec>, ) -> Result { @@ -20,6 +26,8 @@ impl Vecs { db, "block_vbytes", version, + &indexer.vecs.blocks.weight, + block_vbytes, indexes, cached_starts, )?, diff --git a/crates/brk_computer/src/blocks/size/vecs.rs b/crates/brk_computer/src/blocks/size/vecs.rs index 1fbea4bbc..7d57c53d5 100644 --- a/crates/brk_computer/src/blocks/size/vecs.rs +++ b/crates/brk_computer/src/blocks/size/vecs.rs @@ -1,11 +1,11 @@ use brk_traversable::Traversable; -use brk_types::StoredU64; +use brk_types::{StoredU64, Weight}; use vecdb::{Rw, StorageMode}; use crate::internal::{PerBlockFull, PerBlockRolling}; #[derive(Traversable)] pub struct Vecs { - pub vbytes: PerBlockFull, + pub vbytes: PerBlockFull, pub size: PerBlockRolling, } diff --git a/crates/brk_computer/src/blocks/weight/compute.rs b/crates/brk_computer/src/blocks/weight/compute.rs deleted file mode 100644 index 8b75c5b46..000000000 --- a/crates/brk_computer/src/blocks/weight/compute.rs +++ /dev/null @@ -1,20 +0,0 @@ -use brk_error::Result; -use brk_indexer::Indexer; -use brk_types::PartsPerMillion32; -use vecdb::Exit; - -use super::Vecs; - -impl Vecs { - pub(crate) fn compute(&mut self, indexer: &Indexer, exit: &Exit) -> Result<()> { - let starting_height = indexer.safe_lengths().height; - self.fullness.ppm.compute_transform( - starting_height, - &indexer.vecs.blocks.weight, - |(h, weight, ..)| (h, PartsPerMillion32::from(weight.fullness())), - exit, - )?; - - Ok(()) - } -} diff --git a/crates/brk_computer/src/blocks/weight/import.rs b/crates/brk_computer/src/blocks/weight/import.rs index 4f3701ec7..7bae9e73d 100644 --- a/crates/brk_computer/src/blocks/weight/import.rs +++ b/crates/brk_computer/src/blocks/weight/import.rs @@ -1,32 +1,41 @@ -use brk_error::Result; -use brk_types::Version; -use vecdb::Database; +use brk_indexer::Indexer; +use brk_types::{Height, PartsPerMillion32, Version, Weight}; use super::Vecs; use crate::{ blocks::SizeVecs, indexes, - internal::{LazyPerBlockRolling, PercentVec, VBytesToWeight, WindowStartVec, Windows}, + internal::{LazyPerBlockRolling, LazyPercentVec, VBytesToWeight, WindowStartVec, Windows}, }; +fn block_fullness(_: Height, weight: Weight) -> PartsPerMillion32 { + PartsPerMillion32::from(weight.fullness()) +} + impl Vecs { - pub(crate) fn forced_import( - db: &Database, + pub(crate) fn new( version: Version, + indexer: &Indexer, indexes: &indexes::Vecs, cached_starts: &Windows<&WindowStartVec>, size: &SizeVecs, - ) -> Result { - let weight = LazyPerBlockRolling::from_per_block_full::( + ) -> Self { + let weight = LazyPerBlockRolling::from_full_parts::( "block_weight", version, - &size.vbytes, + &size.vbytes.cumulative, + &size.vbytes.rolling, cached_starts, indexes, ); - let fullness = PercentVec::forced_import(db, "block_fullness", version)?; + let fullness = LazyPercentVec::from_indexed_source( + "block_fullness", + version, + &indexer.vecs.blocks.weight, + block_fullness, + ); - Ok(Self { weight, fullness }) + Self { weight, fullness } } } diff --git a/crates/brk_computer/src/blocks/weight/mod.rs b/crates/brk_computer/src/blocks/weight/mod.rs index 1136f9ebd..f8623047a 100644 --- a/crates/brk_computer/src/blocks/weight/mod.rs +++ b/crates/brk_computer/src/blocks/weight/mod.rs @@ -1,4 +1,3 @@ -mod compute; mod import; mod vecs; diff --git a/crates/brk_computer/src/blocks/weight/vecs.rs b/crates/brk_computer/src/blocks/weight/vecs.rs index e1c85c13b..1928067ae 100644 --- a/crates/brk_computer/src/blocks/weight/vecs.rs +++ b/crates/brk_computer/src/blocks/weight/vecs.rs @@ -1,11 +1,10 @@ use brk_traversable::Traversable; use brk_types::{PartsPerMillion32, StoredU64, Weight}; -use vecdb::{Rw, StorageMode}; -use crate::internal::{LazyPerBlockRolling, PercentVec}; +use crate::internal::{LazyPerBlockRolling, LazyPercentVec}; -#[derive(Traversable)] -pub struct Vecs { +#[derive(Clone, Traversable)] +pub struct Vecs { pub weight: LazyPerBlockRolling, - pub fullness: PercentVec, + pub fullness: LazyPercentVec, } diff --git a/crates/brk_computer/src/distribution/addr/activity.rs b/crates/brk_computer/src/distribution/addr/activity.rs index d90aedbff..951d29ea8 100644 --- a/crates/brk_computer/src/distribution/addr/activity.rs +++ b/crates/brk_computer/src/distribution/addr/activity.rs @@ -13,14 +13,14 @@ use brk_cohort::ByAddrType; use brk_error::Result; use brk_traversable::Traversable; -use brk_types::{Height, StoredU32, StoredU64, Version}; +use brk_types::Version; use derive_more::{Deref, DerefMut}; use rayon::prelude::*; -use vecdb::{AnyStoredVec, AnyVec, Database, Exit, Rw, StorageMode, WritableVec}; +use vecdb::{AnyStoredVec, AnyVec, Database, Rw, StorageMode}; use crate::{ indexes, - internal::{PerBlockRollingAverage, WindowStartVec, Windows}, + internal::{CountPerBlockRollingAverage, WindowStartVec, Windows}, }; /// Per-block activity counts - reset each block. @@ -66,15 +66,15 @@ impl AddrTypeToActivityCounts { /// Activity count vectors for a single category (e.g., one address type or "all"). #[derive(Traversable)] pub struct ActivityCountVecs { - pub reactivated: PerBlockRollingAverage, - pub sending: PerBlockRollingAverage, - pub receiving: PerBlockRollingAverage, - pub bidirectional: PerBlockRollingAverage, + pub reactivated: CountPerBlockRollingAverage, + pub sending: CountPerBlockRollingAverage, + pub receiving: CountPerBlockRollingAverage, + pub bidirectional: CountPerBlockRollingAverage, /// Distinct addresses involved in this block (sent ∪ received), /// computed at push time as `sending + receiving - bidirectional` /// via inclusion-exclusion. For per-type instances this is /// per-type. For the `all` aggregate it's the cross-type total. - pub active: PerBlockRollingAverage, + pub active: CountPerBlockRollingAverage, } impl ActivityCountVecs { @@ -90,35 +90,35 @@ impl ActivityCountVecs { cached_starts: &Windows<&WindowStartVec>, ) -> Result { Ok(Self { - reactivated: PerBlockRollingAverage::forced_import( + reactivated: CountPerBlockRollingAverage::forced_import( db, &format!("{prefix}reactivated_addrs"), version, indexes, cached_starts, )?, - sending: PerBlockRollingAverage::forced_import( + sending: CountPerBlockRollingAverage::forced_import( db, &format!("{prefix}sending_addrs"), version, indexes, cached_starts, )?, - receiving: PerBlockRollingAverage::forced_import( + receiving: CountPerBlockRollingAverage::forced_import( db, &format!("{prefix}receiving_addrs"), version, indexes, cached_starts, )?, - bidirectional: PerBlockRollingAverage::forced_import( + bidirectional: CountPerBlockRollingAverage::forced_import( db, &format!("{prefix}bidirectional_addrs"), version, indexes, cached_starts, )?, - active: PerBlockRollingAverage::forced_import( + active: CountPerBlockRollingAverage::forced_import( db, &format!("{prefix}active_addrs"), version, @@ -130,53 +130,44 @@ impl ActivityCountVecs { pub(crate) fn min_stateful_len(&self) -> usize { self.reactivated - .block + .cumulative .len() - .min(self.sending.block.len()) - .min(self.receiving.block.len()) - .min(self.bidirectional.block.len()) - .min(self.active.block.len()) + .min(self.sending.cumulative.len()) + .min(self.receiving.cumulative.len()) + .min(self.bidirectional.cumulative.len()) + .min(self.active.cumulative.len()) } pub(crate) fn par_iter_height_mut( &mut self, ) -> impl ParallelIterator { [ - &mut self.reactivated.block as &mut dyn AnyStoredVec, - &mut self.sending.block, - &mut self.receiving.block, - &mut self.bidirectional.block, - &mut self.active.block, + self.reactivated.stored_mut(), + self.sending.stored_mut(), + self.receiving.stored_mut(), + self.bidirectional.stored_mut(), + self.active.stored_mut(), ] .into_par_iter() } pub(crate) fn reset_height(&mut self) -> Result<()> { - self.reactivated.block.reset()?; - self.sending.block.reset()?; - self.receiving.block.reset()?; - self.bidirectional.block.reset()?; - self.active.block.reset()?; + self.reactivated.reset()?; + self.sending.reset()?; + self.receiving.reset()?; + self.bidirectional.reset()?; + self.active.reset()?; Ok(()) } #[inline(always)] pub(crate) fn push_height(&mut self, counts: &BlockActivityCounts) { - self.reactivated.block.push(counts.reactivated.into()); - self.sending.block.push(counts.sending.into()); - self.receiving.block.push(counts.receiving.into()); - self.bidirectional.block.push(counts.bidirectional.into()); + self.reactivated.push_block(counts.reactivated.into()); + self.sending.push_block(counts.sending.into()); + self.receiving.push_block(counts.receiving.into()); + self.bidirectional.push_block(counts.bidirectional.into()); let active = counts.sending + counts.receiving - counts.bidirectional; - self.active.block.push(active.into()); - } - - pub(crate) fn compute_rest(&mut self, max_from: Height, exit: &Exit) -> Result<()> { - self.reactivated.compute_rest(max_from, exit)?; - self.sending.compute_rest(max_from, exit)?; - self.receiving.compute_rest(max_from, exit)?; - self.bidirectional.compute_rest(max_from, exit)?; - self.active.compute_rest(max_from, exit)?; - Ok(()) + self.active.push_block(active.into()); } } @@ -224,11 +215,11 @@ impl AddrTypeToActivityCountVecs { ) -> impl ParallelIterator { let mut vecs: Vec<&mut dyn AnyStoredVec> = Vec::new(); for type_vecs in self.0.values_mut() { - vecs.push(&mut type_vecs.reactivated.block); - vecs.push(&mut type_vecs.sending.block); - vecs.push(&mut type_vecs.receiving.block); - vecs.push(&mut type_vecs.bidirectional.block); - vecs.push(&mut type_vecs.active.block); + vecs.push(type_vecs.reactivated.stored_mut()); + vecs.push(type_vecs.sending.stored_mut()); + vecs.push(type_vecs.receiving.stored_mut()); + vecs.push(type_vecs.bidirectional.stored_mut()); + vecs.push(type_vecs.active.stored_mut()); } vecs.into_par_iter() } @@ -240,13 +231,6 @@ impl AddrTypeToActivityCountVecs { Ok(()) } - pub(crate) fn compute_rest(&mut self, max_from: Height, exit: &Exit) -> Result<()> { - for type_vecs in self.0.values_mut() { - type_vecs.compute_rest(max_from, exit)?; - } - Ok(()) - } - #[inline(always)] pub(crate) fn push_height(&mut self, counts: &AddrTypeToActivityCounts) { for (vecs, c) in self.0.values_mut().zip(counts.0.values()) { @@ -301,12 +285,6 @@ impl AddrActivityVecs { Ok(()) } - pub(crate) fn compute_rest(&mut self, max_from: Height, exit: &Exit) -> Result<()> { - self.all.compute_rest(max_from, exit)?; - self.by_addr_type.compute_rest(max_from, exit)?; - Ok(()) - } - #[inline(always)] pub(crate) fn push_height(&mut self, counts: &AddrTypeToActivityCounts) { let totals = counts.totals(); diff --git a/crates/brk_computer/src/distribution/addr/new_addr_count.rs b/crates/brk_computer/src/distribution/addr/new_addr_count.rs index 84d848360..59d70a326 100644 --- a/crates/brk_computer/src/distribution/addr/new_addr_count.rs +++ b/crates/brk_computer/src/distribution/addr/new_addr_count.rs @@ -1,67 +1,44 @@ -use brk_error::Result; use brk_traversable::Traversable; -use brk_types::{Height, StoredU64, Version}; +use brk_types::{StoredU64, Version}; use derive_more::{Deref, DerefMut}; -use vecdb::{Database, Exit, Rw, StorageMode}; use crate::{ indexes, - internal::{PerBlockCumulativeRolling, WindowStartVec, Windows, WithAddrTypes}, + internal::{LazyPerBlockCumulativeRolling, WindowStartVec, Windows, WithAddrTypes}, }; use super::TotalAddrCountVecs; /// New address count per block (global + per-type). -#[derive(Deref, DerefMut, Traversable)] -pub struct NewAddrCountVecs( - #[traversable(flatten)] pub WithAddrTypes>, +#[derive(Clone, Deref, DerefMut, Traversable)] +pub struct NewAddrCountVecs( + #[traversable(flatten)] pub WithAddrTypes>, ); impl NewAddrCountVecs { - pub(crate) fn forced_import( - db: &Database, + pub(crate) fn new( version: Version, + total: &TotalAddrCountVecs, indexes: &indexes::Vecs, cached_starts: &Windows<&WindowStartVec>, - ) -> Result { - Ok(Self( - WithAddrTypes::>::forced_import( - db, + ) -> Self { + Self(WithAddrTypes { + all: LazyPerBlockCumulativeRolling::from_source( "new_addr_count", version, - indexes, + &total.all, cached_starts, - )?, - )) - } - - pub(crate) fn compute( - &mut self, - max_from: Height, - total_addr_count: &TotalAddrCountVecs, - exit: &Exit, - ) -> Result<()> { - self.0.all.cumulative.height.compute_transform( - max_from, - &total_addr_count.all.height, - |(height, total, ..)| (height, StoredU64::from(total)), - exit, - )?; - - for ((_, new), (_, total)) in self - .0 - .by_addr_type - .iter_mut() - .zip(total_addr_count.by_addr_type.iter()) - { - new.cumulative.height.compute_transform( - max_from, - &total.height, - |(height, total, ..)| (height, StoredU64::from(total)), - exit, - )?; - } - - Ok(()) + indexes, + ), + by_addr_type: total.by_addr_type.map_with_name(|name, total| { + LazyPerBlockCumulativeRolling::from_source( + &format!("{name}_new_addr_count"), + version, + total, + cached_starts, + indexes, + ) + }), + }) } } diff --git a/crates/brk_computer/src/distribution/addr/reused/events/vecs.rs b/crates/brk_computer/src/distribution/addr/reused/events/vecs.rs index 7134e5caf..a5205d1c2 100644 --- a/crates/brk_computer/src/distribution/addr/reused/events/vecs.rs +++ b/crates/brk_computer/src/distribution/addr/reused/events/vecs.rs @@ -9,8 +9,8 @@ use vecdb::{AnyStoredVec, AnyVec, Database, Exit, Rw, StorageMode, WritableVec}; use crate::{ indexes, inputs, internal::{ - PerBlockCumulativeRolling, PerBlockRollingAverage, PercentCumulativeRolling, - WindowStartVec, Windows, WithAddrTypes, + CountPerBlockRollingAverage, PerBlockCumulativeRolling, PerBlockRollingAverage, + PercentCumulativeRolling, WindowStartVec, Windows, WithAddrTypes, }, outputs, }; @@ -59,10 +59,10 @@ use super::state::AddrTypeToAddrEventCount; /// empty blocks). The denominator (distinct active addrs per block) /// lives on `ActivityCountVecs::active` (`addrs.activity.all.active`), /// derived from `sending + receiving - bidirectional`. Both fields -/// use `PerBlockRollingAverage` so their lazy 24h/1w/1m/1y series are -/// rolling *averages* of the per-block values. Sums and cumulatives of -/// distinct-address counts would be misleading because the same -/// address can appear in multiple blocks. +/// expose lazy 24h/1w/1m/1y rolling *averages* of the per-block values. +/// Sums and cumulatives of distinct-address counts would be misleading +/// because the same address can appear in multiple blocks, so the +/// cumulative count remains an internal source for the lazy views. #[derive(Traversable)] pub struct AddrEventsVecs { pub output_to_reused_addr_count: WithAddrTypes>, @@ -70,7 +70,7 @@ pub struct AddrEventsVecs { pub spendable_output_to_reused_addr_share: PercentCumulativeRolling, pub input_from_reused_addr_count: WithAddrTypes>, pub input_from_reused_addr_share: WithAddrTypes>, - pub active_reused_addr_count: PerBlockRollingAverage, + pub active_reused_addr_count: CountPerBlockRollingAverage, pub active_reused_addr_share: PerBlockRollingAverage, } @@ -118,7 +118,7 @@ impl AddrEventsVecs { let input_from_reused_addr_share = import_percent(&format!("input_from_{name}_addr_share"))?; - let active_reused_addr_count = PerBlockRollingAverage::forced_import( + let active_reused_addr_count = CountPerBlockRollingAverage::forced_import( db, &format!("active_{name}_addr_count"), version, @@ -148,7 +148,7 @@ impl AddrEventsVecs { self.output_to_reused_addr_count .min_stateful_len() .min(self.input_from_reused_addr_count.min_stateful_len()) - .min(self.active_reused_addr_count.block.len()) + .min(self.active_reused_addr_count.cumulative.len()) .min(self.active_reused_addr_share.block.len()) } @@ -159,7 +159,7 @@ impl AddrEventsVecs { .par_iter_height_mut() .chain(self.input_from_reused_addr_count.par_iter_height_mut()) .chain([ - &mut self.active_reused_addr_count.block as &mut dyn AnyStoredVec, + self.active_reused_addr_count.stored_mut(), &mut self.active_reused_addr_share.block as &mut dyn AnyStoredVec, ]) } @@ -167,7 +167,7 @@ impl AddrEventsVecs { pub(crate) fn reset_height(&mut self) -> Result<()> { self.output_to_reused_addr_count.reset_height()?; self.input_from_reused_addr_count.reset_height()?; - self.active_reused_addr_count.block.reset()?; + self.active_reused_addr_count.reset()?; self.active_reused_addr_share.block.reset()?; Ok(()) } @@ -185,8 +185,7 @@ impl AddrEventsVecs { self.input_from_reused_addr_count .push_height(spends.sum(), spends.values().copied()); self.active_reused_addr_count - .block - .push(StoredU32::from(active_reused_addr_count)); + .push_block(StoredU32::from(active_reused_addr_count)); // Stored as a percentage in [0, 100] to match the rest of the // codebase (Unit.percentage on the website expects 0..100). The // `active_addr_count` denominator lives on `ActivityCountVecs` @@ -209,8 +208,6 @@ impl AddrEventsVecs { inputs_by_type: &inputs::ByTypeVecs, exit: &Exit, ) -> Result<()> { - self.active_reused_addr_count - .compute_rest(starting_lengths.height, exit)?; self.active_reused_addr_share .compute_rest(starting_lengths.height, exit)?; diff --git a/crates/brk_computer/src/distribution/cohorts/addr/groups.rs b/crates/brk_computer/src/distribution/cohorts/addr/groups.rs index 791da2445..896cc0a02 100644 --- a/crates/brk_computer/src/distribution/cohorts/addr/groups.rs +++ b/crates/brk_computer/src/distribution/cohorts/addr/groups.rs @@ -4,7 +4,7 @@ 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, StoredU64, Version}; +use brk_types::{Height, Sats, Version}; use derive_more::{Deref, DerefMut}; use rayon::prelude::*; use vecdb::{AnyStoredVec, Database, Exit, ReadableVec, Rw, StorageMode}; @@ -111,18 +111,11 @@ impl AddrCohorts { prices: &price::Vecs, starting_lengths: &Lengths, all_supply_sats: &impl ReadableVec, - all_utxo_count: &impl ReadableVec, exit: &Exit, ) -> Result<()> { - self.0.par_iter_mut().try_for_each(|v| { - v.compute_rest_part2( - prices, - starting_lengths, - all_supply_sats, - all_utxo_count, - exit, - ) - }) + self.0 + .par_iter_mut() + .try_for_each(|v| v.compute_rest_part2(prices, starting_lengths, all_supply_sats, exit)) } /// Returns a parallel iterator over all vecs for parallel writing. diff --git a/crates/brk_computer/src/distribution/cohorts/addr/vecs.rs b/crates/brk_computer/src/distribution/cohorts/addr/vecs.rs index 5f7c9fd7e..45da4b678 100644 --- a/crates/brk_computer/src/distribution/cohorts/addr/vecs.rs +++ b/crates/brk_computer/src/distribution/cohorts/addr/vecs.rs @@ -232,15 +232,9 @@ impl CohortVecs for AddrCohortVecs { prices: &price::Vecs, starting_lengths: &Lengths, all_supply_sats: &impl ReadableVec, - all_utxo_count: &impl ReadableVec, exit: &Exit, ) -> Result<()> { - self.metrics.compute_rest_part2( - prices, - starting_lengths, - all_supply_sats, - all_utxo_count, - exit, - ) + self.metrics + .compute_rest_part2(prices, starting_lengths, all_supply_sats, exit) } } diff --git a/crates/brk_computer/src/distribution/cohorts/traits.rs b/crates/brk_computer/src/distribution/cohorts/traits.rs index 707518b16..63c73e00b 100644 --- a/crates/brk_computer/src/distribution/cohorts/traits.rs +++ b/crates/brk_computer/src/distribution/cohorts/traits.rs @@ -1,6 +1,6 @@ use brk_error::Result; use brk_indexer::Lengths; -use brk_types::{Cents, Height, Sats, StoredU64, Version}; +use brk_types::{Cents, Height, Sats, Version}; use vecdb::{Exit, ReadableVec}; use crate::price; @@ -64,7 +64,6 @@ pub trait CohortVecs: DynCohortVecs { prices: &price::Vecs, starting_lengths: &Lengths, all_supply_sats: &impl ReadableVec, - all_utxo_count: &impl ReadableVec, exit: &Exit, ) -> Result<()>; } diff --git a/crates/brk_computer/src/distribution/cohorts/utxo/groups.rs b/crates/brk_computer/src/distribution/cohorts/utxo/groups.rs index cf179d1ae..3a18e7358 100644 --- a/crates/brk_computer/src/distribution/cohorts/utxo/groups.rs +++ b/crates/brk_computer/src/distribution/cohorts/utxo/groups.rs @@ -627,15 +627,8 @@ impl UTXOCohorts { exit, )?; - // Clone all_supply_sats and all_utxo_count for non-all cohorts. + // Clone all_supply_sats for non-all cohorts. let all_supply_sats = self.all.metrics.supply.total.sats.height.read_only_clone(); - let all_utxo_count = self - .all - .metrics - .outputs - .unspent_count - .height - .read_only_clone(); // Destructure to allow parallel mutable access to independent fields. let Self { @@ -659,7 +652,6 @@ impl UTXOCohorts { let vc = &under_1h_value_created; let vd = &under_1h_value_destroyed; let ss = &all_supply_sats; - let au = &all_utxo_count; let tasks: Vec Result<()> + Send + '_>> = vec![ Box::new(|| { @@ -670,7 +662,6 @@ impl UTXOCohorts { vc, vd, ss, - au, exit, ) }), @@ -680,44 +671,43 @@ impl UTXOCohorts { starting_lengths, height_to_market_cap, ss, - au, exit, ) }), Box::new(|| { age_range.par_iter_mut().try_for_each(|v| { v.metrics - .compute_rest_part2(prices, starting_lengths, ss, au, exit) + .compute_rest_part2(prices, starting_lengths, ss, exit) }) }), Box::new(|| { under_age.par_iter_mut().try_for_each(|v| { v.metrics - .compute_rest_part2(prices, starting_lengths, ss, au, exit) + .compute_rest_part2(prices, starting_lengths, ss, exit) }) }), Box::new(|| { over_age.par_iter_mut().try_for_each(|v| { v.metrics - .compute_rest_part2(prices, starting_lengths, ss, au, exit) + .compute_rest_part2(prices, starting_lengths, ss, exit) }) }), Box::new(|| { over_amount.par_iter_mut().try_for_each(|v| { v.metrics - .compute_rest_part2(prices, starting_lengths, ss, au, exit) + .compute_rest_part2(prices, starting_lengths, ss, exit) }) }), Box::new(|| { epoch.par_iter_mut().try_for_each(|v| { v.metrics - .compute_rest_part2(prices, starting_lengths, ss, au, exit) + .compute_rest_part2(prices, starting_lengths, ss, exit) }) }), Box::new(|| { class.par_iter_mut().try_for_each(|v| { v.metrics - .compute_rest_part2(prices, starting_lengths, ss, au, exit) + .compute_rest_part2(prices, starting_lengths, ss, exit) }) }), Box::new(|| { @@ -727,7 +717,6 @@ impl UTXOCohorts { starting_lengths, height_to_market_cap, ss, - au, exit, ) }) @@ -735,19 +724,19 @@ impl UTXOCohorts { Box::new(|| { amount_range.par_iter_mut().try_for_each(|v| { v.metrics - .compute_rest_part2(prices, starting_lengths, ss, au, exit) + .compute_rest_part2(prices, starting_lengths, ss, exit) }) }), Box::new(|| { under_amount.par_iter_mut().try_for_each(|v| { v.metrics - .compute_rest_part2(prices, starting_lengths, ss, au, exit) + .compute_rest_part2(prices, starting_lengths, ss, exit) }) }), Box::new(|| { type_.par_iter_mut().try_for_each(|v| { v.metrics - .compute_rest_part2(prices, starting_lengths, ss, au, exit) + .compute_rest_part2(prices, starting_lengths, ss, exit) }) }), ]; diff --git a/crates/brk_computer/src/distribution/metrics/cohort/all.rs b/crates/brk_computer/src/distribution/metrics/cohort/all.rs index e8245a94c..6aed205eb 100644 --- a/crates/brk_computer/src/distribution/metrics/cohort/all.rs +++ b/crates/brk_computer/src/distribution/metrics/cohort/all.rs @@ -76,8 +76,8 @@ impl AllCohortMetrics { cfg: &ImportConfig, supply: SupplyCore, ) -> Result { - let unrealized = UnrealizedFull::forced_import(cfg)?; let realized = RealizedFull::forced_import(cfg)?; + let unrealized = UnrealizedFull::forced_import(cfg, &realized.price.ppm)?; let asopr = AdjustedSopr::forced_import(cfg)?; let relative = RelativeForAll::forced_import(cfg)?; @@ -114,13 +114,6 @@ impl AllCohortMetrics { exit, )?; - self.unrealized.compute( - starting_lengths.height, - &prices.spot.cents.height, - &self.realized.price.cents.height, - exit, - )?; - self.asopr.compute_rest_part2( starting_lengths, &self.activity.transfer_volume.inner.cumulative.cents.height, @@ -130,10 +123,6 @@ impl AllCohortMetrics { exit, )?; - let all_utxo_count = self.outputs.unspent_count.height.read_only_clone(); - self.outputs - .compute_part2(starting_lengths.height, &all_utxo_count, exit)?; - self.cost_basis.compute_prices( starting_lengths, &prices.spot.cents.height, diff --git a/crates/brk_computer/src/distribution/metrics/cohort/basic.rs b/crates/brk_computer/src/distribution/metrics/cohort/basic.rs index a51b7e46b..fa60f5e8b 100644 --- a/crates/brk_computer/src/distribution/metrics/cohort/basic.rs +++ b/crates/brk_computer/src/distribution/metrics/cohort/basic.rs @@ -2,7 +2,7 @@ use brk_cohort::Filter; use brk_error::Result; use brk_indexer::Lengths; use brk_traversable::Traversable; -use brk_types::{Height, Sats, StoredU64}; +use brk_types::{Height, Sats}; use vecdb::{AnyStoredVec, Exit, ReadableVec, Rw, StorageMode}; use crate::{ @@ -46,8 +46,8 @@ impl CohortMetricsBase for BasicCohortMetrics { impl BasicCohortMetrics { pub(crate) fn forced_import(cfg: &ImportConfig) -> Result { let supply = SupplyCore::forced_import(cfg)?; - let unrealized = UnrealizedCore::forced_import(cfg)?; let realized = RealizedCore::forced_import(cfg)?; + let unrealized = UnrealizedCore::forced_import(cfg, &realized.price.ppm)?; Ok(Self { filter: cfg.filter.clone(), @@ -64,7 +64,6 @@ impl BasicCohortMetrics { prices: &price::Vecs, starting_lengths: &Lengths, all_supply_sats: &impl ReadableVec, - all_utxo_count: &impl ReadableVec, exit: &Exit, ) -> Result<()> { self.realized.compute_rest_part2( @@ -75,19 +74,9 @@ impl BasicCohortMetrics { exit, )?; - self.unrealized.compute( - starting_lengths.height, - &prices.spot.cents.height, - &self.realized.price.cents.height, - exit, - )?; - self.supply .compute_dominance(starting_lengths.height, all_supply_sats, exit)?; - self.outputs - .compute_part2(starting_lengths.height, all_utxo_count, exit)?; - Ok(()) } } diff --git a/crates/brk_computer/src/distribution/metrics/cohort/core.rs b/crates/brk_computer/src/distribution/metrics/cohort/core.rs index cdc52e396..aa9aadbd1 100644 --- a/crates/brk_computer/src/distribution/metrics/cohort/core.rs +++ b/crates/brk_computer/src/distribution/metrics/cohort/core.rs @@ -2,7 +2,7 @@ use brk_cohort::Filter; use brk_error::Result; use brk_indexer::Lengths; use brk_traversable::Traversable; -use brk_types::{Height, Sats, StoredU64, Version}; +use brk_types::{Height, Sats, Version}; use vecdb::{AnyStoredVec, Exit, ReadableVec, Rw, StorageMode}; use crate::{ @@ -26,13 +26,16 @@ pub struct CoreCohortMetrics { impl CoreCohortMetrics { pub(crate) fn forced_import(cfg: &ImportConfig) -> Result { + let realized = RealizedCore::forced_import(cfg)?; + let unrealized = UnrealizedCore::forced_import(cfg, &realized.price.ppm)?; + Ok(Self { filter: cfg.filter.clone(), supply: Box::new(SupplyCore::forced_import(cfg)?), outputs: Box::new(OutputsBase::forced_import(cfg)?), activity: Box::new(ActivityCore::forced_import(cfg)?), - realized: Box::new(RealizedCore::forced_import(cfg)?), - unrealized: Box::new(UnrealizedCore::forced_import(cfg)?), + realized: Box::new(realized), + unrealized: Box::new(unrealized), }) } @@ -108,8 +111,6 @@ impl CoreCohortMetrics { ) -> Result<()> { self.supply.compute(prices, starting_lengths.height, exit)?; - self.outputs.compute_rest(starting_lengths.height, exit)?; - self.activity .compute_rest_part1(prices, starting_lengths, exit)?; @@ -125,7 +126,6 @@ impl CoreCohortMetrics { prices: &price::Vecs, starting_lengths: &Lengths, all_supply_sats: &impl ReadableVec, - all_utxo_count: &impl ReadableVec, exit: &Exit, ) -> Result<()> { self.realized.compute_rest_part2( @@ -136,19 +136,9 @@ impl CoreCohortMetrics { exit, )?; - self.unrealized.compute( - starting_lengths.height, - &prices.spot.cents.height, - &self.realized.price.cents.height, - exit, - )?; - self.supply .compute_dominance(starting_lengths.height, all_supply_sats, exit)?; - self.outputs - .compute_part2(starting_lengths.height, all_utxo_count, exit)?; - Ok(()) } } diff --git a/crates/brk_computer/src/distribution/metrics/cohort/extended.rs b/crates/brk_computer/src/distribution/metrics/cohort/extended.rs index ee7990b35..7e5849bbb 100644 --- a/crates/brk_computer/src/distribution/metrics/cohort/extended.rs +++ b/crates/brk_computer/src/distribution/metrics/cohort/extended.rs @@ -2,7 +2,7 @@ use brk_cohort::Filter; use brk_error::Result; use brk_indexer::Lengths; use brk_traversable::Traversable; -use brk_types::{Dollars, Height, Sats, StoredU64, Version}; +use brk_types::{Dollars, Height, Sats, Version}; use vecdb::AnyStoredVec; use vecdb::{Exit, ReadableVec, Rw, StorageMode}; @@ -68,10 +68,10 @@ impl CohortMetricsBase for ExtendedCohortMetrics { impl ExtendedCohortMetrics { pub(crate) fn forced_import(cfg: &ImportConfig) -> Result { let supply = SupplyCore::forced_import(cfg)?; - let unrealized = UnrealizedFull::forced_import(cfg)?; let realized = RealizedFull::forced_import(cfg)?; + let unrealized = UnrealizedFull::forced_import(cfg, &realized.price.ppm)?; - let relative = RelativeWithExtended::forced_import(cfg)?; + let relative = RelativeWithExtended::forced_import(cfg, &unrealized)?; Ok(Self { filter: cfg.filter.clone(), @@ -92,7 +92,6 @@ impl ExtendedCohortMetrics { starting_lengths: &Lengths, height_to_market_cap: &impl ReadableVec, all_supply_sats: &impl ReadableVec, - all_utxo_count: &impl ReadableVec, exit: &Exit, ) -> Result<()> { self.realized.compute_rest_part2( @@ -104,13 +103,6 @@ impl ExtendedCohortMetrics { exit, )?; - self.unrealized.compute( - starting_lengths.height, - &prices.spot.cents.height, - &self.realized.price.cents.height, - exit, - )?; - self.cost_basis.compute_prices( starting_lengths, &prices.spot.cents.height, @@ -139,9 +131,6 @@ impl ExtendedCohortMetrics { exit, )?; - self.outputs - .compute_part2(starting_lengths.height, all_utxo_count, exit)?; - Ok(()) } } diff --git a/crates/brk_computer/src/distribution/metrics/cohort/extended_adjusted.rs b/crates/brk_computer/src/distribution/metrics/cohort/extended_adjusted.rs index 1f00068d8..d2dd7dab7 100644 --- a/crates/brk_computer/src/distribution/metrics/cohort/extended_adjusted.rs +++ b/crates/brk_computer/src/distribution/metrics/cohort/extended_adjusted.rs @@ -1,7 +1,7 @@ use brk_error::Result; use brk_indexer::Lengths; use brk_traversable::Traversable; -use brk_types::{Cents, Dollars, Height, Sats, StoredU64, Version}; +use brk_types::{Cents, Dollars, Height, Sats, Version}; use derive_more::{Deref, DerefMut}; use vecdb::{AnyStoredVec, Exit, ReadableVec, Rw, StorageMode}; @@ -66,7 +66,6 @@ impl ExtendedAdjustedCohortMetrics { under_1h_value_created: &impl ReadableVec, under_1h_value_destroyed: &impl ReadableVec, all_supply_sats: &impl ReadableVec, - all_utxo_count: &impl ReadableVec, exit: &Exit, ) -> Result<()> { self.inner.compute_rest_part2( @@ -74,7 +73,6 @@ impl ExtendedAdjustedCohortMetrics { starting_lengths, height_to_market_cap, all_supply_sats, - all_utxo_count, exit, )?; diff --git a/crates/brk_computer/src/distribution/metrics/cohort/minimal.rs b/crates/brk_computer/src/distribution/metrics/cohort/minimal.rs index c5d0b6eef..3bf5fdbde 100644 --- a/crates/brk_computer/src/distribution/metrics/cohort/minimal.rs +++ b/crates/brk_computer/src/distribution/metrics/cohort/minimal.rs @@ -2,7 +2,7 @@ use brk_cohort::Filter; use brk_error::Result; use brk_indexer::Lengths; use brk_traversable::Traversable; -use brk_types::{Height, Sats, StoredU64}; +use brk_types::{Height, Sats}; use vecdb::{AnyStoredVec, Exit, ReadableVec, Rw, StorageMode}; use crate::{ @@ -24,18 +24,21 @@ pub struct MinimalCohortMetrics { pub outputs: Box>, pub activity: Box>, pub realized: Box>, - pub unrealized: Box>, + pub unrealized: Box, } impl MinimalCohortMetrics { pub(crate) fn forced_import(cfg: &ImportConfig) -> Result { + let realized = RealizedMinimal::forced_import(cfg)?; + let unrealized = UnrealizedMinimal::new(cfg, &realized.price.ppm); + Ok(Self { filter: cfg.filter.clone(), supply: Box::new(SupplyBase::forced_import(cfg)?), outputs: Box::new(OutputsBase::forced_import(cfg)?), activity: Box::new(ActivityMinimal::forced_import(cfg)?), - realized: Box::new(RealizedMinimal::forced_import(cfg)?), - unrealized: Box::new(UnrealizedMinimal::forced_import(cfg)?), + realized: Box::new(realized), + unrealized: Box::new(unrealized), }) } @@ -102,7 +105,6 @@ impl MinimalCohortMetrics { exit: &Exit, ) -> Result<()> { self.supply.compute(prices, starting_lengths.height, exit)?; - self.outputs.compute_rest(starting_lengths.height, exit)?; self.activity .compute_rest_part1(prices, starting_lengths, exit)?; Ok(()) @@ -113,7 +115,6 @@ impl MinimalCohortMetrics { prices: &price::Vecs, starting_lengths: &Lengths, all_supply_sats: &impl ReadableVec, - all_utxo_count: &impl ReadableVec, exit: &Exit, ) -> Result<()> { self.realized.compute_rest_part2( @@ -123,19 +124,9 @@ impl MinimalCohortMetrics { exit, )?; - self.unrealized.compute( - starting_lengths.height, - &prices.spot.cents.height, - &self.realized.price.cents.height, - exit, - )?; - self.supply .compute_dominance(starting_lengths.height, all_supply_sats, exit)?; - self.outputs - .compute_part2(starting_lengths.height, all_utxo_count, exit)?; - Ok(()) } } diff --git a/crates/brk_computer/src/distribution/metrics/cohort/type.rs b/crates/brk_computer/src/distribution/metrics/cohort/type.rs index e2b16b149..32f36a2dd 100644 --- a/crates/brk_computer/src/distribution/metrics/cohort/type.rs +++ b/crates/brk_computer/src/distribution/metrics/cohort/type.rs @@ -2,7 +2,7 @@ use brk_cohort::Filter; use brk_error::Result; use brk_indexer::Lengths; use brk_traversable::Traversable; -use brk_types::{Height, Sats, StoredU64}; +use brk_types::{Height, Sats}; use vecdb::{AnyStoredVec, Exit, ReadableVec, Rw, StorageMode}; use crate::{ @@ -28,13 +28,16 @@ pub struct TypeCohortMetrics { impl TypeCohortMetrics { pub(crate) fn forced_import(cfg: &ImportConfig) -> Result { + let realized = RealizedMinimal::forced_import(cfg)?; + let unrealized = UnrealizedBasic::forced_import(cfg, &realized.price.ppm)?; + Ok(Self { filter: cfg.filter.clone(), supply: Box::new(SupplyCore::forced_import(cfg)?), outputs: Box::new(OutputsBase::forced_import(cfg)?), activity: Box::new(ActivityMinimal::forced_import(cfg)?), - realized: Box::new(RealizedMinimal::forced_import(cfg)?), - unrealized: Box::new(UnrealizedBasic::forced_import(cfg)?), + realized: Box::new(realized), + unrealized: Box::new(unrealized), }) } @@ -64,7 +67,6 @@ impl TypeCohortMetrics { exit: &Exit, ) -> Result<()> { self.supply.compute(prices, starting_lengths.height, exit)?; - self.outputs.compute_rest(starting_lengths.height, exit)?; self.activity .compute_rest_part1(prices, starting_lengths, exit)?; Ok(()) @@ -75,7 +77,6 @@ impl TypeCohortMetrics { prices: &price::Vecs, starting_lengths: &Lengths, all_supply_sats: &impl ReadableVec, - all_utxo_count: &impl ReadableVec, exit: &Exit, ) -> Result<()> { self.realized.compute_rest_part2( @@ -85,19 +86,9 @@ impl TypeCohortMetrics { exit, )?; - self.unrealized.compute( - starting_lengths.height, - &prices.spot.cents.height, - &self.realized.price.cents.height, - exit, - )?; - self.supply .compute_dominance(starting_lengths.height, all_supply_sats, exit)?; - self.outputs - .compute_part2(starting_lengths.height, all_utxo_count, exit)?; - Ok(()) } } diff --git a/crates/brk_computer/src/distribution/metrics/mod.rs b/crates/brk_computer/src/distribution/metrics/mod.rs index c04b17ddd..6c674ac5c 100644 --- a/crates/brk_computer/src/distribution/metrics/mod.rs +++ b/crates/brk_computer/src/distribution/metrics/mod.rs @@ -276,8 +276,6 @@ pub trait CohortMetricsBase: ) -> Result<()> { self.supply_mut() .compute(prices, starting_lengths.height, exit)?; - self.outputs_mut() - .compute_rest(starting_lengths.height, exit)?; self.activity_mut() .compute_rest_part1(prices, starting_lengths, exit)?; diff --git a/crates/brk_computer/src/distribution/metrics/outputs/base.rs b/crates/brk_computer/src/distribution/metrics/outputs/base.rs index f495fd324..43d4efe58 100644 --- a/crates/brk_computer/src/distribution/metrics/outputs/base.rs +++ b/crates/brk_computer/src/distribution/metrics/outputs/base.rs @@ -1,15 +1,15 @@ use brk_error::Result; use brk_indexer::Lengths; use brk_traversable::Traversable; -use brk_types::{Height, PartsPerMillionSigned64, StoredF32, StoredI64, StoredU64, Version}; -use vecdb::{AnyStoredVec, AnyVec, Exit, ReadableVec, Rw, StorageMode, WritableVec}; +use brk_types::{PartsPerMillionSigned64, StoredI64, StoredU64, Version}; +use vecdb::{AnyStoredVec, AnyVec, Exit, Rw, StorageMode, WritableVec}; use crate::{ distribution::{ metrics::ImportConfig, state::{CohortState, CostBasisOps, RealizedOps}, }, - internal::{PerBlock, PerBlockCumulativeRolling, PerBlockWithDeltas, RatioU64F32}, + internal::{PerBlockCumulativeRolling, PerBlockWithDeltas}, }; /// Base output metrics: utxo_count + delta. @@ -17,7 +17,6 @@ use crate::{ pub struct OutputsBase { pub unspent_count: PerBlockWithDeltas, pub spent_count: PerBlockCumulativeRolling, - pub utxo_turnover_1y: PerBlock, } impl OutputsBase { @@ -33,7 +32,6 @@ impl OutputsBase { cfg.cached_starts, )?, spent_count: cfg.import("spent_utxo_count", v1)?, - utxo_turnover_1y: cfg.import("utxo_turnover_1y", Version::TWO)?, }) } @@ -60,25 +58,6 @@ impl OutputsBase { ] } - pub(crate) fn compute_rest(&mut self, _max_from: Height, _exit: &Exit) -> Result<()> { - Ok(()) - } - - pub(crate) fn compute_part2( - &mut self, - max_from: Height, - all_utxo_count: &impl ReadableVec, - exit: &Exit, - ) -> Result<()> { - self.utxo_turnover_1y - .compute_binary::( - max_from, - &self.spent_count.sum.0._1y.height, - all_utxo_count, - exit, - ) - } - pub(crate) fn compute_from_stateful( &mut self, starting_lengths: &Lengths, diff --git a/crates/brk_computer/src/distribution/metrics/realized/full.rs b/crates/brk_computer/src/distribution/metrics/realized/full.rs index 276835c7d..5bc964250 100644 --- a/crates/brk_computer/src/distribution/metrics/realized/full.rs +++ b/crates/brk_computer/src/distribution/metrics/realized/full.rs @@ -3,7 +3,7 @@ use brk_indexer::Lengths; use brk_traversable::Traversable; use brk_types::{ Bitcoin, Cents, CentsSats, CentsSigned, CentsSquaredSats, Dollars, Height, PartsPerMillion32, - PartsPerMillionSigned64, StoredF64, Version, + PartsPerMillion64, PartsPerMillionSigned64, StoredF64, Version, }; use derive_more::{Deref, DerefMut}; use vecdb::{AnyStoredVec, AnyVec, BytesVec, Exit, ReadableVec, Rw, StorageMode, WritableVec}; @@ -11,9 +11,9 @@ use vecdb::{AnyStoredVec, AnyVec, BytesVec, Exit, ReadableVec, Rw, StorageMode, use crate::{ distribution::state::{CohortState, CostBasisData, RealizedState, WithCapital}, internal::{ - FiatPerBlockCumulativeWithSums, PercentPerBlock, PercentRollingWindows, - PriceWithRatioPerBlock, RatioCents, RatioCents64, RatioCentsSignedCents, - RatioCentsSignedDollars, RatioDollars, RollingWindows, RollingWindowsFrom1w, + FiatPerBlockCumulativeWithSums, LazyPercentPerBlock, PercentPerBlock, + PercentRollingWindows, PriceWithRatioPerBlock, RatioCents, RatioCents64, + RatioCentsSignedCents, RatioCentsSignedDollars, RollingWindows, RollingWindowsFrom1w, ValuePerBlockCumulativeRolling, }, price, @@ -57,6 +57,8 @@ pub struct RealizedFull { #[traversable(flatten)] pub core: RealizedCore, + #[traversable(wrap = "cap", rename = "to_own_mcap")] + pub cap_to_own_mcap: LazyPercentPerBlock, pub gross_pnl: FiatPerBlockCumulativeWithSums, pub sell_side_risk_ratio: PercentRollingWindows, pub net_pnl: RealizedNetPnl, @@ -68,8 +70,6 @@ pub struct RealizedFull { #[traversable(hidden)] pub cap_raw: M::Stored>, - #[traversable(wrap = "cap", rename = "to_own_mcap")] - pub cap_to_own_mcap: PercentPerBlock, } impl RealizedFull { @@ -78,6 +78,13 @@ impl RealizedFull { let v1 = Version::ONE; let core = RealizedCore::forced_import(cfg)?; + let cap_to_own_mcap = LazyPercentPerBlock::from_indexed_source( + &cfg.name("realized_cap_to_own_mcap"), + cfg.version + Version::TWO, + &core.minimal.price.ppm.height, + mvrv_to_realized_cap_ratio, + cfg.indexes, + ); // Gross PnL let gross_pnl: FiatPerBlockCumulativeWithSums = @@ -108,6 +115,7 @@ impl RealizedFull { Ok(Self { core, + cap_to_own_mcap, gross_pnl, sell_side_risk_ratio, net_pnl, @@ -116,7 +124,6 @@ impl RealizedFull { capitalized, profit_to_loss_ratio: cfg.import("realized_profit_to_loss_ratio", v1)?, cap_raw: cfg.import("cap_raw", v0)?, - cap_to_own_mcap: cfg.import("realized_cap_to_own_mcap", v1)?, }) } @@ -284,15 +291,6 @@ impl RealizedFull { )?; } - // Realized cap relative to own market cap - self.cap_to_own_mcap - .compute_binary::>( - starting_lengths.height, - &self.core.minimal.cap.usd.height, - height_to_market_cap, - exit, - )?; - // Realized profit to loss ratios for ((ratio, profit), loss) in self .profit_to_loss_ratio @@ -313,6 +311,11 @@ impl RealizedFull { } } +#[inline(always)] +fn mvrv_to_realized_cap_ratio(_: Height, mvrv: PartsPerMillion64) -> PartsPerMillion32 { + PartsPerMillion32::from(1.0 / f64::from(mvrv)) +} + #[derive(Default)] pub struct RealizedFullAccum { pub(crate) cap_raw: CentsSats, @@ -331,3 +334,22 @@ impl RealizedFullAccum { self.peak_regret.to_cents() } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn realized_cap_ratio_is_inverse_mvrv() { + assert_eq!( + mvrv_to_realized_cap_ratio(Height::ZERO, PartsPerMillion64::from(2.0)), + PartsPerMillion32::from(0.5), + ); + assert_eq!( + mvrv_to_realized_cap_ratio(Height::ZERO, PartsPerMillion64::from(1.0)), + PartsPerMillion32::from(1.0), + ); + assert!(mvrv_to_realized_cap_ratio(Height::ZERO, PartsPerMillion64::NAN).is_nan()); + assert!(mvrv_to_realized_cap_ratio(Height::ZERO, PartsPerMillion64::ZERO).is_nan()); + } +} diff --git a/crates/brk_computer/src/distribution/metrics/relative/extended_own_market_cap.rs b/crates/brk_computer/src/distribution/metrics/relative/extended_own_market_cap.rs index 2a90c9a7d..433c0ecf9 100644 --- a/crates/brk_computer/src/distribution/metrics/relative/extended_own_market_cap.rs +++ b/crates/brk_computer/src/distribution/metrics/relative/extended_own_market_cap.rs @@ -3,31 +3,35 @@ use brk_traversable::Traversable; use brk_types::{Dollars, Height, PartsPerMillion32, PartsPerMillionSigned32, Version}; use vecdb::{Exit, ReadableVec, Rw, StorageMode}; -use crate::internal::{PercentPerBlock, RatioDollars}; +use crate::internal::{LazyPercentPerBlock, PercentPerBlock, RatioDollars}; use crate::distribution::metrics::{ImportConfig, UnrealizedCore}; /// Extended relative metrics for own market cap (extended && rel_to_all). #[derive(Traversable)] pub struct RelativeExtendedOwnMarketCap { + #[traversable(wrap = "unrealized/net_pnl", rename = "to_own_mcap")] + pub net_unrealized_pnl_to_own_mcap: LazyPercentPerBlock, #[traversable(wrap = "unrealized/profit", rename = "to_own_mcap")] pub unrealized_profit_to_own_mcap: PercentPerBlock, #[traversable(wrap = "unrealized/loss", rename = "to_own_mcap")] pub unrealized_loss_to_own_mcap: PercentPerBlock, - #[traversable(wrap = "unrealized/net_pnl", rename = "to_own_mcap")] - pub net_unrealized_pnl_to_own_mcap: PercentPerBlock, } impl RelativeExtendedOwnMarketCap { - pub(crate) fn forced_import(cfg: &ImportConfig) -> Result { + pub(crate) fn forced_import(cfg: &ImportConfig, unrealized: &UnrealizedCore) -> Result { let v2 = Version::new(2); Ok(Self { + net_unrealized_pnl_to_own_mcap: LazyPercentPerBlock::from_height_source( + &cfg.name("net_unrealized_pnl_to_own_mcap"), + cfg.version + Version::new(4), + unrealized.nupl.ppm.height.clone(), + cfg.indexes, + ), unrealized_profit_to_own_mcap: cfg.import("unrealized_profit_to_own_mcap", v2)?, unrealized_loss_to_own_mcap: cfg .import("unrealized_loss_to_own_mcap", Version::new(3))?, - net_unrealized_pnl_to_own_mcap: cfg - .import("net_unrealized_pnl_to_own_mcap", Version::new(3))?, }) } @@ -52,13 +56,6 @@ impl RelativeExtendedOwnMarketCap { own_market_cap, exit, )?; - self.net_unrealized_pnl_to_own_mcap - .compute_binary::>( - max_from, - &unrealized.net_pnl.usd.height, - own_market_cap, - exit, - )?; Ok(()) } } diff --git a/crates/brk_computer/src/distribution/metrics/relative/extended_own_pnl.rs b/crates/brk_computer/src/distribution/metrics/relative/extended_own_pnl.rs index 17b9c0186..c99c40625 100644 --- a/crates/brk_computer/src/distribution/metrics/relative/extended_own_pnl.rs +++ b/crates/brk_computer/src/distribution/metrics/relative/extended_own_pnl.rs @@ -20,12 +20,11 @@ pub struct RelativeExtendedOwnPnl { impl RelativeExtendedOwnPnl { pub(crate) fn forced_import(cfg: &ImportConfig) -> Result { - let v1 = Version::ONE; - Ok(Self { unrealized_profit_to_own_gross_pnl: cfg - .import("unrealized_profit_to_own_gross_pnl", v1)?, - unrealized_loss_to_own_gross_pnl: cfg.import("unrealized_loss_to_own_gross_pnl", v1)?, + .import("unrealized_profit_to_own_gross_pnl", Version::ONE)?, + unrealized_loss_to_own_gross_pnl: cfg + .import("unrealized_loss_to_own_gross_pnl", Version::ONE)?, net_unrealized_pnl_to_own_gross_pnl: cfg .import("net_unrealized_pnl_to_own_gross_pnl", Version::new(3))?, }) diff --git a/crates/brk_computer/src/distribution/metrics/relative/with_extended.rs b/crates/brk_computer/src/distribution/metrics/relative/with_extended.rs index b71a141b9..ff741a4d1 100644 --- a/crates/brk_computer/src/distribution/metrics/relative/with_extended.rs +++ b/crates/brk_computer/src/distribution/metrics/relative/with_extended.rs @@ -27,10 +27,13 @@ pub struct RelativeWithExtended { } impl RelativeWithExtended { - pub(crate) fn forced_import(cfg: &ImportConfig) -> Result { + pub(crate) fn forced_import(cfg: &ImportConfig, unrealized: &UnrealizedFull) -> Result { Ok(Self { base: RelativeFull::forced_import(cfg)?, - extended_own_market_cap: RelativeExtendedOwnMarketCap::forced_import(cfg)?, + extended_own_market_cap: RelativeExtendedOwnMarketCap::forced_import( + cfg, + &unrealized.inner, + )?, extended_own_pnl: RelativeExtendedOwnPnl::forced_import(cfg)?, invested_capital: RelativeInvestedCapital::forced_import(cfg)?, }) diff --git a/crates/brk_computer/src/distribution/metrics/unrealized/basic.rs b/crates/brk_computer/src/distribution/metrics/unrealized/basic.rs index ed2769f65..e37f957dd 100644 --- a/crates/brk_computer/src/distribution/metrics/unrealized/basic.rs +++ b/crates/brk_computer/src/distribution/metrics/unrealized/basic.rs @@ -1,13 +1,13 @@ use brk_error::Result; use brk_indexer::Lengths; use brk_traversable::Traversable; -use brk_types::{Cents, Dollars, Version}; +use brk_types::{Cents, Dollars, PartsPerMillion64, Version}; use derive_more::{Deref, DerefMut}; use vecdb::{AnyStoredVec, AnyVec, Exit, ReadableCloneableVec, Rw, StorageMode, WritableVec}; use crate::{ distribution::{metrics::ImportConfig, state::UnrealizedState}, - internal::{FiatPerBlock, LazyPerBlock, NegCentsUnsignedToDollars}, + internal::{FiatPerBlock, LazyPerBlock, NegCentsUnsignedToDollars, PerBlock}, }; use super::UnrealizedMinimal; @@ -17,7 +17,7 @@ pub struct UnrealizedBasic { #[deref] #[deref_mut] #[traversable(flatten)] - pub minimal: UnrealizedMinimal, + pub minimal: UnrealizedMinimal, pub profit: FiatPerBlock, pub loss: FiatPerBlock, #[traversable(wrap = "loss", rename = "negative")] @@ -25,7 +25,10 @@ pub struct UnrealizedBasic { } impl UnrealizedBasic { - pub(crate) fn forced_import(cfg: &ImportConfig) -> Result { + pub(crate) fn forced_import( + cfg: &ImportConfig, + mvrv: &PerBlock, + ) -> Result { let v1 = Version::ONE; let loss: FiatPerBlock = cfg.import("unrealized_loss", v1)?; @@ -38,7 +41,7 @@ impl UnrealizedBasic { ); Ok(Self { - minimal: UnrealizedMinimal::forced_import(cfg)?, + minimal: UnrealizedMinimal::new(cfg, mvrv), profit: cfg.import("unrealized_profit", v1)?, loss, neg_loss, diff --git a/crates/brk_computer/src/distribution/metrics/unrealized/core.rs b/crates/brk_computer/src/distribution/metrics/unrealized/core.rs index 808392535..f5e9da8d1 100644 --- a/crates/brk_computer/src/distribution/metrics/unrealized/core.rs +++ b/crates/brk_computer/src/distribution/metrics/unrealized/core.rs @@ -1,13 +1,13 @@ use brk_error::Result; use brk_indexer::Lengths; use brk_traversable::Traversable; -use brk_types::{Cents, CentsSigned, Version}; +use brk_types::{Cents, CentsSigned, PartsPerMillion64, Version}; use derive_more::{Deref, DerefMut}; use vecdb::{AnyStoredVec, Exit, Rw, StorageMode}; use crate::{ distribution::{metrics::ImportConfig, state::UnrealizedState}, - internal::{CentsSubtractToCentsSigned, FiatPerBlock}, + internal::{CentsSubtractToCentsSigned, FiatPerBlock, PerBlock}, }; use super::UnrealizedBasic; @@ -23,8 +23,11 @@ pub struct UnrealizedCore { } impl UnrealizedCore { - pub(crate) fn forced_import(cfg: &ImportConfig) -> Result { - let basic = UnrealizedBasic::forced_import(cfg)?; + pub(crate) fn forced_import( + cfg: &ImportConfig, + mvrv: &PerBlock, + ) -> Result { + let basic = UnrealizedBasic::forced_import(cfg, mvrv)?; let net_unrealized_pnl = cfg.import("net_unrealized_pnl", Version::ZERO)?; Ok(Self { diff --git a/crates/brk_computer/src/distribution/metrics/unrealized/full.rs b/crates/brk_computer/src/distribution/metrics/unrealized/full.rs index 3dc2ffc6e..ba24da30d 100644 --- a/crates/brk_computer/src/distribution/metrics/unrealized/full.rs +++ b/crates/brk_computer/src/distribution/metrics/unrealized/full.rs @@ -1,12 +1,12 @@ use brk_error::Result; use brk_indexer::Lengths; use brk_traversable::Traversable; -use brk_types::{Cents, CentsSigned, CentsSquaredSats, Height, Sats, Version}; +use brk_types::{Cents, CentsSigned, CentsSquaredSats, Height, PartsPerMillion64, Sats, Version}; 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}; +use crate::internal::{CentsSubtractToCentsSigned, FiatPerBlock, PerBlock}; use crate::{distribution::metrics::ImportConfig, price}; use super::UnrealizedCore; @@ -41,9 +41,12 @@ pub struct UnrealizedFull { } impl UnrealizedFull { - pub(crate) fn forced_import(cfg: &ImportConfig) -> Result { + pub(crate) fn forced_import( + cfg: &ImportConfig, + mvrv: &PerBlock, + ) -> Result { let v0 = Version::ZERO; - let inner = UnrealizedCore::forced_import(cfg)?; + let inner = UnrealizedCore::forced_import(cfg, mvrv)?; let gross_pnl = cfg.import("unrealized_gross_pnl", v0)?; diff --git a/crates/brk_computer/src/distribution/metrics/unrealized/minimal.rs b/crates/brk_computer/src/distribution/metrics/unrealized/minimal.rs index be01b4931..fcec8d591 100644 --- a/crates/brk_computer/src/distribution/metrics/unrealized/minimal.rs +++ b/crates/brk_computer/src/distribution/metrics/unrealized/minimal.rs @@ -1,52 +1,54 @@ -use brk_error::Result; use brk_traversable::Traversable; -use brk_types::{Cents, Height, PartsPerMillionSigned32, Version}; -use vecdb::{Exit, ReadableVec, Rw, StorageMode}; +use brk_types::{PartsPerMillion64, PartsPerMillionSigned32, Version}; +use vecdb::UnaryTransform; -use crate::internal::RatioPerBlock; +use crate::internal::{LazyRatioPerBlock, PerBlock}; use crate::distribution::metrics::ImportConfig; -#[derive(Traversable)] -pub struct UnrealizedMinimal { - pub nupl: RatioPerBlock, +const VERSION: Version = Version::new(5); + +#[derive(Clone, Traversable)] +pub struct UnrealizedMinimal { + pub nupl: LazyRatioPerBlock, } impl UnrealizedMinimal { - pub(crate) fn forced_import(cfg: &ImportConfig) -> Result { - Ok(Self { - nupl: RatioPerBlock::forced_import_ppm( - cfg.db, + pub(crate) fn new(cfg: &ImportConfig, mvrv: &PerBlock) -> Self { + Self { + nupl: LazyRatioPerBlock::from_source::( &cfg.name("nupl"), - cfg.version + Version::ONE, - cfg.indexes, - )?, - }) - } - - pub(crate) fn compute( - &mut self, - max_from: Height, - spot_price: &impl ReadableVec, - realized_price: &impl ReadableVec, - exit: &Exit, - ) -> Result<()> { - self.nupl.ppm.height.compute_transform2( - max_from, - spot_price, - realized_price, - |(i, price, realized_price, ..)| { - let p = price.as_u128(); - if p == 0 { - (i, PartsPerMillionSigned32::ZERO) - } else { - let rp = realized_price.as_u128(); - let ratio = (p as f64 - rp as f64) / p as f64; - (i, PartsPerMillionSigned32::from(ratio)) - } - }, - exit, - )?; - Ok(()) + cfg.version + VERSION, + mvrv, + ), + } + } +} + +struct MvrvToNupl; + +impl UnaryTransform for MvrvToNupl { + #[inline(always)] + fn apply(mvrv: PartsPerMillion64) -> PartsPerMillionSigned32 { + PartsPerMillionSigned32::from(1.0 - 1.0 / f64::from(mvrv)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn nupl_is_derived_from_mvrv() { + assert_eq!( + MvrvToNupl::apply(PartsPerMillion64::from(2.0)), + PartsPerMillionSigned32::from(0.5), + ); + assert_eq!( + MvrvToNupl::apply(PartsPerMillion64::from(1.0)), + PartsPerMillionSigned32::ZERO, + ); + assert!(MvrvToNupl::apply(PartsPerMillion64::NAN).is_nan()); + assert!(MvrvToNupl::apply(PartsPerMillion64::ZERO).is_nan()); } } diff --git a/crates/brk_computer/src/distribution/vecs.rs b/crates/brk_computer/src/distribution/vecs.rs index 3d2152ff5..2d97c8853 100644 --- a/crates/brk_computer/src/distribution/vecs.rs +++ b/crates/brk_computer/src/distribution/vecs.rs @@ -48,7 +48,7 @@ pub struct AddrMetricsVecs { pub empty: AddrCountsVecs, pub activity: AddrActivityVecs, pub total: TotalAddrCountVecs, - pub new: NewAddrCountVecs, + pub new: NewAddrCountVecs, pub reused: ReusedAddrVecs, pub respent: ReusedAddrVecs, pub exposed: ExposedAddrVecs, @@ -225,7 +225,8 @@ impl Vecs { let total_addr_count = TotalAddrCountVecs::forced_import(&db, version, indexes)?; // Per-block delta of total (global + per-type) - let new_addr_count = NewAddrCountVecs::forced_import(&db, version, indexes, cached_starts)?; + let new_addr_count = + NewAddrCountVecs::new(version, &total_addr_count, indexes, cached_starts); // Reused address tracking (counts + per-block uses + percent). // `reused_*` uses the receive-side predicate (funded_txo_count > 1, @@ -642,13 +643,6 @@ impl Vecs { exit, )?; - self.addrs - .activity - .compute_rest(starting_lengths.height, exit)?; - self.addrs - .new - .compute(starting_lengths.height, &self.addrs.total, exit)?; - // 7. Compute rest part2 (relative metrics) let height_to_market_cap = self .utxo_cohorts @@ -677,21 +671,8 @@ impl Vecs { .sats .height .read_only_clone(); - let all_utxo_count = self - .utxo_cohorts - .all - .metrics - .outputs - .unspent_count - .height - .read_only_clone(); - self.addr_cohorts.compute_rest_part2( - prices, - &starting_lengths, - &all_supply_sats, - &all_utxo_count, - exit, - )?; + self.addr_cohorts + .compute_rest_part2(prices, &starting_lengths, &all_supply_sats, exit)?; let exit = exit.clone(); self.db.run_bg(move |db| { diff --git a/crates/brk_computer/src/frameworks/coinflow/compute.rs b/crates/brk_computer/src/frameworks/coinflow/compute.rs index 43ee0de36..fe964837e 100644 --- a/crates/brk_computer/src/frameworks/coinflow/compute.rs +++ b/crates/brk_computer/src/frameworks/coinflow/compute.rs @@ -231,7 +231,6 @@ impl Vecs { .spending_exposure .height .push(StoredF64::from(exposures[index])); - cohort.mobility.height.push(mobility); cohort.supply.mobile.sats.height.push(cohort_mobile_supply); cohort .supply @@ -301,7 +300,7 @@ impl Vecs { } fn primary_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> { - let mut vecs = Vec::with_capacity(AGE_COHORT_COUNT * 5 + 5 + HORIZON_COUNT); + let mut vecs = Vec::with_capacity(AGE_COHORT_COUNT * 4 + 5 + HORIZON_COUNT); for cohort in self.age_range.iter_mut() { vecs.extend(cohort.primary_vecs_mut()); @@ -324,11 +323,10 @@ impl Vecs { } impl CohortVecs { - fn primary_vecs_mut(&mut self) -> [&mut dyn AnyStoredVec; 5] { + fn primary_vecs_mut(&mut self) -> [&mut dyn AnyStoredVec; 4] { [ &mut self.spending_rate.height, &mut self.spending_exposure.height, - &mut self.mobility.height, &mut self.supply.mobile.sats.height, &mut self.supply.immobile.sats.height, ] diff --git a/crates/brk_computer/src/frameworks/coinflow/import.rs b/crates/brk_computer/src/frameworks/coinflow/import.rs index a651838a1..92afcfe6a 100644 --- a/crates/brk_computer/src/frameworks/coinflow/import.rs +++ b/crates/brk_computer/src/frameworks/coinflow/import.rs @@ -2,19 +2,29 @@ use std::path::Path; use brk_cohort::{AgeRange, CohortContext}; use brk_error::Result; -use brk_types::Version; +use brk_types::{StoredF64, Version}; +use vecdb::{ReadableCloneableVec, UnaryTransform}; -use super::{CohortVecs, DB_NAME, HorizonVecs, Horizons, Split, Vecs}; +use super::{CohortVecs, DB_NAME, HorizonVecs, Horizons, Split, Vecs, mobility}; use crate::{ indexes, internal::{ - FiatPerBlock, PerBlock, PriceWithRatioPerBlock, ValuePerBlock, + FiatPerBlock, LazyPerBlock, PerBlock, PriceWithRatioPerBlock, ValuePerBlock, db_utils::{finalize_db, open_db}, }, }; const VERSION: Version = Version::TWO; +struct ExposureToMobility; + +impl UnaryTransform for ExposureToMobility { + #[inline(always)] + fn apply(exposure: StoredF64) -> StoredF64 { + StoredF64::from(mobility(*exposure)) + } +} + fn import_split(mut import: impl FnMut(&str) -> Result) -> Result> { Ok(Split { mobile: import("mobile")?, @@ -29,20 +39,21 @@ impl CohortVecs { version: Version, indexes: &indexes::Vecs, ) -> Result { + let spending_rate = + PerBlock::forced_import(db, &format!("{name}_spending_rate"), version, indexes)?; + let spending_exposure = + PerBlock::forced_import(db, &format!("{name}_spending_exposure"), version, indexes)?; + let mobility = LazyPerBlock::from_computed::( + &format!("{name}_mobility"), + version, + spending_exposure.height.read_only_boxed_clone(), + &spending_exposure, + ); + Ok(Self { - spending_rate: PerBlock::forced_import( - db, - &format!("{name}_spending_rate"), - version, - indexes, - )?, - spending_exposure: PerBlock::forced_import( - db, - &format!("{name}_spending_exposure"), - version, - indexes, - )?, - mobility: PerBlock::forced_import(db, &format!("{name}_mobility"), version, indexes)?, + spending_rate, + spending_exposure, + mobility, supply: import_split(|side| { ValuePerBlock::forced_import(db, &format!("{name}_{side}_supply"), version, indexes) })?, diff --git a/crates/brk_computer/src/frameworks/coinflow/vecs.rs b/crates/brk_computer/src/frameworks/coinflow/vecs.rs index d0631ae86..e8826f5b8 100644 --- a/crates/brk_computer/src/frameworks/coinflow/vecs.rs +++ b/crates/brk_computer/src/frameworks/coinflow/vecs.rs @@ -3,7 +3,9 @@ use brk_traversable::Traversable; use brk_types::{Cents, StoredF64}; use vecdb::{Database, Rw, StorageMode}; -use crate::internal::{FiatPerBlock, PerBlock, PriceWithRatioPerBlock, ValuePerBlock}; +use crate::internal::{ + FiatPerBlock, LazyPerBlock, PerBlock, PriceWithRatioPerBlock, ValuePerBlock, +}; #[derive(Clone, Copy, Traversable)] pub struct Horizons { @@ -110,7 +112,7 @@ pub struct Split { pub struct CohortVecs { pub spending_rate: PerBlock, pub spending_exposure: PerBlock, - pub mobility: PerBlock, + pub mobility: LazyPerBlock, pub supply: Split>, } diff --git a/crates/brk_computer/src/frameworks/cointime/activity/compute.rs b/crates/brk_computer/src/frameworks/cointime/activity/compute.rs index 2021be238..5807c5bad 100644 --- a/crates/brk_computer/src/frameworks/cointime/activity/compute.rs +++ b/crates/brk_computer/src/frameworks/cointime/activity/compute.rs @@ -28,13 +28,6 @@ pub(crate) fn compute_rest( exit, )?; - derived.ratio.height.compute_divide( - starting_height, - &derived.liveliness.height, - &derived.vaultedness.height, - exit, - )?; - Ok(()) } diff --git a/crates/brk_computer/src/frameworks/cointime/activity/import.rs b/crates/brk_computer/src/frameworks/cointime/activity/import.rs index 9c567231c..7ffbc5652 100644 --- a/crates/brk_computer/src/frameworks/cointime/activity/import.rs +++ b/crates/brk_computer/src/frameworks/cointime/activity/import.rs @@ -6,7 +6,8 @@ use super::{DerivedVecs, Vecs}; use crate::{ indexes, internal::{ - LazyPerBlock, OneMinusF64, PerBlock, PerBlockCumulativeRolling, WindowStartVec, Windows, + LazyPerBlock, OddsF64, OneMinusF64, PerBlock, PerBlockCumulativeRolling, WindowStartVec, + Windows, }, }; @@ -32,11 +33,17 @@ impl DerivedVecs { liveliness.height.read_only_boxed_clone(), &liveliness, ); + let ratio = LazyPerBlock::from_computed::( + &name("activity_to_vaultedness"), + version, + liveliness.height.read_only_boxed_clone(), + &liveliness, + ); Ok(Self { liveliness, vaultedness, - ratio: PerBlock::forced_import(db, &name("activity_to_vaultedness"), version, indexes)?, + ratio, }) } } diff --git a/crates/brk_computer/src/frameworks/cointime/activity/vecs.rs b/crates/brk_computer/src/frameworks/cointime/activity/vecs.rs index d477709fc..11074a02e 100644 --- a/crates/brk_computer/src/frameworks/cointime/activity/vecs.rs +++ b/crates/brk_computer/src/frameworks/cointime/activity/vecs.rs @@ -9,7 +9,7 @@ use crate::internal::{LazyPerBlock, PerBlock, PerBlockCumulativeRolling}; pub struct DerivedVecs { pub liveliness: PerBlock, pub vaultedness: LazyPerBlock, - pub ratio: PerBlock, + pub ratio: LazyPerBlock, } #[derive(Deref, DerefMut, Traversable)] diff --git a/crates/brk_computer/src/indexes/height.rs b/crates/brk_computer/src/indexes/height.rs index c234ca772..7260430b6 100644 --- a/crates/brk_computer/src/indexes/height.rs +++ b/crates/brk_computer/src/indexes/height.rs @@ -3,7 +3,7 @@ use brk_types::{ Day1, Day3, Epoch, Halving, Height, Hour1, Hour4, Hour12, Minute10, Minute30, Month1, Month3, Month6, StoredU64, Version, Week1, Year1, Year10, }; -use vecdb::{Database, EagerVec, ImportableVec, PcoVec, Rw, StorageMode}; +use vecdb::{CachedVec, Database, EagerVec, ImportableVec, PcoVec, Rw, StorageMode}; use brk_error::Result; @@ -14,7 +14,7 @@ pub struct Vecs { pub hour1: M::Stored>>, pub hour4: M::Stored>>, pub hour12: M::Stored>>, - pub day1: M::Stored>>, + pub day1: CachedVec>>>, pub day3: M::Stored>>, pub epoch: M::Stored>>, pub halving: M::Stored>>, @@ -35,7 +35,7 @@ impl Vecs { hour1: EagerVec::forced_import(db, "hour1", version)?, hour4: EagerVec::forced_import(db, "hour4", version)?, hour12: EagerVec::forced_import(db, "hour12", version)?, - day1: EagerVec::forced_import(db, "day1", version)?, + day1: CachedVec::wrap(EagerVec::forced_import(db, "day1", version)?), day3: EagerVec::forced_import(db, "day3", version)?, epoch: EagerVec::forced_import(db, "epoch", version)?, halving: EagerVec::forced_import(db, "halving", version)?, diff --git a/crates/brk_computer/src/indexes/mod.rs b/crates/brk_computer/src/indexes/mod.rs index 4c7b412f1..427a18555 100644 --- a/crates/brk_computer/src/indexes/mod.rs +++ b/crates/brk_computer/src/indexes/mod.rs @@ -254,7 +254,7 @@ impl Vecs { .collect_one(prev_height) .unwrap_or_default(); - self.height.day1.compute_transform( + self.height.day1.inner.compute_transform( starting_height, &self.timestamp.monotonic, |(h, ts, ..)| (h, Day1::try_from(Date::from(ts)).unwrap()), diff --git a/crates/brk_computer/src/inputs/compute.rs b/crates/brk_computer/src/inputs/compute.rs index 308c721c4..db51c0c48 100644 --- a/crates/brk_computer/src/inputs/compute.rs +++ b/crates/brk_computer/src/inputs/compute.rs @@ -14,11 +14,8 @@ impl Vecs { ) -> Result<()> { self.db.sync_bg_tasks()?; - let starting_lengths = indexer.safe_lengths(); - self.spent.compute(indexer, exit)?; self.count.compute(indexer, blocks, exit)?; - self.per_sec.compute(&self.count, &starting_lengths, exit)?; self.by_type.compute(indexer, exit)?; let exit = exit.clone(); diff --git a/crates/brk_computer/src/inputs/import.rs b/crates/brk_computer/src/inputs/import.rs index accf03032..7794d5c8f 100644 --- a/crates/brk_computer/src/inputs/import.rs +++ b/crates/brk_computer/src/inputs/import.rs @@ -6,12 +6,12 @@ use brk_types::Version; use crate::{ indexes, internal::{ - WindowStartVec, Windows, + LazyPerSecondWindows, WindowStartVec, Windows, db_utils::{finalize_db, open_db}, }, }; -use super::{ByTypeVecs, CountVecs, PerSecVecs, SpentVecs, Vecs}; +use super::{ByTypeVecs, CountVecs, SpentVecs, Vecs}; impl Vecs { pub(crate) fn forced_import( @@ -25,7 +25,7 @@ impl Vecs { let spent = SpentVecs::forced_import(&db, version)?; let count = CountVecs::forced_import(&db, version, indexes, cached_starts)?; - let per_sec = PerSecVecs::forced_import(&db, version, indexes)?; + let per_sec = LazyPerSecondWindows::new("inputs_per_sec", version, &count.rolling.sum); let by_type = ByTypeVecs::forced_import(&db, version, indexes, cached_starts)?; let this = Self { diff --git a/crates/brk_computer/src/inputs/mod.rs b/crates/brk_computer/src/inputs/mod.rs index 79aeef505..7564053e2 100644 --- a/crates/brk_computer/src/inputs/mod.rs +++ b/crates/brk_computer/src/inputs/mod.rs @@ -1,6 +1,5 @@ pub mod by_type; pub mod count; -pub mod per_sec; pub mod spent; mod compute; @@ -9,9 +8,10 @@ mod import; use brk_traversable::Traversable; use vecdb::{Database, Rw, StorageMode}; +use crate::internal::LazyPerSecondWindows; + pub use by_type::Vecs as ByTypeVecs; pub use count::Vecs as CountVecs; -pub use per_sec::Vecs as PerSecVecs; pub use spent::Vecs as SpentVecs; pub const DB_NAME: &str = "inputs"; @@ -23,6 +23,6 @@ pub struct Vecs { pub spent: SpentVecs, pub count: CountVecs, - pub per_sec: PerSecVecs, + pub per_sec: LazyPerSecondWindows, pub by_type: ByTypeVecs, } diff --git a/crates/brk_computer/src/inputs/per_sec/compute.rs b/crates/brk_computer/src/inputs/per_sec/compute.rs deleted file mode 100644 index 0a617e93f..000000000 --- a/crates/brk_computer/src/inputs/per_sec/compute.rs +++ /dev/null @@ -1,29 +0,0 @@ -use brk_error::Result; -use brk_indexer::Lengths; -use brk_types::StoredF32; -use vecdb::Exit; - -use super::Vecs; -use crate::{inputs::CountVecs, internal::Windows}; - -impl Vecs { - pub(crate) fn compute( - &mut self, - count: &CountVecs, - starting_lengths: &Lengths, - exit: &Exit, - ) -> Result<()> { - let h = starting_lengths.height; - let sums = count.rolling.sum.0.as_array(); - let per_sec = self.0.as_mut_array(); - for (i, &secs) in Windows::<()>::SECS.iter().enumerate() { - per_sec[i].height.compute_transform( - h, - &sums[i].height, - |(h, sum, ..)| (h, StoredF32::from(*sum as f64 / secs)), - exit, - )?; - } - Ok(()) - } -} diff --git a/crates/brk_computer/src/inputs/per_sec/import.rs b/crates/brk_computer/src/inputs/per_sec/import.rs deleted file mode 100644 index 7cd0ccdfc..000000000 --- a/crates/brk_computer/src/inputs/per_sec/import.rs +++ /dev/null @@ -1,21 +0,0 @@ -use brk_error::Result; -use brk_types::Version; -use vecdb::Database; - -use super::Vecs; -use crate::{ - indexes, - internal::{PerBlock, Windows}, -}; - -impl Vecs { - pub(crate) fn forced_import( - db: &Database, - version: Version, - indexes: &indexes::Vecs, - ) -> Result { - Ok(Self(Windows::try_from_fn(|suffix| { - PerBlock::forced_import(db, &format!("inputs_per_sec_{suffix}"), version, indexes) - })?)) - } -} diff --git a/crates/brk_computer/src/inputs/per_sec/mod.rs b/crates/brk_computer/src/inputs/per_sec/mod.rs deleted file mode 100644 index 1136f9ebd..000000000 --- a/crates/brk_computer/src/inputs/per_sec/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -mod compute; -mod import; -mod vecs; - -pub use vecs::Vecs; diff --git a/crates/brk_computer/src/inputs/per_sec/vecs.rs b/crates/brk_computer/src/inputs/per_sec/vecs.rs deleted file mode 100644 index ee741b415..000000000 --- a/crates/brk_computer/src/inputs/per_sec/vecs.rs +++ /dev/null @@ -1,8 +0,0 @@ -use brk_traversable::Traversable; -use brk_types::StoredF32; -use vecdb::{Rw, StorageMode}; - -use crate::internal::{PerBlock, Windows}; - -#[derive(Traversable)] -pub struct Vecs(#[traversable(flatten)] pub Windows>); diff --git a/crates/brk_computer/src/internal/per_block/computed/cumulative_rolling.rs b/crates/brk_computer/src/internal/per_block/computed/cumulative_rolling.rs index a1206c682..70e360834 100644 --- a/crates/brk_computer/src/internal/per_block/computed/cumulative_rolling.rs +++ b/crates/brk_computer/src/internal/per_block/computed/cumulative_rolling.rs @@ -8,10 +8,10 @@ use brk_traversable::Traversable; use brk_types::{Height, Version}; use schemars::JsonSchema; use vecdb::{ - AnyStoredVec, AnyVec, Database, Exit, ReadableCloneableVec, ReadableVec, Rw, StorageMode, - VecValue, WritableVec, + AnyStoredVec, AnyVec, Database, Exit, ReadableVec, Rw, StorageMode, VecValue, WritableVec, }; +use super::lazy_cumulative_rolling::lazy_parts; use crate::{ indexes, internal::{ @@ -46,26 +46,12 @@ where ) -> Result { let cumulative = PerBlock::forced_import(db, &format!("{name}_cumulative"), version, indexes)?; + let (block, sum, average) = + lazy_parts(name, version, &cumulative.height, cached_starts, indexes); let last_cumulative = cumulative .height .collect_last() .map(|value| (cumulative.height.len(), value)); - let cumulative_source = cumulative.height.read_only_boxed_clone(); - let block = LazyPreviousDeltaVec::new(name, version, cumulative_source); - let sum = LazyRollingSumsFromHeight::new( - &format!("{name}_sum"), - version, - &cumulative.height, - cached_starts, - indexes, - ); - let average = LazyRollingAvgsFromHeight::new( - &format!("{name}_average"), - version, - &cumulative.height, - cached_starts, - indexes, - ); Ok(Self { block, @@ -166,13 +152,13 @@ where #[cfg(test)] mod tests { - use brk_types::{Height, StoredU64, Version}; + use brk_types::{Height, StoredU32, StoredU64, Version}; use vecdb::{ AnyStoredVec, Database, EagerVec, ImportableVec, PcoVec, ReadableCloneableVec, ReadableVec, WritableVec, }; - use crate::internal::LazyPreviousDeltaVec; + use crate::internal::{LazyPreviousDeltaVec, StoredU64ToStoredU32}; #[test] fn lazy_block_is_the_delta_of_cumulative() { @@ -203,7 +189,27 @@ mod tests { block.collect_range_at(0, 3), [1_u64, 2, 3].map(StoredU64::from) ); + assert_eq!( + block.collect_range_at(1, 3), + [2_u64, 3].map(StoredU64::from) + ); + let transformed = + LazyPreviousDeltaVec::::transformed( + "transformed", + Version::ONE, + cumulative.read_only_boxed_clone(), + ); + assert_eq!( + transformed.collect_range_at(0, 3), + [1_u32, 2, 3].map(StoredU32::from) + ); + assert_eq!( + transformed.collect_range_at(1, 3), + [2_u32, 3].map(StoredU32::from) + ); + + drop(transformed); drop(block); drop(cumulative); drop(db); diff --git a/crates/brk_computer/src/internal/per_block/computed/full.rs b/crates/brk_computer/src/internal/per_block/computed/full.rs index b1364522e..7e9cfa532 100644 --- a/crates/brk_computer/src/internal/per_block/computed/full.rs +++ b/crates/brk_computer/src/internal/per_block/computed/full.rs @@ -1,12 +1,12 @@ -//! PerBlockFull - base EagerVec + cumulative PerBlock + RollingComplete. -//! -//! For metrics with stored per-block data, cumulative sums, and rolling windows. +//! Stored cumulative and rolling statistics with a lazy one-source block view. use brk_error::Result; use brk_traversable::Traversable; use brk_types::{Height, Version}; use schemars::JsonSchema; -use vecdb::{Database, EagerVec, Exit, ImportableVec, PcoVec, Rw, StorageMode}; +use vecdb::{ + Database, Exit, LazyVecFrom1, ReadableCloneableVec, ReadableVec, Rw, StorageMode, VecValue, +}; use crate::{ indexes, @@ -14,28 +14,33 @@ use crate::{ }; #[derive(Traversable)] -pub struct PerBlockFull +pub struct PerBlockFull where T: NumericValue + JsonSchema, + S: VecValue, { - pub block: M::Stored>>, + pub block: LazyVecFrom1, pub cumulative: PerBlock, #[traversable(flatten)] pub rolling: RollingComplete, } -impl PerBlockFull +impl PerBlockFull where T: NumericValue + JsonSchema, + S: VecValue, { pub(crate) fn forced_import( db: &Database, name: &str, version: Version, + source: &(impl ReadableCloneableVec + 'static), + compute_block: fn(Height, S) -> T, indexes: &indexes::Vecs, cached_starts: &Windows<&WindowStartVec>, ) -> Result { - let block = EagerVec::forced_import(db, name, version)?; + let block = + LazyVecFrom1::init(name, version, source.read_only_boxed_clone(), compute_block); let cumulative = PerBlock::forced_import(db, &format!("{name}_cumulative"), version, indexes)?; let rolling = RollingComplete::forced_import( @@ -54,23 +59,42 @@ where }) } - /// Compute base data via closure, then cumulative + rolling distribution. pub(crate) fn compute( &mut self, max_from: Height, windows: &WindowStarts<'_>, exit: &Exit, - compute_base: impl FnOnce(&mut EagerVec>) -> Result<()>, ) -> Result<()> where T: From + Default + Copy + Ord, f64: From, { - compute_base(&mut self.block)?; - self.cumulative - .height - .compute_cumulative(max_from, &self.block, exit)?; - self.rolling.compute(max_from, windows, &self.block, exit)?; - Ok(()) + compute_rest( + &mut self.cumulative, + &mut self.rolling, + &self.block, + max_from, + windows, + exit, + ) } } + +fn compute_rest( + cumulative: &mut PerBlock, + rolling: &mut RollingComplete, + source: &impl ReadableVec, + max_from: Height, + windows: &WindowStarts<'_>, + exit: &Exit, +) -> Result<()> +where + T: NumericValue + JsonSchema + From + Default + Copy + Ord, + f64: From, +{ + cumulative + .height + .compute_cumulative(max_from, source, exit)?; + rolling.compute(max_from, windows, source, exit)?; + Ok(()) +} diff --git a/crates/brk_computer/src/internal/per_block/computed/lazy_cumulative_rolling.rs b/crates/brk_computer/src/internal/per_block/computed/lazy_cumulative_rolling.rs new file mode 100644 index 000000000..84e238f71 --- /dev/null +++ b/crates/brk_computer/src/internal/per_block/computed/lazy_cumulative_rolling.rs @@ -0,0 +1,120 @@ +//! Lazy counterpart to `PerBlockCumulativeRolling`. + +use brk_traversable::Traversable; +use brk_types::{Height, Version}; +use schemars::JsonSchema; +use vecdb::{ReadableCloneableVec, VecValue}; + +use crate::{ + indexes, + internal::{ + Identity, LazyPerBlock, LazyPreviousDeltaVec, LazyRollingAvgsFromHeight, + LazyRollingSumsFromHeight, NumericValue, PerBlock, WindowStartVec, Windows, + }, +}; + +pub(super) fn lazy_parts( + name: &str, + version: Version, + cumulative: &(impl ReadableCloneableVec + 'static), + cached_starts: &Windows<&WindowStartVec>, + indexes: &indexes::Vecs, +) -> ( + LazyPreviousDeltaVec, + LazyRollingSumsFromHeight, + LazyRollingAvgsFromHeight, +) +where + T: NumericValue + JsonSchema, +{ + ( + LazyPreviousDeltaVec::new(name, version, cumulative.read_only_boxed_clone()), + LazyRollingSumsFromHeight::new( + &format!("{name}_sum"), + version, + cumulative, + cached_starts, + indexes, + ), + LazyRollingAvgsFromHeight::new( + &format!("{name}_average"), + version, + cumulative, + cached_starts, + indexes, + ), + ) +} + +#[derive(Clone, Traversable)] +pub struct LazyPerBlockCumulativeRolling +where + T: NumericValue + JsonSchema, +{ + pub block: LazyPreviousDeltaVec, + pub cumulative: LazyPerBlock, + pub sum: LazyRollingSumsFromHeight, + pub average: LazyRollingAvgsFromHeight, +} + +impl LazyPerBlockCumulativeRolling +where + T: NumericValue + JsonSchema, +{ + fn from_cumulative( + name: &str, + version: Version, + cumulative: LazyPerBlock, + cached_starts: &Windows<&WindowStartVec>, + indexes: &indexes::Vecs, + ) -> Self { + let (block, sum, average) = + lazy_parts(name, version, &cumulative.height, cached_starts, indexes); + + Self { + block, + cumulative, + sum, + average, + } + } + + pub(crate) fn from_source( + name: &str, + version: Version, + source: &PerBlock, + cached_starts: &Windows<&WindowStartVec>, + indexes: &indexes::Vecs, + ) -> Self { + let cumulative = LazyPerBlock::from_computed::>( + &format!("{name}_cumulative"), + version, + source.height.read_only_boxed_clone(), + source, + ); + + Self::from_cumulative(name, version, cumulative, cached_starts, indexes) + } + + pub(crate) fn from_indexed_source( + name: &str, + version: Version, + source: &(impl ReadableCloneableVec + 'static), + compute_cumulative: fn(Height, S) -> T, + cached_starts: &Windows<&WindowStartVec>, + indexes: &indexes::Vecs, + ) -> Self + where + S: VecValue, + { + let cumulative = LazyPerBlock::from_indexed_source( + &format!("{name}_cumulative"), + version, + source, + compute_cumulative, + indexes, + ); + + Self::from_cumulative(name, version, cumulative, cached_starts, indexes) + } +} diff --git a/crates/brk_computer/src/internal/per_block/computed/lazy_rolling.rs b/crates/brk_computer/src/internal/per_block/computed/lazy_rolling.rs index 5060924e2..fab82a1b3 100644 --- a/crates/brk_computer/src/internal/per_block/computed/lazy_rolling.rs +++ b/crates/brk_computer/src/internal/per_block/computed/lazy_rolling.rs @@ -6,13 +6,14 @@ use vecdb::{ReadableCloneableVec, UnaryTransform}; use crate::{ indexes, internal::{ - ComputedVecValue, LazyPerBlock, LazyRollingComplete, NumericValue, PerBlockFull, - WindowStartVec, Windows, + ComputedVecValue, LazyPerBlock, LazyRollingComplete, NumericValue, PerBlock, + RollingComplete, WindowStartVec, Windows, }, }; /// Lazy analog of `PerBlockRolling`: lazy cumulative + lazy rolling complete. -/// Derived by transforming a `PerBlockFull`. Zero stored vecs. +/// Derived by transforming another metric's cumulative and rolling parts. +/// Zero stored vecs. #[derive(Clone, Traversable)] pub struct LazyPerBlockRolling where @@ -29,25 +30,26 @@ where T: NumericValue + JsonSchema + 'static, S1T: NumericValue + JsonSchema, { - pub(crate) fn from_per_block_full>( + pub(crate) fn from_full_parts>( name: &str, version: Version, - source: &PerBlockFull, + source_cumulative: &PerBlock, + source_rolling: &RollingComplete, cached_starts: &Windows<&WindowStartVec>, indexes: &indexes::Vecs, ) -> Self { let cumulative = LazyPerBlock::from_computed::( &format!("{name}_cumulative"), version, - source.cumulative.height.read_only_boxed_clone(), - &source.cumulative, + source_cumulative.height.read_only_boxed_clone(), + source_cumulative, ); let rolling = LazyRollingComplete::from_rolling_complete::( name, version, &cumulative.height, - &source.rolling, + source_rolling, cached_starts, indexes, ); diff --git a/crates/brk_computer/src/internal/per_block/computed/mod.rs b/crates/brk_computer/src/internal/per_block/computed/mod.rs index 0945878af..913ed2289 100644 --- a/crates/brk_computer/src/internal/per_block/computed/mod.rs +++ b/crates/brk_computer/src/internal/per_block/computed/mod.rs @@ -4,6 +4,7 @@ mod cached; mod cumulative_rolling; mod distribution; mod full; +mod lazy_cumulative_rolling; mod lazy_distribution; mod lazy_rolling; mod resolutions; @@ -17,6 +18,7 @@ pub use cached::*; pub use cumulative_rolling::*; pub use distribution::*; pub use full::*; +pub use lazy_cumulative_rolling::*; pub use lazy_distribution::*; pub use lazy_rolling::*; pub use resolutions::*; diff --git a/crates/brk_computer/src/internal/per_block/computed/rolling_average.rs b/crates/brk_computer/src/internal/per_block/computed/rolling_average.rs index ee002a6b3..945be8adf 100644 --- a/crates/brk_computer/src/internal/per_block/computed/rolling_average.rs +++ b/crates/brk_computer/src/internal/per_block/computed/rolling_average.rs @@ -1,24 +1,142 @@ -//! PerBlock with rolling average (no distribution stats). +//! Per-block values with rolling averages derived from cumulative prefix sums. //! -//! Stored height data + cumulative + lazy 4-window rolling averages. -//! Rolling averages are computed on-the-fly from the cumulative via DeltaAvg. -//! -//! Type parameters: -//! - `T`: per-block value type -//! - `C`: cumulative type, defaults to `T`. Use a wider type (e.g., `StoredU64`) -//! when the prefix sum of `T` values could overflow `T`. +//! Exact integer/timestamp metrics use a cumulative source of truth and a lazy +//! block view. Float metrics retain their stored block values because +//! subtracting cumulative floats would not reproduce them exactly. use brk_error::Result; use brk_traversable::Traversable; -use brk_types::{Height, Version}; +use brk_types::{Height, StoredU32, StoredU64, Version}; use schemars::JsonSchema; -use vecdb::{Database, EagerVec, Exit, ImportableVec, PcoVec, Rw, StorageMode}; +use vecdb::{ + AnyStoredVec, AnyVec, Database, EagerVec, Exit, Ident, ImportableVec, PcoVec, + ReadableCloneableVec, ReadableVec, Rw, StorageMode, UnaryTransform, VecValue, WritableVec, +}; use crate::indexes; -use crate::internal::{LazyRollingAvgsFromHeight, NumericValue, WindowStartVec, Windows}; +use crate::internal::{ + LazyPreviousDeltaVec, LazyRollingAvgsFromHeight, NumericValue, StoredU64ToStoredU32, + WindowStartVec, Windows, +}; +/// Cumulative source of truth with lazy exact per-block values and rolling averages. +#[derive(Traversable)] +pub struct PerBlockCumulativeAverage +where + T: NumericValue + JsonSchema, + C: NumericValue + JsonSchema, + F: UnaryTransform, +{ + pub block: LazyPreviousDeltaVec, + #[traversable(hidden)] + pub cumulative: M::Stored>>, + #[traversable(flatten)] + pub average: LazyRollingAvgsFromHeight, + #[traversable(skip)] + last_cumulative: Option<(usize, C)>, +} + +pub type CountPerBlockRollingAverage = + PerBlockCumulativeAverage; + +impl PerBlockCumulativeAverage +where + T: NumericValue + JsonSchema + Into, + C: NumericValue + JsonSchema, + F: UnaryTransform, +{ + pub(crate) fn forced_import( + db: &Database, + name: &str, + version: Version, + indexes: &indexes::Vecs, + cached_starts: &Windows<&WindowStartVec>, + ) -> Result { + let cumulative_version = version + Version::TWO; + let cumulative: EagerVec> = + EagerVec::forced_import(db, &format!("{name}_cumulative"), cumulative_version)?; + let last_cumulative = cumulative + .collect_last() + .map(|value| (cumulative.len(), value)); + let block = + LazyPreviousDeltaVec::transformed(name, version, cumulative.read_only_boxed_clone()); + let average = LazyRollingAvgsFromHeight::new( + &format!("{name}_average"), + cumulative_version, + &cumulative, + cached_starts, + indexes, + ); + + Ok(Self { + block, + cumulative, + average, + last_cumulative, + }) + } + + #[inline(always)] + pub(crate) fn push_block(&mut self, value: T) + where + C: Copy, + { + let len = self.cumulative.len(); + let mut cumulative = match self.last_cumulative { + Some((cached_len, value)) if cached_len == len => value, + _ => self.cumulative.collect_last().unwrap_or_default(), + }; + cumulative += value.into(); + self.cumulative.push(cumulative); + self.last_cumulative = Some((len + 1, cumulative)); + } + + pub(crate) fn compute_from( + &mut self, + max_from: Height, + source: &impl ReadableVec, + mut transform: impl FnMut(Height, S) -> T, + exit: &Exit, + ) -> Result<()> + where + S: VecValue, + C: Copy, + { + let mut cumulative = None; + self.cumulative.compute_transform( + max_from, + source, + |(height, value, this)| { + let cumulative = cumulative.get_or_insert_with(|| { + height + .decremented() + .and_then(|height| this.collect_one(height)) + .unwrap_or_default() + }); + *cumulative += transform(height, value).into(); + (height, *cumulative) + }, + exit, + )?; + self.last_cumulative = None; + Ok(()) + } + + pub(crate) fn reset(&mut self) -> Result<()> { + self.last_cumulative = None; + self.cumulative.reset()?; + Ok(()) + } + + pub(crate) fn stored_mut(&mut self) -> &mut dyn AnyStoredVec { + self.last_cumulative = None; + &mut self.cumulative + } +} + +/// Stored-block fallback for values whose cumulative delta is not exact, such as floats. #[derive(Traversable)] pub struct PerBlockRollingAverage where @@ -62,17 +180,6 @@ where }) } - /// Compute height data via closure, then cumulative. Rolling averages are lazy. - pub(crate) fn compute( - &mut self, - max_from: Height, - exit: &Exit, - compute_height: impl FnOnce(&mut EagerVec>) -> Result<()>, - ) -> Result<()> { - compute_height(&mut self.block)?; - self.compute_rest(max_from, exit) - } - /// Compute cumulative from already-populated height data. Rolling averages are lazy. pub(crate) fn compute_rest(&mut self, max_from: Height, exit: &Exit) -> Result<()> { self.cumulative diff --git a/crates/brk_computer/src/internal/per_block/lazy/base.rs b/crates/brk_computer/src/internal/per_block/lazy/base.rs index 142c2fe30..e8c10d964 100644 --- a/crates/brk_computer/src/internal/per_block/lazy/base.rs +++ b/crates/brk_computer/src/internal/per_block/lazy/base.rs @@ -4,13 +4,14 @@ use derive_more::{Deref, DerefMut}; use schemars::JsonSchema; use vecdb::{ LazyVecFrom1, ReadOnlyClone, ReadableBoxedVec, ReadableCloneableVec, ReadableVec, TypedVec, - UnaryTransform, + UnaryTransform, VecValue, }; use crate::{ indexes, internal::{ - CachedPerBlock, ComputedVecValue, DerivedResolutions, NumericValue, PerBlock, Resolutions, + CachedPerBlock, ComputedVecValue, DerivedResolutions, Identity, NumericValue, PerBlock, + Resolutions, }, }; #[derive(Clone, Deref, DerefMut, Traversable)] @@ -125,6 +126,31 @@ where } } +impl LazyPerBlock +where + T: NumericValue + JsonSchema + 'static, +{ + /// Derive a per-block metric from one height-indexed source and the height itself. + pub(crate) fn from_indexed_source( + name: &str, + version: Version, + source: &(impl ReadableCloneableVec + 'static), + compute: fn(Height, S) -> T, + indexes: &indexes::Vecs, + ) -> Self + where + S: VecValue, + { + let indexed = LazyVecFrom1::init( + &format!("{name}_source"), + version, + source.read_only_boxed_clone(), + compute, + ); + Self::from_height_source::, _>(name, version, indexed, indexes) + } +} + impl ReadOnlyClone for LazyPerBlock where T: ComputedVecValue + PartialOrd + JsonSchema, diff --git a/crates/brk_computer/src/internal/per_block/lazy/indexed.rs b/crates/brk_computer/src/internal/per_block/lazy/indexed.rs new file mode 100644 index 000000000..19b0d1f4b --- /dev/null +++ b/crates/brk_computer/src/internal/per_block/lazy/indexed.rs @@ -0,0 +1,195 @@ +use std::sync::Arc; + +use brk_traversable::{Traversable, TreeNode, make_leaf}; +use schemars::JsonSchema; +use serde::Serialize; +use vecdb::{ + AnyExportableVec, AnyVec, CachedBoxedVec, Formattable, ReadableBoxedVec, ReadableVec, TypedVec, + VecIndex, VecValue, Version, short_type_name, +}; + +/// Lazily transforms one metric source with aligned index metadata. +pub struct LazyIndexedVec +where + I: VecIndex, + S: VecValue, + M: VecValue, + T: VecValue, +{ + name: Arc, + base_version: Version, + source: ReadableBoxedVec, + metadata: CachedBoxedVec, + compute: Arc T + Send + Sync>, +} + +impl LazyIndexedVec +where + I: VecIndex, + S: VecValue, + M: VecValue, + T: VecValue, +{ + pub fn new( + name: &str, + version: Version, + source: ReadableBoxedVec, + metadata: CachedBoxedVec, + compute: impl Fn(I, S, M) -> T + Send + Sync + 'static, + ) -> Self { + Self { + name: Arc::from(name), + base_version: version, + source, + metadata, + compute: Arc::new(compute), + } + } + + fn for_each_value(&self, from: usize, to: usize, mut each: impl FnMut(T)) { + let metadata = self.metadata.cached(); + let to = to.min(self.len()).min(metadata.len()); + if from >= to { + return; + } + + let source = self.source.collect_range_dyn(from, to); + for (offset, (source, metadata)) in source + .into_iter() + .zip(metadata[from..to].iter().cloned()) + .enumerate() + { + each((self.compute)(I::from(from + offset), source, metadata)); + } + } +} + +impl Clone for LazyIndexedVec +where + I: VecIndex, + S: VecValue, + M: VecValue, + T: VecValue, +{ + fn clone(&self) -> Self { + Self { + name: Arc::clone(&self.name), + base_version: self.base_version, + source: self.source.clone(), + metadata: self.metadata.clone(), + compute: Arc::clone(&self.compute), + } + } +} + +impl AnyVec for LazyIndexedVec +where + I: VecIndex, + S: VecValue, + M: VecValue, + T: VecValue, +{ + fn version(&self) -> Version { + self.base_version + self.source.version() + self.metadata.version() + } + + fn name(&self) -> &str { + &self.name + } + + fn index_type_to_string(&self) -> &'static str { + I::to_string() + } + + fn len(&self) -> usize { + self.source.len() + } + + fn value_type_to_size_of(&self) -> usize { + size_of::() + } + + fn value_type_to_string(&self) -> &'static str { + short_type_name::() + } + + fn region_names(&self) -> Vec { + Vec::new() + } +} + +impl TypedVec for LazyIndexedVec +where + I: VecIndex, + S: VecValue, + M: VecValue, + T: VecValue, +{ + type I = I; + type T = T; +} + +impl ReadableVec for LazyIndexedVec +where + I: VecIndex, + S: VecValue, + M: VecValue, + T: VecValue, +{ + fn read_into_at(&self, from: usize, to: usize, buf: &mut Vec) { + buf.reserve(to.saturating_sub(from)); + self.for_each_value(from, to, |value| buf.push(value)); + } + + fn for_each_range_dyn_at(&self, from: usize, to: usize, f: &mut dyn FnMut(T)) { + self.for_each_value(from, to, f); + } + + fn fold_range_at B>(&self, from: usize, to: usize, init: B, f: F) -> B { + let mut values = Vec::with_capacity(to.saturating_sub(from)); + self.read_into_at(from, to, &mut values); + values.into_iter().fold(init, f) + } + + fn try_fold_range_at Result>( + &self, + from: usize, + to: usize, + init: B, + f: F, + ) -> Result { + let mut values = Vec::with_capacity(to.saturating_sub(from)); + self.read_into_at(from, to, &mut values); + values.into_iter().try_fold(init, f) + } + + fn collect_one_at(&self, index: usize) -> Option { + let source = self.source.collect_one_at(index)?; + let metadata = self.metadata.cached().get(index)?.clone(); + Some((self.compute)(I::from(index), source, metadata)) + } + + fn read_sorted_into_at(&self, indices: &[usize], out: &mut Vec) { + out.reserve(indices.len()); + indices + .iter() + .filter_map(|&index| self.collect_one_at(index)) + .for_each(|value| out.push(value)); + } +} + +impl Traversable for LazyIndexedVec +where + I: VecIndex, + S: VecValue, + M: VecValue, + T: VecValue + Formattable + Serialize + JsonSchema, +{ + fn iter_any_exportable(&self) -> impl Iterator { + std::iter::once(self as &dyn AnyExportableVec) + } + + fn to_tree_node(&self) -> TreeNode { + make_leaf::(self) + } +} diff --git a/crates/brk_computer/src/internal/per_block/lazy/lookback.rs b/crates/brk_computer/src/internal/per_block/lazy/lookback.rs new file mode 100644 index 000000000..560a279cc --- /dev/null +++ b/crates/brk_computer/src/internal/per_block/lazy/lookback.rs @@ -0,0 +1,203 @@ +use std::sync::Arc; + +use brk_traversable::{Traversable, TreeNode, make_leaf}; +use schemars::JsonSchema; +use serde::Serialize; +use vecdb::{ + AnyExportableVec, AnyVec, Formattable, ReadableBoxedVec, ReadableVec, TypedVec, VecIndex, + VecValue, Version, short_type_name, +}; + +/// Lazily combines values a fixed number of positions apart from one source. +/// +/// Current and previous ranges are read separately, so a large lookback does +/// not force reading the gap between them. +pub struct LazyLookbackVec +where + I: VecIndex, + S: VecValue, + T: VecValue, +{ + name: Arc, + base_version: Version, + source: ReadableBoxedVec, + lookback: usize, + compute: fn(S, Option) -> T, +} + +impl LazyLookbackVec +where + I: VecIndex, + S: VecValue, + T: VecValue, +{ + pub(crate) fn new( + name: &str, + version: Version, + source: ReadableBoxedVec, + lookback: usize, + compute: fn(S, Option) -> T, + ) -> Self { + Self { + name: Arc::from(name), + base_version: version, + source, + lookback, + compute, + } + } +} + +impl LazyLookbackVec +where + I: VecIndex, + S: VecValue, + T: VecValue, +{ + fn for_each_lookback(&self, from: usize, to: usize, mut each: impl FnMut(T)) { + let to = to.min(self.len()); + if from >= to { + return; + } + + let previous_from = from.saturating_sub(self.lookback); + let previous_to = to.saturating_sub(self.lookback); + let previous = self.source.collect_range_dyn(previous_from, previous_to); + let current = self.source.collect_range_dyn(from, to); + + for (offset, current) in current.into_iter().enumerate() { + let index = from + offset; + let previous = index + .checked_sub(self.lookback) + .map(|index| previous[index - previous_from].clone()); + each((self.compute)(current, previous)); + } + } +} + +impl Clone for LazyLookbackVec +where + I: VecIndex, + S: VecValue, + T: VecValue, +{ + fn clone(&self) -> Self { + Self { + name: Arc::clone(&self.name), + base_version: self.base_version, + source: self.source.clone(), + lookback: self.lookback, + compute: self.compute, + } + } +} + +impl AnyVec for LazyLookbackVec +where + I: VecIndex, + S: VecValue, + T: VecValue, +{ + fn version(&self) -> Version { + self.base_version + self.source.version() + } + + fn name(&self) -> &str { + &self.name + } + + fn index_type_to_string(&self) -> &'static str { + I::to_string() + } + + fn len(&self) -> usize { + self.source.len() + } + + fn value_type_to_size_of(&self) -> usize { + size_of::() + } + + fn value_type_to_string(&self) -> &'static str { + short_type_name::() + } + + fn region_names(&self) -> Vec { + Vec::new() + } +} + +impl TypedVec for LazyLookbackVec +where + I: VecIndex, + S: VecValue, + T: VecValue, +{ + type I = I; + type T = T; +} + +impl ReadableVec for LazyLookbackVec +where + I: VecIndex, + S: VecValue, + T: VecValue, +{ + fn read_into_at(&self, from: usize, to: usize, buf: &mut Vec) { + buf.reserve(to.saturating_sub(from)); + self.for_each_lookback(from, to, |value| buf.push(value)); + } + + fn for_each_range_dyn_at(&self, from: usize, to: usize, f: &mut dyn FnMut(T)) { + self.for_each_lookback(from, to, f); + } + + fn fold_range_at B>(&self, from: usize, to: usize, init: B, f: F) -> B { + let mut values = Vec::with_capacity(to.saturating_sub(from)); + self.read_into_at(from, to, &mut values); + values.into_iter().fold(init, f) + } + + fn try_fold_range_at Result>( + &self, + from: usize, + to: usize, + init: B, + f: F, + ) -> Result { + let mut values = Vec::with_capacity(to.saturating_sub(from)); + self.read_into_at(from, to, &mut values); + values.into_iter().try_fold(init, f) + } + + fn collect_one_at(&self, index: usize) -> Option { + let current = self.source.collect_one_at(index)?; + let previous = index + .checked_sub(self.lookback) + .and_then(|index| self.source.collect_one_at(index)); + Some((self.compute)(current, previous)) + } + + fn read_sorted_into_at(&self, indices: &[usize], out: &mut Vec) { + out.reserve(indices.len()); + indices + .iter() + .filter_map(|&index| self.collect_one_at(index)) + .for_each(|value| out.push(value)); + } +} + +impl Traversable for LazyLookbackVec +where + I: VecIndex, + S: VecValue, + T: VecValue + Formattable + Serialize + JsonSchema, +{ + fn iter_any_exportable(&self) -> impl Iterator { + std::iter::once(self as &dyn AnyExportableVec) + } + + fn to_tree_node(&self) -> TreeNode { + make_leaf::(self) + } +} diff --git a/crates/brk_computer/src/internal/per_block/lazy/mod.rs b/crates/brk_computer/src/internal/per_block/lazy/mod.rs index 48587df11..81342d912 100644 --- a/crates/brk_computer/src/internal/per_block/lazy/mod.rs +++ b/crates/brk_computer/src/internal/per_block/lazy/mod.rs @@ -1,11 +1,21 @@ mod base; mod derived_resolutions; +mod indexed; +mod lookback; mod map_option; +mod per_second; mod previous_delta; +mod since_day; mod transform_last; +mod window; pub use base::*; pub use derived_resolutions::*; +pub use indexed::*; +pub use lookback::*; pub use map_option::*; +pub use per_second::*; pub use previous_delta::*; +pub use since_day::*; pub use transform_last::*; +pub use window::*; diff --git a/crates/brk_computer/src/internal/per_block/lazy/per_second.rs b/crates/brk_computer/src/internal/per_block/lazy/per_second.rs new file mode 100644 index 000000000..e33e19d07 --- /dev/null +++ b/crates/brk_computer/src/internal/per_block/lazy/per_second.rs @@ -0,0 +1,40 @@ +use brk_traversable::Traversable; +use brk_types::{StoredF32, StoredU64, Version}; +use derive_more::{Deref, DerefMut}; +use vecdb::ReadableCloneableVec; + +use crate::internal::{ + LazyPerBlock, LazyRollingSumFromHeight, LazyRollingSumsFromHeight, PerSecond, Windows, +}; + +#[derive(Clone, Deref, DerefMut, Traversable)] +#[traversable(transparent)] +pub struct LazyPerSecondWindows(pub Windows>); + +impl LazyPerSecondWindows { + pub(crate) fn new( + name: &str, + version: Version, + source: &LazyRollingSumsFromHeight, + ) -> Self { + fn window( + name: &str, + version: Version, + source: &LazyRollingSumFromHeight, + ) -> LazyPerBlock { + LazyPerBlock::from_resolutions::>( + name, + version, + source.height.read_only_boxed_clone(), + &source.resolutions, + ) + } + + Self(Windows { + _24h: window::<86_400>(&format!("{name}_24h"), version, &source._24h), + _1w: window::<604_800>(&format!("{name}_1w"), version, &source._1w), + _1m: window::<2_592_000>(&format!("{name}_1m"), version, &source._1m), + _1y: window::<31_536_000>(&format!("{name}_1y"), version, &source._1y), + }) + } +} diff --git a/crates/brk_computer/src/internal/per_block/lazy/previous_delta.rs b/crates/brk_computer/src/internal/per_block/lazy/previous_delta.rs index a1eddcb1e..95dd6a30a 100644 --- a/crates/brk_computer/src/internal/per_block/lazy/previous_delta.rs +++ b/crates/brk_computer/src/internal/per_block/lazy/previous_delta.rs @@ -1,44 +1,59 @@ -use std::sync::Arc; +use std::{marker::PhantomData, sync::Arc}; use brk_traversable::{Traversable, TreeNode, make_leaf}; use schemars::JsonSchema; use serde::Serialize; use vecdb::{ - AnyExportableVec, AnyVec, CheckedSub, Formattable, ReadableBoxedVec, ReadableVec, TypedVec, - VecIndex, VecValue, Version, short_type_name, + AnyExportableVec, AnyVec, CheckedSub, Formattable, Ident, ReadableBoxedVec, ReadableVec, + TypedVec, UnaryTransform, VecIndex, VecValue, Version, short_type_name, }; /// Lazy `source[index] - source[index - 1]`, with zero before the first value. /// /// This is a single-source view: it follows source growth automatically and /// stores nothing on disk. -pub struct LazyPreviousDeltaVec +pub struct LazyPreviousDeltaVec where I: VecIndex, + S: VecValue, T: VecValue, { name: Arc, base_version: Version, - source: ReadableBoxedVec, + source: ReadableBoxedVec, + _output: PhantomData (T, F)>, } -impl LazyPreviousDeltaVec +impl LazyPreviousDeltaVec where I: VecIndex, + S: VecValue, +{ + pub fn new(name: &str, version: Version, source: ReadableBoxedVec) -> Self { + Self::transformed(name, version, source) + } +} + +impl LazyPreviousDeltaVec +where + I: VecIndex, + S: VecValue, T: VecValue, { - pub fn new(name: &str, version: Version, source: ReadableBoxedVec) -> Self { + pub fn transformed(name: &str, version: Version, source: ReadableBoxedVec) -> Self { Self { name: Arc::from(name), base_version: version, source, + _output: PhantomData, } } } -impl Clone for LazyPreviousDeltaVec +impl Clone for LazyPreviousDeltaVec where I: VecIndex, + S: VecValue, T: VecValue, { fn clone(&self) -> Self { @@ -46,13 +61,15 @@ where name: Arc::clone(&self.name), base_version: self.base_version, source: self.source.clone(), + _output: PhantomData, } } } -impl AnyVec for LazyPreviousDeltaVec +impl AnyVec for LazyPreviousDeltaVec where I: VecIndex, + S: VecValue, T: VecValue, { fn version(&self) -> Version { @@ -84,19 +101,22 @@ where } } -impl TypedVec for LazyPreviousDeltaVec +impl TypedVec for LazyPreviousDeltaVec where I: VecIndex, + S: VecValue, T: VecValue, { type I = I; type T = T; } -impl LazyPreviousDeltaVec +impl LazyPreviousDeltaVec where I: VecIndex, - T: VecValue + CheckedSub + Default, + S: VecValue + CheckedSub + Default, + T: VecValue, + F: UnaryTransform, { fn for_each_delta(&self, from: usize, to: usize, mut each: impl FnMut(T)) { let to = to.min(self.len()); @@ -108,22 +128,26 @@ where let values = self.source.collect_range_dyn(read_from, to); let mut values = values.into_iter(); let mut previous = if from == 0 { - T::default() + S::default() } else { values.next().unwrap() }; for current in values { - each(current.clone().checked_sub(previous).unwrap_or_default()); + each(F::apply( + current.clone().checked_sub(previous).unwrap_or_default(), + )); previous = current; } } } -impl ReadableVec for LazyPreviousDeltaVec +impl ReadableVec for LazyPreviousDeltaVec where I: VecIndex, - T: VecValue + CheckedSub + Default, + S: VecValue + CheckedSub + Default, + T: VecValue, + F: UnaryTransform, { fn read_into_at(&self, from: usize, to: usize, buf: &mut Vec) { buf.reserve(to.saturating_sub(from)); @@ -134,18 +158,18 @@ where self.for_each_delta(from, to, f); } - fn fold_range_at B>(&self, from: usize, to: usize, init: B, f: F) -> B { + fn fold_range_at B>(&self, from: usize, to: usize, init: B, f: G) -> B { let mut values = Vec::with_capacity(to.saturating_sub(from)); self.read_into_at(from, to, &mut values); values.into_iter().fold(init, f) } - fn try_fold_range_at Result>( + fn try_fold_range_at Result>( &self, from: usize, to: usize, init: B, - f: F, + f: G, ) -> Result { let mut values = Vec::with_capacity(to.saturating_sub(from)); self.read_into_at(from, to, &mut values); @@ -158,7 +182,7 @@ where .checked_sub(1) .and_then(|index| self.source.collect_one_at(index)) .unwrap_or_default(); - Some(current.checked_sub(previous).unwrap_or_default()) + Some(F::apply(current.checked_sub(previous).unwrap_or_default())) } fn read_sorted_into_at(&self, indices: &[usize], out: &mut Vec) { @@ -170,10 +194,12 @@ where } } -impl Traversable for LazyPreviousDeltaVec +impl Traversable for LazyPreviousDeltaVec where I: VecIndex, - T: VecValue + CheckedSub + Default + Formattable + Serialize + JsonSchema, + S: VecValue + CheckedSub + Default, + T: VecValue + Formattable + Serialize + JsonSchema, + F: UnaryTransform, { fn iter_any_exportable(&self) -> impl Iterator { std::iter::once(self as &dyn AnyExportableVec) diff --git a/crates/brk_computer/src/internal/per_block/lazy/since_day.rs b/crates/brk_computer/src/internal/per_block/lazy/since_day.rs new file mode 100644 index 000000000..7c051f609 --- /dev/null +++ b/crates/brk_computer/src/internal/per_block/lazy/since_day.rs @@ -0,0 +1,221 @@ +use std::sync::Arc; + +use brk_traversable::{Traversable, TreeNode, make_leaf}; +use brk_types::{Day1, Height}; +use schemars::JsonSchema; +use serde::Serialize; +use vecdb::{ + AnyExportableVec, AnyVec, CachedBoxedVec, Formattable, PrintableIndex, ReadableBoxedVec, + ReadableVec, TypedVec, VecValue, Version, short_type_name, +}; + +/// Lazily derives values accumulated since a fixed day from one cumulative source. +pub struct LazySinceDayVec +where + S: VecValue, + T: VecValue, +{ + name: Arc, + base_version: Version, + source: ReadableBoxedVec, + days: CachedBoxedVec, + start_day: Day1, + compute: Arc T + Send + Sync>, +} + +impl LazySinceDayVec +where + S: VecValue + Default, + T: VecValue + Default, +{ + pub fn new( + name: &str, + version: Version, + source: ReadableBoxedVec, + days: CachedBoxedVec, + start_day: Day1, + compute: impl Fn(S, S) -> T + Send + Sync + 'static, + ) -> Self { + Self { + name: Arc::from(name), + base_version: version, + source, + days, + start_day, + compute: Arc::new(compute), + } + } + + fn start_height(&self) -> usize { + let days = self.days.cached(); + let mut left = 0; + let mut right = self.len().min(days.len()); + while left < right { + let middle = left + (right - left) / 2; + if days[middle] < self.start_day { + left = middle + 1; + } else { + right = middle; + } + } + left + } + + fn for_each_value(&self, from: usize, to: usize, mut each: impl FnMut(T)) { + let to = to.min(self.len()); + if from >= to { + return; + } + + let start = self.start_height(); + let active_from = from.max(start).min(to); + for _ in from..active_from { + each(T::default()); + } + + if active_from == to { + return; + } + + let before = start + .checked_sub(1) + .and_then(|index| self.source.collect_one_at(index)) + .unwrap_or_default(); + for current in self.source.collect_range_dyn(active_from, to) { + each((self.compute)(current, before.clone())); + } + } +} + +impl Clone for LazySinceDayVec +where + S: VecValue, + T: VecValue, +{ + fn clone(&self) -> Self { + Self { + name: Arc::clone(&self.name), + base_version: self.base_version, + source: self.source.clone(), + days: self.days.clone(), + start_day: self.start_day, + compute: Arc::clone(&self.compute), + } + } +} + +impl AnyVec for LazySinceDayVec +where + S: VecValue, + T: VecValue, +{ + fn version(&self) -> Version { + self.base_version + self.source.version() + self.days.version() + } + + fn name(&self) -> &str { + &self.name + } + + fn index_type_to_string(&self) -> &'static str { + ::to_string() + } + + fn len(&self) -> usize { + self.source.len() + } + + fn value_type_to_size_of(&self) -> usize { + size_of::() + } + + fn value_type_to_string(&self) -> &'static str { + short_type_name::() + } + + fn region_names(&self) -> Vec { + Vec::new() + } +} + +impl TypedVec for LazySinceDayVec +where + S: VecValue, + T: VecValue, +{ + type I = Height; + type T = T; +} + +impl ReadableVec for LazySinceDayVec +where + S: VecValue + Default, + T: VecValue + Default, +{ + fn read_into_at(&self, from: usize, to: usize, buf: &mut Vec) { + buf.reserve(to.saturating_sub(from)); + self.for_each_value(from, to, |value| buf.push(value)); + } + + fn for_each_range_dyn_at(&self, from: usize, to: usize, f: &mut dyn FnMut(T)) { + self.for_each_value(from, to, f); + } + + fn fold_range_at B>(&self, from: usize, to: usize, init: B, f: F) -> B { + let mut values = Vec::with_capacity(to.saturating_sub(from)); + self.read_into_at(from, to, &mut values); + values.into_iter().fold(init, f) + } + + fn try_fold_range_at Result>( + &self, + from: usize, + to: usize, + init: B, + f: F, + ) -> Result { + let mut values = Vec::with_capacity(to.saturating_sub(from)); + self.read_into_at(from, to, &mut values); + values.into_iter().try_fold(init, f) + } + + fn collect_one_at(&self, index: usize) -> Option { + if index >= self.len() { + return None; + } + + let start = self.start_height(); + if index < start { + return Some(T::default()); + } + + let current = self.source.collect_one_at(index)?; + let before = start + .checked_sub(1) + .and_then(|index| self.source.collect_one_at(index)) + .unwrap_or_default(); + Some((self.compute)(current, before)) + } + + fn read_sorted_into_at(&self, indices: &[usize], out: &mut Vec) { + out.reserve(indices.len()); + indices + .iter() + .filter_map(|&index| self.collect_one_at(index)) + .for_each(|value| out.push(value)); + } +} + +impl Traversable for LazySinceDayVec +where + S: VecValue + Default, + T: VecValue + Default + Formattable + Serialize + JsonSchema, +{ + fn iter_any_exportable(&self) -> impl Iterator { + std::iter::once(self as &dyn AnyExportableVec) + } + + fn to_tree_node(&self) -> TreeNode { + make_leaf::(self) + } +} diff --git a/crates/brk_computer/src/internal/per_block/lazy/window.rs b/crates/brk_computer/src/internal/per_block/lazy/window.rs new file mode 100644 index 000000000..4c147c56e --- /dev/null +++ b/crates/brk_computer/src/internal/per_block/lazy/window.rs @@ -0,0 +1,241 @@ +use std::sync::Arc; + +use brk_traversable::{Traversable, TreeNode, make_leaf}; +use schemars::JsonSchema; +use serde::Serialize; +use vecdb::{ + AnyExportableVec, AnyVec, CachedBoxedVec, Formattable, ReadableBoxedVec, ReadableVec, TypedVec, + VecIndex, VecValue, Version, short_type_name, +}; + +/// Lazily combines the current and window-start values of one metric source. +/// +/// `window_starts` is index metadata, not a second metric source. Reads are +/// split into current and lookback ranges so long windows do not force reading +/// the entire gap between them. +pub struct LazyWindowVec +where + I: VecIndex, + S: VecValue, + T: VecValue, +{ + name: Arc, + base_version: Version, + source: ReadableBoxedVec, + window_starts: CachedBoxedVec, + inclusive: bool, + compute: Arc T + Send + Sync>, +} + +impl LazyWindowVec +where + I: VecIndex, + S: VecValue + Default, + T: VecValue, +{ + pub fn new( + name: &str, + version: Version, + source: ReadableBoxedVec, + window_starts: CachedBoxedVec, + inclusive: bool, + compute: impl Fn(S, S, usize) -> T + Send + Sync + 'static, + ) -> Self { + Self { + name: Arc::from(name), + base_version: version, + source, + window_starts, + inclusive, + compute: Arc::new(compute), + } + } + + #[inline] + fn ago_index(&self, start: usize) -> Option { + if self.inclusive { + start.checked_sub(1) + } else { + Some(start) + } + } + + #[inline] + fn count(&self, current: usize, start: usize) -> usize { + if self.inclusive { + current - start + 1 + } else { + current - start + } + } + + fn for_each_window(&self, from: usize, to: usize, mut each: impl FnMut(T)) { + let window_starts = self.window_starts.cached(); + let to = to.min(self.len()).min(window_starts.len()); + if from >= to { + return; + } + + let starts = &window_starts[from..to]; + let current = self.source.collect_range_dyn(from, to); + + let first_ago = starts + .iter() + .find_map(|start| self.ago_index(start.to_usize())); + let last_ago = starts + .iter() + .rev() + .find_map(|start| self.ago_index(start.to_usize())); + let ago = first_ago + .zip(last_ago) + .map(|(first, last)| self.source.collect_range_dyn(first, last + 1)); + + for (offset, (current, start)) in current.into_iter().zip(starts).enumerate() { + let start = start.to_usize(); + let previous = self + .ago_index(start) + .and_then(|index| { + ago.as_ref() + .zip(first_ago) + .map(|(values, first)| values[index - first].clone()) + }) + .unwrap_or_default(); + each((self.compute)( + current, + previous, + self.count(from + offset, start), + )); + } + } +} + +impl Clone for LazyWindowVec +where + I: VecIndex, + S: VecValue, + T: VecValue, +{ + fn clone(&self) -> Self { + Self { + name: Arc::clone(&self.name), + base_version: self.base_version, + source: self.source.clone(), + window_starts: self.window_starts.clone(), + inclusive: self.inclusive, + compute: Arc::clone(&self.compute), + } + } +} + +impl AnyVec for LazyWindowVec +where + I: VecIndex, + S: VecValue, + T: VecValue, +{ + fn version(&self) -> Version { + self.base_version + self.source.version() + self.window_starts.version() + } + + fn name(&self) -> &str { + &self.name + } + + fn index_type_to_string(&self) -> &'static str { + I::to_string() + } + + fn len(&self) -> usize { + self.source.len() + } + + fn value_type_to_size_of(&self) -> usize { + size_of::() + } + + fn value_type_to_string(&self) -> &'static str { + short_type_name::() + } + + fn region_names(&self) -> Vec { + Vec::new() + } +} + +impl TypedVec for LazyWindowVec +where + I: VecIndex, + S: VecValue, + T: VecValue, +{ + type I = I; + type T = T; +} + +impl ReadableVec for LazyWindowVec +where + I: VecIndex, + S: VecValue + Default, + T: VecValue, +{ + fn read_into_at(&self, from: usize, to: usize, buf: &mut Vec) { + buf.reserve(to.saturating_sub(from)); + self.for_each_window(from, to, |value| buf.push(value)); + } + + fn for_each_range_dyn_at(&self, from: usize, to: usize, f: &mut dyn FnMut(T)) { + self.for_each_window(from, to, f); + } + + fn fold_range_at B>(&self, from: usize, to: usize, init: B, f: F) -> B { + let mut values = Vec::with_capacity(to.saturating_sub(from)); + self.read_into_at(from, to, &mut values); + values.into_iter().fold(init, f) + } + + fn try_fold_range_at Result>( + &self, + from: usize, + to: usize, + init: B, + f: F, + ) -> Result { + let mut values = Vec::with_capacity(to.saturating_sub(from)); + self.read_into_at(from, to, &mut values); + values.into_iter().try_fold(init, f) + } + + fn collect_one_at(&self, index: usize) -> Option { + let window_starts = self.window_starts.cached(); + let start = window_starts.get(index)?.to_usize(); + let current = self.source.collect_one_at(index)?; + let previous = self + .ago_index(start) + .and_then(|index| self.source.collect_one_at(index)) + .unwrap_or_default(); + Some((self.compute)(current, previous, self.count(index, start))) + } + + fn read_sorted_into_at(&self, indices: &[usize], out: &mut Vec) { + out.reserve(indices.len()); + indices + .iter() + .filter_map(|&index| self.collect_one_at(index)) + .for_each(|value| out.push(value)); + } +} + +impl Traversable for LazyWindowVec +where + I: VecIndex, + S: VecValue + Default, + T: VecValue + Formattable + Serialize + JsonSchema, +{ + fn iter_any_exportable(&self) -> impl Iterator { + std::iter::once(self as &dyn AnyExportableVec) + } + + fn to_tree_node(&self) -> TreeNode { + make_leaf::(self) + } +} diff --git a/crates/brk_computer/src/internal/per_block/percent/lazy.rs b/crates/brk_computer/src/internal/per_block/percent/lazy.rs index c875b4d49..2fe14dee0 100644 --- a/crates/brk_computer/src/internal/per_block/percent/lazy.rs +++ b/crates/brk_computer/src/internal/per_block/percent/lazy.rs @@ -1,14 +1,19 @@ use brk_traversable::Traversable; -use brk_types::{StoredF32, Version}; +use brk_types::{Height, StoredF32, Version}; use derive_more::{Deref, DerefMut}; -use vecdb::{ReadableCloneableVec, UnaryTransform}; +use vecdb::{ReadableCloneableVec, ReadableVec, TypedVec, UnaryTransform, VecValue}; -use crate::internal::{FixedRatio, LazyPerBlock, Percent, PercentPerBlock}; +use crate::{ + indexes, + internal::{ + Cagr, FixedRatio, Identity, LazyLookbackVec, LazyPerBlock, Percent, PercentPerBlock, + }, +}; /// Fully lazy variant of `PercentPerBlock` — no stored vecs. /// -/// PPM values are lazily derived from a source `PercentPerBlock` via a unary transform, -/// and ratio/percent float views are chained from them. +/// PPM values are lazily derived from one source, and ratio/percent float views +/// are chained from them. #[derive(Clone, Deref, DerefMut, Traversable)] #[traversable(transparent)] pub struct LazyPercentPerBlock( @@ -16,6 +21,65 @@ pub struct LazyPercentPerBlock( ); impl LazyPercentPerBlock { + pub(crate) fn from_height_source( + name: &str, + version: Version, + source: V, + indexes: &indexes::Vecs, + ) -> Self + where + V: TypedVec + ReadableVec + Clone + 'static, + { + let ppm_name = format!("{name}_{}", B::SUFFIX); + let ppm = + LazyPerBlock::from_height_source::, _>(&ppm_name, version, source, indexes); + Self::from_ppm(name, version, ppm) + } + + /// Create from one height-indexed source. The fixed-point ratio is computed + /// lazily at height, then all coarser resolutions are derived from it. + pub(crate) fn from_indexed_source( + name: &str, + version: Version, + source: &(impl ReadableCloneableVec + 'static), + compute: fn(Height, S) -> B, + indexes: &indexes::Vecs, + ) -> Self + where + S: VecValue, + { + let ppm_name = format!("{name}_{}", B::SUFFIX); + let ppm = LazyPerBlock::from_indexed_source(&ppm_name, version, source, compute, indexes); + + Self::from_ppm(name, version, ppm) + } + + /// Create from two values a fixed distance apart in one height source. + pub(crate) fn from_lookback_source( + name: &str, + version: Version, + source: &(impl ReadableCloneableVec + 'static), + lookback: usize, + compute: fn(S, Option) -> B, + indexes: &indexes::Vecs, + ) -> Self + where + S: VecValue, + { + let ppm_name = format!("{name}_{}", B::SUFFIX); + let source = LazyLookbackVec::new( + &format!("{ppm_name}_source"), + version, + source.read_only_boxed_clone(), + lookback, + compute, + ); + let ppm = + LazyPerBlock::from_height_source::, _>(&ppm_name, version, source, indexes); + + Self::from_ppm(name, version, ppm) + } + /// Create from a stored `PercentPerBlock` source via a same-unit unary transform. pub(crate) fn from_percent>( name: &str, @@ -29,11 +93,23 @@ impl LazyPercentPerBlock { &source.ppm, ); + Self::from_ppm(name, version, ppm) + } + + pub(crate) fn from_lazy_percent>( + name: &str, + version: Version, + source: &Self, + ) -> Self { + let ppm = + LazyPerBlock::from_lazy::(&format!("{name}_{}", B::SUFFIX), version, &source.ppm); + Self::from_ppm(name, version, ppm) + } + + fn from_ppm(name: &str, version: Version, ppm: LazyPerBlock) -> Self { let ratio = LazyPerBlock::from_lazy::(&format!("{name}_ratio"), version, &ppm); - let percent = LazyPerBlock::from_lazy::(name, version, &ppm); - Self(Percent { ppm, ratio, @@ -41,3 +117,36 @@ impl LazyPercentPerBlock { }) } } + +impl LazyPercentPerBlock { + pub(crate) fn from_cagr( + name: &str, + version: Version, + years: u8, + source: &PercentPerBlock, + ) -> Self { + match years { + 2 => Self::from_percent::>(name, version, source), + 3 => Self::from_percent::>(name, version, source), + 4 => Self::from_percent::>(name, version, source), + 5 => Self::from_percent::>(name, version, source), + 6 => Self::from_percent::>(name, version, source), + 8 => Self::from_percent::>(name, version, source), + 10 => Self::from_percent::>(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::>(name, version, source), + 3 => Self::from_lazy_percent::>(name, version, source), + 4 => Self::from_lazy_percent::>(name, version, source), + 5 => Self::from_lazy_percent::>(name, version, source), + 6 => Self::from_lazy_percent::>(name, version, source), + 8 => Self::from_lazy_percent::>(name, version, source), + 10 => Self::from_lazy_percent::>(name, version, source), + _ => unreachable!("unsupported DCA CAGR period: {years} years"), + } + } +} diff --git a/crates/brk_computer/src/internal/per_block/percent/lazy_vec.rs b/crates/brk_computer/src/internal/per_block/percent/lazy_vec.rs new file mode 100644 index 000000000..7074a5fec --- /dev/null +++ b/crates/brk_computer/src/internal/per_block/percent/lazy_vec.rs @@ -0,0 +1,43 @@ +use brk_traversable::Traversable; +use brk_types::{Height, StoredF32, Version}; +use derive_more::{Deref, DerefMut}; +use vecdb::{LazyVecFrom1, ReadableCloneableVec, VecValue}; + +use crate::internal::{FixedRatio, Percent}; + +/// Fully lazy lightweight percent container with no derived resolutions. +#[derive(Clone, Deref, DerefMut, Traversable)] +#[traversable(transparent)] +#[allow(clippy::type_complexity)] +pub struct LazyPercentVec( + pub Percent, LazyVecFrom1>, +); + +impl LazyPercentVec { + pub(crate) fn from_indexed_source( + name: &str, + version: Version, + source: &(impl ReadableCloneableVec + 'static), + compute: fn(Height, S) -> B, + ) -> Self { + let ppm = LazyVecFrom1::init( + &format!("{name}_{}", B::SUFFIX), + version, + source.read_only_boxed_clone(), + compute, + ); + let ppm_source = ppm.read_only_boxed_clone(); + let ratio = LazyVecFrom1::transformed::( + &format!("{name}_ratio"), + version, + ppm_source.clone(), + ); + let percent = LazyVecFrom1::transformed::(name, version, ppm_source); + + Self(Percent { + ppm, + ratio, + percent, + }) + } +} diff --git a/crates/brk_computer/src/internal/per_block/percent/lazy_windows.rs b/crates/brk_computer/src/internal/per_block/percent/lazy_windows.rs index 1c835e30e..1ab2c3069 100644 --- a/crates/brk_computer/src/internal/per_block/percent/lazy_windows.rs +++ b/crates/brk_computer/src/internal/per_block/percent/lazy_windows.rs @@ -1,21 +1,60 @@ use brk_traversable::Traversable; -use brk_types::Version; +use brk_types::{Height, Version}; use derive_more::{Deref, DerefMut}; -use vecdb::UnaryTransform; +use schemars::JsonSchema; +use vecdb::{DeltaAvg, LazyDeltaVec, ReadOnlyClone, ReadableCloneableVec, UnaryTransform}; -use crate::internal::{FixedRatio, PercentRollingWindows, Windows}; +use crate::{ + indexes, + internal::{FixedRatio, NumericValue, PercentRollingWindows, WindowStartVec, Windows}, +}; use super::LazyPercentPerBlock; /// Fully lazy rolling percent windows — 4 windows (24h, 1w, 1m, 1y), /// each with lazy PPM + lazy ratio/percent float views. /// -/// No stored vecs. All values derived from a source `PercentRollingWindows`. +/// No stored vecs. All values are derived from one source. #[derive(Clone, Deref, DerefMut, Traversable)] #[traversable(transparent)] pub struct LazyPercentRollingWindows(pub Windows>); impl LazyPercentRollingWindows { + /// Rolling percentages derived lazily from a single cumulative source. + pub(crate) fn from_cumulative_average( + name: &str, + version: Version, + cumulative: &(impl ReadableCloneableVec + 'static), + cached_starts: &Windows<&WindowStartVec>, + indexes: &indexes::Vecs, + ) -> Self + where + T: NumericValue + JsonSchema, + { + let cumulative_source = cumulative.read_only_boxed_clone(); + + Self(cached_starts.map_with_suffix(|suffix, cached_start| { + let full_name = format!("{name}_{suffix}"); + let cached = cached_start.read_only_clone(); + let starts_version = cached.version(); + let average = LazyDeltaVec::::new( + &format!("{full_name}_{}_source", B::SUFFIX), + version, + cumulative_source.clone(), + starts_version, + move || cached.cached(), + ); + + LazyPercentPerBlock::from_indexed_source( + &full_name, + version, + &average, + |_, ratio| ratio, + indexes, + ) + })) + } + /// Create from a stored source via a same-unit unary transform. pub(crate) fn from_rolling>( name: &str, diff --git a/crates/brk_computer/src/internal/per_block/percent/mod.rs b/crates/brk_computer/src/internal/per_block/percent/mod.rs index eb379862c..51de0f09f 100644 --- a/crates/brk_computer/src/internal/per_block/percent/mod.rs +++ b/crates/brk_computer/src/internal/per_block/percent/mod.rs @@ -2,14 +2,14 @@ mod base; mod cumulative_rolling; mod lazy; mod lazy_cumulative_rolling; +mod lazy_vec; mod lazy_windows; -mod vec; mod windows; pub use base::*; pub use cumulative_rolling::*; pub use lazy::*; pub use lazy_cumulative_rolling::*; +pub use lazy_vec::*; pub use lazy_windows::*; -pub use vec::*; pub use windows::*; diff --git a/crates/brk_computer/src/internal/per_block/percent/vec.rs b/crates/brk_computer/src/internal/per_block/percent/vec.rs deleted file mode 100644 index 277b7c9e9..000000000 --- a/crates/brk_computer/src/internal/per_block/percent/vec.rs +++ /dev/null @@ -1,43 +0,0 @@ -use brk_traversable::Traversable; -use brk_types::{Height, StoredF32, Version}; -use derive_more::{Deref, DerefMut}; -use vecdb::{ - Database, EagerVec, ImportableVec, LazyVecFrom1, PcoVec, ReadableCloneableVec, Rw, StorageMode, -}; - -use crate::internal::{FixedRatio, Percent}; - -/// Lightweight percent container: fixed-point height vec + lazy ratio + lazy percent. -/// No resolutions, no rolling stats. -#[derive(Clone, Deref, DerefMut, Traversable)] -#[traversable(transparent)] -#[allow(clippy::type_complexity)] -pub struct PercentVec( - pub Percent>>, LazyVecFrom1>, -); - -impl PercentVec { - pub(crate) fn forced_import( - db: &Database, - name: &str, - version: Version, - ) -> brk_error::Result { - let ppm: EagerVec> = - EagerVec::forced_import(db, &format!("{name}_{}", B::SUFFIX), version)?; - let ppm_clone = ppm.read_only_boxed_clone(); - - let ratio = LazyVecFrom1::transformed::( - &format!("{name}_ratio"), - version, - ppm_clone.clone(), - ); - - let percent = LazyVecFrom1::transformed::(name, version, ppm_clone); - - Ok(Self(Percent { - ppm, - ratio, - percent, - })) - } -} diff --git a/crates/brk_computer/src/internal/per_block/price.rs b/crates/brk_computer/src/internal/per_block/price.rs index aa03cc6db..6d91de894 100644 --- a/crates/brk_computer/src/internal/per_block/price.rs +++ b/crates/brk_computer/src/internal/per_block/price.rs @@ -8,7 +8,9 @@ use brk_error::Result; use brk_traversable::Traversable; use brk_types::{Cents, Dollars, SatsFract, Version}; use schemars::JsonSchema; -use vecdb::{Database, ReadableCloneableVec, UnaryTransform}; +use vecdb::{ + Database, ReadableCloneableVec, ReadableVec, TypedVec, UnaryTransform, +}; use super::{LazyPerBlock, PerBlock}; use crate::{ @@ -73,3 +75,33 @@ where Self { usd, cents, sats } } } + +impl Price> { + pub(crate) fn from_height_source( + name: &str, + version: Version, + source: V, + indexes: &indexes::Vecs, + ) -> Self + where + V: TypedVec + + ReadableVec + + Clone + + 'static, + { + let cents = LazyPerBlock::from_height_source::, _>( + &format!("{name}_cents"), + version, + source, + indexes, + ); + let usd = + LazyPerBlock::from_lazy::(name, version, ¢s); + let sats = LazyPerBlock::from_lazy::( + &format!("{name}_sats"), + version, + &usd, + ); + Self { usd, cents, sats } + } +} diff --git a/crates/brk_computer/src/internal/per_block/ratio/lazy.rs b/crates/brk_computer/src/internal/per_block/ratio/lazy.rs new file mode 100644 index 000000000..b528712a9 --- /dev/null +++ b/crates/brk_computer/src/internal/per_block/ratio/lazy.rs @@ -0,0 +1,38 @@ +use brk_traversable::Traversable; +use brk_types::{StoredF32, Version}; +use schemars::JsonSchema; +use vecdb::{ReadableCloneableVec, UnaryTransform}; + +use crate::internal::{FixedRatio, LazyPerBlock, NumericValue, PerBlock}; + +/// Fully lazy variant of `RatioPerBlock` derived from one per-block source. +#[derive(Clone, Traversable)] +pub struct LazyRatioPerBlock +where + R: FixedRatio, + S: NumericValue + JsonSchema, +{ + pub ppm: LazyPerBlock, + pub ratio: LazyPerBlock, +} + +impl LazyRatioPerBlock +where + R: FixedRatio, + S: NumericValue + JsonSchema, +{ + pub(crate) fn from_source(name: &str, version: Version, source: &PerBlock) -> Self + where + F: UnaryTransform, + { + let ppm = LazyPerBlock::from_computed::( + &format!("{name}_{}", R::SUFFIX), + version, + source.height.read_only_boxed_clone(), + source, + ); + let ratio = LazyPerBlock::from_lazy::(name, version, &ppm); + + Self { ppm, ratio } + } +} diff --git a/crates/brk_computer/src/internal/per_block/ratio/mod.rs b/crates/brk_computer/src/internal/per_block/ratio/mod.rs index f3cf939e9..9f8abdc28 100644 --- a/crates/brk_computer/src/internal/per_block/ratio/mod.rs +++ b/crates/brk_computer/src/internal/per_block/ratio/mod.rs @@ -1,7 +1,9 @@ mod base; +mod lazy; mod price; mod windows; pub use base::*; +pub use lazy::*; pub use price::*; pub use windows::*; diff --git a/crates/brk_computer/src/internal/per_block/ratio/price.rs b/crates/brk_computer/src/internal/per_block/ratio/price.rs index ce3cfbe39..80deb1deb 100644 --- a/crates/brk_computer/src/internal/per_block/ratio/price.rs +++ b/crates/brk_computer/src/internal/per_block/ratio/price.rs @@ -2,7 +2,7 @@ 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}; +use vecdb::{Database, EagerVec, Exit, PcoVec, ReadableVec, Rw, StorageMode, unlikely}; use crate::internal::{LazyPerBlock, PerBlock, Price}; use crate::{indexes, price}; @@ -26,7 +26,7 @@ impl PriceWithRatioPerBlock { indexes: &indexes::Vecs, ) -> Result { let price = Price::forced_import(db, name, version, indexes)?; - let ratio = RatioPerBlock::forced_import(db, name, version, indexes)?; + let ratio = RatioPerBlock::forced_import(db, name, version + Version::ONE, indexes)?; Ok(Self { usd: price.usd, cents: price.cents, @@ -47,16 +47,7 @@ impl PriceWithRatioPerBlock { starting_lengths.height, close_price, &self.cents.height, - |(i, close, price, ..)| { - if price == Cents::ZERO { - (i, PartsPerMillion64::from(1.0)) - } else { - ( - i, - PartsPerMillion64::from(f64::from(close) / f64::from(price)), - ) - } - }, + |(i, close, price, ..)| (i, price_ratio(close, price)), exit, )?; Ok(()) @@ -77,3 +68,25 @@ impl PriceWithRatioPerBlock { self.compute_ratio(starting_lengths, &prices.spot.cents.height, exit) } } + +fn price_ratio(close: Cents, price: Cents) -> PartsPerMillion64 { + if unlikely(price == Cents::ZERO) { + PartsPerMillion64::NAN + } else { + PartsPerMillion64::from(f64::from(close) / f64::from(price)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn zero_price_has_no_ratio() { + assert!(price_ratio(Cents::new(100), Cents::ZERO).is_nan()); + assert_eq!( + price_ratio(Cents::new(100), Cents::new(50)), + PartsPerMillion64::from(2.0), + ); + } +} diff --git a/crates/brk_computer/src/internal/transform/arithmetic.rs b/crates/brk_computer/src/internal/transform/arithmetic.rs index f0921797f..cf7788df9 100644 --- a/crates/brk_computer/src/internal/transform/arithmetic.rs +++ b/crates/brk_computer/src/internal/transform/arithmetic.rs @@ -120,6 +120,27 @@ impl UnaryTransform for BlocksToDaysF32 { } } +pub struct StoredU64ToStoredU32; + +impl UnaryTransform for StoredU64ToStoredU32 { + #[inline(always)] + fn apply(value: StoredU64) -> StoredU32 { + StoredU32::new( + u32::try_from(u64::from(value)) + .expect("per-block value reconstructed from StoredU64 must fit StoredU32"), + ) + } +} + +pub struct PerSecond; + +impl UnaryTransform for PerSecond { + #[inline(always)] + fn apply(value: StoredU64) -> StoredF32 { + StoredF32::from(u64::from(value) as f64 / SECONDS as f64) + } +} + pub struct OneMinusF64; impl UnaryTransform for OneMinusF64 { @@ -129,6 +150,15 @@ impl UnaryTransform for OneMinusF64 { } } +pub struct OddsF64; + +impl UnaryTransform for OddsF64 { + #[inline(always)] + fn apply(value: StoredF64) -> StoredF64 { + value / StoredF64::from(1.0 - *value) + } +} + pub struct DifficultyToHashF64; impl UnaryTransform for DifficultyToHashF64 { @@ -165,3 +195,16 @@ impl UnaryTransform for WeightToVSize { VSize::from(weight) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn odds_are_the_ratio_to_the_complement() { + assert_eq!(OddsF64::apply(StoredF64::from(0.0)), StoredF64::from(0.0)); + assert_eq!(OddsF64::apply(StoredF64::from(0.5)), StoredF64::from(1.0)); + assert_eq!(OddsF64::apply(StoredF64::from(0.75)), StoredF64::from(3.0)); + assert_eq!(OddsF64::apply(StoredF64::from(1.0)), StoredF64::NAN); + } +} diff --git a/crates/brk_computer/src/internal/transform/currency.rs b/crates/brk_computer/src/internal/transform/currency.rs index cf7b509b4..6ad2e2136 100644 --- a/crates/brk_computer/src/internal/transform/currency.rs +++ b/crates/brk_computer/src/internal/transform/currency.rs @@ -1,5 +1,5 @@ use brk_types::{Bitcoin, Cents, CentsSigned, Dollars, Sats, SatsFract, SatsSigned, StoredF32}; -use vecdb::{BinaryTransform, UnaryTransform}; +use vecdb::{BinaryTransform, UnaryTransform, unlikely}; pub struct SatsToBitcoin; @@ -42,7 +42,11 @@ pub struct SatsToCents; impl BinaryTransform for SatsToCents { #[inline(always)] fn apply(sats: Sats, price_cents: Cents) -> Cents { - Cents::from(sats.as_u128() * price_cents.as_u128() / Sats::ONE_BTC_U128) + if unlikely(price_cents.is_nan()) { + Cents::NAN + } else { + Cents::from(sats.as_u128() * price_cents.as_u128() / Sats::ONE_BTC_U128) + } } } @@ -78,6 +82,9 @@ pub struct CentsUnsignedToSats; impl UnaryTransform for CentsUnsignedToSats { #[inline(always)] fn apply(cents: Cents) -> Sats { + if unlikely(cents.is_nan()) { + panic!("Cents::NAN cannot be converted to whole Sats"); + } let dollars = Dollars::from(cents); if dollars == Dollars::ZERO { Sats::ZERO @@ -92,7 +99,13 @@ pub struct CentsSubtractToCentsSigned; impl BinaryTransform for CentsSubtractToCentsSigned { #[inline(always)] fn apply(a: Cents, b: Cents) -> CentsSigned { - CentsSigned::from(a.inner() as i64 - b.inner() as i64) + if unlikely(a.is_nan() || b.is_nan()) { + panic!("Cents::NAN cannot be converted to CentsSigned"); + } + let difference = i128::from(a.inner()) - i128::from(b.inner()); + CentsSigned::new( + i64::try_from(difference).expect("Cents subtraction overflowed CentsSigned"), + ) } } @@ -101,7 +114,11 @@ pub struct CentsTimesTenths; impl UnaryTransform for CentsTimesTenths { #[inline(always)] fn apply(c: Cents) -> Cents { - Cents::from(c.as_u128() * V as u128 / 10) + if unlikely(c.is_nan()) { + Cents::NAN + } else { + Cents::from(c.as_u128() * V as u128 / 10) + } } } @@ -113,3 +130,26 @@ impl UnaryTransform for DollarsToSatsFract { SatsFract::ONE_BTC / usd } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn cents_outputs_propagate_nan() { + assert!(SatsToCents::apply(Sats::ONE_BTC, Cents::NAN).is_nan()); + assert!(CentsTimesTenths::<24>::apply(Cents::NAN).is_nan()); + } + + #[test] + #[should_panic(expected = "Cents::NAN cannot be converted to whole Sats")] + fn whole_sats_reject_nan() { + CentsUnsignedToSats::apply(Cents::NAN); + } + + #[test] + #[should_panic(expected = "Cents::NAN cannot be converted to CentsSigned")] + fn signed_cents_reject_nan() { + CentsSubtractToCentsSigned::apply(Cents::NAN, Cents::ZERO); + } +} diff --git a/crates/brk_computer/src/internal/transform/derived.rs b/crates/brk_computer/src/internal/transform/derived.rs index 4c0c90955..c12a87b73 100644 --- a/crates/brk_computer/src/internal/transform/derived.rs +++ b/crates/brk_computer/src/internal/transform/derived.rs @@ -1,7 +1,7 @@ use std::marker::PhantomData; -use brk_types::{Cents, StoredF32, StoredF64}; -use vecdb::{BinaryTransform, UnaryTransform}; +use brk_types::{Cents, PartsPerMillionSigned64, StoredF32, StoredF64}; +use vecdb::{BinaryTransform, UnaryTransform, unlikely}; use crate::internal::FixedRatio; @@ -14,6 +14,18 @@ impl UnaryTransform for DaysToYears { } } +pub struct Cagr; + +impl UnaryTransform + for Cagr +{ + #[inline(always)] + fn apply(value: PartsPerMillionSigned64) -> PartsPerMillionSigned64 { + let ratio = f64::from(value); + PartsPerMillionSigned64::from((ratio + 1.0).powf(1.0 / YEARS as f64) - 1.0) + } +} + pub trait SqrtDays { const FACTOR: f32; } @@ -61,10 +73,22 @@ pub struct RatioCents64; impl BinaryTransform for RatioCents64 { #[inline(always)] fn apply(numerator: Cents, denominator: Cents) -> StoredF64 { - if denominator == Cents::ZERO { + let denominator = f64::from(denominator); + if unlikely(denominator == 0.0) { StoredF64::from(1.0) } else { - StoredF64::from(numerator.inner() as f64 / denominator.inner() as f64) + StoredF64::from(f64::from(numerator) / denominator) } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn cents_ratio_64_propagates_nan() { + assert!(RatioCents64::apply(Cents::NAN, Cents::new(100)).is_nan()); + assert!(RatioCents64::apply(Cents::new(100), Cents::NAN).is_nan()); + } +} diff --git a/crates/brk_computer/src/internal/transform/mod.rs b/crates/brk_computer/src/internal/transform/mod.rs index c2199fed0..a0b49038c 100644 --- a/crates/brk_computer/src/internal/transform/mod.rs +++ b/crates/brk_computer/src/internal/transform/mod.rs @@ -7,8 +7,8 @@ mod specialized; pub use arithmetic::{ BlocksToDaysF32, DifficultyToHashF64, HalveCents, HalveDollars, HalveSats, HalveSatsToBitcoin, - Identity, MaskSats, OneMinusF64, OneMinusPpm, ReturnF32Tenths, ReturnI8, ReturnU16, - ThsToPhsF32, VBytesToWeight, WeightToVSize, + Identity, MaskSats, OddsF64, OneMinusF64, OneMinusPpm, PerSecond, ReturnF32Tenths, ReturnI8, + ReturnU16, StoredU64ToStoredU32, ThsToPhsF32, VBytesToWeight, WeightToVSize, }; pub use currency::{ AvgCentsToUsd, AvgSatsToBtc, CentsSignedToDollars, CentsSubtractToCentsSigned, @@ -16,12 +16,12 @@ pub use currency::{ NegCentsUnsignedToDollars, SatsSignedToBitcoin, SatsToBitcoin, SatsToCents, }; pub use derived::{ - Days1, Days7, Days30, Days365, DaysToYears, PriceTimesRatio, RatioCents64, TimesSqrt, + Cagr, Days1, Days7, Days30, Days365, DaysToYears, PriceTimesRatio, RatioCents64, TimesSqrt, }; pub use fixed_ratio::{FixedToPercent, FixedToRatio}; pub use ratio::{ RatioCents, RatioCentsSignedCents, RatioCentsSignedDollars, RatioDiffCents, RatioDiffDollars, - RatioDiffF32, RatioDollars, RatioSats, RatioU64, RatioU64F32, + RatioDiffF32, RatioDollars, RatioSats, RatioU64, }; pub use specialized::{ BlockCountTarget1m, BlockCountTarget1w, BlockCountTarget1y, BlockCountTarget24h, diff --git a/crates/brk_computer/src/internal/transform/ratio.rs b/crates/brk_computer/src/internal/transform/ratio.rs index 56fcd9eba..b94b687c8 100644 --- a/crates/brk_computer/src/internal/transform/ratio.rs +++ b/crates/brk_computer/src/internal/transform/ratio.rs @@ -1,7 +1,7 @@ use std::marker::PhantomData; use brk_types::{Cents, CentsSigned, Dollars, Sats, StoredF32, StoredU64}; -use vecdb::BinaryTransform; +use vecdb::{BinaryTransform, unlikely}; use crate::internal::FixedRatio; @@ -36,10 +36,11 @@ pub struct RatioCents

(PhantomData

); impl BinaryTransform for RatioCents

{ #[inline(always)] fn apply(numerator: Cents, denominator: Cents) -> P { - if denominator == Cents::ZERO { + let denominator = f64::from(denominator); + if unlikely(denominator == 0.0) { P::default() } else { - P::from(numerator.inner() as f64 / denominator.inner() as f64) + P::from(f64::from(numerator) / denominator) } } } @@ -63,10 +64,11 @@ pub struct RatioCentsSignedCents

(PhantomData

); impl BinaryTransform for RatioCentsSignedCents

{ #[inline(always)] fn apply(numerator: CentsSigned, denominator: Cents) -> P { - if denominator == Cents::ZERO { + let denominator = f64::from(denominator); + if unlikely(denominator == 0.0) { P::default() } else { - P::from(numerator.inner() as f64 / denominator.inner() as f64) + P::from(numerator.inner() as f64 / denominator) } } } @@ -85,19 +87,6 @@ impl BinaryTransform for RatioCentsSigne } } -pub struct RatioU64F32; - -impl BinaryTransform for RatioU64F32 { - #[inline(always)] - fn apply(numerator: StoredU64, denominator: StoredU64) -> StoredF32 { - if *denominator > 0 { - StoredF32::from(*numerator as f64 / *denominator as f64) - } else { - StoredF32::default() - } - } -} - pub struct RatioDiffF32

(PhantomData

); impl BinaryTransform for RatioDiffF32

{ @@ -138,3 +127,20 @@ impl BinaryTransform for RatioDiffCents

{ } } } + +#[cfg(test)] +mod tests { + use brk_types::PartsPerMillion32; + + use super::*; + + #[test] + fn cents_ratios_propagate_nan() { + assert!(RatioCents::::apply(Cents::NAN, Cents::new(100)).is_nan()); + assert!(RatioCents::::apply(Cents::new(100), Cents::NAN).is_nan()); + assert!( + RatioCentsSignedCents::::apply(CentsSigned::new(100), Cents::NAN,) + .is_nan() + ); + } +} diff --git a/crates/brk_computer/src/investing/by_class.rs b/crates/brk_computer/src/investing/by_class.rs index c7a2a668b..71eaf493d 100644 --- a/crates/brk_computer/src/investing/by_class.rs +++ b/crates/brk_computer/src/investing/by_class.rs @@ -73,6 +73,91 @@ impl ByDcaClass { }) } + pub(crate) fn try_from_class( + class: &ByDcaClass, + mut create: F, + ) -> Result + where + F: FnMut(&'static str, u16, Day1, &U) -> Result, + { + let n = DCA_CLASS_NAMES; + let y = DCA_CLASS_YEARS; + Ok(Self { + from_2015: create( + n.from_2015, + y.from_2015, + Self::day1(y.from_2015), + &class.from_2015, + )?, + from_2016: create( + n.from_2016, + y.from_2016, + Self::day1(y.from_2016), + &class.from_2016, + )?, + from_2017: create( + n.from_2017, + y.from_2017, + Self::day1(y.from_2017), + &class.from_2017, + )?, + from_2018: create( + n.from_2018, + y.from_2018, + Self::day1(y.from_2018), + &class.from_2018, + )?, + from_2019: create( + n.from_2019, + y.from_2019, + Self::day1(y.from_2019), + &class.from_2019, + )?, + from_2020: create( + n.from_2020, + y.from_2020, + Self::day1(y.from_2020), + &class.from_2020, + )?, + from_2021: create( + n.from_2021, + y.from_2021, + Self::day1(y.from_2021), + &class.from_2021, + )?, + from_2022: create( + n.from_2022, + y.from_2022, + Self::day1(y.from_2022), + &class.from_2022, + )?, + from_2023: create( + n.from_2023, + y.from_2023, + Self::day1(y.from_2023), + &class.from_2023, + )?, + from_2024: create( + n.from_2024, + y.from_2024, + Self::day1(y.from_2024), + &class.from_2024, + )?, + from_2025: create( + n.from_2025, + y.from_2025, + Self::day1(y.from_2025), + &class.from_2025, + )?, + from_2026: create( + n.from_2026, + y.from_2026, + Self::day1(y.from_2026), + &class.from_2026, + )?, + }) + } + pub(crate) fn day1(year: u16) -> Day1 { Day1::try_from(Date::new(year, 1, 1)).unwrap() } @@ -113,21 +198,4 @@ impl ByDcaClass { .into_iter() } - pub(crate) fn start_days() -> [Day1; 12] { - let y = DCA_CLASS_YEARS; - [ - Self::day1(y.from_2015), - Self::day1(y.from_2016), - Self::day1(y.from_2017), - Self::day1(y.from_2018), - Self::day1(y.from_2019), - Self::day1(y.from_2020), - Self::day1(y.from_2021), - Self::day1(y.from_2022), - Self::day1(y.from_2023), - Self::day1(y.from_2024), - Self::day1(y.from_2025), - Self::day1(y.from_2026), - ] - } } diff --git a/crates/brk_computer/src/investing/by_period.rs b/crates/brk_computer/src/investing/by_period.rs index 04c9b2cb8..87d201799 100644 --- a/crates/brk_computer/src/investing/by_period.rs +++ b/crates/brk_computer/src/investing/by_period.rs @@ -96,6 +96,31 @@ impl ByDcaPeriod { }) } + pub(crate) fn try_from_period( + period: &ByDcaPeriod, + mut create: F, + ) -> Result + where + F: FnMut(&'static str, u32, &U) -> Result, + { + let n = DCA_PERIOD_NAMES; + let d = DCA_PERIOD_DAYS; + Ok(Self { + _1w: create(n._1w, d._1w, &period._1w)?, + _1m: create(n._1m, d._1m, &period._1m)?, + _3m: create(n._3m, d._3m, &period._3m)?, + _6m: create(n._6m, d._6m, &period._6m)?, + _1y: create(n._1y, d._1y, &period._1y)?, + _2y: create(n._2y, d._2y, &period._2y)?, + _3y: create(n._3y, d._3y, &period._3y)?, + _4y: create(n._4y, d._4y, &period._4y)?, + _5y: create(n._5y, d._5y, &period._5y)?, + _6y: create(n._6y, d._6y, &period._6y)?, + _8y: create(n._8y, d._8y, &period._8y)?, + _10y: create(n._10y, d._10y, &period._10y)?, + }) + } + pub(crate) fn iter_mut(&mut self) -> impl Iterator { [ &mut self._1w, @@ -133,46 +158,6 @@ impl ByDcaPeriod { .into_iter() } - pub(crate) fn iter_mut_with_days(&mut self) -> impl Iterator { - let d = DCA_PERIOD_DAYS; - [ - (&mut self._1w, d._1w), - (&mut self._1m, d._1m), - (&mut self._3m, d._3m), - (&mut self._6m, d._6m), - (&mut self._1y, d._1y), - (&mut self._2y, d._2y), - (&mut self._3y, d._3y), - (&mut self._4y, d._4y), - (&mut self._5y, d._5y), - (&mut self._6y, d._6y), - (&mut self._8y, d._8y), - (&mut self._10y, d._10y), - ] - .into_iter() - } - - pub(crate) fn zip_mut_with_days<'a, U>( - &'a mut self, - other: &'a ByDcaPeriod, - ) -> impl Iterator { - let d = DCA_PERIOD_DAYS; - [ - (&mut self._1w, &other._1w, d._1w), - (&mut self._1m, &other._1m, d._1m), - (&mut self._3m, &other._3m, d._3m), - (&mut self._6m, &other._6m, d._6m), - (&mut self._1y, &other._1y, d._1y), - (&mut self._2y, &other._2y, d._2y), - (&mut self._3y, &other._3y, d._3y), - (&mut self._4y, &other._4y, d._4y), - (&mut self._5y, &other._5y, d._5y), - (&mut self._6y, &other._6y, d._6y), - (&mut self._8y, &other._8y, d._8y), - (&mut self._10y, &other._10y, d._10y), - ] - .into_iter() - } } impl ByDcaPeriod<&T> { @@ -208,38 +193,24 @@ pub struct ByDcaCagr { } impl ByDcaCagr { - pub(crate) fn try_new(mut create: F) -> Result + pub(crate) fn try_new( + period: &ByDcaPeriod, + mut create: F, + ) -> Result where - F: FnMut(&'static str, u32) -> Result, + F: FnMut(&'static str, u32, &U) -> Result, { let n = DCA_CAGR_NAMES; let d = DCA_CAGR_DAYS; Ok(Self { - _2y: create(n._2y, d._2y)?, - _3y: create(n._3y, d._3y)?, - _4y: create(n._4y, d._4y)?, - _5y: create(n._5y, d._5y)?, - _6y: create(n._6y, d._6y)?, - _8y: create(n._8y, d._8y)?, - _10y: create(n._10y, d._10y)?, + _2y: create(n._2y, d._2y, &period._2y)?, + _3y: create(n._3y, d._3y, &period._3y)?, + _4y: create(n._4y, d._4y, &period._4y)?, + _5y: create(n._5y, d._5y, &period._5y)?, + _6y: create(n._6y, d._6y, &period._6y)?, + _8y: create(n._8y, d._8y, &period._8y)?, + _10y: create(n._10y, d._10y, &period._10y)?, }) } - /// Zip with the matching subset of a ByDcaPeriod - pub(crate) fn zip_mut_with_period<'a, U>( - &'a mut self, - period: &'a ByDcaPeriod, - ) -> impl Iterator { - let d = DCA_CAGR_DAYS; - [ - (&mut self._2y, &period._2y, d._2y), - (&mut self._3y, &period._3y, d._3y), - (&mut self._4y, &period._4y, d._4y), - (&mut self._5y, &period._5y, d._5y), - (&mut self._6y, &period._6y, d._6y), - (&mut self._8y, &period._8y, d._8y), - (&mut self._10y, &period._10y, d._10y), - ] - .into_iter() - } } diff --git a/crates/brk_computer/src/investing/compute.rs b/crates/brk_computer/src/investing/compute.rs index a97ca2951..edd4f4c4e 100644 --- a/crates/brk_computer/src/investing/compute.rs +++ b/crates/brk_computer/src/investing/compute.rs @@ -1,12 +1,11 @@ use brk_error::Result; use brk_indexer::Indexer; -use brk_types::{Bitcoin, Cents, Date, Day1, Dollars, PartsPerMillionSigned64, Sats}; +use brk_types::{Bitcoin, Cents, PartsPerMillionSigned64, Sats}; use vecdb::{AnyVec, Exit, ReadableOptionVec, ReadableVec, VecIndex}; -use super::{ByDcaPeriod, Vecs}; -use crate::{blocks, indexes, internal::RatioDiffCents, market, price}; - -const DCA_AMOUNT: Dollars = Dollars::mint(100.0); +use super::vecs::DcaStack; +use super::{DCA_AMOUNT, Vecs}; +use crate::{indexes, internal::{RatioDiffCents, SatsToCents}, price}; impl Vecs { pub(crate) fn compute( @@ -14,262 +13,73 @@ impl Vecs { indexer: &Indexer, indexes: &indexes::Vecs, prices: &price::Vecs, - blocks: &blocks::Vecs, - lookback: &market::lookback::Vecs, exit: &Exit, ) -> Result<()> { self.db.sync_bg_tasks()?; - let starting_lengths = indexer.safe_lengths(); + let starting_height = indexer.safe_lengths().height; let h2d = &indexes.height.day1; let close = &prices.split.close.usd.day1; - let first_price_di = Day1::try_from(Date::new(2010, 7, 12)).unwrap().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)) + .unwrap_or_default(); + let mut last_day = start + .checked_sub(1) + .and_then(|height| h2d.collect_one_at(height)); - // Compute per-height DCA sats contribution once (reused by all periods). - // Value = sats_from_dca(close_price) on day-boundary blocks, Sats::ZERO otherwise. - { - let mut last_di: Option = None; - self.sats_per_day.compute_transform( - starting_lengths.height, - h2d, - |(h, di, _)| { - if last_di.is_none() && h.to_usize() > 0 { - last_di = Some(h2d.collect_one_at(h.to_usize() - 1).unwrap()); - } - let same_day = last_di.is_some_and(|prev| prev == di); - last_di = Some(di); - if same_day { - (h, Sats::ZERO) - } else { - let s = close - .collect_one_flat(di) - .map(sats_from_dca) - .unwrap_or(Sats::ZERO); - (h, s) - } - }, - exit, - )?; - } + self.sats_cumulative.compute_transform( + starting_height, + h2d, + |(height, day, _)| { + if last_day != Some(day) { + cumulative += close + .collect_one_flat(day) + .map(sats_from_dca) + .unwrap_or_default(); + } + last_day = Some(day); + (height, cumulative) + }, + exit, + )?; - // DCA by period - stack (rolling sum via _start vecs) - for (stack, days) in self.period.dca_stack.iter_mut_with_days() { - let window_starts = blocks.lookback.start_vec(days as usize); - stack.sats.height.compute_rolling_sum( - starting_lengths.height, - window_starts, - &self.sats_per_day, - exit, - )?; - } - - // DCA by period - stack cents (sats × price) for stack in self.period.dca_stack.iter_mut() { - stack.compute(prices, starting_lengths.height, exit)?; + stack.compute_cents(prices, starting_height, exit)?; + } + for stack in self.class.dca_stack.iter_mut() { + stack.compute_cents(prices, starting_height, exit)?; } - // DCA by period - average price (derived from stack) - let starting_height_usize = starting_lengths.height.to_usize(); - for (average_price, stack, days) in self - .period - .dca_cost_basis - .zip_mut_with_days(&self.period.dca_stack) - { - let days = days as usize; - average_price.cents.height.compute_transform2( - starting_lengths.height, - h2d, - &stack.sats.height, - |(h, di, stack_sats, ..)| { - let di_usize = di.to_usize(); - let avg = if di_usize > first_price_di { - let num_days = days.min(di_usize + 1 - first_price_di); - Cents::from(DCA_AMOUNT * num_days / Bitcoin::from(stack_sats)) - } else { - Cents::ZERO - }; - (h, avg) - }, - exit, - )?; - } - - // DCA by period - returns (compute from average price) - for (returns, (average_price, _)) in self + for (returns, (cost_basis, _)) in self .period .dca_return .iter_mut() .zip(self.period.dca_cost_basis.iter_with_days()) { returns.compute_binary::>( - starting_lengths.height, + starting_height, &prices.spot.cents.height, - &average_price.cents.height, + &cost_basis.cents.height, exit, )?; } - // DCA by period - CAGR (computed from returns at height level) - for (cagr, returns, days) in self - .period - .dca_cagr - .zip_mut_with_period(&self.period.dca_return) - { - let years = days as f64 / 365.0; - cagr.ppm.height.compute_transform( - starting_lengths.height, - &returns.ppm.height, - |(h, r, ..)| { - let ratio = f64::from(r); - let v = (ratio + 1.0).powf(1.0 / years) - 1.0; - (h, PartsPerMillionSigned64::from(v)) - }, - exit, - )?; - } - - // Lump sum by period - stack - let lookback_dca = ByDcaPeriod::from_lookback(&lookback.price_past); - for (stack, lookback_price, days) in - self.period.lump_sum_stack.zip_mut_with_days(&lookback_dca) - { - let total_invested = DCA_AMOUNT * days as usize; - stack.sats.height.compute_transform2( - starting_lengths.height, - h2d, - &lookback_price.cents.height, - |(h, _di, lp, ..)| { - let sats = if lp == Cents::ZERO { - Sats::ZERO - } else { - Sats::from(Bitcoin::from(total_invested / Dollars::from(lp))) - }; - (h, sats) - }, - exit, - )?; - } - - // Lump sum by period - stack cents (sats × price) - for stack in self.period.lump_sum_stack.iter_mut() { - stack.compute(prices, starting_lengths.height, exit)?; - } - - // Lump sum by period - returns (compute from lookback price) - for (returns, (lookback_price, _)) in self - .period - .lump_sum_return - .iter_mut() - .zip(lookback_dca.iter_with_days()) - { - returns.compute_binary::>( - starting_lengths.height, - &prices.spot.cents.height, - &lookback_price.cents.height, - exit, - )?; - } - - // DCA by year class - stack (cumulative sum from class start date) - let start_days = super::ByDcaClass::<()>::start_days(); - for (stack, day1) in self.class.dca_stack.iter_mut().zip(start_days) { - let mut last_di: Option = None; - let cls_start = stack.sats.height.len().min(starting_height_usize); - let mut prev_value = if cls_start > 0 { - stack - .sats - .height - .collect_one_at(cls_start - 1) - .unwrap_or_default() - } else { - Sats::ZERO - }; - - stack.sats.height.compute_transform( - starting_lengths.height, - h2d, - |(h, di, _)| { - let hi = h.to_usize(); - - if last_di.is_none() && hi > 0 { - last_di = Some(h2d.collect_one_at(hi - 1).unwrap()); - } - - if di < day1 { - last_di = Some(di); - prev_value = Sats::ZERO; - return (h, Sats::ZERO); - } - - let prev_di = last_di; - last_di = Some(di); - - let same_day = prev_di.is_some_and(|prev| prev == di); - let result = if same_day { - prev_value - } else { - let prev = if hi > 0 && prev_di.is_some_and(|pd| pd >= day1) { - prev_value - } else { - Sats::ZERO - }; - let s = close - .collect_one_flat(di) - .map(sats_from_dca) - .unwrap_or(Sats::ZERO); - prev + s - }; - prev_value = result; - (h, result) - }, - exit, - )?; - } - - // DCA by year class - stack cents (sats × price) - for stack in self.class.dca_stack.iter_mut() { - stack.compute(prices, starting_lengths.height, exit)?; - } - - // DCA by year class - average price (derived from stack) - let start_days = super::ByDcaClass::<()>::start_days(); - for ((average_price, stack), from) in self - .class - .dca_cost_basis - .iter_mut() - .zip(self.class.dca_stack.iter()) - .zip(start_days) - { - let from_usize = from.to_usize(); - average_price.cents.height.compute_transform2( - starting_lengths.height, - h2d, - &stack.sats.height, - |(h, di, stack_sats, ..)| { - let di_usize = di.to_usize(); - if di_usize < from_usize { - return (h, Cents::ZERO); - } - let num_days = di_usize + 1 - from_usize; - let avg = Cents::from(DCA_AMOUNT * num_days / Bitcoin::from(stack_sats)); - (h, avg) - }, - exit, - )?; - } - - // DCA by year class - returns (compute from average price) - for (returns, average_price) in self + for (returns, cost_basis) in self .class .dca_return .iter_mut() .zip(self.class.dca_cost_basis.iter()) { returns.compute_binary::>( - starting_lengths.height, + starting_height, &prices.spot.cents.height, - &average_price.cents.height, + &cost_basis.cents.height, exit, )?; } @@ -283,8 +93,24 @@ impl Vecs { } } -fn sats_from_dca(price: Dollars) -> Sats { - if price == Dollars::ZERO { +impl DcaStack { + fn compute_cents( + &mut self, + prices: &price::Vecs, + starting_height: brk_types::Height, + exit: &Exit, + ) -> Result<()> { + self.cents.compute_binary::( + 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 } else { Sats::from(Bitcoin::from(DCA_AMOUNT / price)) diff --git a/crates/brk_computer/src/investing/import.rs b/crates/brk_computer/src/investing/import.rs index fdca29a06..3d8d03f0d 100644 --- a/crates/brk_computer/src/investing/import.rs +++ b/crates/brk_computer/src/investing/import.rs @@ -1,17 +1,25 @@ use std::path::Path; -use brk_error::Result; -use brk_types::Version; -use vecdb::ImportableVec; +use brk_error::{Error, Result}; +use brk_types::{ + Bitcoin, Cents, Date, Day1, Dollars, Height, PartsPerMillionSigned64, Sats, Version, +}; +use vecdb::{ + BinaryTransform, CachedBoxedVec, CheckedSub, EagerVec, ImportableVec, PcoVec, + ReadableCloneableVec, ReadableVec, TypedVec, VecIndex, +}; -use super::vecs::{ClassVecs, PeriodVecs}; -use super::{ByDcaCagr, ByDcaClass, ByDcaPeriod, Vecs}; +use super::vecs::{ClassVecs, DcaStack, LumpSumStack, PeriodVecs}; +use super::{ByDcaCagr, ByDcaClass, ByDcaPeriod, DCA_AMOUNT, Vecs}; use crate::{ - indexes, + blocks, indexes, internal::{ - PercentPerBlock, Price, ValuePerBlock, + CentsUnsignedToDollars, Identity, LazyIndexedVec, LazyPerBlock, LazyPercentPerBlock, + LazyPreviousDeltaVec, LazySinceDayVec, LazyWindowVec, PerBlock, PercentPerBlock, Price, + RatioDiffCents, SatsToBitcoin, SatsToCents, db_utils::{finalize_db, open_db}, }, + price, }; impl Vecs { @@ -19,57 +27,175 @@ impl Vecs { parent_path: &Path, parent_version: Version, indexes: &indexes::Vecs, + blocks: &blocks::Vecs, + prices: &price::Vecs, ) -> Result { let db = open_db(parent_path, super::DB_NAME, 50_000)?; let version = parent_version; - let stack = ByDcaPeriod::try_new(|name, _days| { - ValuePerBlock::forced_import(&db, &format!("dca_stack_{name}"), version, indexes) + + let sats_cumulative: EagerVec> = + ImportableVec::forced_import(&db, "dca_sats_cumulative", version)?; + let sats_per_day = LazyPreviousDeltaVec::new( + "dca_sats_per_day", + version, + sats_cumulative.read_only_boxed_clone(), + ); + + let cached_starts = ByDcaPeriod::try_new(|_, days| { + Ok::<_, Error>( + blocks + .lookback + .cached_start_vec(days as usize) + .read_only_cached_boxed_clone(), + ) + })?; + let cached_days = indexes.height.day1.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::::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 cost_basis = ByDcaPeriod::try_new(|name, _days| { - Price::forced_import(&db, &format!("dca_cost_basis_{name}"), version, indexes) - })?; + 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 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 r#return = ByDcaPeriod::try_new(|name, _days| { - PercentPerBlock::forced_import(&db, &format!("dca_return_{name}"), version, indexes) - })?; - - let cagr = ByDcaCagr::try_new(|name, _days| { - PercentPerBlock::forced_import(&db, &format!("dca_cagr_{name}"), version, indexes) - })?; - - let lump_sum_stack = ByDcaPeriod::try_new(|name, _days| { - ValuePerBlock::forced_import(&db, &format!("lump_sum_stack_{name}"), version, indexes) - })?; - - let lump_sum_return = ByDcaPeriod::try_new(|name, _days| { + let dca_return = ByDcaPeriod::try_new(|name, _days| { PercentPerBlock::forced_import( &db, - &format!("lump_sum_return_{name}"), + &format!("dca_return_{name}"), version, indexes, ) })?; - let class_stack = ByDcaClass::try_new(|name, _year, _day1| { - ValuePerBlock::forced_import(&db, &format!("dca_stack_{name}"), version, indexes) + 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, + ) })?; - let class_cost_basis = ByDcaClass::try_new(|name, _year, _day1| { - Price::forced_import(&db, &format!("dca_cost_basis_{name}"), version, indexes) + 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::::new( + &format!("{metric_name}_ppm_source"), + version, + prices.spot.cents.height.read_only_boxed_clone(), + window_starts.clone(), + false, + |current, past, _| { + RatioDiffCents::::apply(current, past) + }, + ); + Ok::<_, Error>(LazyPercentPerBlock::from_height_source( + &metric_name, + version, + source, + indexes, + )) })?; - let class_return = ByDcaClass::try_new(|name, _year, _day1| { - PercentPerBlock::forced_import(&db, &format!("dca_return_{name}"), version, indexes) + let class_stack = ByDcaClass::try_new(|name, _year, day| { + let metric_name = format!("dca_stack_{name}"); + let source = LazySinceDayVec::new( + &format!("{metric_name}_sats_source"), + version, + sats_cumulative.read_only_boxed_clone(), + cached_days.clone(), + day, + |current, before| current.checked_sub(before).unwrap_or_default(), + ); + dca_stack_from_source(&db, &metric_name, version, indexes, source) + })?; + + let class_cost_basis = + ByDcaClass::try_from_class(&class_stack, |name, _year, from, 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 < from { + return Cents::ZERO; + } + let num_days = day.to_usize() + 1 - from.to_usize(); + Cents::from(DCA_AMOUNT * num_days / Bitcoin::from(stack_sats)) + }, + ); + Ok::<_, Error>(Price::from_height_source( + &metric_name, + version, + source, + indexes, + )) + })?; + + let class_return = ByDcaClass::try_new(|name, _year, _day| { + PercentPerBlock::forced_import( + &db, + &format!("dca_return_{name}"), + version, + indexes, + ) })?; let this = Self { - sats_per_day: ImportableVec::forced_import(&db, "dca_sats_per_day", version)?, + sats_per_day, + sats_cumulative, period: PeriodVecs { - dca_stack: stack, - dca_cost_basis: cost_basis, - dca_return: r#return, - dca_cagr: cagr, + dca_stack, + dca_cost_basis, + dca_return, + dca_cagr, lump_sum_stack, lump_sum_return, }, @@ -84,3 +210,97 @@ impl Vecs { Ok(this) } } + +fn dca_stack_from_source( + db: &vecdb::Database, + name: &str, + version: Version, + indexes: &indexes::Vecs, + source: V, +) -> Result +where + V: TypedVec + ReadableVec + Clone + 'static, +{ + let sats = + LazyPerBlock::from_height_source::, _>(&format!("{name}_sats"), version, source, indexes); + let btc = LazyPerBlock::from_lazy::(name, version, &sats); + let cents = PerBlock::forced_import(db, &format!("{name}_cents"), version, indexes)?; + let usd = LazyPerBlock::from_computed::( + &format!("{name}_usd"), + version, + cents.height.read_only_boxed_clone(), + ¢s, + ); + Ok(DcaStack { + btc, + sats, + usd, + cents, + }) +} + +fn lump_sum_stack( + name: &str, + days: u32, + version: Version, + indexes: &indexes::Vecs, + window_starts: &CachedBoxedVec, + prices: &price::Vecs, +) -> Result { + let total_invested = DCA_AMOUNT * days as usize; + + let sats_source = LazyWindowVec::::new( + &format!("{name}_sats_source"), + version, + prices.spot.cents.height.read_only_boxed_clone(), + window_starts.clone(), + false, + move |_, past, _| lump_sum_sats(total_invested, past), + ); + let sats = LazyPerBlock::from_height_source::, _>( + &format!("{name}_sats"), + version, + sats_source, + indexes, + ); + let btc = LazyPerBlock::from_lazy::(name, version, &sats); + + let cents_source = LazyWindowVec::::new( + &format!("{name}_cents_source"), + version, + prices.spot.cents.height.read_only_boxed_clone(), + window_starts.clone(), + false, + move |current, past, _| { + SatsToCents::apply(lump_sum_sats(total_invested, past), current) + }, + ); + let cents = LazyPerBlock::from_height_source::, _>( + &format!("{name}_cents"), + version, + cents_source, + indexes, + ); + let usd = LazyPerBlock::from_lazy::( + &format!("{name}_usd"), + version, + ¢s, + ); + + Ok(LumpSumStack { + btc, + sats, + usd, + cents, + }) +} + +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), + )) + } +} diff --git a/crates/brk_computer/src/investing/mod.rs b/crates/brk_computer/src/investing/mod.rs index cac6e47bc..cab8e1810 100644 --- a/crates/brk_computer/src/investing/mod.rs +++ b/crates/brk_computer/src/investing/mod.rs @@ -8,4 +8,7 @@ pub use by_class::*; pub use by_period::*; pub use vecs::Vecs; +use brk_types::Dollars; + pub const DB_NAME: &str = "investing"; +pub(crate) const DCA_AMOUNT: Dollars = Dollars::mint(100.0); diff --git a/crates/brk_computer/src/investing/vecs.rs b/crates/brk_computer/src/investing/vecs.rs index 618c81769..2f664f0f2 100644 --- a/crates/brk_computer/src/investing/vecs.rs +++ b/crates/brk_computer/src/investing/vecs.rs @@ -1,24 +1,42 @@ use brk_traversable::Traversable; -use brk_types::{Cents, Height, PartsPerMillionSigned64, Sats}; +use brk_types::{Bitcoin, Cents, Dollars, Height, PartsPerMillionSigned64, Sats}; use vecdb::{Database, EagerVec, PcoVec, Rw, StorageMode}; use super::{ByDcaCagr, ByDcaClass, ByDcaPeriod}; -use crate::internal::{PerBlock, PercentPerBlock, Price, ValuePerBlock}; +use crate::internal::{ + LazyPerBlock, LazyPercentPerBlock, LazyPreviousDeltaVec, PerBlock, PercentPerBlock, Price, +}; + +#[derive(Traversable)] +pub struct DcaStack { + pub btc: LazyPerBlock, + pub sats: LazyPerBlock, + pub usd: LazyPerBlock, + pub cents: PerBlock, +} + +#[derive(Clone, Traversable)] +pub struct LumpSumStack { + pub btc: LazyPerBlock, + pub sats: LazyPerBlock, + pub usd: LazyPerBlock, + pub cents: LazyPerBlock, +} #[derive(Traversable)] pub struct PeriodVecs { - pub dca_stack: ByDcaPeriod>, - pub dca_cost_basis: ByDcaPeriod>>, + pub dca_stack: ByDcaPeriod>, + pub dca_cost_basis: ByDcaPeriod>>, pub dca_return: ByDcaPeriod>, - pub dca_cagr: ByDcaCagr>, - pub lump_sum_stack: ByDcaPeriod>, - pub lump_sum_return: ByDcaPeriod>, + pub dca_cagr: ByDcaCagr>, + pub lump_sum_stack: ByDcaPeriod, + pub lump_sum_return: ByDcaPeriod>, } #[derive(Traversable)] pub struct ClassVecs { - pub dca_stack: ByDcaClass>, - pub dca_cost_basis: ByDcaClass>>, + pub dca_stack: ByDcaClass>, + pub dca_cost_basis: ByDcaClass>>, pub dca_return: ByDcaClass>, } @@ -26,7 +44,9 @@ pub struct ClassVecs { pub struct Vecs { #[traversable(skip)] pub(crate) db: Database, - pub sats_per_day: M::Stored>>, + pub sats_per_day: LazyPreviousDeltaVec, + #[traversable(hidden)] + pub sats_cumulative: M::Stored>>, pub period: PeriodVecs, pub class: ClassVecs, } diff --git a/crates/brk_computer/src/lib.rs b/crates/brk_computer/src/lib.rs index 47f51584d..6a30b441a 100644 --- a/crates/brk_computer/src/lib.rs +++ b/crates/brk_computer/src/lib.rs @@ -198,6 +198,8 @@ impl Computer { &computed_path, VERSION, &indexes, + &blocks, + &price, )?)) })?; @@ -214,6 +216,8 @@ impl Computer { &computed_path, VERSION, &indexes, + &blocks, + &price, )?)) })?; @@ -422,14 +426,8 @@ impl Computer { thread::scope(|scope| -> Result<()> { let pools = scope.spawn(|| { timed("Computed pools", || { - self.pools.compute( - indexer, - &self.indexes, - &self.blocks, - &self.price, - &self.mining, - exit, - ) + self.pools + .compute(indexer, &self.indexes, &self.price, &self.mining, exit) }) }); @@ -439,8 +437,6 @@ impl Computer { indexer, &self.indexes, &self.price, - &self.blocks, - &self.market.lookback, exit, ) }) diff --git a/crates/brk_computer/src/market/compute.rs b/crates/brk_computer/src/market/compute.rs index f8fa98967..1ceec4e7f 100644 --- a/crates/brk_computer/src/market/compute.rs +++ b/crates/brk_computer/src/market/compute.rs @@ -19,12 +19,7 @@ impl Vecs { // Phase 1: Independent sub-modules in parallel let (r1, r2) = rayon::join( - || { - rayon::join( - || self.ath.compute(indexer, prices, indexes, exit), - || self.lookback.compute(indexer, blocks, prices, exit), - ) - }, + || self.ath.compute(indexer, prices, indexes, exit), || { rayon::join( || self.range.compute(indexer, prices, blocks, exit), @@ -32,14 +27,12 @@ impl Vecs { ) }, ); - r1.0?; - r1.1?; + r1?; r2.0?; r2.1?; - // Phase 2: Depend on lookback - self.returns - .compute(indexer, prices, blocks, &self.lookback, exit)?; + // 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( diff --git a/crates/brk_computer/src/market/import.rs b/crates/brk_computer/src/market/import.rs index a0645f70a..e086c831f 100644 --- a/crates/brk_computer/src/market/import.rs +++ b/crates/brk_computer/src/market/import.rs @@ -1,16 +1,17 @@ use std::path::Path; -use brk_error::Result; +use brk_error::{Error, Result}; use brk_types::Version; use crate::{ - indexes, + blocks, indexes, internal::db_utils::{finalize_db, open_db}, + price, }; use super::{ AthVecs, LookbackVecs, MovingAverageVecs, RangeVecs, ReturnsVecs, TechnicalVecs, Vecs, - VolatilityVecs, + VolatilityVecs, lookback::ByLookbackPeriod, }; impl Vecs { @@ -18,13 +19,24 @@ impl Vecs { parent_path: &Path, parent_version: Version, indexes: &indexes::Vecs, + blocks: &blocks::Vecs, + prices: &price::Vecs, ) -> Result { let db = open_db(parent_path, super::DB_NAME, 250_000)?; let version = parent_version; let ath = AthVecs::forced_import(&db, version, indexes)?; - let lookback = LookbackVecs::forced_import(&db, version, indexes)?; - let returns = ReturnsVecs::forced_import(&db, version, indexes)?; + let cached_starts = ByLookbackPeriod::try_new(|_, days| { + Ok::<_, Error>( + blocks + .lookback + .cached_start_vec(days as usize) + .read_only_cached_boxed_clone(), + ) + })?; + let lookback = LookbackVecs::forced_import(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)?; diff --git a/crates/brk_computer/src/market/lookback/by_period.rs b/crates/brk_computer/src/market/lookback/by_period.rs index f5eb411f8..85f932ffb 100644 --- a/crates/brk_computer/src/market/lookback/by_period.rs +++ b/crates/brk_computer/src/market/lookback/by_period.rs @@ -76,43 +76,30 @@ impl ByLookbackPeriod { }) } - pub(crate) fn iter_with_days(&self) -> impl Iterator { + pub(crate) fn try_from_period( + period: &ByLookbackPeriod, + mut create: F, + ) -> Result + where + F: FnMut(&'static str, u32, &U) -> Result, + { + let n = LOOKBACK_PERIOD_NAMES; let d = LOOKBACK_PERIOD_DAYS; - [ - (&self._24h, d._24h), - (&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() + Ok(Self { + _24h: create(n._24h, d._24h, &period._24h)?, + _1w: create(n._1w, d._1w, &period._1w)?, + _1m: create(n._1m, d._1m, &period._1m)?, + _3m: create(n._3m, d._3m, &period._3m)?, + _6m: create(n._6m, d._6m, &period._6m)?, + _1y: create(n._1y, d._1y, &period._1y)?, + _2y: create(n._2y, d._2y, &period._2y)?, + _3y: create(n._3y, d._3y, &period._3y)?, + _4y: create(n._4y, d._4y, &period._4y)?, + _5y: create(n._5y, d._5y, &period._5y)?, + _6y: create(n._6y, d._6y, &period._6y)?, + _8y: create(n._8y, d._8y, &period._8y)?, + _10y: create(n._10y, d._10y, &period._10y)?, + }) } - pub(crate) fn iter_mut_with_days(&mut self) -> impl Iterator { - let d = LOOKBACK_PERIOD_DAYS; - [ - (&mut self._24h, d._24h), - (&mut self._1w, d._1w), - (&mut self._1m, d._1m), - (&mut self._3m, d._3m), - (&mut self._6m, d._6m), - (&mut self._1y, d._1y), - (&mut self._2y, d._2y), - (&mut self._3y, d._3y), - (&mut self._4y, d._4y), - (&mut self._5y, d._5y), - (&mut self._6y, d._6y), - (&mut self._8y, d._8y), - (&mut self._10y, d._10y), - ] - .into_iter() - } } diff --git a/crates/brk_computer/src/market/lookback/compute.rs b/crates/brk_computer/src/market/lookback/compute.rs deleted file mode 100644 index 50bbe5875..000000000 --- a/crates/brk_computer/src/market/lookback/compute.rs +++ /dev/null @@ -1,31 +0,0 @@ -use brk_error::Result; -use brk_indexer::Indexer; -use vecdb::Exit; - -use super::Vecs; -use crate::{blocks, price}; - -impl Vecs { - pub(crate) fn compute( - &mut self, - indexer: &Indexer, - blocks: &blocks::Vecs, - prices: &price::Vecs, - exit: &Exit, - ) -> Result<()> { - let starting_height = indexer.safe_lengths().height; - let price = &prices.spot.cents.height; - - for (price_past, days) in self.price_past.iter_mut_with_days() { - let window_starts = blocks.lookback.start_vec(days as usize); - price_past.cents.height.compute_lookback( - starting_height, - window_starts, - price, - exit, - )?; - } - - Ok(()) - } -} diff --git a/crates/brk_computer/src/market/lookback/import.rs b/crates/brk_computer/src/market/lookback/import.rs index e55295be4..02eed5354 100644 --- a/crates/brk_computer/src/market/lookback/import.rs +++ b/crates/brk_computer/src/market/lookback/import.rs @@ -1,18 +1,36 @@ -use brk_error::Result; +use brk_error::{Error, Result}; +use brk_types::{Cents, Height}; use brk_types::Version; -use vecdb::Database; +use vecdb::CachedBoxedVec; use super::{ByLookbackPeriod, Vecs}; -use crate::{indexes, internal::Price}; +use crate::{indexes, internal::{LazyWindowVec, Price}, price}; impl Vecs { pub(crate) fn forced_import( - db: &Database, version: Version, indexes: &indexes::Vecs, + cached_starts: &ByLookbackPeriod>, + prices: &price::Vecs, ) -> Result { - let price_past = ByLookbackPeriod::try_new(|name, _days| { - Price::forced_import(db, &format!("price_past_{name}"), version, indexes) + let price_past = ByLookbackPeriod::try_from_period( + cached_starts, + |name, _days, window_starts| { + let metric_name = format!("price_past_{name}"); + let source = LazyWindowVec::::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 }) diff --git a/crates/brk_computer/src/market/lookback/mod.rs b/crates/brk_computer/src/market/lookback/mod.rs index bf7af787f..9e88f8a95 100644 --- a/crates/brk_computer/src/market/lookback/mod.rs +++ b/crates/brk_computer/src/market/lookback/mod.rs @@ -1,5 +1,4 @@ mod by_period; -mod compute; mod import; mod vecs; diff --git a/crates/brk_computer/src/market/lookback/vecs.rs b/crates/brk_computer/src/market/lookback/vecs.rs index 1ae66c358..bd801755a 100644 --- a/crates/brk_computer/src/market/lookback/vecs.rs +++ b/crates/brk_computer/src/market/lookback/vecs.rs @@ -1,11 +1,10 @@ use brk_traversable::Traversable; use brk_types::Cents; -use vecdb::{Rw, StorageMode}; use super::ByLookbackPeriod; -use crate::internal::{PerBlock, Price}; -#[derive(Traversable)] -pub struct Vecs { +use crate::internal::{LazyPerBlock, Price}; +#[derive(Clone, Traversable)] +pub struct Vecs { #[traversable(flatten)] - pub price_past: ByLookbackPeriod>>, + pub price_past: ByLookbackPeriod>>, } diff --git a/crates/brk_computer/src/market/mod.rs b/crates/brk_computer/src/market/mod.rs index 335bea087..673694638 100644 --- a/crates/brk_computer/src/market/mod.rs +++ b/crates/brk_computer/src/market/mod.rs @@ -25,7 +25,7 @@ pub struct Vecs { #[traversable(skip)] pub(crate) db: Database, pub ath: AthVecs, - pub lookback: LookbackVecs, + pub lookback: LookbackVecs, pub returns: ReturnsVecs, pub volatility: VolatilityVecs, pub range: RangeVecs, diff --git a/crates/brk_computer/src/market/returns/compute.rs b/crates/brk_computer/src/market/returns/compute.rs index b30cb3758..b545e7e69 100644 --- a/crates/brk_computer/src/market/returns/compute.rs +++ b/crates/brk_computer/src/market/returns/compute.rs @@ -1,52 +1,19 @@ use brk_error::Result; use brk_indexer::Indexer; -use brk_types::{Dollars, PartsPerMillionSigned64}; use vecdb::Exit; use super::Vecs; -use crate::{blocks, internal::RatioDiffDollars, investing::ByDcaPeriod, market::lookback, price}; +use crate::blocks; impl Vecs { pub(crate) fn compute( &mut self, indexer: &Indexer, - prices: &price::Vecs, blocks: &blocks::Vecs, - lookback: &lookback::Vecs, exit: &Exit, ) -> Result<()> { let starting_lengths = indexer.safe_lengths(); - // Compute price returns at height level - for ((returns, _), (lookback_price, _)) in self - .periods - .iter_mut_with_days() - .zip(lookback.price_past.iter_with_days()) - { - returns.compute_binary::>( - starting_lengths.height, - &prices.spot.usd.height, - &lookback_price.usd.height, - exit, - )?; - } - - // CAGR computed from returns at height level (2y+ periods only) - let price_return_dca = ByDcaPeriod::from_lookback(&self.periods); - for (cagr, returns, days) in self.cagr.zip_mut_with_period(&price_return_dca) { - let years = days as f64 / 365.0; - cagr.ppm.height.compute_transform( - starting_lengths.height, - &returns.ppm.height, - |(h, r, ..)| { - let ratio = f64::from(r); - let v = (ratio + 1.0).powf(1.0 / years) - 1.0; - (h, PartsPerMillionSigned64::from(v)) - }, - exit, - )?; - } - let _24h_price_return_ratio = &self.periods._24h.ratio.height; for sd in self.sd_24h.as_mut_array() { diff --git a/crates/brk_computer/src/market/returns/import.rs b/crates/brk_computer/src/market/returns/import.rs index 1e964bb76..878948272 100644 --- a/crates/brk_computer/src/market/returns/import.rs +++ b/crates/brk_computer/src/market/returns/import.rs @@ -1,13 +1,16 @@ -use brk_error::Result; -use brk_types::Version; -use vecdb::Database; +use brk_error::{Error, Result}; +use brk_types::{Dollars, Height, PartsPerMillionSigned64, Version}; +use vecdb::{BinaryTransform, CachedBoxedVec, Database, ReadableCloneableVec}; use super::super::lookback::ByLookbackPeriod; use super::Vecs; use crate::{ indexes, - internal::{PercentPerBlock, StdDevPerBlock, Windows}, - investing::ByDcaCagr, + internal::{ + LazyPercentPerBlock, LazyWindowVec, RatioDiffDollars, StdDevPerBlock, Windows, + }, + investing::{ByDcaCagr, ByDcaPeriod}, + price, }; impl Vecs { @@ -15,15 +18,39 @@ impl Vecs { db: &Database, version: Version, indexes: &indexes::Vecs, + cached_starts: &ByLookbackPeriod>, + prices: &price::Vecs, ) -> Result { - let v1 = Version::ONE; - - let periods = ByLookbackPeriod::try_new(|name, _days| { - PercentPerBlock::forced_import(db, &format!("price_return_{name}"), version, indexes) + let periods = ByLookbackPeriod::try_from_period( + cached_starts, + |name, _days, window_starts| { + let metric_name = format!("price_return_{name}"); + let source = LazyWindowVec::::new( + &format!("{metric_name}_ppm_source"), + version, + prices.spot.usd.height.read_only_boxed_clone(), + window_starts.clone(), + false, + |current, past, _| { + RatioDiffDollars::::apply(current, past) + }, + ); + Ok::<_, Error>(LazyPercentPerBlock::from_height_source( + &metric_name, + version, + source, + indexes, + )) })?; - let cagr = ByDcaCagr::try_new(|name, _days| { - PercentPerBlock::forced_import(db, &format!("price_cagr_{name}"), version, indexes) + let dca_periods = ByDcaPeriod::from_lookback(&periods); + let cagr = ByDcaCagr::try_new(&dca_periods, |name, days, source| { + Ok::<_, Error>(LazyPercentPerBlock::from_lazy_cagr( + &format!("price_cagr_{name}"), + version, + (days / 365) as u8, + *source, + )) })?; let mut days_iter = Windows::<()>::DAYS.iter(); @@ -34,7 +61,7 @@ impl Vecs { "price_return_24h", suffix, days, - version + v1, + version + Version::ONE, indexes, ) })?; diff --git a/crates/brk_computer/src/market/returns/vecs.rs b/crates/brk_computer/src/market/returns/vecs.rs index 58a1da512..29605c58d 100644 --- a/crates/brk_computer/src/market/returns/vecs.rs +++ b/crates/brk_computer/src/market/returns/vecs.rs @@ -3,14 +3,14 @@ use brk_types::PartsPerMillionSigned64; use vecdb::{Rw, StorageMode}; use crate::{ - internal::{PercentPerBlock, StdDevPerBlock, Windows}, + internal::{LazyPercentPerBlock, StdDevPerBlock, Windows}, investing::ByDcaCagr, market::lookback::ByLookbackPeriod, }; #[derive(Traversable)] pub struct Vecs { - pub periods: ByLookbackPeriod>, - pub cagr: ByDcaCagr>, + pub periods: ByLookbackPeriod>, + pub cagr: ByDcaCagr>, pub sd_24h: Windows>, } diff --git a/crates/brk_computer/src/mining/rewards/compute.rs b/crates/brk_computer/src/mining/rewards/compute.rs index 35487a976..662343e42 100644 --- a/crates/brk_computer/src/mining/rewards/compute.rs +++ b/crates/brk_computer/src/mining/rewards/compute.rs @@ -1,14 +1,10 @@ use brk_error::Result; use brk_indexer::Indexer; -use brk_types::{CheckedSub, Dollars, Halving, PartsPerMillion32, PartsPerMillion64, Sats}; +use brk_types::{CheckedSub, Halving, PartsPerMillion32, PartsPerMillion64, Sats}; use vecdb::{Exit, ReadableVec, VecIndex}; use super::Vecs; -use crate::{ - blocks, indexes, - internal::{RatioDollars, RatioSats}, - price, transactions, -}; +use crate::{blocks, indexes, internal::RatioSats, price, transactions}; impl Vecs { #[allow(clippy::too_many_arguments)] @@ -110,11 +106,11 @@ impl Vecs { exit, )?; - self.fee_to_subsidy_ratio - .compute_binary::, _, _>( + self.fee_to_subsidy + .compute_binary::, _, _>( starting_height, - self.coinbase.sum.as_array().map(|w| &w.usd.height), - self.fees.sum.as_array().map(|w| &w.usd.height), + self.fees.sum.as_array().map(|w| &w.sats.height), + self.subsidy.sum.as_array().map(|w| &w.sats.height), exit, )?; diff --git a/crates/brk_computer/src/mining/rewards/import.rs b/crates/brk_computer/src/mining/rewards/import.rs index 0147a655b..07856be0a 100644 --- a/crates/brk_computer/src/mining/rewards/import.rs +++ b/crates/brk_computer/src/mining/rewards/import.rs @@ -6,7 +6,7 @@ use super::Vecs; use crate::{ indexes, internal::{ - LazyPercentCumulativeRolling, OneMinusPpm, PercentCumulativeRolling, RatioRollingWindows, + LazyPercentCumulativeRolling, OneMinusPpm, PercentCumulativeRolling, PercentRollingWindows, ValuePerBlockCumulative, ValuePerBlockCumulativeRolling, ValuePerBlockFull, WindowStartVec, Windows, }, @@ -53,10 +53,10 @@ impl Vecs { )?, fee_dominance, subsidy_dominance, - fee_to_subsidy_ratio: RatioRollingWindows::forced_import( + fee_to_subsidy: PercentRollingWindows::forced_import( db, - "fee_to_subsidy_ratio", - version, + "fee_to_subsidy", + version + Version::ONE, indexes, )?, }) diff --git a/crates/brk_computer/src/mining/rewards/vecs.rs b/crates/brk_computer/src/mining/rewards/vecs.rs index f4fa0cfd1..c6331d231 100644 --- a/crates/brk_computer/src/mining/rewards/vecs.rs +++ b/crates/brk_computer/src/mining/rewards/vecs.rs @@ -3,7 +3,7 @@ use brk_types::{Height, PartsPerMillion32, PartsPerMillion64, Sats}; use vecdb::{EagerVec, PcoVec, Rw, StorageMode}; use crate::internal::{ - LazyPercentCumulativeRolling, PercentCumulativeRolling, RatioRollingWindows, + LazyPercentCumulativeRolling, PercentCumulativeRolling, PercentRollingWindows, ValuePerBlockCumulative, ValuePerBlockCumulativeRolling, ValuePerBlockFull, }; @@ -18,6 +18,6 @@ pub struct Vecs { pub fee_dominance: PercentCumulativeRolling, #[traversable(wrap = "subsidy", rename = "dominance")] pub subsidy_dominance: LazyPercentCumulativeRolling, - #[traversable(wrap = "fees", rename = "to_subsidy_ratio")] - pub fee_to_subsidy_ratio: RatioRollingWindows, + #[traversable(wrap = "fees", rename = "to_subsidy")] + pub fee_to_subsidy: PercentRollingWindows, } diff --git a/crates/brk_computer/src/models/bedrock/compute.rs b/crates/brk_computer/src/models/bedrock/compute.rs index 3b9b6e282..5b1d334aa 100644 --- a/crates/brk_computer/src/models/bedrock/compute.rs +++ b/crates/brk_computer/src/models/bedrock/compute.rs @@ -3,7 +3,7 @@ use std::{cmp::Ordering, collections::BTreeMap, fs, path::Path}; use brk_cohort::{AGE_RANGE_NAMES, CohortContext}; use brk_error::Result; use brk_indexer::Indexer; -use brk_types::{CentsCompact, Date, Day1, Dollars, Sats, StoredF64, UrpdRaw, Version}; +use brk_types::{Cents, CentsCompact, Date, Day1, Sats, StoredF64, UrpdRaw, Version}; use vecdb::{AnyStoredVec, AnyVec, Exit, ReadableVec, VecValue, WritableVec}; use super::vecs::{Levels, MODE_COUNT, ModeVecs, Percentiles, Vecs}; @@ -34,8 +34,8 @@ type ModeWeights = [Option<[f64; AGE_COHORT_COUNT]>; MODE_COUNT]; struct DayResult { loss_threshold: [[StoredF64; PERCENTILE_COUNT]; MODE_COUNT], - floor: [[Dollars; PERCENTILE_COUNT]; MODE_COUNT], - level: [[Dollars; LEVEL_COUNT]; MODE_COUNT], + floor: [[Cents; PERCENTILE_COUNT]; MODE_COUNT], + level: [[Cents; LEVEL_COUNT]; MODE_COUNT], } impl DayResult { @@ -46,8 +46,8 @@ impl DayResult { .map(|values| values.map(StoredF64::from)) .unwrap_or([StoredF64::NAN; PERCENTILE_COUNT]) }), - floor: [[Dollars::NAN; PERCENTILE_COUNT]; MODE_COUNT], - level: [[Dollars::NAN; LEVEL_COUNT]; MODE_COUNT], + floor: [[Cents::NAN; PERCENTILE_COUNT]; MODE_COUNT], + level: [[Cents::NAN; LEVEL_COUNT]; MODE_COUNT], } } } @@ -138,13 +138,13 @@ impl ModeVecs { self.floor .as_mut_array() .into_iter() - .map(|vec| &mut vec.day1 as &mut dyn AnyStoredVec), + .map(|vec| &mut vec.cents.day1 as &mut dyn AnyStoredVec), ); vecs.extend( self.level .as_mut_array() .into_iter() - .map(|vec| &mut vec.day1 as &mut dyn AnyStoredVec), + .map(|vec| &mut vec.cents.day1 as &mut dyn AnyStoredVec), ); vecs } @@ -152,8 +152,8 @@ impl ModeVecs { fn push( &mut self, loss_threshold: [StoredF64; PERCENTILE_COUNT], - floor: [Dollars; PERCENTILE_COUNT], - level: [Dollars; LEVEL_COUNT], + floor: [Cents; PERCENTILE_COUNT], + level: [Cents; LEVEL_COUNT], ) { for (vec, value) in self .loss_threshold @@ -164,10 +164,10 @@ impl ModeVecs { vec.day1.push(value); } for (vec, value) in self.floor.as_mut_array().into_iter().zip(floor) { - vec.day1.push(value); + vec.cents.day1.push(value); } for (vec, value) in self.level.as_mut_array().into_iter().zip(level) { - vec.day1.push(value); + vec.cents.day1.push(value); } } } @@ -198,7 +198,7 @@ impl Vecs { let coinflow_mobility: Vec<_> = coinflow .age_range .iter() - .map(|cohort| &cohort.mobility.day1) + .map(|cohort| &cohort.mobility.day1.0) .collect(); let coinflow_spending_rate: Vec<_> = coinflow .age_range @@ -505,14 +505,14 @@ fn evaluate_day( } let mut remaining_loss = denominator; - let mut floors = [Dollars::NAN; PERCENTILE_COUNT]; + let mut floors = [Cents::NAN; PERCENTILE_COUNT]; let mut p95_floor = None; for (price, buckets) in weighted { remaining_loss -= buckets[mode]; let remaining_share = remaining_loss / denominator; for percentile in 0..PERCENTILE_COUNT { if floors[percentile].is_nan() && remaining_share <= thresholds[percentile] { - floors[percentile] = Dollars::from(*price); + floors[percentile] = Cents::from(*price); if percentile == 0 { p95_floor = Some(*price); } @@ -533,8 +533,8 @@ fn conditional_levels( weighted: &BTreeMap, mode: usize, lower: CentsCompact, -) -> [Dollars; LEVEL_COUNT] { - let mut levels = [Dollars::NAN; LEVEL_COUNT]; +) -> [Cents; LEVEL_COUNT] { + let mut levels = [Cents::NAN; LEVEL_COUNT]; let total = weighted .range(lower..) .map(|(_, buckets)| buckets[mode]) @@ -553,7 +553,7 @@ fn conditional_levels( } cumulative += mass; while percentile < LEVEL_COUNT && cumulative >= total * LEVEL_PERCENTILES[percentile] { - levels[percentile] = Dollars::from(*price); + levels[percentile] = Cents::from(*price); percentile += 1; } if percentile == LEVEL_COUNT { @@ -632,20 +632,20 @@ mod tests { ); assert_eq!( result.floor[COINFLOW_MODE], - [Dollars::from(1.0); PERCENTILE_COUNT] + [Cents::new(100); PERCENTILE_COUNT] ); assert_eq!( result.level[COINFLOW_MODE], [ - Dollars::from(1.0), - Dollars::from(1.0), - Dollars::from(1.0), - Dollars::from(1.0), - Dollars::from(1.0), - Dollars::from(2.0), - Dollars::from(2.0), - Dollars::from(2.0), - Dollars::from(2.0), + Cents::new(100), + Cents::new(100), + Cents::new(100), + Cents::new(100), + Cents::new(100), + Cents::new(200), + Cents::new(200), + Cents::new(200), + Cents::new(200), ] ); assert_eq!( diff --git a/crates/brk_computer/src/models/bedrock/import.rs b/crates/brk_computer/src/models/bedrock/import.rs index 1fe197155..ee95c50c7 100644 --- a/crates/brk_computer/src/models/bedrock/import.rs +++ b/crates/brk_computer/src/models/bedrock/import.rs @@ -1,11 +1,12 @@ use std::path::Path; use brk_error::Result; -use brk_types::{Dollars, StoredF64, Version}; +use brk_types::{StoredF64, Version}; use vecdb::Database; use super::{ DB_NAME, + price::Price, urpd_metric::{UrpdMappings, UrpdMetric}, vecs::{Levels, ModeVecs, Modes, Percentiles, Vecs}, }; @@ -14,7 +15,7 @@ use crate::{ internal::db_utils::{finalize_db, open_db}, }; -const VERSION: Version = Version::new(3); +const VERSION: Version = Version::new(4); fn import_percentiles(mut import: impl FnMut(&str) -> Result) -> Result> { Ok(Percentiles { @@ -54,8 +55,8 @@ fn import_price( name: &str, version: Version, mappings: &UrpdMappings, -) -> Result> { - UrpdMetric::forced_import(db, name, version, mappings) +) -> Result { + Price::forced_import(db, name, version, mappings) } fn import_mode( diff --git a/crates/brk_computer/src/models/bedrock/mod.rs b/crates/brk_computer/src/models/bedrock/mod.rs index 4c4760c49..2c8204916 100644 --- a/crates/brk_computer/src/models/bedrock/mod.rs +++ b/crates/brk_computer/src/models/bedrock/mod.rs @@ -1,5 +1,6 @@ mod compute; mod import; +mod price; mod urpd_metric; mod vecs; diff --git a/crates/brk_computer/src/models/bedrock/price.rs b/crates/brk_computer/src/models/bedrock/price.rs new file mode 100644 index 000000000..313500959 --- /dev/null +++ b/crates/brk_computer/src/models/bedrock/price.rs @@ -0,0 +1,87 @@ +//! Bedrock-local price wrapper for metrics whose stored source is indexed by day. +//! +//! This mirrors the per-block `Price` conversion chain while preserving Bedrock's +//! custom daily repeat/last-day views: cents are stored, USD is derived from cents, +//! and sats are derived from USD. + +use brk_traversable::Traversable; +use brk_types::{Cents, Day1, Dollars, SatsFract, Version}; +use schemars::JsonSchema; +use vecdb::{ + LazyVecFrom1, ReadableBoxedVec, ReadableCloneableVec, Rw, StorageMode, UnaryTransform, +}; + +use super::urpd_metric::{UrpdMappings, UrpdMetric, UrpdViews}; +use crate::internal::{CentsUnsignedToDollars, DollarsToSatsFract, NumericValue}; + +type LazyDay = LazyVecFrom1; + +#[derive(Clone, Traversable)] +#[traversable(merge)] +pub struct LazyUrpdMetric +where + T: NumericValue + JsonSchema, + S: NumericValue, +{ + pub day1: LazyDay, + #[traversable(flatten)] + pub views: Box>, +} + +impl LazyUrpdMetric +where + T: NumericValue + JsonSchema + 'static, + S: NumericValue + JsonSchema, +{ + fn from_source( + name: &str, + version: Version, + source: ReadableBoxedVec, + mappings: &UrpdMappings, + ) -> Self + where + F: UnaryTransform, + { + let day1 = LazyVecFrom1::transformed::(name, version, source); + let views = Box::new(UrpdViews::new( + name, + day1.read_only_boxed_clone(), + version, + mappings, + )); + + Self { day1, views } + } +} + +#[derive(Traversable)] +pub struct Price { + pub usd: LazyUrpdMetric, + pub cents: UrpdMetric, + pub sats: LazyUrpdMetric, +} + +impl Price { + pub(crate) fn forced_import( + db: &vecdb::Database, + name: &str, + version: Version, + mappings: &UrpdMappings, + ) -> brk_error::Result { + let cents = UrpdMetric::forced_import(db, &format!("{name}_cents"), version, mappings)?; + let usd = LazyUrpdMetric::from_source::( + name, + version, + cents.day1.read_only_boxed_clone(), + mappings, + ); + let sats = LazyUrpdMetric::from_source::( + &format!("{name}_sats"), + version, + usd.day1.read_only_boxed_clone(), + mappings, + ); + + Ok(Self { usd, cents, sats }) + } +} diff --git a/crates/brk_computer/src/models/bedrock/urpd_metric.rs b/crates/brk_computer/src/models/bedrock/urpd_metric.rs index 14e21a85b..92a331a3d 100644 --- a/crates/brk_computer/src/models/bedrock/urpd_metric.rs +++ b/crates/brk_computer/src/models/bedrock/urpd_metric.rs @@ -112,7 +112,7 @@ impl UrpdViews where T: NumericValue + JsonSchema, { - fn new( + pub(super) fn new( name: &str, source: ReadableBoxedVec, version: Version, diff --git a/crates/brk_computer/src/models/bedrock/vecs.rs b/crates/brk_computer/src/models/bedrock/vecs.rs index ebdad950b..c3eb82925 100644 --- a/crates/brk_computer/src/models/bedrock/vecs.rs +++ b/crates/brk_computer/src/models/bedrock/vecs.rs @@ -1,9 +1,9 @@ use brk_traversable::Traversable; -use brk_types::{Dollars, StoredF64}; +use brk_types::StoredF64; use derive_more::{Deref, DerefMut}; use vecdb::{Database, Rw, StorageMode}; -use super::urpd_metric::UrpdMetric; +use super::{price::Price, urpd_metric::UrpdMetric}; pub(crate) const MODE_COUNT: usize = 10; @@ -32,8 +32,8 @@ pub struct Levels { #[derive(Traversable)] pub struct ModeVecs { pub loss_threshold: Percentiles>, - pub floor: Percentiles>, - pub level: Levels>, + pub floor: Percentiles>, + pub level: Levels>, } #[derive(Traversable)] diff --git a/crates/brk_computer/src/outputs/compute.rs b/crates/brk_computer/src/outputs/compute.rs index f567dd57b..5c4d7d89c 100644 --- a/crates/brk_computer/src/outputs/compute.rs +++ b/crates/brk_computer/src/outputs/compute.rs @@ -20,7 +20,6 @@ impl Vecs { let starting_lengths = indexer.safe_lengths(); self.count.compute(indexer, blocks, exit)?; - self.per_sec.compute(&self.count, &starting_lengths, exit)?; self.value.compute(indexer, prices, exit)?; self.by_type.compute(indexer, exit)?; self.unspent.compute( diff --git a/crates/brk_computer/src/outputs/import.rs b/crates/brk_computer/src/outputs/import.rs index c8e009e7b..dff2a5bfa 100644 --- a/crates/brk_computer/src/outputs/import.rs +++ b/crates/brk_computer/src/outputs/import.rs @@ -6,12 +6,12 @@ use brk_types::Version; use crate::{ indexes, internal::{ - WindowStartVec, Windows, + LazyPerSecondWindows, WindowStartVec, Windows, db_utils::{finalize_db, open_db}, }, }; -use super::{ByTypeVecs, CountVecs, PerSecVecs, SpentVecs, UnspentVecs, ValueVecs, Vecs}; +use super::{ByTypeVecs, CountVecs, SpentVecs, UnspentVecs, ValueVecs, Vecs}; impl Vecs { pub(crate) fn forced_import( @@ -25,7 +25,8 @@ impl Vecs { let spent = SpentVecs::forced_import(&db, version)?; let count = CountVecs::forced_import(&db, version, indexes, cached_starts)?; - let per_sec = PerSecVecs::forced_import(&db, version, indexes)?; + let per_sec = + LazyPerSecondWindows::new("outputs_per_sec", version, &count.total.rolling.sum); let unspent = UnspentVecs::forced_import(&db, version, indexes)?; let by_type = ByTypeVecs::forced_import(&db, version, indexes, cached_starts)?; let value = ValueVecs::forced_import(&db, version, indexes)?; diff --git a/crates/brk_computer/src/outputs/mod.rs b/crates/brk_computer/src/outputs/mod.rs index c87c35554..cf1adb442 100644 --- a/crates/brk_computer/src/outputs/mod.rs +++ b/crates/brk_computer/src/outputs/mod.rs @@ -1,6 +1,5 @@ pub mod by_type; pub mod count; -pub mod per_sec; pub mod spent; pub mod unspent; pub mod value; @@ -11,9 +10,10 @@ mod import; use brk_traversable::Traversable; use vecdb::{Database, Rw, StorageMode}; +use crate::internal::LazyPerSecondWindows; + pub use by_type::Vecs as ByTypeVecs; pub use count::Vecs as CountVecs; -pub use per_sec::Vecs as PerSecVecs; pub use spent::Vecs as SpentVecs; pub use unspent::Vecs as UnspentVecs; pub use value::Vecs as ValueVecs; @@ -27,7 +27,7 @@ pub struct Vecs { pub spent: SpentVecs, pub count: CountVecs, - pub per_sec: PerSecVecs, + pub per_sec: LazyPerSecondWindows, pub unspent: UnspentVecs, pub by_type: ByTypeVecs, pub value: ValueVecs, diff --git a/crates/brk_computer/src/outputs/per_sec/compute.rs b/crates/brk_computer/src/outputs/per_sec/compute.rs deleted file mode 100644 index 7da14b644..000000000 --- a/crates/brk_computer/src/outputs/per_sec/compute.rs +++ /dev/null @@ -1,29 +0,0 @@ -use brk_error::Result; -use brk_indexer::Lengths; -use brk_types::StoredF32; -use vecdb::Exit; - -use super::Vecs; -use crate::{internal::Windows, outputs::CountVecs}; - -impl Vecs { - pub(crate) fn compute( - &mut self, - count: &CountVecs, - starting_lengths: &Lengths, - exit: &Exit, - ) -> Result<()> { - let h = starting_lengths.height; - let sums = count.total.rolling.sum.0.as_array(); - let per_sec = self.0.as_mut_array(); - for (i, &secs) in Windows::<()>::SECS.iter().enumerate() { - per_sec[i].height.compute_transform( - h, - &sums[i].height, - |(h, sum, ..)| (h, StoredF32::from(*sum as f64 / secs)), - exit, - )?; - } - Ok(()) - } -} diff --git a/crates/brk_computer/src/outputs/per_sec/import.rs b/crates/brk_computer/src/outputs/per_sec/import.rs deleted file mode 100644 index 35ea03672..000000000 --- a/crates/brk_computer/src/outputs/per_sec/import.rs +++ /dev/null @@ -1,21 +0,0 @@ -use brk_error::Result; -use brk_types::Version; -use vecdb::Database; - -use super::Vecs; -use crate::{ - indexes, - internal::{PerBlock, Windows}, -}; - -impl Vecs { - pub(crate) fn forced_import( - db: &Database, - version: Version, - indexes: &indexes::Vecs, - ) -> Result { - Ok(Self(Windows::try_from_fn(|suffix| { - PerBlock::forced_import(db, &format!("outputs_per_sec_{suffix}"), version, indexes) - })?)) - } -} diff --git a/crates/brk_computer/src/outputs/per_sec/mod.rs b/crates/brk_computer/src/outputs/per_sec/mod.rs deleted file mode 100644 index 1136f9ebd..000000000 --- a/crates/brk_computer/src/outputs/per_sec/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -mod compute; -mod import; -mod vecs; - -pub use vecs::Vecs; diff --git a/crates/brk_computer/src/outputs/per_sec/vecs.rs b/crates/brk_computer/src/outputs/per_sec/vecs.rs deleted file mode 100644 index ee741b415..000000000 --- a/crates/brk_computer/src/outputs/per_sec/vecs.rs +++ /dev/null @@ -1,8 +0,0 @@ -use brk_traversable::Traversable; -use brk_types::StoredF32; -use vecdb::{Rw, StorageMode}; - -use crate::internal::{PerBlock, Windows}; - -#[derive(Traversable)] -pub struct Vecs(#[traversable(flatten)] pub Windows>); diff --git a/crates/brk_computer/src/pools/major.rs b/crates/brk_computer/src/pools/major.rs index d4a6b6ae5..5c1a6c915 100644 --- a/crates/brk_computer/src/pools/major.rs +++ b/crates/brk_computer/src/pools/major.rs @@ -1,14 +1,14 @@ use brk_error::Result; use brk_indexer::Indexer; use brk_traversable::Traversable; -use brk_types::{Height, PartsPerMillion32, PoolSlug, StoredU64}; +use brk_types::{Height, PartsPerMillion32, PoolSlug}; use derive_more::{Deref, DerefMut}; use vecdb::{BinaryTransform, Database, Exit, ReadableVec, Rw, StorageMode, Version}; use crate::{ - blocks, indexes, + indexes, internal::{ - MaskSats, PercentRollingWindows, RatioU64, ValuePerBlockCumulativeRolling, WindowStartVec, + LazyPercentRollingWindows, MaskSats, ValuePerBlockCumulativeRolling, WindowStartVec, Windows, }, mining, price, @@ -25,7 +25,7 @@ pub struct Vecs { pub rewards: ValuePerBlockCumulativeRolling, #[traversable(rename = "dominance")] - pub dominance_rolling: PercentRollingWindows, + pub dominance_rolling: LazyPercentRollingWindows, } impl Vecs { @@ -48,8 +48,13 @@ impl Vecs { cached_starts, )?; - let dominance_rolling = - PercentRollingWindows::forced_import(db, &suffix("dominance"), version, indexes)?; + let dominance_rolling = LazyPercentRollingWindows::from_cumulative_average( + &suffix("dominance"), + version, + &base.blocks_mined.cumulative.height, + cached_starts, + indexes, + ); Ok(Self { base, @@ -62,30 +67,13 @@ impl Vecs { &mut self, indexer: &Indexer, pool: &impl ReadableVec, - blocks: &blocks::Vecs, prices: &price::Vecs, mining: &mining::Vecs, exit: &Exit, ) -> Result<()> { let starting_height = indexer.safe_lengths().height; - self.base.compute(indexer, pool, blocks, exit)?; - - for (dom, (mined, total)) in self.dominance_rolling.as_mut_array().into_iter().zip( - self.base - .blocks_mined - .sum - .as_array() - .into_iter() - .zip(blocks.count.total.sum.as_array()), - ) { - dom.compute_binary::>( - starting_height, - &mined.height, - &total.height, - exit, - )?; - } + self.base.compute(indexer, pool, exit)?; self.rewards.compute_from_pair( starting_height, diff --git a/crates/brk_computer/src/pools/minor.rs b/crates/brk_computer/src/pools/minor.rs index dedc80802..670e67256 100644 --- a/crates/brk_computer/src/pools/minor.rs +++ b/crates/brk_computer/src/pools/minor.rs @@ -2,20 +2,62 @@ use brk_error::Result; use brk_indexer::Indexer; use brk_traversable::Traversable; use brk_types::{Height, PartsPerMillion32, PoolSlug, StoredU64}; -use vecdb::{Database, Exit, ReadableVec, Rw, StorageMode, Version}; +use vecdb::{Database, Exit, ReadableCloneableVec, ReadableVec, Rw, StorageMode, Version}; use crate::{ - blocks, indexes, - internal::{PerBlockCumulativeRolling, PercentPerBlock, RatioU64, WindowStartVec, Windows}, + indexes, + internal::{ + LazyPercentPerBlock, LazyPreviousDeltaVec, LazyRollingSumsFromHeight, PerBlock, + WindowStartVec, Windows, + }, }; +#[derive(Traversable)] +pub struct BlocksMined { + pub block: LazyPreviousDeltaVec, + pub cumulative: PerBlock, + pub sum: LazyRollingSumsFromHeight, +} + +impl BlocksMined { + fn forced_import( + db: &Database, + name: &str, + version: Version, + indexes: &indexes::Vecs, + cached_starts: &Windows<&WindowStartVec>, + ) -> Result { + let cumulative = + PerBlock::forced_import(db, &format!("{name}_cumulative"), version, indexes)?; + let block = + LazyPreviousDeltaVec::new(name, version, cumulative.height.read_only_boxed_clone()); + let sum = LazyRollingSumsFromHeight::new( + &format!("{name}_sum"), + version, + &cumulative.height, + cached_starts, + indexes, + ); + + Ok(Self { + block, + cumulative, + sum, + }) + } +} + +fn pool_dominance(height: Height, blocks_mined: StoredU64) -> PartsPerMillion32 { + PartsPerMillion32::from(u64::from(blocks_mined) as f64 / (u64::from(height) + 1) as f64) +} + #[derive(Traversable)] pub struct Vecs { #[traversable(skip)] slug: PoolSlug, - pub blocks_mined: PerBlockCumulativeRolling, - pub dominance: PercentPerBlock, + pub blocks_mined: BlocksMined, + pub dominance: LazyPercentPerBlock, } impl Vecs { @@ -28,7 +70,7 @@ impl Vecs { ) -> Result { let suffix = |s: &str| format!("{}_{s}", slug); - let blocks_mined = PerBlockCumulativeRolling::forced_import( + let blocks_mined = BlocksMined::forced_import( db, &suffix("blocks_mined"), version + Version::ONE, @@ -36,7 +78,13 @@ impl Vecs { cached_starts, )?; - let dominance = PercentPerBlock::forced_import(db, &suffix("dominance"), version, indexes)?; + let dominance = LazyPercentPerBlock::from_indexed_source( + &suffix("dominance"), + version, + &blocks_mined.cumulative.height, + pool_dominance, + indexes, + ); Ok(Self { slug, @@ -49,7 +97,6 @@ impl Vecs { &mut self, indexer: &Indexer, pool: &impl ReadableVec, - blocks: &blocks::Vecs, exit: &Exit, ) -> Result<()> { let starting_height = indexer.safe_lengths().height; @@ -59,14 +106,23 @@ impl Vecs { .height .compute_cumulative_count(starting_height, pool, |id| *id == self.slug, exit)?; - self.dominance - .compute_binary::>( - starting_height, - &self.blocks_mined.cumulative.height, - &blocks.count.total.cumulative.height, - exit, - )?; - Ok(()) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn dominance_is_cumulative_share_of_chain() { + assert_eq!( + pool_dominance(Height::from(3_u32), StoredU64::from(1_u64)), + PartsPerMillion32::from(0.25), + ); + assert_eq!( + pool_dominance(Height::from(9_u32), StoredU64::from(10_u64)), + PartsPerMillion32::ONE, + ); + } +} diff --git a/crates/brk_computer/src/pools/mod.rs b/crates/brk_computer/src/pools/mod.rs index 54793f13e..3310591f1 100644 --- a/crates/brk_computer/src/pools/mod.rs +++ b/crates/brk_computer/src/pools/mod.rs @@ -17,7 +17,7 @@ mod pool_heights; pub use pool_heights::PoolHeights; use crate::{ - blocks, indexes, + indexes, internal::{ WindowStartVec, Windows, db_utils::{finalize_db, open_db}, @@ -89,7 +89,6 @@ impl Vecs { &mut self, indexer: &Indexer, indexes: &indexes::Vecs, - blocks: &blocks::Vecs, prices: &price::Vecs, mining: &mining::Vecs, exit: &Exit, @@ -98,13 +97,13 @@ impl Vecs { self.compute_pool(indexer, indexes, exit)?; - self.major.par_iter_mut().try_for_each(|(_, vecs)| { - vecs.compute(indexer, &self.pool, blocks, prices, mining, exit) - })?; + self.major + .par_iter_mut() + .try_for_each(|(_, vecs)| vecs.compute(indexer, &self.pool, prices, mining, exit))?; self.minor .par_iter_mut() - .try_for_each(|(_, vecs)| vecs.compute(indexer, &self.pool, blocks, exit))?; + .try_for_each(|(_, vecs)| vecs.compute(indexer, &self.pool, exit))?; let exit = exit.clone(); self.db.run_bg(move |db| { diff --git a/crates/brk_computer/src/supply/compute.rs b/crates/brk_computer/src/supply/compute.rs index b4c815080..1e98942ea 100644 --- a/crates/brk_computer/src/supply/compute.rs +++ b/crates/brk_computer/src/supply/compute.rs @@ -1,11 +1,7 @@ use brk_error::Result; use brk_indexer::Indexer; -use brk_types::Sats; use vecdb::Exit; -/// Initial block subsidy (50 BTC) in sats, as f64 for floating-point comparisons. -const INITIAL_SUBSIDY: f64 = Sats::ONE_BTC_U64 as f64 * 50.0; - use super::Vecs; use crate::{blocks, distribution, mining, outputs, price, transactions}; @@ -30,32 +26,11 @@ impl Vecs { self.burned .compute(indexer, outputs, mining, prices, exit)?; - // 2. Compute inflation rate: (supply[h] / supply[1y_ago]) - 1 - // Skip when lookback supply <= first block (50 BTC = 5B sats), - // i.e. the lookback points to block 0 or 1 in the genesis era. - let circulating_supply = &distribution.utxo_cohorts.all.metrics.supply.total.sats; - self.inflation_rate - .ppm - .height - .compute_rolling_from_window_starts( - starting_height, - &blocks.lookback._1y, - &circulating_supply.height, - exit, - |current, previous| { - if previous.is_nan() || previous <= INITIAL_SUBSIDY { - f64::NAN - } else { - current / previous - 1.0 - } - }, - )?; - - // 3. Compute velocity at height level + // 2. Compute velocity at height level self.velocity .compute(indexer, blocks, transactions, distribution, exit)?; - // 4. market_cap_rate - realized_cap_rate per window + // 3. market_cap_rate - realized_cap_rate per window let all_realized = &distribution.utxo_cohorts.all.metrics.realized; let mcr_arr = self.market_cap_delta.rate.as_array(); let diff_arr = self.market_minus_realized_cap_growth_rate.0.as_mut_array(); diff --git a/crates/brk_computer/src/supply/import.rs b/crates/brk_computer/src/supply/import.rs index 73f76cf4e..869326243 100644 --- a/crates/brk_computer/src/supply/import.rs +++ b/crates/brk_computer/src/supply/import.rs @@ -1,15 +1,16 @@ use std::path::Path; use brk_error::Result; -use brk_types::Version; +use brk_types::{Height, PartsPerMillionSigned64, Sats, Version}; +use vecdb::ReadableCloneableVec; use crate::{ distribution, frameworks::cointime, indexes, internal::{ - LazyFiatPerBlock, LazyRollingDeltasFiatFromHeight, LazyValuePerBlock, PercentPerBlock, - RollingWindows, WindowStartVec, Windows, + LazyFiatPerBlock, LazyPercentPerBlock, LazyRollingDeltasFiatFromHeight, LazyValuePerBlock, + LazyWindowVec, RollingWindows, WindowStartVec, Windows, db_utils::{finalize_db, open_db}, }, supply::burned, @@ -38,9 +39,27 @@ impl Vecs { let burned = burned::Vecs::forced_import(&db, version, indexes)?; - // Inflation rate - let inflation_rate = - PercentPerBlock::forced_import(&db, "inflation_rate", version + Version::TWO, indexes)?; + let inflation_version = version + Version::TWO; + let inflation_source = LazyWindowVec::::new( + "inflation_rate_ppm_source", + inflation_version, + supply_metrics.total.sats.height.read_only_boxed_clone(), + cached_starts._1y.read_only_cached_boxed_clone(), + false, + |current, previous, _| { + if previous <= Sats::FIFTY_BTC { + PartsPerMillionSigned64::from(f64::NAN) + } else { + PartsPerMillionSigned64::from(f64::from(current) / f64::from(previous) - 1.0) + } + }, + ); + let inflation_rate = LazyPercentPerBlock::from_height_source( + "inflation_rate", + inflation_version, + inflation_source, + indexes, + ); // Velocity let velocity = super::velocity::Vecs::forced_import(&db, version, indexes)?; diff --git a/crates/brk_computer/src/supply/vecs.rs b/crates/brk_computer/src/supply/vecs.rs index 734a5a03c..186ed27ca 100644 --- a/crates/brk_computer/src/supply/vecs.rs +++ b/crates/brk_computer/src/supply/vecs.rs @@ -4,7 +4,7 @@ use vecdb::{Database, Rw, StorageMode}; use super::{burned, velocity}; use crate::internal::{ - LazyFiatPerBlock, LazyRollingDeltasFiatFromHeight, LazyValuePerBlock, PercentPerBlock, + LazyFiatPerBlock, LazyPercentPerBlock, LazyRollingDeltasFiatFromHeight, LazyValuePerBlock, RollingWindows, }; @@ -15,7 +15,7 @@ pub struct Vecs { pub circulating: LazyValuePerBlock, pub burned: burned::Vecs, - pub inflation_rate: PercentPerBlock, + pub inflation_rate: LazyPercentPerBlock, pub velocity: velocity::Vecs, pub market_cap: LazyFiatPerBlock, #[traversable(wrap = "market_cap", rename = "delta")] diff --git a/crates/brk_computer/src/transactions/compute.rs b/crates/brk_computer/src/transactions/compute.rs index e64dd3072..1fe8e47d4 100644 --- a/crates/brk_computer/src/transactions/compute.rs +++ b/crates/brk_computer/src/transactions/compute.rs @@ -19,7 +19,7 @@ impl Vecs { self.db.sync_bg_tasks()?; let (r1, (r2, r3)) = rayon::join( - || self.count.compute(indexer, indexes, &blocks.lookback, exit), + || self.count.compute(indexer, &blocks.lookback, exit), || { rayon::join( || self.versions.compute(indexer, exit), @@ -40,7 +40,7 @@ impl Vecs { self.policy.compute(indexer, indexes, &self.fees, exit)?; self.volume - .compute(indexer, indexes, prices, &self.count, &self.fees, exit)?; + .compute(indexer, indexes, prices, &self.fees, exit)?; let exit = exit.clone(); self.db.run_bg(move |db| { diff --git a/crates/brk_computer/src/transactions/count/compute.rs b/crates/brk_computer/src/transactions/count/compute.rs index 778acdaef..5f11c59ee 100644 --- a/crates/brk_computer/src/transactions/count/compute.rs +++ b/crates/brk_computer/src/transactions/count/compute.rs @@ -3,28 +3,19 @@ use brk_indexer::Indexer; use vecdb::Exit; use super::Vecs; -use crate::{blocks, indexes}; +use crate::blocks; impl Vecs { pub(crate) fn compute( &mut self, indexer: &Indexer, - indexes: &indexes::Vecs, lookback: &blocks::LookbackVecs, exit: &Exit, ) -> Result<()> { let starting_height = indexer.safe_lengths().height; let window_starts = lookback.window_starts(); - self.total - .compute(starting_height, &window_starts, exit, |height| { - Ok(height.compute_transform( - starting_height, - &indexes.height.tx_index_count, - |(height, count, ..)| (height, count), - exit, - )?) - })?; + self.total.compute(starting_height, &window_starts, exit)?; Ok(()) } diff --git a/crates/brk_computer/src/transactions/count/import.rs b/crates/brk_computer/src/transactions/count/import.rs index bcd8a7151..316a0868a 100644 --- a/crates/brk_computer/src/transactions/count/import.rs +++ b/crates/brk_computer/src/transactions/count/import.rs @@ -1,5 +1,5 @@ use brk_error::Result; -use brk_types::Version; +use brk_types::{Height, StoredU64, Version}; use vecdb::Database; use super::Vecs; @@ -8,6 +8,10 @@ use crate::{ internal::{PerBlockFull, WindowStartVec, Windows}, }; +fn tx_count(_: Height, count: StoredU64) -> StoredU64 { + count +} + impl Vecs { pub(crate) fn forced_import( db: &Database, @@ -16,7 +20,15 @@ impl Vecs { cached_starts: &Windows<&WindowStartVec>, ) -> Result { Ok(Self { - total: PerBlockFull::forced_import(db, "tx_count", version, indexes, cached_starts)?, + total: PerBlockFull::forced_import( + db, + "tx_count", + version, + &indexes.height.tx_index_count, + tx_count, + indexes, + cached_starts, + )?, }) } } diff --git a/crates/brk_computer/src/transactions/count/vecs.rs b/crates/brk_computer/src/transactions/count/vecs.rs index 3b9c1e438..b99daf85f 100644 --- a/crates/brk_computer/src/transactions/count/vecs.rs +++ b/crates/brk_computer/src/transactions/count/vecs.rs @@ -6,5 +6,5 @@ use crate::internal::PerBlockFull; #[derive(Traversable)] pub struct Vecs { - pub total: PerBlockFull, + pub total: PerBlockFull, } diff --git a/crates/brk_computer/src/transactions/import.rs b/crates/brk_computer/src/transactions/import.rs index 30a3ca389..5a1337ae3 100644 --- a/crates/brk_computer/src/transactions/import.rs +++ b/crates/brk_computer/src/transactions/import.rs @@ -33,7 +33,13 @@ impl Vecs { let patterns = PatternsVecs::forced_import(&db, version)?; let policy = PolicyVecs::forced_import(&db, version)?; let versions = VersionsVecs::forced_import(&db, version, indexes, cached_starts)?; - let volume = VolumeVecs::forced_import(&db, version, indexes, cached_starts)?; + let volume = VolumeVecs::forced_import( + &db, + version, + indexes, + cached_starts, + &count.total.rolling.sum, + )?; let this = Self { db, diff --git a/crates/brk_computer/src/transactions/volume/compute.rs b/crates/brk_computer/src/transactions/volume/compute.rs index af6115e00..8a9d47807 100644 --- a/crates/brk_computer/src/transactions/volume/compute.rs +++ b/crates/brk_computer/src/transactions/volume/compute.rs @@ -1,20 +1,17 @@ use brk_error::Result; use brk_indexer::Indexer; -use brk_types::StoredF32; use vecdb::Exit; use super::Vecs; -use crate::transactions::{count, fees}; -use crate::{indexes, internal::Windows, price}; +use crate::transactions::fees; +use crate::{indexes, price}; impl Vecs { - #[allow(clippy::too_many_arguments)] pub(crate) fn compute( &mut self, indexer: &Indexer, indexes: &indexes::Vecs, prices: &price::Vecs, - count_vecs: &count::Vecs, fees_vecs: &fees::Vecs, exit: &Exit, ) -> Result<()> { @@ -29,16 +26,6 @@ impl Vecs { |sats| !sats.is_max(), exit, )?; - let tx_sums = count_vecs.total.rolling.sum.0.as_array(); - let tx_per_sec = self.tx_per_sec.as_mut_array(); - for (i, &secs) in Windows::<()>::SECS.iter().enumerate() { - tx_per_sec[i].height.compute_transform( - starting_height, - &tx_sums[i].height, - |(h, sum, ..)| (h, StoredF32::from(*sum as f64 / secs)), - exit, - )?; - } Ok(()) } diff --git a/crates/brk_computer/src/transactions/volume/import.rs b/crates/brk_computer/src/transactions/volume/import.rs index 5bc18a930..f2e0a85f2 100644 --- a/crates/brk_computer/src/transactions/volume/import.rs +++ b/crates/brk_computer/src/transactions/volume/import.rs @@ -1,11 +1,14 @@ use brk_error::Result; -use brk_types::Version; +use brk_types::{StoredU64, Version}; use vecdb::Database; use super::Vecs; use crate::{ indexes, - internal::{PerBlock, ValuePerBlockCumulativeRolling, WindowStartVec, Windows}, + internal::{ + LazyPerSecondWindows, LazyRollingSumsFromHeight, ValuePerBlockCumulativeRolling, + WindowStartVec, Windows, + }, }; impl Vecs { @@ -14,6 +17,7 @@ impl Vecs { version: Version, indexes: &indexes::Vecs, cached_starts: &Windows<&WindowStartVec>, + tx_count_sums: &LazyRollingSumsFromHeight, ) -> Result { let v = version + Version::TWO; Ok(Self { @@ -24,9 +28,7 @@ impl Vecs { indexes, cached_starts, )?, - tx_per_sec: Windows::try_from_fn(|suffix| { - PerBlock::forced_import(db, &format!("tx_per_sec_{suffix}"), v, indexes) - })?, + tx_per_sec: LazyPerSecondWindows::new("tx_per_sec", v, tx_count_sums), }) } } diff --git a/crates/brk_computer/src/transactions/volume/vecs.rs b/crates/brk_computer/src/transactions/volume/vecs.rs index 4c6815992..0dc87e210 100644 --- a/crates/brk_computer/src/transactions/volume/vecs.rs +++ b/crates/brk_computer/src/transactions/volume/vecs.rs @@ -1,11 +1,10 @@ use brk_traversable::Traversable; -use brk_types::StoredF32; use vecdb::{Rw, StorageMode}; -use crate::internal::{PerBlock, ValuePerBlockCumulativeRolling, Windows}; +use crate::internal::{LazyPerSecondWindows, ValuePerBlockCumulativeRolling}; #[derive(Traversable)] pub struct Vecs { pub transfer_volume: ValuePerBlockCumulativeRolling, - pub tx_per_sec: Windows>, + pub tx_per_sec: LazyPerSecondWindows, } diff --git a/crates/brk_types/src/cents.rs b/crates/brk_types/src/cents.rs index b663c3a17..14bde49ab 100644 --- a/crates/brk_types/src/cents.rs +++ b/crates/brk_types/src/cents.rs @@ -6,12 +6,13 @@ use std::{ use derive_more::Deref; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use vecdb::{CheckedSub, Formattable, Pco}; +use vecdb::{CheckedSub, Formattable, Pco, unlikely}; use super::{CentsSats, Dollars, Sats, StoredF64}; /// Unsigned cents (u64) - for values that should never be negative. /// Used for invested capital, realized cap, etc. +/// `u64::MAX` is reserved as a NaN sentinel. #[derive( Debug, Default, @@ -32,53 +33,105 @@ pub struct Cents(u64); impl Cents { pub const ZERO: Self = Self(0); - pub const MAX: Self = Self(u64::MAX); + pub const MAX_FINITE: Self = Self(u64::MAX - 1); + pub const NAN: Self = Self(u64::MAX); + pub const MAX: Self = Self::NAN; #[inline] pub const fn new(value: u64) -> Self { + assert!(value != u64::MAX, "u64::MAX is reserved as Cents::NAN"); Self(value) } - #[inline(always)] - pub const fn inner(self) -> u64 { - self.0 + #[inline] + pub const fn is_nan(self) -> bool { + self.0 == u64::MAX } - #[inline(always)] + #[inline] + pub const fn finite_inner(self) -> Option { + if self.is_nan() { None } else { Some(self.0) } + } + + #[inline] + pub const fn inner(self) -> u64 { + match self.finite_inner() { + Some(value) => value, + None => panic!("Cents::NAN has no finite integer representation"), + } + } + + #[inline] pub const fn as_u128(self) -> u128 { - self.0 as u128 + self.inner() as u128 + } + + #[inline] + fn checked_finite(value: Option) -> Option { + match value { + Some(u64::MAX) | None => None, + Some(value) => Some(Self(value)), + } + } + + #[inline] + fn from_finite_u128(value: u128) -> Self { + assert!( + value < u64::MAX as u128, + "u128 overflow or Cents::NAN sentinel collision" + ); + Self(value as u64) } #[inline] pub fn checked_sub(self, rhs: Self) -> Option { - self.0.checked_sub(rhs.0).map(Self) + if unlikely(self.is_nan() || rhs.is_nan()) { + Some(Self::NAN) + } else { + self.0.checked_sub(rhs.0).map(Self) + } } #[inline] pub fn saturating_sub(self, rhs: Self) -> Self { - Self(self.0.saturating_sub(rhs.0)) + if unlikely(self.is_nan() || rhs.is_nan()) { + Self::NAN + } else { + Self(self.0.saturating_sub(rhs.0)) + } } #[inline] pub fn checked_add(self, rhs: Self) -> Option { - self.0.checked_add(rhs.0).map(Self) + if unlikely(self.is_nan() || rhs.is_nan()) { + Some(Self::NAN) + } else { + Self::checked_finite(self.0.checked_add(rhs.0)) + } } pub fn to_dollars(self) -> Dollars { - Dollars::from(self.0 as f64 / 100.0) + if unlikely(self.is_nan()) { + Dollars::NAN + } else { + Dollars::from(self.0 as f64 / 100.0) + } } /// Round to N significant digits. /// E.g., 12345 (= $123.45) with round_to(4) → 12350 (= $123.50) /// E.g., 12345 (= $123.45) with round_to(3) → 12300 (= $123.00) pub fn round_to(self, digits: i32) -> Self { + if unlikely(self.is_nan()) { + return Self::NAN; + } let v = self.0; let ilog10 = v.checked_ilog10().unwrap_or(0) as i32; if ilog10 >= digits { let log_diff = ilog10 - digits + 1; - let pow = 10u64.pow(log_diff as u32); + let pow = 10u128.pow(log_diff as u32); // Add half for rounding - Self(((v + pow / 2) / pow) * pow) + Self::from_finite_u128(((v as u128 + pow / 2) / pow) * pow) } else { self } @@ -89,18 +142,21 @@ impl Cents { /// E.g., 1234567 (= $12345.67) → 1234600 (= $12346.00) with 5 digits #[inline] pub fn round_to_dollar(self, digits: i32) -> Self { + if unlikely(self.is_nan()) { + return Self::NAN; + } // Round to nearest dollar (nearest 100 cents) - let dollars = (self.0 + 50) / 100; + let dollars = (self.0 as u128 + 50) / 100; // Apply significant digit rounding to dollars, then convert back to cents let ilog10 = dollars.checked_ilog10().unwrap_or(0) as i32; let rounded_dollars = if ilog10 >= digits { let log_diff = ilog10 - digits + 1; - let pow = 10u64.pow(log_diff as u32); + let pow = 10u128.pow(log_diff as u32); ((dollars + pow / 2) / pow) * pow } else { dollars }; - Self(rounded_dollars * 100) + Self::from_finite_u128(rounded_dollars * 100) } } @@ -108,10 +164,17 @@ impl From for Cents { #[inline] fn from(value: Dollars) -> Self { let f = f64::from(value); - if f.is_nan() || f < 0.0 { + if unlikely(!f.is_finite()) { + Self::NAN + } else if f < 0.0 { Self::ZERO } else { - Self((f * 100.0).round() as u64) + let cents = (f * 100.0).round(); + if cents >= u64::MAX as f64 { + Self::MAX_FINITE + } else { + Self(cents as u64) + } } } } @@ -126,51 +189,58 @@ impl From for Dollars { impl From for Cents { #[inline] fn from(value: u64) -> Self { - Self(value) + Self::new(value) } } impl From for Cents { #[inline] fn from(value: usize) -> Self { - Self(value as u64) + Self::new(u64::try_from(value).expect("usize overflow to Cents")) } } impl From for u64 { #[inline] fn from(value: Cents) -> Self { - value.0 + value.inner() } } impl From for Cents { #[inline] fn from(value: u128) -> Self { - debug_assert!(value <= u64::MAX as u128, "u128 overflow to CentsUnsigned"); - Self(value as u64) + Self::from_finite_u128(value) } } impl From for u128 { #[inline] fn from(value: Cents) -> Self { - value.0 as u128 + value.as_u128() } } impl From for f64 { #[inline] fn from(value: Cents) -> Self { - value.0 as f64 + if unlikely(value.is_nan()) { + f64::NAN + } else { + value.0 as f64 + } } } impl From for Cents { #[inline] fn from(value: f64) -> Self { - if value.is_nan() || value < 0.0 { + if unlikely(!value.is_finite()) { + Self::NAN + } else if value < 0.0 { Self::ZERO + } else if value >= u64::MAX as f64 { + Self::MAX_FINITE } else { Self(value as u64) } @@ -181,20 +251,20 @@ impl Add for Cents { type Output = Self; #[inline] fn add(self, rhs: Self) -> Self::Output { - Self(self.0 + rhs.0) + self.checked_add(rhs).expect("Cents overflow") } } impl AddAssign for Cents { #[inline] fn add_assign(&mut self, rhs: Self) { - self.0 += rhs.0; + *self = *self + rhs; } } impl Sum for Cents { fn sum>(iter: I) -> Self { - Self(iter.map(|c| c.0).sum()) + iter.fold(Self::ZERO, |sum, value| sum + value) } } @@ -202,14 +272,14 @@ impl Sub for Cents { type Output = Self; #[inline] fn sub(self, rhs: Self) -> Self::Output { - Self(self.0 - rhs.0) + self.checked_sub(rhs).expect("Cents underflow") } } impl SubAssign for Cents { #[inline] fn sub_assign(&mut self, rhs: Self) { - self.0 -= rhs.0; + *self = *self - rhs; } } @@ -217,7 +287,11 @@ impl Mul for Cents { type Output = Self; #[inline] fn mul(self, rhs: Self) -> Self::Output { - Self(self.0.checked_mul(rhs.0).expect("CentsUnsigned overflow")) + if unlikely(self.is_nan() || rhs.is_nan()) { + Self::NAN + } else { + Self::checked_finite(self.0.checked_mul(rhs.0)).expect("Cents overflow") + } } } @@ -225,7 +299,11 @@ impl Mul for Cents { type Output = Self; #[inline] fn mul(self, rhs: u64) -> Self::Output { - Self(self.0 * rhs) + if unlikely(self.is_nan()) { + Self::NAN + } else { + Self::checked_finite(self.0.checked_mul(rhs)).expect("Cents overflow") + } } } @@ -233,7 +311,12 @@ impl Mul for Cents { type Output = Self; #[inline] fn mul(self, rhs: usize) -> Self::Output { - Self(self.0 * rhs as u64) + if unlikely(self.is_nan()) { + Self::NAN + } else { + let rhs = u64::try_from(rhs).expect("usize overflow to Cents multiplier"); + Self::checked_finite(self.0.checked_mul(rhs)).expect("Cents overflow") + } } } @@ -257,7 +340,11 @@ impl Div for Cents { type Output = Self; #[inline] fn div(self, rhs: Self) -> Self::Output { - Self(self.0 / rhs.0) + if unlikely(self.is_nan() || rhs.is_nan()) { + Self::NAN + } else { + Self(self.0 / rhs.0) + } } } @@ -265,7 +352,11 @@ impl Div for Cents { type Output = Self; #[inline] fn div(self, rhs: u64) -> Self::Output { - Self(self.0 / rhs) + if unlikely(self.is_nan()) { + Self::NAN + } else { + Self(self.0 / rhs) + } } } @@ -273,13 +364,17 @@ impl Div for Cents { type Output = Self; #[inline] fn div(self, rhs: usize) -> Self::Output { - Self(self.0 / rhs as u64) + if unlikely(self.is_nan()) { + Self::NAN + } else { + Self(self.0 / rhs as u64) + } } } impl CheckedSub for Cents { fn checked_sub(self, rhs: Self) -> Option { - self.0.checked_sub(rhs.0).map(Self) + Cents::checked_sub(self, rhs) } } @@ -297,4 +392,63 @@ impl Formattable for Cents { let mut b = itoa::Buffer::new(); buf.extend_from_slice(b.format(self.0).as_bytes()); } + + #[inline(always)] + fn fmt_json(&self, buf: &mut Vec) { + if unlikely(self.is_nan()) { + buf.extend_from_slice(b"null"); + } else { + self.write_to(buf); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn conversions_and_sentinel() { + assert_eq!(Cents::MAX, Cents::NAN); + assert_eq!(Cents::MAX_FINITE.inner(), u64::MAX - 1); + assert!(Cents::NAN.is_nan()); + assert_eq!(Cents::NAN.finite_inner(), None); + assert!(f64::from(Dollars::from(Cents::NAN)).is_nan()); + assert!(f64::from(Cents::NAN).is_nan()); + assert!(Cents::from(Dollars::NAN).is_nan()); + assert_eq!(Cents::from(f64::MAX), Cents::MAX_FINITE); + + let mut json = Vec::new(); + Cents::NAN.fmt_json(&mut json); + assert_eq!(json, b"null"); + } + + #[test] + fn nan_propagates_through_cents_arithmetic() { + let finite = Cents::new(2); + + assert_eq!(Cents::NAN + finite, Cents::NAN); + assert_eq!(Cents::NAN - finite, Cents::NAN); + assert_eq!(Cents::NAN * finite, Cents::NAN); + assert_eq!(Cents::NAN / finite, Cents::NAN); + assert_eq!(Cents::NAN.checked_add(finite), Some(Cents::NAN)); + assert_eq!(Cents::NAN.checked_sub(finite), Some(Cents::NAN)); + } + + #[test] + fn finite_arithmetic_cannot_create_nan() { + assert_eq!(Cents::MAX_FINITE.checked_add(Cents::new(1)), None,); + + let exact_sentinel_factor = u64::MAX / 3; + assert!( + std::panic::catch_unwind(|| Cents::new(3) * Cents::new(exact_sentinel_factor)).is_err() + ); + } + + #[test] + fn raw_integer_access_rejects_nan() { + assert!(std::panic::catch_unwind(|| Cents::NAN.inner()).is_err()); + assert!(std::panic::catch_unwind(|| Cents::NAN.as_u128()).is_err()); + assert!(std::panic::catch_unwind(|| u64::from(Cents::NAN)).is_err()); + } } diff --git a/modules/brk-client/index.js b/modules/brk-client/index.js index 3bd8c3f12..53ab2212a 100644 --- a/modules/brk-client/index.js +++ b/modules/brk-client/index.js @@ -338,6 +338,7 @@ prior template's transactions or a full transaction body. /** * Unsigned cents (u64) - for values that should never be negative. * Used for invested capital, realized cap, etc. + * `u64::MAX` is reserved as a NaN sentinel. * * @typedef {number} Cents */ @@ -3222,15 +3223,15 @@ function createCapitalizedGrossInvestedLossNetNuplProfitSentimentPattern2(client /** * @typedef {Object} Pct10Pct20Pct30Pct40Pct50Pct60Pct70Pct80Pct90Pattern - * @property {SeriesPattern1} pct10 - * @property {SeriesPattern1} pct20 - * @property {SeriesPattern1} pct30 - * @property {SeriesPattern1} pct40 - * @property {SeriesPattern1} pct50 - * @property {SeriesPattern1} pct60 - * @property {SeriesPattern1} pct70 - * @property {SeriesPattern1} pct80 - * @property {SeriesPattern1} pct90 + * @property {CentsSatsUsdPattern} pct10 + * @property {CentsSatsUsdPattern} pct20 + * @property {CentsSatsUsdPattern} pct30 + * @property {CentsSatsUsdPattern} pct40 + * @property {CentsSatsUsdPattern} pct50 + * @property {CentsSatsUsdPattern} pct60 + * @property {CentsSatsUsdPattern} pct70 + * @property {CentsSatsUsdPattern} pct80 + * @property {CentsSatsUsdPattern} pct90 */ /** @@ -3241,15 +3242,15 @@ function createCapitalizedGrossInvestedLossNetNuplProfitSentimentPattern2(client */ function createPct10Pct20Pct30Pct40Pct50Pct60Pct70Pct80Pct90Pattern(client, acc) { return { - pct10: createSeriesPattern1(client, _m(acc, 'pct10')), - pct20: createSeriesPattern1(client, _m(acc, 'pct20')), - pct30: createSeriesPattern1(client, _m(acc, 'pct30')), - pct40: createSeriesPattern1(client, _m(acc, 'pct40')), - pct50: createSeriesPattern1(client, _m(acc, 'pct50')), - pct60: createSeriesPattern1(client, _m(acc, 'pct60')), - pct70: createSeriesPattern1(client, _m(acc, 'pct70')), - pct80: createSeriesPattern1(client, _m(acc, 'pct80')), - pct90: createSeriesPattern1(client, _m(acc, 'pct90')), + pct10: createCentsSatsUsdPattern(client, _m(acc, 'pct10')), + pct20: createCentsSatsUsdPattern(client, _m(acc, 'pct20')), + pct30: createCentsSatsUsdPattern(client, _m(acc, 'pct30')), + pct40: createCentsSatsUsdPattern(client, _m(acc, 'pct40')), + pct50: createCentsSatsUsdPattern(client, _m(acc, 'pct50')), + pct60: createCentsSatsUsdPattern(client, _m(acc, 'pct60')), + pct70: createCentsSatsUsdPattern(client, _m(acc, 'pct70')), + pct80: createCentsSatsUsdPattern(client, _m(acc, 'pct80')), + pct90: createCentsSatsUsdPattern(client, _m(acc, 'pct90')), }; } @@ -3358,7 +3359,7 @@ function create_1m1w1y24hPercentPpmRatioPattern(client, acc) { * @property {CoindaysCoinyearsDormancyTransferPattern} activity * @property {InMaxMinPerSupplyPattern} costBasis * @property {InPattern} investedCapital - * @property {SpentUnspentUtxoPattern} outputs + * @property {SpentUnspentPattern} outputs * @property {CapCapitalizedGrossLossMvrvNetPeakPriceProfitSellSoprPattern2} realized * @property {DeltaDominanceHalfInTotalPattern2} supply * @property {CapitalizedGrossInvestedLossNetNuplProfitSentimentPattern2} unrealized @@ -3375,7 +3376,7 @@ function createActivityCostInvestedOutputsRealizedSupplyUnrealizedPattern2(clien activity: createCoindaysCoinyearsDormancyTransferPattern(client, acc), costBasis: createInMaxMinPerSupplyPattern(client, acc), investedCapital: createInPattern(client, _m(acc, 'invested_capital_in')), - outputs: createSpentUnspentUtxoPattern(client, acc), + outputs: createSpentUnspentPattern(client, acc), realized: createCapCapitalizedGrossLossMvrvNetPeakPriceProfitSellSoprPattern2(client, acc), supply: createDeltaDominanceHalfInTotalPattern2(client, _m(acc, 'supply')), unrealized: createCapitalizedGrossInvestedLossNetNuplProfitSentimentPattern2(client, acc), @@ -3533,7 +3534,7 @@ function createMaxMedianMinPct10Pct25Pct75Pct90Pattern(client, acc) { * @typedef {Object} ActivityAddrOutputsRealizedSupplyUnrealizedPattern * @property {TransferPattern} activity * @property {BaseDeltaPattern} addrCount - * @property {SpentUnspentUtxoPattern} outputs + * @property {SpentUnspentPattern} outputs * @property {CapLossMvrvPriceProfitPattern} realized * @property {DeltaDominanceTotalPattern} supply * @property {NuplPattern} unrealized @@ -3549,7 +3550,7 @@ function createActivityAddrOutputsRealizedSupplyUnrealizedPattern(client, acc) { return { activity: createTransferPattern(client, _m(acc, 'transfer_volume')), addrCount: createBaseDeltaPattern(client, _m(acc, 'addr_count')), - outputs: createSpentUnspentUtxoPattern(client, acc), + outputs: createSpentUnspentPattern(client, acc), realized: createCapLossMvrvPriceProfitPattern(client, acc), supply: createDeltaDominanceTotalPattern(client, _m(acc, 'supply')), unrealized: createNuplPattern(client, _m(acc, 'nupl')), @@ -3742,7 +3743,7 @@ function createActiveBidirectionalReactivatedReceivingSendingPattern(client, acc /** * @typedef {Object} ActivityOutputsRealizedSupplyUnrealizedPattern * @property {CoindaysTransferPattern} activity - * @property {SpentUnspentUtxoPattern} outputs + * @property {SpentUnspentPattern} outputs * @property {CapLossMvrvNetPriceProfitSoprPattern} realized * @property {DeltaDominanceHalfInTotalPattern} supply * @property {LossNetNuplProfitPattern} unrealized @@ -3757,7 +3758,7 @@ function createActiveBidirectionalReactivatedReceivingSendingPattern(client, acc function createActivityOutputsRealizedSupplyUnrealizedPattern(client, acc) { return { activity: createCoindaysTransferPattern(client, acc), - outputs: createSpentUnspentUtxoPattern(client, acc), + outputs: createSpentUnspentPattern(client, acc), realized: createCapLossMvrvNetPriceProfitSoprPattern(client, acc), supply: createDeltaDominanceHalfInTotalPattern(client, _m(acc, 'supply')), unrealized: createLossNetNuplProfitPattern(client, acc), @@ -3767,7 +3768,7 @@ function createActivityOutputsRealizedSupplyUnrealizedPattern(client, acc) { /** * @typedef {Object} ActivityOutputsRealizedSupplyUnrealizedPattern3 * @property {TransferPattern} activity - * @property {SpentUnspentUtxoPattern} outputs + * @property {SpentUnspentPattern} outputs * @property {CapLossMvrvPriceProfitPattern} realized * @property {DeltaDominanceHalfInTotalPattern} supply * @property {LossNuplProfitPattern} unrealized @@ -3782,7 +3783,7 @@ function createActivityOutputsRealizedSupplyUnrealizedPattern(client, acc) { function createActivityOutputsRealizedSupplyUnrealizedPattern3(client, acc) { return { activity: createTransferPattern(client, _m(acc, 'transfer_volume')), - outputs: createSpentUnspentUtxoPattern(client, acc), + outputs: createSpentUnspentPattern(client, acc), realized: createCapLossMvrvPriceProfitPattern(client, acc), supply: createDeltaDominanceHalfInTotalPattern(client, _m(acc, 'supply')), unrealized: createLossNuplProfitPattern(client, acc), @@ -3792,7 +3793,7 @@ function createActivityOutputsRealizedSupplyUnrealizedPattern3(client, acc) { /** * @typedef {Object} ActivityOutputsRealizedSupplyUnrealizedPattern2 * @property {TransferPattern} activity - * @property {SpentUnspentUtxoPattern} outputs + * @property {SpentUnspentPattern} outputs * @property {CapLossMvrvPriceProfitPattern} realized * @property {DeltaDominanceTotalPattern} supply * @property {NuplPattern} unrealized @@ -3807,7 +3808,7 @@ function createActivityOutputsRealizedSupplyUnrealizedPattern3(client, acc) { function createActivityOutputsRealizedSupplyUnrealizedPattern2(client, acc) { return { activity: createTransferPattern(client, _m(acc, 'transfer_volume')), - outputs: createSpentUnspentUtxoPattern(client, acc), + outputs: createSpentUnspentPattern(client, acc), realized: createCapLossMvrvPriceProfitPattern(client, acc), supply: createDeltaDominanceTotalPattern(client, _m(acc, 'supply')), unrealized: createNuplPattern(client, _m(acc, 'nupl')), @@ -3982,6 +3983,56 @@ function createCentsToUsdPattern4(client, acc) { * @property {SeriesPattern1} signal */ +/** + * @typedef {Object} Pct95Pct98Pct99Pattern + * @property {CentsSatsUsdPattern} pct95 + * @property {CentsSatsUsdPattern} pct98 + * @property {CentsSatsUsdPattern} pct99 + * @property {CentsSatsUsdPattern} pct995 + * @property {CentsSatsUsdPattern} pct999 + */ + +/** + * Create a Pct95Pct98Pct99Pattern pattern node + * @param {BrkClient} client + * @param {string} acc - Accumulated series name + * @returns {Pct95Pct98Pct99Pattern} + */ +function createPct95Pct98Pct99Pattern(client, acc) { + return { + pct95: createCentsSatsUsdPattern(client, _m(acc, 'pct95')), + pct98: createCentsSatsUsdPattern(client, _m(acc, 'pct98')), + pct99: createCentsSatsUsdPattern(client, _m(acc, 'pct99')), + pct995: createCentsSatsUsdPattern(client, _m(acc, 'pct99_5')), + pct999: createCentsSatsUsdPattern(client, _m(acc, 'pct99_9')), + }; +} + +/** + * @typedef {Object} Pct95Pct98Pct99Pattern2 + * @property {SeriesPattern1} pct95 + * @property {SeriesPattern1} pct98 + * @property {SeriesPattern1} pct99 + * @property {SeriesPattern1} pct995 + * @property {SeriesPattern1} pct999 + */ + +/** + * Create a Pct95Pct98Pct99Pattern2 pattern node + * @param {BrkClient} client + * @param {string} acc - Accumulated series name + * @returns {Pct95Pct98Pct99Pattern2} + */ +function createPct95Pct98Pct99Pattern2(client, acc) { + return { + pct95: createSeriesPattern1(client, _m(acc, 'pct95')), + pct98: createSeriesPattern1(client, _m(acc, 'pct98')), + pct99: createSeriesPattern1(client, _m(acc, 'pct99')), + pct995: createSeriesPattern1(client, _m(acc, 'pct99_5')), + pct999: createSeriesPattern1(client, _m(acc, 'pct99_9')), + }; +} + /** * @typedef {Object} PhsReboundThsPattern * @property {SeriesPattern1} phs @@ -4007,33 +4058,6 @@ function createPhsReboundThsPattern(client, acc) { }; } -/** - * @template T - * @typedef {Object} Pct95Pct98Pct99Pattern - * @property {SeriesPattern1} pct95 - * @property {SeriesPattern1} pct98 - * @property {SeriesPattern1} pct99 - * @property {SeriesPattern1} pct995 - * @property {SeriesPattern1} pct999 - */ - -/** - * Create a Pct95Pct98Pct99Pattern pattern node - * @template T - * @param {BrkClient} client - * @param {string} acc - Accumulated series name - * @returns {Pct95Pct98Pct99Pattern} - */ -function createPct95Pct98Pct99Pattern(client, acc) { - return { - pct95: createSeriesPattern1(client, _m(acc, 'pct95')), - pct98: createSeriesPattern1(client, _m(acc, 'pct98')), - pct99: createSeriesPattern1(client, _m(acc, 'pct99')), - pct995: createSeriesPattern1(client, _m(acc, 'pct99_5')), - pct999: createSeriesPattern1(client, _m(acc, 'pct99_9')), - }; -} - /** * @typedef {Object} _1m1w1y24hPattern4 * @property {BtcCentsSatsUsdPattern} _1m @@ -4586,9 +4610,30 @@ function createBlockCumulativeSumPattern(client, acc) { }; } +/** + * @typedef {Object} BlockCumulativeSumPattern2 + * @property {SeriesPattern18} block + * @property {SeriesPattern1} cumulative + * @property {_1m1w1y24hPattern} sum + */ + +/** + * Create a BlockCumulativeSumPattern2 pattern node + * @param {BrkClient} client + * @param {string} acc - Accumulated series name + * @returns {BlockCumulativeSumPattern2} + */ +function createBlockCumulativeSumPattern2(client, acc) { + return { + block: createSeriesPattern18(client, acc), + cumulative: createSeriesPattern1(client, _m(acc, 'cumulative')), + sum: create_1m1w1y24hPattern(client, _m(acc, 'sum')), + }; +} + /** * @typedef {Object} BlocksDominanceRewardsPattern - * @property {AverageBlockCumulativeSumPattern} blocksMined + * @property {BlockCumulativeSumPattern2} blocksMined * @property {_1m1w1y24hPercentPpmRatioPattern} dominance * @property {AverageBlockCumulativeSumPattern2} rewards */ @@ -4601,7 +4646,7 @@ function createBlockCumulativeSumPattern(client, acc) { */ function createBlocksDominanceRewardsPattern(client, acc) { return { - blocksMined: createAverageBlockCumulativeSumPattern(client, _m(acc, 'blocks_mined')), + blocksMined: createBlockCumulativeSumPattern2(client, _m(acc, 'blocks_mined')), dominance: create_1m1w1y24hPercentPpmRatioPattern(client, _m(acc, 'dominance')), rewards: createAverageBlockCumulativeSumPattern2(client, _m(acc, 'rewards')), }; @@ -4742,9 +4787,9 @@ function createDeltaDominanceTotalPattern(client, acc) { /** * @typedef {Object} FloorLevelLossPattern - * @property {Pct95Pct98Pct99Pattern} floor + * @property {Pct95Pct98Pct99Pattern} floor * @property {Pct10Pct20Pct30Pct40Pct50Pct60Pct70Pct80Pct90Pattern} level - * @property {Pct95Pct98Pct99Pattern} lossThreshold + * @property {Pct95Pct98Pct99Pattern2} lossThreshold */ /** @@ -4757,7 +4802,7 @@ function createFloorLevelLossPattern(client, acc) { return { floor: createPct95Pct98Pct99Pattern(client, _m(acc, 'floor')), level: createPct10Pct20Pct30Pct40Pct50Pct60Pct70Pct80Pct90Pattern(client, _m(acc, 'level')), - lossThreshold: createPct95Pct98Pct99Pattern(client, _m(acc, 'loss_threshold')), + lossThreshold: createPct95Pct98Pct99Pattern2(client, _m(acc, 'loss_threshold')), }; } @@ -4824,6 +4869,27 @@ function createPercentPpmRatioPattern2(client, acc) { }; } +/** + * @typedef {Object} PercentPpmRatioPattern5 + * @property {SeriesPattern1} percent + * @property {SeriesPattern1} ppm + * @property {SeriesPattern1} ratio + */ + +/** + * Create a PercentPpmRatioPattern5 pattern node + * @param {BrkClient} client + * @param {string} acc - Accumulated series name + * @returns {PercentPpmRatioPattern5} + */ +function createPercentPpmRatioPattern5(client, acc) { + return { + percent: createSeriesPattern1(client, acc), + ppm: createSeriesPattern1(client, _m(acc, 'ppm')), + ratio: createSeriesPattern1(client, _m(acc, 'ratio')), + }; +} + /** * @typedef {Object} PercentPpmRatioPattern3 * @property {SeriesPattern1} percent @@ -4931,27 +4997,6 @@ function createRsiStochPattern(client, acc, disc) { }; } -/** - * @typedef {Object} SpentUnspentUtxoPattern - * @property {AverageBlockCumulativeSumPattern} spentCount - * @property {BaseDeltaPattern} unspentCount - * @property {SeriesPattern1} utxoTurnover1y - */ - -/** - * Create a SpentUnspentUtxoPattern pattern node - * @param {BrkClient} client - * @param {string} acc - Accumulated series name - * @returns {SpentUnspentUtxoPattern} - */ -function createSpentUnspentUtxoPattern(client, acc) { - return { - spentCount: createAverageBlockCumulativeSumPattern(client, _m(acc, 'spent_utxo_count')), - unspentCount: createBaseDeltaPattern(client, _m(acc, 'utxo_count')), - utxoTurnover1y: createSeriesPattern1(client, _m(acc, 'utxo_turnover_1y')), - }; -} - /** * @template T * @typedef {Object} _6bBlockTxPattern @@ -5168,7 +5213,7 @@ function createBlockCumulativePattern(client, acc) { /** * @typedef {Object} BlocksDominancePattern - * @property {AverageBlockCumulativeSumPattern} blocksMined + * @property {BlockCumulativeSumPattern2} blocksMined * @property {PercentPpmRatioPattern2} dominance */ @@ -5180,7 +5225,7 @@ function createBlockCumulativePattern(client, acc) { */ function createBlocksDominancePattern(client, acc) { return { - blocksMined: createAverageBlockCumulativeSumPattern(client, _m(acc, 'blocks_mined')), + blocksMined: createBlockCumulativeSumPattern2(client, _m(acc, 'blocks_mined')), dominance: createPercentPpmRatioPattern2(client, _m(acc, 'dominance')), }; } @@ -5495,6 +5540,25 @@ function createRatioValuePattern(client, acc) { * @property {SeriesPattern1} sma */ +/** + * @typedef {Object} SpentUnspentPattern + * @property {AverageBlockCumulativeSumPattern} spentCount + * @property {BaseDeltaPattern} unspentCount + */ + +/** + * Create a SpentUnspentPattern pattern node + * @param {BrkClient} client + * @param {string} acc - Accumulated series name + * @returns {SpentUnspentPattern} + */ +function createSpentUnspentPattern(client, acc) { + return { + spentCount: createAverageBlockCumulativeSumPattern(client, _m(acc, 'spent_utxo_count')), + unspentCount: createBaseDeltaPattern(client, _m(acc, 'utxo_count')), + }; +} + /** * @typedef {Object} ToPattern * @property {PercentPpmRatioPattern} toMcap @@ -6484,15 +6548,15 @@ function createTransferPattern(client, acc) { * @property {_1m1w1y24hPattern4} pct75 * @property {_1m1w1y24hPattern4} pct90 * @property {_1m1w1y24hPercentPpmRatioPattern} dominance - * @property {SeriesTree_Mining_Rewards_Fees_ToSubsidyRatio} toSubsidyRatio + * @property {SeriesTree_Mining_Rewards_Fees_ToSubsidy} toSubsidy */ /** - * @typedef {Object} SeriesTree_Mining_Rewards_Fees_ToSubsidyRatio - * @property {PpmRatioPattern3} _24h - * @property {PpmRatioPattern3} _1w - * @property {PpmRatioPattern3} _1m - * @property {PpmRatioPattern3} _1y + * @typedef {Object} SeriesTree_Mining_Rewards_Fees_ToSubsidy + * @property {PercentPpmRatioPattern5} _24h + * @property {PercentPpmRatioPattern5} _1w + * @property {PercentPpmRatioPattern5} _1m + * @property {PercentPpmRatioPattern5} _1y */ /** @@ -7573,7 +7637,6 @@ function createTransferPattern(client, acc) { * @typedef {Object} SeriesTree_Cohorts_Utxo_All_Outputs * @property {BaseDeltaPattern} unspentCount * @property {AverageBlockCumulativeSumPattern} spentCount - * @property {SeriesPattern1} utxoTurnover1y */ /** @@ -7672,7 +7735,7 @@ function createTransferPattern(client, acc) { /** * @typedef {Object} SeriesTree_Cohorts_Utxo_Sth * @property {DeltaDominanceHalfInTotalPattern2} supply - * @property {SpentUnspentUtxoPattern} outputs + * @property {SpentUnspentPattern} outputs * @property {CoindaysCoinyearsDormancyTransferPattern} activity * @property {CapCapitalizedGrossLossMvrvNetPeakPriceProfitSellSoprPattern} realized * @property {InMaxMinPerSupplyPattern} costBasis @@ -9818,11 +9881,11 @@ class BrkClient extends BrkClientBase { pct75: create_1m1w1y24hPattern4(this, 'fees_pct75'), pct90: create_1m1w1y24hPattern4(this, 'fees_pct90'), dominance: create_1m1w1y24hPercentPpmRatioPattern(this, 'fee_dominance'), - toSubsidyRatio: { - _24h: createPpmRatioPattern3(this, 'fee_to_subsidy_ratio_24h'), - _1w: createPpmRatioPattern3(this, 'fee_to_subsidy_ratio_1w'), - _1m: createPpmRatioPattern3(this, 'fee_to_subsidy_ratio_1m'), - _1y: createPpmRatioPattern3(this, 'fee_to_subsidy_ratio_1y'), + toSubsidy: { + _24h: createPercentPpmRatioPattern5(this, 'fee_to_subsidy_24h'), + _1w: createPercentPpmRatioPattern5(this, 'fee_to_subsidy_1w'), + _1m: createPercentPpmRatioPattern5(this, 'fee_to_subsidy_1m'), + _1y: createPercentPpmRatioPattern5(this, 'fee_to_subsidy_1y'), }, }, outputVolume: createSeriesPattern18(this, 'output_volume'), @@ -10598,7 +10661,6 @@ class BrkClient extends BrkClientBase { outputs: { unspentCount: createBaseDeltaPattern(this, 'utxo_count'), spentCount: createAverageBlockCumulativeSumPattern(this, 'spent_utxo_count'), - utxoTurnover1y: createSeriesPattern1(this, 'utxo_turnover_1y'), }, activity: { transferVolume: createAverageBlockCumulativeInSumPattern(this, 'transfer_volume'), @@ -10671,7 +10733,7 @@ class BrkClient extends BrkClientBase { }, sth: { supply: createDeltaDominanceHalfInTotalPattern2(this, 'sth_supply'), - outputs: createSpentUnspentUtxoPattern(this, 'sth'), + outputs: createSpentUnspentPattern(this, 'sth'), activity: createCoindaysCoinyearsDormancyTransferPattern(this, 'sth'), realized: createCapCapitalizedGrossLossMvrvNetPeakPriceProfitSellSoprPattern(this, 'sth'), costBasis: createInMaxMinPerSupplyPattern(this, 'sth'), diff --git a/packages/brk_client/brk_client/__init__.py b/packages/brk_client/brk_client/__init__.py index 9be9de60a..174eb12e8 100644 --- a/packages/brk_client/brk_client/__init__.py +++ b/packages/brk_client/brk_client/__init__.py @@ -96,6 +96,7 @@ TxVersionRaw = int BlockTemplateDiffEntry = Union[int, "Transaction"] # Unsigned cents (u64) - for values that should never be negative. # Used for invested capital, realized cap, etc. +# `u64::MAX` is reserved as a NaN sentinel. Cents = int # Cents × Sats (u128) - price in cents multiplied by amount in sats. # Uses u128 because large amounts at any price can overflow u64. @@ -3339,15 +3340,15 @@ class Pct10Pct20Pct30Pct40Pct50Pct60Pct70Pct80Pct90Pattern: def __init__(self, client: BrkClient, acc: str): """Create pattern node with accumulated series name.""" - self.pct10: SeriesPattern1[Dollars] = SeriesPattern1(client, _m(acc, 'pct10')) - self.pct20: SeriesPattern1[Dollars] = SeriesPattern1(client, _m(acc, 'pct20')) - self.pct30: SeriesPattern1[Dollars] = SeriesPattern1(client, _m(acc, 'pct30')) - self.pct40: SeriesPattern1[Dollars] = SeriesPattern1(client, _m(acc, 'pct40')) - self.pct50: SeriesPattern1[Dollars] = SeriesPattern1(client, _m(acc, 'pct50')) - self.pct60: SeriesPattern1[Dollars] = SeriesPattern1(client, _m(acc, 'pct60')) - self.pct70: SeriesPattern1[Dollars] = SeriesPattern1(client, _m(acc, 'pct70')) - self.pct80: SeriesPattern1[Dollars] = SeriesPattern1(client, _m(acc, 'pct80')) - self.pct90: SeriesPattern1[Dollars] = SeriesPattern1(client, _m(acc, 'pct90')) + self.pct10: CentsSatsUsdPattern = CentsSatsUsdPattern(client, _m(acc, 'pct10')) + self.pct20: CentsSatsUsdPattern = CentsSatsUsdPattern(client, _m(acc, 'pct20')) + self.pct30: CentsSatsUsdPattern = CentsSatsUsdPattern(client, _m(acc, 'pct30')) + self.pct40: CentsSatsUsdPattern = CentsSatsUsdPattern(client, _m(acc, 'pct40')) + self.pct50: CentsSatsUsdPattern = CentsSatsUsdPattern(client, _m(acc, 'pct50')) + self.pct60: CentsSatsUsdPattern = CentsSatsUsdPattern(client, _m(acc, 'pct60')) + self.pct70: CentsSatsUsdPattern = CentsSatsUsdPattern(client, _m(acc, 'pct70')) + self.pct80: CentsSatsUsdPattern = CentsSatsUsdPattern(client, _m(acc, 'pct80')) + self.pct90: CentsSatsUsdPattern = CentsSatsUsdPattern(client, _m(acc, 'pct90')) class ChainDataFeeFeesOutputTxPattern: """Pattern struct for repeated tree structure.""" @@ -3401,7 +3402,7 @@ class ActivityCostInvestedOutputsRealizedSupplyUnrealizedPattern2: self.activity: CoindaysCoinyearsDormancyTransferPattern = CoindaysCoinyearsDormancyTransferPattern(client, acc) self.cost_basis: InMaxMinPerSupplyPattern = InMaxMinPerSupplyPattern(client, acc) self.invested_capital: InPattern = InPattern(client, _m(acc, 'invested_capital_in')) - self.outputs: SpentUnspentUtxoPattern = SpentUnspentUtxoPattern(client, acc) + self.outputs: SpentUnspentPattern = SpentUnspentPattern(client, acc) self.realized: CapCapitalizedGrossLossMvrvNetPeakPriceProfitSellSoprPattern2 = CapCapitalizedGrossLossMvrvNetPeakPriceProfitSellSoprPattern2(client, acc) self.supply: DeltaDominanceHalfInTotalPattern2 = DeltaDominanceHalfInTotalPattern2(client, _m(acc, 'supply')) self.unrealized: CapitalizedGrossInvestedLossNetNuplProfitSentimentPattern2 = CapitalizedGrossInvestedLossNetNuplProfitSentimentPattern2(client, acc) @@ -3478,7 +3479,7 @@ class ActivityAddrOutputsRealizedSupplyUnrealizedPattern: """Create pattern node with accumulated series name.""" self.activity: TransferPattern = TransferPattern(client, _m(acc, 'transfer_volume')) self.addr_count: BaseDeltaPattern = BaseDeltaPattern(client, _m(acc, 'addr_count')) - self.outputs: SpentUnspentUtxoPattern = SpentUnspentUtxoPattern(client, acc) + self.outputs: SpentUnspentPattern = SpentUnspentPattern(client, acc) self.realized: CapLossMvrvPriceProfitPattern = CapLossMvrvPriceProfitPattern(client, acc) self.supply: DeltaDominanceTotalPattern = DeltaDominanceTotalPattern(client, _m(acc, 'supply')) self.unrealized: NuplPattern = NuplPattern(client, _m(acc, 'nupl')) @@ -3570,7 +3571,7 @@ class ActivityOutputsRealizedSupplyUnrealizedPattern: def __init__(self, client: BrkClient, acc: str): """Create pattern node with accumulated series name.""" self.activity: CoindaysTransferPattern = CoindaysTransferPattern(client, acc) - self.outputs: SpentUnspentUtxoPattern = SpentUnspentUtxoPattern(client, acc) + self.outputs: SpentUnspentPattern = SpentUnspentPattern(client, acc) self.realized: CapLossMvrvNetPriceProfitSoprPattern = CapLossMvrvNetPriceProfitSoprPattern(client, acc) self.supply: DeltaDominanceHalfInTotalPattern = DeltaDominanceHalfInTotalPattern(client, _m(acc, 'supply')) self.unrealized: LossNetNuplProfitPattern = LossNetNuplProfitPattern(client, acc) @@ -3581,7 +3582,7 @@ class ActivityOutputsRealizedSupplyUnrealizedPattern3: def __init__(self, client: BrkClient, acc: str): """Create pattern node with accumulated series name.""" self.activity: TransferPattern = TransferPattern(client, _m(acc, 'transfer_volume')) - self.outputs: SpentUnspentUtxoPattern = SpentUnspentUtxoPattern(client, acc) + self.outputs: SpentUnspentPattern = SpentUnspentPattern(client, acc) self.realized: CapLossMvrvPriceProfitPattern = CapLossMvrvPriceProfitPattern(client, acc) self.supply: DeltaDominanceHalfInTotalPattern = DeltaDominanceHalfInTotalPattern(client, _m(acc, 'supply')) self.unrealized: LossNuplProfitPattern = LossNuplProfitPattern(client, acc) @@ -3592,7 +3593,7 @@ class ActivityOutputsRealizedSupplyUnrealizedPattern2: def __init__(self, client: BrkClient, acc: str): """Create pattern node with accumulated series name.""" self.activity: TransferPattern = TransferPattern(client, _m(acc, 'transfer_volume')) - self.outputs: SpentUnspentUtxoPattern = SpentUnspentUtxoPattern(client, acc) + self.outputs: SpentUnspentPattern = SpentUnspentPattern(client, acc) self.realized: CapLossMvrvPriceProfitPattern = CapLossMvrvPriceProfitPattern(client, acc) self.supply: DeltaDominanceTotalPattern = DeltaDominanceTotalPattern(client, _m(acc, 'supply')) self.unrealized: NuplPattern = NuplPattern(client, _m(acc, 'nupl')) @@ -3671,6 +3672,28 @@ class EmaHistogramLineSignalPattern: """Pattern struct for repeated tree structure.""" pass +class Pct95Pct98Pct99Pattern: + """Pattern struct for repeated tree structure.""" + + def __init__(self, client: BrkClient, acc: str): + """Create pattern node with accumulated series name.""" + self.pct95: CentsSatsUsdPattern = CentsSatsUsdPattern(client, _m(acc, 'pct95')) + self.pct98: CentsSatsUsdPattern = CentsSatsUsdPattern(client, _m(acc, 'pct98')) + self.pct99: CentsSatsUsdPattern = CentsSatsUsdPattern(client, _m(acc, 'pct99')) + self.pct99_5: CentsSatsUsdPattern = CentsSatsUsdPattern(client, _m(acc, 'pct99_5')) + self.pct99_9: CentsSatsUsdPattern = CentsSatsUsdPattern(client, _m(acc, 'pct99_9')) + +class Pct95Pct98Pct99Pattern2: + """Pattern struct for repeated tree structure.""" + + def __init__(self, client: BrkClient, acc: str): + """Create pattern node with accumulated series name.""" + self.pct95: SeriesPattern1[StoredF64] = SeriesPattern1(client, _m(acc, 'pct95')) + self.pct98: SeriesPattern1[StoredF64] = SeriesPattern1(client, _m(acc, 'pct98')) + self.pct99: SeriesPattern1[StoredF64] = SeriesPattern1(client, _m(acc, 'pct99')) + self.pct99_5: SeriesPattern1[StoredF64] = SeriesPattern1(client, _m(acc, 'pct99_5')) + self.pct99_9: SeriesPattern1[StoredF64] = SeriesPattern1(client, _m(acc, 'pct99_9')) + class PhsReboundThsPattern: """Pattern struct for repeated tree structure.""" @@ -3682,17 +3705,6 @@ class PhsReboundThsPattern: self.ths: SeriesPattern1[StoredF32] = SeriesPattern1(client, _m(acc, 'ths')) self.ths_min: SeriesPattern1[StoredF32] = SeriesPattern1(client, _m(acc, 'ths_min')) -class Pct95Pct98Pct99Pattern(Generic[T]): - """Pattern struct for repeated tree structure.""" - - def __init__(self, client: BrkClient, acc: str): - """Create pattern node with accumulated series name.""" - self.pct95: SeriesPattern1[T] = SeriesPattern1(client, _m(acc, 'pct95')) - self.pct98: SeriesPattern1[T] = SeriesPattern1(client, _m(acc, 'pct98')) - self.pct99: SeriesPattern1[T] = SeriesPattern1(client, _m(acc, 'pct99')) - self.pct99_5: SeriesPattern1[T] = SeriesPattern1(client, _m(acc, 'pct99_5')) - self.pct99_9: SeriesPattern1[T] = SeriesPattern1(client, _m(acc, 'pct99_9')) - class _1m1w1y24hPattern4: """Pattern struct for repeated tree structure.""" @@ -3931,12 +3943,21 @@ class BlockCumulativeSumPattern: self.cumulative: CentsUsdPattern3 = CentsUsdPattern3(client, _m(acc, 'cumulative')) self.sum: _1m1w1y24hPattern6 = _1m1w1y24hPattern6(client, _m(acc, 'sum')) +class BlockCumulativeSumPattern2: + """Pattern struct for repeated tree structure.""" + + def __init__(self, client: BrkClient, acc: str): + """Create pattern node with accumulated series name.""" + self.block: SeriesPattern18[StoredU64] = SeriesPattern18(client, acc) + self.cumulative: SeriesPattern1[StoredU64] = SeriesPattern1(client, _m(acc, 'cumulative')) + self.sum: _1m1w1y24hPattern[StoredU64] = _1m1w1y24hPattern(client, _m(acc, 'sum')) + class BlocksDominanceRewardsPattern: """Pattern struct for repeated tree structure.""" def __init__(self, client: BrkClient, acc: str): """Create pattern node with accumulated series name.""" - self.blocks_mined: AverageBlockCumulativeSumPattern[StoredU64] = AverageBlockCumulativeSumPattern(client, _m(acc, 'blocks_mined')) + self.blocks_mined: BlockCumulativeSumPattern2 = BlockCumulativeSumPattern2(client, _m(acc, 'blocks_mined')) self.dominance: _1m1w1y24hPercentPpmRatioPattern = _1m1w1y24hPercentPpmRatioPattern(client, _m(acc, 'dominance')) self.rewards: AverageBlockCumulativeSumPattern2 = AverageBlockCumulativeSumPattern2(client, _m(acc, 'rewards')) @@ -4003,9 +4024,9 @@ class FloorLevelLossPattern: def __init__(self, client: BrkClient, acc: str): """Create pattern node with accumulated series name.""" - self.floor: Pct95Pct98Pct99Pattern[Dollars] = Pct95Pct98Pct99Pattern(client, _m(acc, 'floor')) + self.floor: Pct95Pct98Pct99Pattern = Pct95Pct98Pct99Pattern(client, _m(acc, 'floor')) self.level: Pct10Pct20Pct30Pct40Pct50Pct60Pct70Pct80Pct90Pattern = Pct10Pct20Pct30Pct40Pct50Pct60Pct70Pct80Pct90Pattern(client, _m(acc, 'level')) - self.loss_threshold: Pct95Pct98Pct99Pattern[StoredF64] = Pct95Pct98Pct99Pattern(client, _m(acc, 'loss_threshold')) + self.loss_threshold: Pct95Pct98Pct99Pattern2 = Pct95Pct98Pct99Pattern2(client, _m(acc, 'loss_threshold')) class GreedNetPainPattern: """Pattern struct for repeated tree structure.""" @@ -4034,6 +4055,15 @@ class PercentPpmRatioPattern2: self.ppm: SeriesPattern1[PartsPerMillion32] = SeriesPattern1(client, _m(acc, 'ppm')) self.ratio: SeriesPattern1[StoredF32] = SeriesPattern1(client, _m(acc, 'ratio')) +class PercentPpmRatioPattern5: + """Pattern struct for repeated tree structure.""" + + def __init__(self, client: BrkClient, acc: str): + """Create pattern node with accumulated series name.""" + self.percent: SeriesPattern1[StoredF32] = SeriesPattern1(client, acc) + self.ppm: SeriesPattern1[PartsPerMillion64] = SeriesPattern1(client, _m(acc, 'ppm')) + self.ratio: SeriesPattern1[StoredF32] = SeriesPattern1(client, _m(acc, 'ratio')) + class PercentPpmRatioPattern3: """Pattern struct for repeated tree structure.""" @@ -4079,15 +4109,6 @@ class RsiStochPattern: self.stoch_rsi_d: PercentPpmRatioPattern2 = PercentPpmRatioPattern2(client, _m(acc, f'stoch_d_{disc}')) self.stoch_rsi_k: PercentPpmRatioPattern2 = PercentPpmRatioPattern2(client, _m(acc, f'stoch_k_{disc}')) -class SpentUnspentUtxoPattern: - """Pattern struct for repeated tree structure.""" - - def __init__(self, client: BrkClient, acc: str): - """Create pattern node with accumulated series name.""" - self.spent_count: AverageBlockCumulativeSumPattern[StoredU64] = AverageBlockCumulativeSumPattern(client, _m(acc, 'spent_utxo_count')) - self.unspent_count: BaseDeltaPattern = BaseDeltaPattern(client, _m(acc, 'utxo_count')) - self.utxo_turnover_1y: SeriesPattern1[StoredF32] = SeriesPattern1(client, _m(acc, 'utxo_turnover_1y')) - class _6bBlockTxPattern(Generic[T]): """Pattern struct for repeated tree structure.""" @@ -4182,7 +4203,7 @@ class BlocksDominancePattern: def __init__(self, client: BrkClient, acc: str): """Create pattern node with accumulated series name.""" - self.blocks_mined: AverageBlockCumulativeSumPattern[StoredU64] = AverageBlockCumulativeSumPattern(client, _m(acc, 'blocks_mined')) + self.blocks_mined: BlockCumulativeSumPattern2 = BlockCumulativeSumPattern2(client, _m(acc, 'blocks_mined')) self.dominance: PercentPpmRatioPattern2 = PercentPpmRatioPattern2(client, _m(acc, 'dominance')) class BtcSatsPattern: @@ -4317,6 +4338,14 @@ class SdSmaPattern: """Pattern struct for repeated tree structure.""" pass +class SpentUnspentPattern: + """Pattern struct for repeated tree structure.""" + + def __init__(self, client: BrkClient, acc: str): + """Create pattern node with accumulated series name.""" + self.spent_count: AverageBlockCumulativeSumPattern[StoredU64] = AverageBlockCumulativeSumPattern(client, _m(acc, 'spent_utxo_count')) + self.unspent_count: BaseDeltaPattern = BaseDeltaPattern(client, _m(acc, 'utxo_count')) + class ToPattern: """Pattern struct for repeated tree structure.""" @@ -5237,14 +5266,14 @@ class SeriesTree_Mining_Rewards_Subsidy: self.average: _1m1w1y24hPattern3 = _1m1w1y24hPattern3(client, 'subsidy_average') self.dominance: _1m1w1y24hPercentPpmRatioPattern = _1m1w1y24hPercentPpmRatioPattern(client, 'subsidy_dominance') -class SeriesTree_Mining_Rewards_Fees_ToSubsidyRatio: +class SeriesTree_Mining_Rewards_Fees_ToSubsidy: """Series tree node.""" def __init__(self, client: BrkClient, base_path: str = ''): - self._24h: PpmRatioPattern3 = PpmRatioPattern3(client, 'fee_to_subsidy_ratio_24h') - self._1w: PpmRatioPattern3 = PpmRatioPattern3(client, 'fee_to_subsidy_ratio_1w') - self._1m: PpmRatioPattern3 = PpmRatioPattern3(client, 'fee_to_subsidy_ratio_1m') - self._1y: PpmRatioPattern3 = PpmRatioPattern3(client, 'fee_to_subsidy_ratio_1y') + self._24h: PercentPpmRatioPattern5 = PercentPpmRatioPattern5(client, 'fee_to_subsidy_24h') + self._1w: PercentPpmRatioPattern5 = PercentPpmRatioPattern5(client, 'fee_to_subsidy_1w') + self._1m: PercentPpmRatioPattern5 = PercentPpmRatioPattern5(client, 'fee_to_subsidy_1m') + self._1y: PercentPpmRatioPattern5 = PercentPpmRatioPattern5(client, 'fee_to_subsidy_1y') class SeriesTree_Mining_Rewards_Fees: """Series tree node.""" @@ -5262,7 +5291,7 @@ class SeriesTree_Mining_Rewards_Fees: self.pct75: _1m1w1y24hPattern4 = _1m1w1y24hPattern4(client, 'fees_pct75') self.pct90: _1m1w1y24hPattern4 = _1m1w1y24hPattern4(client, 'fees_pct90') self.dominance: _1m1w1y24hPercentPpmRatioPattern = _1m1w1y24hPercentPpmRatioPattern(client, 'fee_dominance') - self.to_subsidy_ratio: SeriesTree_Mining_Rewards_Fees_ToSubsidyRatio = SeriesTree_Mining_Rewards_Fees_ToSubsidyRatio(client) + self.to_subsidy: SeriesTree_Mining_Rewards_Fees_ToSubsidy = SeriesTree_Mining_Rewards_Fees_ToSubsidy(client) class SeriesTree_Mining_Rewards: """Series tree node.""" @@ -6421,7 +6450,6 @@ class SeriesTree_Cohorts_Utxo_All_Outputs: def __init__(self, client: BrkClient, base_path: str = ''): self.unspent_count: BaseDeltaPattern = BaseDeltaPattern(client, 'utxo_count') self.spent_count: AverageBlockCumulativeSumPattern[StoredU64] = AverageBlockCumulativeSumPattern(client, 'spent_utxo_count') - self.utxo_turnover_1y: SeriesPattern1[StoredF32] = SeriesPattern1(client, 'utxo_turnover_1y') class SeriesTree_Cohorts_Utxo_All_Activity: """Series tree node.""" @@ -6543,7 +6571,7 @@ class SeriesTree_Cohorts_Utxo_Sth: def __init__(self, client: BrkClient, base_path: str = ''): self.supply: DeltaDominanceHalfInTotalPattern2 = DeltaDominanceHalfInTotalPattern2(client, 'sth_supply') - self.outputs: SpentUnspentUtxoPattern = SpentUnspentUtxoPattern(client, 'sth') + self.outputs: SpentUnspentPattern = SpentUnspentPattern(client, 'sth') self.activity: CoindaysCoinyearsDormancyTransferPattern = CoindaysCoinyearsDormancyTransferPattern(client, 'sth') self.realized: CapCapitalizedGrossLossMvrvNetPeakPriceProfitSellSoprPattern = CapCapitalizedGrossLossMvrvNetPeakPriceProfitSellSoprPattern(client, 'sth') self.cost_basis: InMaxMinPerSupplyPattern = InMaxMinPerSupplyPattern(client, 'sth') diff --git a/website/llms-full.txt b/website/llms-full.txt index 5af65be5b..82436912f 100644 --- a/website/llms-full.txt +++ b/website/llms-full.txt @@ -4,7 +4,7 @@ - Version: `v0.3.6` - Base URL: https://bitview.space -- Metrics: 57530 +- Metrics: 56972 - Operations: 97 For machine-readable tool construction, use [https://bitview.space/openapi.json](https://bitview.space/openapi.json). For the complete source-derived series tree, use [https://bitview.space/api/series](https://bitview.space/api/series). diff --git a/website/llms.txt b/website/llms.txt index 2742aa55c..8abbb9344 100644 --- a/website/llms.txt +++ b/website/llms.txt @@ -1,6 +1,6 @@ # Bitcoin Research Kit (BRK) -> Free, open-source Bitcoin analytics API and block explorer. 57530 on-chain time-series and 97 API operations. No authentication required. +> Free, open-source Bitcoin analytics API and block explorer. 56972 on-chain time-series and 97 API operations. No authentication required. ## API diff --git a/website/scripts/options/distribution/holdings.js b/website/scripts/options/distribution/holdings.js index 103a60cd0..5388f6e34 100644 --- a/website/scripts/options/distribution/holdings.js +++ b/website/scripts/options/distribution/holdings.js @@ -76,18 +76,6 @@ function groupedOutputsFolder(list, all, title) { seriesFn: line, unit: Unit.count, }), }, - { - name: "1Y Turnover", - title: title("1 Year UTXO Turnover"), - bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => - line({ - series: tree.outputs.utxoTurnover1y, - name, - color, - unit: Unit.ratio, - }), - ), - }, ], }; } @@ -312,18 +300,6 @@ function outputsFolder(outputs, color, title) { name: "Spent", tree: chartsFromCount({ pattern: outputs.spentCount, title, metric: "Spent UTXO Count", unit: Unit.count, color }), }, - { - name: "1Y Turnover", - title: title("1 Year UTXO Turnover"), - bottom: [ - line({ - series: outputs.utxoTurnover1y, - name: "Turnover", - color, - unit: Unit.ratio, - }), - ], - }, ], }; } diff --git a/website/scripts/options/mining.js b/website/scripts/options/mining.js index ec6c888e1..013e46341 100644 --- a/website/scripts/options/mining.js +++ b/website/scripts/options/mining.js @@ -412,14 +412,11 @@ export function createMiningSection() { tree: ROLLING_WINDOWS.map((w) => ({ name: w.name, title: `${w.title} Fee-to-Subsidy Ratio`, - bottom: [ - line({ - series: mining.rewards.fees.toSubsidyRatio[w.key].ratio, - name: "Ratio", - color: colors.mining.fee, - unit: Unit.ratio, - }), - ], + bottom: percentRatio({ + pattern: mining.rewards.fees.toSubsidy[w.key], + name: "Fees / Subsidy", + color: colors.mining.fee, + }), })), }, { diff --git a/website/scripts/options/models/bedrock.js b/website/scripts/options/models/bedrock.js index 313a6c337..252f627ad 100644 --- a/website/scripts/options/models/bedrock.js +++ b/website/scripts/options/models/bedrock.js @@ -1,7 +1,7 @@ import { brk } from "../../utils/client.js"; import { colors } from "../../utils/colors.js"; import { Unit } from "../../utils/units.js"; -import { line } from "../series.js"; +import { line, price } from "../series.js"; const FLOOR_PERCENTILES = /** @type {const} */ ([ { key: "pct95", name: "P95" }, @@ -28,15 +28,15 @@ const LEVEL_PERCENTILES = /** @type {const} */ ([ * @property {string} name * @property {AnySeriesPattern} inLoss * @property {{ - * floor: Record, - * level: Record, + * floor: Record, + * level: Record, * lossThreshold: Record, * }} tree */ /** * @param {BedrockMode} mode - * @param {AnySeriesPattern} ath + * @param {AnyPricePattern} ath * @returns {PartialChartOption} */ function modeChart(mode, ath) { @@ -45,27 +45,24 @@ function modeChart(mode, ath) { title: `Bitcoin Bedrock Model: ${mode.name}`, top: [ ...FLOOR_PERCENTILES.map((percentile, index) => - line({ + price({ series: mode.tree.floor[percentile.key], name: percentile.name, color: colors.bedrock.percentiles[index], - unit: Unit.usd, }), ), ...LEVEL_PERCENTILES.map((percentile, index) => - line({ + price({ series: mode.tree.level[percentile.key], name: `L${percentile.name.slice(1)}`, color: colors.bedrock.levels[index], - unit: Unit.usd, style: 1, }), ), - line({ + price({ series: ath, name: "L100", color: colors.bedrock.levels[9], - unit: Unit.usd, style: 1, }), ], @@ -131,6 +128,6 @@ export function createBedrockSection() { return { name: "Bedrock", - tree: modes.map((mode) => modeChart(mode, market.ath.high.usd)), + tree: modes.map((mode) => modeChart(mode, market.ath.high)), }; } diff --git a/website_next/llms-full.txt b/website_next/llms-full.txt index 5af65be5b..82436912f 100644 --- a/website_next/llms-full.txt +++ b/website_next/llms-full.txt @@ -4,7 +4,7 @@ - Version: `v0.3.6` - Base URL: https://bitview.space -- Metrics: 57530 +- Metrics: 56972 - Operations: 97 For machine-readable tool construction, use [https://bitview.space/openapi.json](https://bitview.space/openapi.json). For the complete source-derived series tree, use [https://bitview.space/api/series](https://bitview.space/api/series). diff --git a/website_next/llms.txt b/website_next/llms.txt index 2742aa55c..8abbb9344 100644 --- a/website_next/llms.txt +++ b/website_next/llms.txt @@ -1,6 +1,6 @@ # Bitcoin Research Kit (BRK) -> Free, open-source Bitcoin analytics API and block explorer. 57530 on-chain time-series and 97 API operations. No authentication required. +> Free, open-source Bitcoin analytics API and block explorer. 56972 on-chain time-series and 97 API operations. No authentication required. ## API