From 08ba4ad996afcda8b780bee8697ce3364cdc87a1 Mon Sep 17 00:00:00 2001 From: nym21 Date: Wed, 15 Apr 2026 12:51:30 +0200 Subject: [PATCH] global: snap --- Cargo.lock | 95 +--- Cargo.toml | 8 +- crates/brk_client/src/lib.rs | 66 ++- crates/brk_computer/src/indicators/import.rs | 6 +- crates/brk_computer/src/indicators/mod.rs | 2 +- .../inner.rs} | 91 ++-- .../src/indicators/rarity_meter/mod.rs | 88 ++++ crates/brk_computer/src/indicators/vecs.rs | 4 +- crates/brk_computer/src/lib.rs | 3 +- crates/brk_reader/examples/last_n_bench.rs | 52 ++ crates/brk_reader/src/bisect.rs | 2 +- crates/brk_reader/src/parse.rs | 12 +- crates/brk_reader/src/pipeline/forward.rs | 1 + crates/brk_reader/src/pipeline/tail.rs | 136 +++-- crates/brk_reader/src/scan.rs | 37 +- modules/brk-client/index.js | 67 ++- packages/brk_client/brk_client/__init__.py | 33 +- website/scripts/options/cointime.js | 24 +- website/scripts/options/distribution/data.js | 24 +- website/scripts/options/distribution/index.js | 17 + website/scripts/options/market.js | 496 +++++++++--------- website/scripts/options/mining.js | 7 +- website/scripts/options/network.js | 390 +++++++++++--- website/scripts/options/partial.js | 35 +- 24 files changed, 1076 insertions(+), 620 deletions(-) rename crates/brk_computer/src/indicators/{realized_envelope.rs => rarity_meter/inner.rs} (75%) create mode 100644 crates/brk_computer/src/indicators/rarity_meter/mod.rs create mode 100644 crates/brk_reader/examples/last_n_bench.rs diff --git a/Cargo.lock b/Cargo.lock index 1a4d90942..f5e89cd97 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -41,9 +41,9 @@ dependencies = [ [[package]] name = "aide" -version = "0.16.0-alpha.3" +version = "0.16.0-alpha.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87a769cd3a8984b7236931cd48f4d1f6b99c0475d60987a1f69490b079116306" +checksum = "390515b47251185fa076ac92a7a582d9d383b03e13cef0c801e7670cf928229b" dependencies = [ "axum", "bytes", @@ -122,9 +122,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "axum" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" dependencies = [ "axum-core", "bytes", @@ -233,7 +233,7 @@ version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "cexpr", "clang-sys", "itertools", @@ -332,9 +332,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" [[package]] name = "brk" @@ -1358,7 +1358,7 @@ version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c7e611d49285d4c4b2e1727b72cf05353558885cc5252f93707b845dfcaf3d3" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "byteorder", "core-foundation", "core-graphics", @@ -1424,21 +1424,6 @@ dependencies = [ "pkg-config", ] -[[package]] -name = "futures" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - [[package]] name = "futures-channel" version = "0.3.32" @@ -1446,7 +1431,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", - "futures-sink", ] [[package]] @@ -1455,34 +1439,6 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" -[[package]] -name = "futures-executor" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" - -[[package]] -name = "futures-macro" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "futures-sink" version = "0.3.32" @@ -1501,13 +1457,8 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ - "futures-channel", "futures-core", - "futures-io", - "futures-macro", - "futures-sink", "futures-task", - "memchr", "pin-project-lite", "slab", ] @@ -2564,9 +2515,9 @@ dependencies = [ [[package]] name = "rayon" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" dependencies = [ "either", "rayon-core", @@ -2588,7 +2539,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", ] [[package]] @@ -2701,7 +2652,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "errno", "libc", "linux-raw-sys", @@ -2734,9 +2685,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.11" +version = "0.103.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20a6af516fea4b20eccceaf166e8aa666ac996208e8a644ce3ef5aa783bc7cd4" +checksum = "8279bb85272c9f10811ae6a6c547ff594d6a7f3c6c6b02ee9726d1d0dcfcdd06" dependencies = [ "ring", "rustls-pki-types", @@ -2907,15 +2858,15 @@ dependencies = [ [[package]] name = "serde_qs" -version = "0.14.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b417bedc008acbdf6d6b4bc482d29859924114bbe2650b7921fb68a261d0aa6" +checksum = "c2316d01592c3382277c5062105510e35e0a6bfb2851e30028485f7af8cf1240" dependencies = [ "axum", - "futures", + "itoa", "percent-encoding", + "ryu", "serde", - "thiserror", ] [[package]] @@ -3148,9 +3099,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.51.1" +version = "1.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f66bf9585cda4b724d3e78ab34b73fb2bbaba9011b9bfdf69dc836382ea13b8c" +checksum = "a91135f59b1cbf38c91e73cf3386fca9bb77915c45ce2771460c9d92f0f3d776" dependencies = [ "libc", "mio", @@ -3245,7 +3196,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ "async-compression", - "bitflags 2.11.0", + "bitflags 2.11.1", "bytes", "futures-core", "futures-util", @@ -3587,7 +3538,7 @@ version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "hashbrown 0.15.5", "indexmap", "semver", @@ -3863,7 +3814,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags 2.11.0", + "bitflags 2.11.1", "indexmap", "log", "serde", diff --git a/Cargo.toml b/Cargo.toml index a875af257..d5c30d954 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,8 +36,8 @@ inherits = "release" debug = true [workspace.dependencies] -aide = { version = "0.16.0-alpha.3", features = ["axum-json", "axum-query"] } -axum = { version = "0.8.8", default-features = false, features = ["http1", "json", "query", "tokio", "tracing"] } +aide = { version = "0.16.0-alpha.4", features = ["axum-json", "axum-query"] } +axum = { version = "0.8.9", default-features = false, features = ["http1", "json", "query", "tokio", "tracing"] } bitcoin = { version = "0.32.8", features = ["serde"] } bitcoincore-rpc = "0.19.0" brk_alloc = { version = "0.3.0-beta.1", path = "crates/brk_alloc" } @@ -76,7 +76,7 @@ jiff = { version = "0.2.23", features = ["perf-inline", "tz-system"], default-fe owo-colors = "4.3.0" parking_lot = "0.12.5" pco = "1.0.1" -rayon = "1.11.0" +rayon = "1.12.0" rustc-hash = "2.1.2" schemars = { version = "1.2.1", features = ["indexmap2"] } serde = "1.0.228" @@ -84,7 +84,7 @@ serde_bytes = "0.11.19" serde_derive = "1.0.228" serde_json = { version = "1.0.149", features = ["float_roundtrip", "preserve_order"] } smallvec = "1.15.1" -tokio = { version = "1.51.1", features = ["rt-multi-thread"] } +tokio = { version = "1.52.0", features = ["rt-multi-thread"] } tower-http = { version = "0.6.8", features = ["catch-panic", "compression-br", "compression-gzip", "compression-zstd", "cors", "normalize-path", "timeout", "trace"] } tower-layer = "0.3" tracing = { version = "0.1", default-features = false, features = ["std"] } diff --git a/crates/brk_client/src/lib.rs b/crates/brk_client/src/lib.rs index 8b514eda3..718854ada 100644 --- a/crates/brk_client/src/lib.rs +++ b/crates/brk_client/src/lib.rs @@ -1277,6 +1277,38 @@ impl AverageBaseCumulativeMaxMedianMinPct10Pct25Pct75Pct90S } } +/// Pattern struct for repeated tree structure. +pub struct IndexPct0Pct1Pct2Pct5Pct95Pct98Pct99ScorePattern { + pub index: SeriesPattern1, + pub pct0_5: CentsSatsUsdPattern, + pub pct1: CentsSatsUsdPattern, + pub pct2: CentsSatsUsdPattern, + pub pct5: CentsSatsUsdPattern, + pub pct95: CentsSatsUsdPattern, + pub pct98: CentsSatsUsdPattern, + pub pct99: CentsSatsUsdPattern, + pub pct99_5: CentsSatsUsdPattern, + pub score: SeriesPattern1, +} + +impl IndexPct0Pct1Pct2Pct5Pct95Pct98Pct99ScorePattern { + /// Create a new pattern node with accumulated series name. + pub fn new(client: Arc, acc: String) -> Self { + Self { + index: SeriesPattern1::new(client.clone(), _m(&acc, "index")), + pct0_5: CentsSatsUsdPattern::new(client.clone(), _m(&acc, "pct0_5")), + pct1: CentsSatsUsdPattern::new(client.clone(), _m(&acc, "pct01")), + pct2: CentsSatsUsdPattern::new(client.clone(), _m(&acc, "pct02")), + pct5: CentsSatsUsdPattern::new(client.clone(), _m(&acc, "pct05")), + 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")), + score: SeriesPattern1::new(client.clone(), _m(&acc, "score")), + } + } +} + /// Pattern struct for repeated tree structure. pub struct AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern5 { pub all: AverageBlockCumulativeSumPattern, @@ -5619,7 +5651,7 @@ pub struct SeriesTree_Indicators { pub dormancy: SeriesTree_Indicators_Dormancy, pub stock_to_flow: SeriesPattern1, pub seller_exhaustion: SeriesPattern1, - pub realized_envelope: SeriesTree_Indicators_RealizedEnvelope, + pub rarity_meter: SeriesTree_Indicators_RarityMeter, } impl SeriesTree_Indicators { @@ -5635,7 +5667,7 @@ impl SeriesTree_Indicators { dormancy: SeriesTree_Indicators_Dormancy::new(client.clone(), format!("{base_path}_dormancy")), stock_to_flow: SeriesPattern1::new(client.clone(), "stock_to_flow".to_string()), seller_exhaustion: SeriesPattern1::new(client.clone(), "seller_exhaustion".to_string()), - realized_envelope: SeriesTree_Indicators_RealizedEnvelope::new(client.clone(), format!("{base_path}_realized_envelope")), + rarity_meter: SeriesTree_Indicators_RarityMeter::new(client.clone(), format!("{base_path}_rarity_meter")), } } } @@ -5656,32 +5688,18 @@ impl SeriesTree_Indicators_Dormancy { } /// Series tree node. -pub struct SeriesTree_Indicators_RealizedEnvelope { - pub pct0_5: CentsSatsUsdPattern, - pub pct1: CentsSatsUsdPattern, - pub pct2: CentsSatsUsdPattern, - pub pct5: CentsSatsUsdPattern, - pub pct95: CentsSatsUsdPattern, - pub pct98: CentsSatsUsdPattern, - pub pct99: CentsSatsUsdPattern, - pub pct99_5: CentsSatsUsdPattern, - pub index: SeriesPattern1, - pub score: SeriesPattern1, +pub struct SeriesTree_Indicators_RarityMeter { + pub full: IndexPct0Pct1Pct2Pct5Pct95Pct98Pct99ScorePattern, + pub local: IndexPct0Pct1Pct2Pct5Pct95Pct98Pct99ScorePattern, + pub cycle: IndexPct0Pct1Pct2Pct5Pct95Pct98Pct99ScorePattern, } -impl SeriesTree_Indicators_RealizedEnvelope { +impl SeriesTree_Indicators_RarityMeter { pub fn new(client: Arc, base_path: String) -> Self { Self { - pct0_5: CentsSatsUsdPattern::new(client.clone(), "realized_envelope_pct0_5".to_string()), - pct1: CentsSatsUsdPattern::new(client.clone(), "realized_envelope_pct01".to_string()), - pct2: CentsSatsUsdPattern::new(client.clone(), "realized_envelope_pct02".to_string()), - pct5: CentsSatsUsdPattern::new(client.clone(), "realized_envelope_pct05".to_string()), - pct95: CentsSatsUsdPattern::new(client.clone(), "realized_envelope_pct95".to_string()), - pct98: CentsSatsUsdPattern::new(client.clone(), "realized_envelope_pct98".to_string()), - pct99: CentsSatsUsdPattern::new(client.clone(), "realized_envelope_pct99".to_string()), - pct99_5: CentsSatsUsdPattern::new(client.clone(), "realized_envelope_pct99_5".to_string()), - index: SeriesPattern1::new(client.clone(), "realized_envelope_index".to_string()), - score: SeriesPattern1::new(client.clone(), "realized_envelope_score".to_string()), + full: IndexPct0Pct1Pct2Pct5Pct95Pct98Pct99ScorePattern::new(client.clone(), "rarity_meter".to_string()), + local: IndexPct0Pct1Pct2Pct5Pct95Pct98Pct99ScorePattern::new(client.clone(), "local_rarity_meter".to_string()), + cycle: IndexPct0Pct1Pct2Pct5Pct95Pct98Pct99ScorePattern::new(client.clone(), "cycle_rarity_meter".to_string()), } } } diff --git a/crates/brk_computer/src/indicators/import.rs b/crates/brk_computer/src/indicators/import.rs index 38736c7ab..081bcbc3d 100644 --- a/crates/brk_computer/src/indicators/import.rs +++ b/crates/brk_computer/src/indicators/import.rs @@ -3,7 +3,7 @@ use std::path::Path; use brk_error::Result; use brk_types::Version; -use super::{Vecs, realized_envelope::RealizedEnvelope}; +use super::{Vecs, rarity_meter::RarityMeter}; use crate::{ indexes, internal::{ @@ -40,7 +40,7 @@ impl Vecs { let stock_to_flow = PerBlock::forced_import(&db, "stock_to_flow", v, indexes)?; let seller_exhaustion = PerBlock::forced_import(&db, "seller_exhaustion", v, indexes)?; - let realized_envelope = RealizedEnvelope::forced_import(&db, v, indexes)?; + let rarity_meter = RarityMeter::forced_import(&db, v, indexes)?; let this = Self { db, @@ -54,7 +54,7 @@ impl Vecs { dormancy, stock_to_flow, seller_exhaustion, - realized_envelope, + rarity_meter, }; finalize_db(&this.db, &this)?; Ok(this) diff --git a/crates/brk_computer/src/indicators/mod.rs b/crates/brk_computer/src/indicators/mod.rs index 7ba5a603b..983111898 100644 --- a/crates/brk_computer/src/indicators/mod.rs +++ b/crates/brk_computer/src/indicators/mod.rs @@ -1,7 +1,7 @@ mod compute; mod gini; mod import; -pub mod realized_envelope; +pub mod rarity_meter; mod vecs; pub use vecs::Vecs; diff --git a/crates/brk_computer/src/indicators/realized_envelope.rs b/crates/brk_computer/src/indicators/rarity_meter/inner.rs similarity index 75% rename from crates/brk_computer/src/indicators/realized_envelope.rs rename to crates/brk_computer/src/indicators/rarity_meter/inner.rs index 41adb3d1d..561d31665 100644 --- a/crates/brk_computer/src/indicators/realized_envelope.rs +++ b/crates/brk_computer/src/indicators/rarity_meter/inner.rs @@ -4,13 +4,12 @@ use brk_types::{Cents, Height, Indexes, StoredI8, Version}; use vecdb::{AnyVec, Database, Exit, ReadableVec, Rw, StorageMode, WritableVec}; use crate::{ - cointime, distribution, indexes, + indexes, internal::{PerBlock, Price, RatioPerBlockPercentiles}, - prices, }; #[derive(Traversable)] -pub struct RealizedEnvelope { +pub struct RarityMeterInner { pub pct0_5: Price>, pub pct1: Price>, pub pct2: Price>, @@ -23,113 +22,85 @@ pub struct RealizedEnvelope { pub score: PerBlock, } -const VERSION: Version = Version::new(3); - -impl RealizedEnvelope { +impl RarityMeterInner { pub(crate) fn forced_import( db: &Database, + prefix: &str, version: Version, indexes: &indexes::Vecs, ) -> Result { - let v = version + VERSION; Ok(Self { - pct0_5: Price::forced_import(db, "realized_envelope_pct0_5", v, indexes)?, - pct1: Price::forced_import(db, "realized_envelope_pct01", v, indexes)?, - pct2: Price::forced_import(db, "realized_envelope_pct02", v, indexes)?, - pct5: Price::forced_import(db, "realized_envelope_pct05", v, indexes)?, - pct95: Price::forced_import(db, "realized_envelope_pct95", v, indexes)?, - pct98: Price::forced_import(db, "realized_envelope_pct98", v, indexes)?, - pct99: Price::forced_import(db, "realized_envelope_pct99", v, indexes)?, - pct99_5: Price::forced_import(db, "realized_envelope_pct99_5", v, indexes)?, - index: PerBlock::forced_import(db, "realized_envelope_index", v, indexes)?, - score: PerBlock::forced_import(db, "realized_envelope_score", v, indexes)?, + pct0_5: Price::forced_import(db, &format!("{prefix}_pct0_5"), version, indexes)?, + pct1: Price::forced_import(db, &format!("{prefix}_pct01"), version, indexes)?, + pct2: Price::forced_import(db, &format!("{prefix}_pct02"), version, indexes)?, + pct5: Price::forced_import(db, &format!("{prefix}_pct05"), version, indexes)?, + pct95: Price::forced_import(db, &format!("{prefix}_pct95"), version, indexes)?, + pct98: Price::forced_import(db, &format!("{prefix}_pct98"), version, indexes)?, + pct99: Price::forced_import(db, &format!("{prefix}_pct99"), version, indexes)?, + pct99_5: Price::forced_import(db, &format!("{prefix}_pct99_5"), version, indexes)?, + index: PerBlock::forced_import(db, &format!("{prefix}_index"), version, indexes)?, + score: PerBlock::forced_import(db, &format!("{prefix}_score"), version, indexes)?, }) } - pub(crate) fn compute( + pub(super) fn compute( &mut self, - distribution: &distribution::Vecs, - cointime: &cointime::Vecs, - prices: &prices::Vecs, + models: &[&RatioPerBlockPercentiles], + spot: &impl ReadableVec, starting_indexes: &Indexes, exit: &Exit, ) -> Result<()> { - let realized = &distribution.utxo_cohorts.all.metrics.realized; - let ct = &cointime.prices; - - let sth_realized = &distribution.utxo_cohorts.sth.metrics.realized; - let lth_realized = &distribution.utxo_cohorts.lth.metrics.realized; - - let models: [&RatioPerBlockPercentiles; 10] = [ - &realized.price_ratio_percentiles, - &realized.investor.price.percentiles, - &sth_realized.price_ratio_percentiles, - &sth_realized.investor.price.percentiles, - <h_realized.price_ratio_percentiles, - <h_realized.investor.price.percentiles, - &ct.vaulted.percentiles, - &ct.active.percentiles, - &ct.true_market_mean.percentiles, - &ct.cointime.percentiles, - ]; - - macro_rules! sources { - ($pct:ident) => { - models.each_ref().map(|m| &m.$pct.price.cents.height) - }; - } + let gather = |f: fn(&RatioPerBlockPercentiles) -> &_| -> Vec<_> { + models.iter().map(|m| f(m)).collect() + }; // Lower percentiles: max across all models (tightest lower bound) self.pct0_5.cents.height.compute_max_of_others( starting_indexes.height, - &sources!(pct0_5), + &gather(|m| &m.pct0_5.price.cents.height), exit, )?; self.pct1.cents.height.compute_max_of_others( starting_indexes.height, - &sources!(pct1), + &gather(|m| &m.pct1.price.cents.height), exit, )?; self.pct2.cents.height.compute_max_of_others( starting_indexes.height, - &sources!(pct2), + &gather(|m| &m.pct2.price.cents.height), exit, )?; self.pct5.cents.height.compute_max_of_others( starting_indexes.height, - &sources!(pct5), + &gather(|m| &m.pct5.price.cents.height), exit, )?; // Upper percentiles: min across all models (tightest upper bound) self.pct95.cents.height.compute_min_of_others( starting_indexes.height, - &sources!(pct95), + &gather(|m| &m.pct95.price.cents.height), exit, )?; self.pct98.cents.height.compute_min_of_others( starting_indexes.height, - &sources!(pct98), + &gather(|m| &m.pct98.price.cents.height), exit, )?; self.pct99.cents.height.compute_min_of_others( starting_indexes.height, - &sources!(pct99), + &gather(|m| &m.pct99.price.cents.height), exit, )?; self.pct99_5.cents.height.compute_min_of_others( starting_indexes.height, - &sources!(pct99_5), + &gather(|m| &m.pct99_5.price.cents.height), exit, )?; - let spot = &prices.spot.cents.height; - - // Zone: spot vs own envelope bands (-4 to +4) self.compute_index(spot, starting_indexes, exit)?; - // Temperature: per-model band crossings (-40 to +40) - self.compute_score(&models, spot, starting_indexes, exit)?; + self.compute_score(models, spot, starting_indexes, exit)?; Ok(()) } @@ -140,7 +111,7 @@ impl RealizedEnvelope { starting_indexes: &Indexes, exit: &Exit, ) -> Result<()> { - let bands: [&_; 8] = [ + let bands = [ &self.pct0_5.cents.height, &self.pct1.cents.height, &self.pct2.cents.height, @@ -213,7 +184,7 @@ impl RealizedEnvelope { fn compute_score( &mut self, - models: &[&RatioPerBlockPercentiles; 10], + models: &[&RatioPerBlockPercentiles], spot: &impl ReadableVec, starting_indexes: &Indexes, exit: &Exit, diff --git a/crates/brk_computer/src/indicators/rarity_meter/mod.rs b/crates/brk_computer/src/indicators/rarity_meter/mod.rs new file mode 100644 index 000000000..9890fec23 --- /dev/null +++ b/crates/brk_computer/src/indicators/rarity_meter/mod.rs @@ -0,0 +1,88 @@ +mod inner; + +use brk_error::Result; +use brk_traversable::Traversable; +use brk_types::{Indexes, Version}; +use vecdb::{Database, Exit, Rw, StorageMode}; + +use crate::{distribution, indexes, prices}; + +pub use inner::RarityMeterInner; + +#[derive(Traversable)] +pub struct RarityMeter { + pub full: RarityMeterInner, + pub local: RarityMeterInner, + pub cycle: RarityMeterInner, +} + +const VERSION: Version = Version::new(4); + +impl RarityMeter { + pub(crate) fn forced_import( + db: &Database, + version: Version, + indexes: &indexes::Vecs, + ) -> Result { + let v = version + VERSION; + Ok(Self { + full: RarityMeterInner::forced_import(db, "rarity_meter", v, indexes)?, + local: RarityMeterInner::forced_import(db, "local_rarity_meter", v, indexes)?, + cycle: RarityMeterInner::forced_import(db, "cycle_rarity_meter", v, indexes)?, + }) + } + + pub(crate) fn compute( + &mut self, + distribution: &distribution::Vecs, + prices: &prices::Vecs, + starting_indexes: &Indexes, + exit: &Exit, + ) -> Result<()> { + let realized = &distribution.utxo_cohorts.all.metrics.realized; + let sth_realized = &distribution.utxo_cohorts.sth.metrics.realized; + let lth_realized = &distribution.utxo_cohorts.lth.metrics.realized; + let spot = &prices.spot.cents.height; + + // Full: all + sth + lth (rp + ip), 6 models + self.full.compute( + &[ + &realized.price_ratio_percentiles, + &realized.investor.price.percentiles, + &sth_realized.price_ratio_percentiles, + &sth_realized.investor.price.percentiles, + <h_realized.price_ratio_percentiles, + <h_realized.investor.price.percentiles, + ], + spot, + starting_indexes, + exit, + )?; + + // Local: sth only, 2 models + self.local.compute( + &[ + &sth_realized.price_ratio_percentiles, + &sth_realized.investor.price.percentiles, + ], + spot, + starting_indexes, + exit, + )?; + + // Cycle: all + lth, 4 models + self.cycle.compute( + &[ + &realized.price_ratio_percentiles, + &realized.investor.price.percentiles, + <h_realized.price_ratio_percentiles, + <h_realized.investor.price.percentiles, + ], + spot, + starting_indexes, + exit, + )?; + + Ok(()) + } +} diff --git a/crates/brk_computer/src/indicators/vecs.rs b/crates/brk_computer/src/indicators/vecs.rs index 0976016e4..baf70834f 100644 --- a/crates/brk_computer/src/indicators/vecs.rs +++ b/crates/brk_computer/src/indicators/vecs.rs @@ -2,7 +2,7 @@ use brk_traversable::Traversable; use brk_types::{BasisPoints16, BasisPoints32, StoredF32}; use vecdb::{Database, Rw, StorageMode}; -use super::realized_envelope::RealizedEnvelope; +use super::rarity_meter::RarityMeter; use crate::internal::{PerBlock, PercentPerBlock, RatioPerBlock}; #[derive(Traversable)] @@ -25,5 +25,5 @@ pub struct Vecs { pub dormancy: DormancyVecs, pub stock_to_flow: PerBlock, pub seller_exhaustion: PerBlock, - pub realized_envelope: RealizedEnvelope, + pub rarity_meter: RarityMeter, } diff --git a/crates/brk_computer/src/lib.rs b/crates/brk_computer/src/lib.rs index b6a1f2cc4..46e132da4 100644 --- a/crates/brk_computer/src/lib.rs +++ b/crates/brk_computer/src/lib.rs @@ -467,9 +467,8 @@ impl Computer { Ok(()) })?; - self.indicators.realized_envelope.compute( + self.indicators.rarity_meter.compute( &self.distribution, - &self.cointime, &self.prices, &starting_indexes, exit, diff --git a/crates/brk_reader/examples/last_n_bench.rs b/crates/brk_reader/examples/last_n_bench.rs new file mode 100644 index 000000000..d19c3d5a7 --- /dev/null +++ b/crates/brk_reader/examples/last_n_bench.rs @@ -0,0 +1,52 @@ +//! Times `Reader::after` for a handful of tail-clustered catchup +//! sizes. `N ≤ ~1024` lands in the tail strategy (chunked reverse +//! reader); `N = 10_000` falls through to the forward strategy since +//! it's past the 8-newest-files window. +//! +//! Run with: +//! cargo run --release -p brk_reader --example last_n_bench +//! +//! Requires a running bitcoind with a cookie file at the default path. + +use std::time::Instant; + +use brk_error::Result; +use brk_reader::Reader; +use brk_rpc::{Auth, Client}; +use brk_types::Height; + +const SCENARIOS: &[u32] = &[1, 10, 100, 1_000, 10_000]; + +fn main() -> Result<()> { + let bitcoin_dir = Client::default_bitcoin_path(); + let client = Client::new( + Client::default_url(), + Auth::CookieFile(bitcoin_dir.join(".cookie")), + )?; + let reader = Reader::new(bitcoin_dir.join("blocks"), &client); + + let tip = client.get_last_height()?; + println!("Tip: {tip}"); + println!(); + println!("{:>6} {:>14} {:>10}", "blocks", "elapsed", "blk/s"); + println!("{}", "-".repeat(36)); + + for &n in SCENARIOS { + let anchor_height = Height::from(tip.saturating_sub(n)); + let anchor_hash = client.get_block_hash(*anchor_height as u64)?; + let anchor = Some(anchor_hash); + + let start = Instant::now(); + let mut count = 0usize; + for block in reader.after(anchor)? { + let _ = block?; + count += 1; + } + let elapsed = start.elapsed(); + + let blk_per_s = count as f64 / elapsed.as_secs_f64().max(f64::EPSILON); + println!("{n:>6} {elapsed:>14?} {blk_per_s:>10.0}"); + } + + Ok(()) +} diff --git a/crates/brk_reader/src/bisect.rs b/crates/brk_reader/src/bisect.rs index 2964f8cc0..ceba50f63 100644 --- a/crates/brk_reader/src/bisect.rs +++ b/crates/brk_reader/src/bisect.rs @@ -53,7 +53,7 @@ pub(crate) fn first_block_height( } xor_i.bytes(&mut buf[magic_end..header_end], xor_bytes); - let header = Header::consensus_decode(&mut &buf[magic_end + 4..header_end])?; + let header = Header::consensus_decode_from_finite_reader(&mut &buf[magic_end + 4..header_end])?; let height = client.get_block_info(&header.block_hash())?.height as u32; Ok(Height::new(height)) diff --git a/crates/brk_reader/src/parse.rs b/crates/brk_reader/src/parse.rs index 44116f9a5..de593b9f3 100644 --- a/crates/brk_reader/src/parse.rs +++ b/crates/brk_reader/src/parse.rs @@ -27,7 +27,7 @@ pub(crate) fn peek_canonical( let mut header_buf = [0u8; HEADER_LEN]; header_buf.copy_from_slice(&bytes[..HEADER_LEN]); xor_state.bytes(&mut header_buf, xor_bytes); - let header = Header::consensus_decode(&mut &header_buf[..]).ok()?; + let header = Header::consensus_decode_from_finite_reader(&mut &header_buf[..]).ok()?; let offset = canonical.offset_of(&BlockHash::from(header.block_hash()))?; Some((offset, header)) } @@ -52,14 +52,20 @@ pub(crate) fn parse_canonical_body( let mut cursor = Cursor::new(bytes); cursor.set_position(HEADER_LEN as u64); - let tx_count = VarInt::consensus_decode(&mut cursor)?.0 as usize; + // `consensus_decode_from_finite_reader` skips the `Take` wrap + // that `consensus_decode` applies to every nested field for + // memory-safety — our cursor is already a bounded `Vec`, so + // the extra wrapping is pure overhead. Per the crate docs it's + // "marginally faster", but for a ~2000-tx block the per-field + // compounding adds up. + let tx_count = VarInt::consensus_decode_from_finite_reader(&mut cursor)?.0 as usize; let mut txdata = Vec::with_capacity(tx_count); let mut tx_metadata = Vec::with_capacity(tx_count); let mut tx_offsets = Vec::with_capacity(tx_count); for _ in 0..tx_count { let tx_start = cursor.position() as u32; tx_offsets.push(tx_start); - let tx = Transaction::consensus_decode(&mut cursor)?; + let tx = Transaction::consensus_decode_from_finite_reader(&mut cursor)?; let tx_len = cursor.position() as u32 - tx_start; txdata.push(tx); tx_metadata.push(BlkMetadata::new(metadata.position() + tx_start, tx_len)); diff --git a/crates/brk_reader/src/pipeline/forward.rs b/crates/brk_reader/src/pipeline/forward.rs index 30325ccd6..ae71ed603 100644 --- a/crates/brk_reader/src/pipeline/forward.rs +++ b/crates/brk_reader/src/pipeline/forward.rs @@ -135,6 +135,7 @@ fn read_and_dispatch( scan_bytes( &mut bytes, blk_index, + 0, xor_bytes, |metadata, block_bytes, xor_state| { if stop.get().is_some() { diff --git a/crates/brk_reader/src/pipeline/tail.rs b/crates/brk_reader/src/pipeline/tail.rs index f3e9f3a01..05c29ca45 100644 --- a/crates/brk_reader/src/pipeline/tail.rs +++ b/crates/brk_reader/src/pipeline/tail.rs @@ -1,10 +1,10 @@ //! Tail pipeline: single-threaded reverse scan of the newest blk -//! files until every canonical hash is matched, then forward-emit -//! with an inline chain check. Avoids the forward pipeline's -//! bisection + out-of-order backoff (~2.7 GB of reads) for any -//! tip-clustered catchup. +//! files, reading each file in `TAIL_CHUNK`-sized slices from tail +//! to head so we only touch bytes covering the canonical window. +//! Matches fill offset slots and are emitted forward with an inline +//! chain check. -use std::{fs, ops::ControlFlow}; +use std::{fs::File, ops::ControlFlow, os::unix::fs::FileExt}; use brk_error::{Error, Result}; use brk_rpc::Client; @@ -18,6 +18,8 @@ use crate::{ scan::scan_bytes, }; +const TAIL_CHUNK: usize = 8 * 1024 * 1024; + pub(super) fn pipeline_tail( client: &Client, paths: &BlkIndexToBlkPath, @@ -34,7 +36,7 @@ pub(super) fn pipeline_tail( // miss doesn't scan the entire chain in reverse. let mut below_floor_streak: usize = 0; - for (&blk_index, path) in paths.iter().rev() { + 'files: for (&blk_index, path) in paths.iter().rev() { // If this file's first block is below the lowest still-missing // canonical height, we've walked past the window. if let Some(missing_idx) = slots.iter().position(Option::is_none) @@ -53,51 +55,85 @@ pub(super) fn pipeline_tail( } } - let mut bytes = fs::read(path)?; - scan_bytes( - &mut bytes, - blk_index, - xor_bytes, - |metadata, block_bytes, xor_state| { - let Some((offset, header)) = - peek_canonical(block_bytes, xor_state, xor_bytes, canonical) - else { - return ControlFlow::Continue(()); - }; - if slots[offset as usize].is_some() { - return ControlFlow::Continue(()); - } - let height = Height::from(*canonical.start + offset); - match parse_canonical_body( - block_bytes.to_vec(), - metadata, - xor_state, - xor_bytes, - height, - header, - ) { - Ok(block) => { - slots[offset as usize] = Some(block); - remaining -= 1; - } - Err(e) => { - parse_failure = Some(e); - return ControlFlow::Break(()); - } - } - if remaining == 0 { - ControlFlow::Break(()) - } else { - ControlFlow::Continue(()) - } - }, - ); - - if let Some(e) = parse_failure { - return Err(e); + let file = File::open(path)?; + let file_len = file.metadata()?.len() as usize; + if file_len == 0 { + continue; } - if remaining == 0 { - break; + + // Chunked reverse read. `end` is the file position we've + // already covered (exclusive). Each iteration reads + // [end - TAIL_CHUNK..end] and prepends it to any `spillover` + // carried from the previous iteration — the pre-first-magic + // bytes of that chunk, which must belong to a block that + // started in this earlier region. + let mut end = file_len; + let mut spillover: Vec = Vec::new(); + + while end > 0 && remaining > 0 { + let start = end.saturating_sub(TAIL_CHUNK); + let chunk_len = end - start; + let mut buf = vec![0u8; chunk_len + spillover.len()]; + file.read_exact_at(&mut buf[..chunk_len], start as u64)?; + buf[chunk_len..].copy_from_slice(&spillover); + spillover.clear(); + + // `buf` now represents file bytes [start..start + buf.len()]. + let result = scan_bytes( + &mut buf, + blk_index, + start, + xor_bytes, + |metadata, block_bytes, xor_state| { + let Some((offset, header)) = + peek_canonical(block_bytes, xor_state, xor_bytes, canonical) + else { + return ControlFlow::Continue(()); + }; + if slots[offset as usize].is_some() { + return ControlFlow::Continue(()); + } + let height = Height::from(*canonical.start + offset); + match parse_canonical_body( + block_bytes.to_vec(), + metadata, + xor_state, + xor_bytes, + height, + header, + ) { + Ok(block) => { + slots[offset as usize] = Some(block); + remaining -= 1; + } + Err(e) => { + parse_failure = Some(e); + return ControlFlow::Break(()); + } + } + if remaining == 0 { + ControlFlow::Break(()) + } else { + ControlFlow::Continue(()) + } + }, + ); + + if let Some(e) = parse_failure { + return Err(e); + } + if remaining == 0 { + break 'files; + } + + // Carry pre-first-magic bytes into the next (earlier) + // chunk so a block that straddled this chunk's start is + // stitched back together. + end = start; + if end > 0 { + let prefix_len = result.first_magic.unwrap_or(buf.len()); + spillover.extend_from_slice(&buf[..prefix_len]); + } } } diff --git a/crates/brk_reader/src/scan.rs b/crates/brk_reader/src/scan.rs index 39dfd570d..4bc397cd8 100644 --- a/crates/brk_reader/src/scan.rs +++ b/crates/brk_reader/src/scan.rs @@ -8,7 +8,7 @@ const MAGIC_BYTES: [u8; 4] = [0xF9, 0xBE, 0xB4, 0xD9]; /// Returns the position **immediately after** the matched magic, or /// `None` if no match. Advances `xor_i` by the bytes consumed either -/// way. +/// way. First-byte fast-fail keeps the inner loop tight. pub(crate) fn find_magic(bytes: &[u8], xor_i: &mut XORIndex, xor_bytes: XORBytes) -> Option { let len = bytes.len(); if len < MAGIC_BYTES.len() { @@ -42,36 +42,51 @@ pub(crate) fn find_magic(bytes: &[u8], xor_i: &mut XORIndex, xor_bytes: XORBytes None } -/// Scans `buf` (the full contents of one blk file) for blocks, -/// calling `on_block` for each. The block bytes are passed as a -/// mutable borrow so the callback can clone (to ship to a parser -/// thread) or process in place (to peek the header). +/// Position (relative to `buf`) of the first matched magic byte. +/// Used by the chunked tail pipeline to carry pre-first-magic bytes +/// into the next (earlier) chunk. +pub(crate) struct ScanResult { + pub first_magic: Option, +} + +/// Scans `buf` for blocks and calls `on_block` for each. `file_offset` +/// is the absolute file position of `buf[0]` — used to seed the XOR +/// phase and to report absolute `BlkPosition`s so the chunked tail +/// pipeline can read mid-file slices. pub(crate) fn scan_bytes( buf: &mut [u8], blk_index: u16, + file_offset: usize, xor_bytes: XORBytes, mut on_block: impl FnMut(BlkMetadata, &mut [u8], XORIndex) -> ControlFlow<()>, -) { - let mut xor_i = XORIndex::default(); +) -> ScanResult { + let mut xor_i = XORIndex::at_offset(file_offset); + let mut first_magic: Option = None; let mut i = 0; while let Some(off) = find_magic(&buf[i..], &mut xor_i, xor_bytes) { + first_magic.get_or_insert(i + off - MAGIC_BYTES.len()); i += off; if i + 4 > buf.len() { - return; + break; } let mut size_bytes = [buf[i], buf[i + 1], buf[i + 2], buf[i + 3]]; xor_i.bytes(&mut size_bytes, xor_bytes); let len = u32::from_le_bytes(size_bytes) as usize; i += 4; if i + len > buf.len() { - return; + break; } - let metadata = BlkMetadata::new(BlkPosition::new(blk_index, i as u32), len as u32); + let metadata = BlkMetadata::new( + BlkPosition::new(blk_index, (file_offset + i) as u32), + len as u32, + ); if on_block(metadata, &mut buf[i..i + len], xor_i).is_break() { - return; + break; } i += len; xor_i.add_assign(len); } + + ScanResult { first_magic } } diff --git a/modules/brk-client/index.js b/modules/brk-client/index.js index 26e684b7c..9fa807118 100644 --- a/modules/brk-client/index.js +++ b/modules/brk-client/index.js @@ -2254,6 +2254,41 @@ function createAverageBaseCumulativeMaxMedianMinPct10Pct25Pct75Pct90SumPattern(c }; } +/** + * @typedef {Object} IndexPct0Pct1Pct2Pct5Pct95Pct98Pct99ScorePattern + * @property {SeriesPattern1} index + * @property {CentsSatsUsdPattern} pct05 + * @property {CentsSatsUsdPattern} pct1 + * @property {CentsSatsUsdPattern} pct2 + * @property {CentsSatsUsdPattern} pct5 + * @property {CentsSatsUsdPattern} pct95 + * @property {CentsSatsUsdPattern} pct98 + * @property {CentsSatsUsdPattern} pct99 + * @property {CentsSatsUsdPattern} pct995 + * @property {SeriesPattern1} score + */ + +/** + * Create a IndexPct0Pct1Pct2Pct5Pct95Pct98Pct99ScorePattern pattern node + * @param {BrkClientBase} client + * @param {string} acc - Accumulated series name + * @returns {IndexPct0Pct1Pct2Pct5Pct95Pct98Pct99ScorePattern} + */ +function createIndexPct0Pct1Pct2Pct5Pct95Pct98Pct99ScorePattern(client, acc) { + return { + index: createSeriesPattern1(client, _m(acc, 'index')), + pct05: createCentsSatsUsdPattern(client, _m(acc, 'pct0_5')), + pct1: createCentsSatsUsdPattern(client, _m(acc, 'pct01')), + pct2: createCentsSatsUsdPattern(client, _m(acc, 'pct02')), + pct5: createCentsSatsUsdPattern(client, _m(acc, 'pct05')), + 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')), + score: createSeriesPattern1(client, _m(acc, 'score')), + }; +} + /** * @typedef {Object} AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern5 * @property {AverageBlockCumulativeSumPattern} all @@ -5597,7 +5632,7 @@ function createTransferPattern(client, acc) { * @property {SeriesTree_Indicators_Dormancy} dormancy * @property {SeriesPattern1} stockToFlow * @property {SeriesPattern1} sellerExhaustion - * @property {SeriesTree_Indicators_RealizedEnvelope} realizedEnvelope + * @property {SeriesTree_Indicators_RarityMeter} rarityMeter */ /** @@ -5607,17 +5642,10 @@ function createTransferPattern(client, acc) { */ /** - * @typedef {Object} SeriesTree_Indicators_RealizedEnvelope - * @property {CentsSatsUsdPattern} pct05 - * @property {CentsSatsUsdPattern} pct1 - * @property {CentsSatsUsdPattern} pct2 - * @property {CentsSatsUsdPattern} pct5 - * @property {CentsSatsUsdPattern} pct95 - * @property {CentsSatsUsdPattern} pct98 - * @property {CentsSatsUsdPattern} pct99 - * @property {CentsSatsUsdPattern} pct995 - * @property {SeriesPattern1} index - * @property {SeriesPattern1} score + * @typedef {Object} SeriesTree_Indicators_RarityMeter + * @property {IndexPct0Pct1Pct2Pct5Pct95Pct98Pct99ScorePattern} full + * @property {IndexPct0Pct1Pct2Pct5Pct95Pct98Pct99ScorePattern} local + * @property {IndexPct0Pct1Pct2Pct5Pct95Pct98Pct99ScorePattern} cycle */ /** @@ -8808,17 +8836,10 @@ class BrkClient extends BrkClientBase { }, stockToFlow: createSeriesPattern1(this, 'stock_to_flow'), sellerExhaustion: createSeriesPattern1(this, 'seller_exhaustion'), - realizedEnvelope: { - pct05: createCentsSatsUsdPattern(this, 'realized_envelope_pct0_5'), - pct1: createCentsSatsUsdPattern(this, 'realized_envelope_pct01'), - pct2: createCentsSatsUsdPattern(this, 'realized_envelope_pct02'), - pct5: createCentsSatsUsdPattern(this, 'realized_envelope_pct05'), - pct95: createCentsSatsUsdPattern(this, 'realized_envelope_pct95'), - pct98: createCentsSatsUsdPattern(this, 'realized_envelope_pct98'), - pct99: createCentsSatsUsdPattern(this, 'realized_envelope_pct99'), - pct995: createCentsSatsUsdPattern(this, 'realized_envelope_pct99_5'), - index: createSeriesPattern1(this, 'realized_envelope_index'), - score: createSeriesPattern1(this, 'realized_envelope_score'), + rarityMeter: { + full: createIndexPct0Pct1Pct2Pct5Pct95Pct98Pct99ScorePattern(this, 'rarity_meter'), + local: createIndexPct0Pct1Pct2Pct5Pct95Pct98Pct99ScorePattern(this, 'local_rarity_meter'), + cycle: createIndexPct0Pct1Pct2Pct5Pct95Pct98Pct99ScorePattern(this, 'cycle_rarity_meter'), }, }, investing: { diff --git a/packages/brk_client/brk_client/__init__.py b/packages/brk_client/brk_client/__init__.py index f300eca4c..6d8178f70 100644 --- a/packages/brk_client/brk_client/__init__.py +++ b/packages/brk_client/brk_client/__init__.py @@ -2722,6 +2722,22 @@ class AverageBaseCumulativeMaxMedianMinPct10Pct25Pct75Pct90SumPattern(Generic[T] self.pct90: _1m1w1y24hPattern[T] = _1m1w1y24hPattern(client, _m(acc, 'pct90')) self.sum: _1m1w1y24hPattern[T] = _1m1w1y24hPattern(client, _m(acc, 'sum')) +class IndexPct0Pct1Pct2Pct5Pct95Pct98Pct99ScorePattern: + """Pattern struct for repeated tree structure.""" + + def __init__(self, client: BrkClientBase, acc: str): + """Create pattern node with accumulated series name.""" + self.index: SeriesPattern1[StoredI8] = SeriesPattern1(client, _m(acc, 'index')) + self.pct0_5: CentsSatsUsdPattern = CentsSatsUsdPattern(client, _m(acc, 'pct0_5')) + self.pct1: CentsSatsUsdPattern = CentsSatsUsdPattern(client, _m(acc, 'pct01')) + self.pct2: CentsSatsUsdPattern = CentsSatsUsdPattern(client, _m(acc, 'pct02')) + self.pct5: CentsSatsUsdPattern = CentsSatsUsdPattern(client, _m(acc, 'pct05')) + 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.score: SeriesPattern1[StoredI8] = SeriesPattern1(client, _m(acc, 'score')) + class AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern5: """Pattern struct for repeated tree structure.""" @@ -4816,20 +4832,13 @@ class SeriesTree_Indicators_Dormancy: self.supply_adj: SeriesPattern1[StoredF32] = SeriesPattern1(client, 'dormancy_supply_adj') self.flow: SeriesPattern1[StoredF32] = SeriesPattern1(client, 'dormancy_flow') -class SeriesTree_Indicators_RealizedEnvelope: +class SeriesTree_Indicators_RarityMeter: """Series tree node.""" def __init__(self, client: BrkClientBase, base_path: str = ''): - self.pct0_5: CentsSatsUsdPattern = CentsSatsUsdPattern(client, 'realized_envelope_pct0_5') - self.pct1: CentsSatsUsdPattern = CentsSatsUsdPattern(client, 'realized_envelope_pct01') - self.pct2: CentsSatsUsdPattern = CentsSatsUsdPattern(client, 'realized_envelope_pct02') - self.pct5: CentsSatsUsdPattern = CentsSatsUsdPattern(client, 'realized_envelope_pct05') - self.pct95: CentsSatsUsdPattern = CentsSatsUsdPattern(client, 'realized_envelope_pct95') - self.pct98: CentsSatsUsdPattern = CentsSatsUsdPattern(client, 'realized_envelope_pct98') - self.pct99: CentsSatsUsdPattern = CentsSatsUsdPattern(client, 'realized_envelope_pct99') - self.pct99_5: CentsSatsUsdPattern = CentsSatsUsdPattern(client, 'realized_envelope_pct99_5') - self.index: SeriesPattern1[StoredI8] = SeriesPattern1(client, 'realized_envelope_index') - self.score: SeriesPattern1[StoredI8] = SeriesPattern1(client, 'realized_envelope_score') + self.full: IndexPct0Pct1Pct2Pct5Pct95Pct98Pct99ScorePattern = IndexPct0Pct1Pct2Pct5Pct95Pct98Pct99ScorePattern(client, 'rarity_meter') + self.local: IndexPct0Pct1Pct2Pct5Pct95Pct98Pct99ScorePattern = IndexPct0Pct1Pct2Pct5Pct95Pct98Pct99ScorePattern(client, 'local_rarity_meter') + self.cycle: IndexPct0Pct1Pct2Pct5Pct95Pct98Pct99ScorePattern = IndexPct0Pct1Pct2Pct5Pct95Pct98Pct99ScorePattern(client, 'cycle_rarity_meter') class SeriesTree_Indicators: """Series tree node.""" @@ -4845,7 +4854,7 @@ class SeriesTree_Indicators: self.dormancy: SeriesTree_Indicators_Dormancy = SeriesTree_Indicators_Dormancy(client) self.stock_to_flow: SeriesPattern1[StoredF32] = SeriesPattern1(client, 'stock_to_flow') self.seller_exhaustion: SeriesPattern1[StoredF32] = SeriesPattern1(client, 'seller_exhaustion') - self.realized_envelope: SeriesTree_Indicators_RealizedEnvelope = SeriesTree_Indicators_RealizedEnvelope(client) + self.rarity_meter: SeriesTree_Indicators_RarityMeter = SeriesTree_Indicators_RarityMeter(client) class SeriesTree_Investing_Period_DcaCostBasis: """Series tree node.""" diff --git a/website/scripts/options/cointime.js b/website/scripts/options/cointime.js index 37ea565d8..b5ee0ef5e 100644 --- a/website/scripts/options/cointime.js +++ b/website/scripts/options/cointime.js @@ -353,6 +353,17 @@ export function createCointimeSection() { { name: "Indicators", tree: [ + { + name: "AVIV", + title: "AVIV Ratio", + bottom: [ + line({ + series: cap.aviv.ratio, + name: "AVIV", + unit: Unit.ratio, + }), + ], + }, { name: "Reserve Risk", title: "Reserve Risk", @@ -365,22 +376,11 @@ export function createCointimeSection() { }), ], }, - { - name: "AVIV", - title: "AVIV Ratio", - bottom: [ - line({ - series: cap.aviv.ratio, - name: "AVIV", - unit: Unit.ratio, - }), - ], - }, ], }, { - name: "Cointime-Adjusted", + name: "Adjusted", tree: [ { name: "Inflation", diff --git a/website/scripts/options/distribution/data.js b/website/scripts/options/distribution/data.js index b85520902..2b5d2d0e2 100644 --- a/website/scripts/options/distribution/data.js +++ b/website/scripts/options/distribution/data.js @@ -4,14 +4,14 @@ import { brk } from "../../utils/client.js"; /** @type {readonly AddressableType[]} */ const ADDRESSABLE_TYPES = [ - "p2pk65", - "p2pk33", - "p2pkh", - "p2sh", - "p2wpkh", - "p2wsh", - "p2tr", "p2a", + "p2tr", + "p2wsh", + "p2wpkh", + "p2sh", + "p2pkh", + "p2pk33", + "p2pk65", ]; /** @type {(key: SpendableType) => key is AddressableType} */ @@ -162,12 +162,13 @@ export function buildCohortData() { }, ); - const typeAddressable = ADDRESSABLE_TYPES.map((key, i, arr) => { + const typeAddressable = ADDRESSABLE_TYPES.map((key) => { const names = SPENDABLE_TYPE_NAMES[key]; return { + key, name: names.short, title: names.short, - color: colors.at(i, arr.length), + color: colors.scriptType[key], tree: utxoCohorts.type[key], addressCount: { base: addrs.funded[key], @@ -178,10 +179,11 @@ export function buildCohortData() { const typeOther = entries(SPENDABLE_TYPE_NAMES) .filter(([key]) => !isAddressable(key)) - .map(([key, names], i, arr) => ({ + .map(([key, names]) => ({ + key, name: names.short, title: names.short, - color: colors.at(i, arr.length), + color: colors.scriptType[key], tree: utxoCohorts.type[key], })); diff --git a/website/scripts/options/distribution/index.js b/website/scripts/options/distribution/index.js index 61ec8400f..2505fbd5a 100644 --- a/website/scripts/options/distribution/index.js +++ b/website/scripts/options/distribution/index.js @@ -25,6 +25,7 @@ import { } from "../series.js"; import { Unit } from "../../utils/units.js"; import { colors } from "../../utils/colors.js"; +import { brk } from "../../utils/client.js"; // Section builders import { @@ -743,3 +744,19 @@ export function createUtxoProfitabilitySection({ range, profit, loss }) { ], }; } + +/** + * Gini leaf for Distribution > Address Balance + * @returns {AnyPartialOption} + */ +export function createAddressBalanceGiniLeaf() { + return { + name: "Gini", + title: "Address Balance Gini Coefficient", + bottom: percentRatio({ + pattern: brk.series.indicators.gini, + name: "Gini", + color: colors.loss, + }), + }; +} diff --git a/website/scripts/options/market.js b/website/scripts/options/market.js index b1024c75c..50c9c99c4 100644 --- a/website/scripts/options/market.js +++ b/website/scripts/options/market.js @@ -653,120 +653,117 @@ export function createMarketSection() { ], }, - // Momentum + // RSI { - name: "Momentum", + name: "RSI", tree: [ { - name: "RSI", - tree: [ - { - name: "Compare", - title: "RSI", - bottom: [ - ...ROLLING_WINDOWS_TO_1M.flatMap((w) => - indexRatio({ - pattern: technical.rsi[w.key].rsi, - name: w.name, - color: w.color, - }), - ), - priceLine({ unit: Unit.index, number: 70 }), - priceLine({ unit: Unit.index, number: 30 }), - ], - }, - ...ROLLING_WINDOWS_TO_1M.map((w) => { - const rsi = technical.rsi[w.key]; - return { + name: "Compare", + title: "RSI", + bottom: [ + ...ROLLING_WINDOWS_TO_1M.flatMap((w) => + indexRatio({ + pattern: technical.rsi[w.key].rsi, name: w.name, - title: `${w.title} RSI`, - bottom: [ - ...indexRatio({ - pattern: rsi.rsi, - name: "RSI", - color: colors.indicator.main, - }), - priceLine({ unit: Unit.index, number: 70 }), - priceLine({ - unit: Unit.index, - number: 50, - defaultActive: false, - }), - priceLine({ unit: Unit.index, number: 30 }), - ], - }; - }), - { - name: "Stochastic", - tree: ROLLING_WINDOWS_TO_1M.map((w) => { - const rsi = technical.rsi[w.key]; - return { - name: w.name, - title: `${w.title} Stochastic RSI`, - bottom: [ - ...indexRatio({ - pattern: rsi.stochRsiK, - name: "K", - color: colors.indicator.fast, - }), - ...indexRatio({ - pattern: rsi.stochRsiD, - name: "D", - color: colors.indicator.slow, - }), - ...priceLines({ - unit: Unit.index, - numbers: [80, 20], - }), - ], - }; + color: w.color, }), - }, + ), + priceLine({ unit: Unit.index, number: 70 }), + priceLine({ unit: Unit.index, number: 30 }), ], }, + ...ROLLING_WINDOWS_TO_1M.map((w) => { + const rsi = technical.rsi[w.key]; + return { + name: w.name, + title: `${w.title} RSI`, + bottom: [ + ...indexRatio({ + pattern: rsi.rsi, + name: "RSI", + color: colors.indicator.main, + }), + priceLine({ unit: Unit.index, number: 70 }), + priceLine({ + unit: Unit.index, + number: 50, + defaultActive: false, + }), + priceLine({ unit: Unit.index, number: 30 }), + ], + }; + }), { - name: "MACD", - tree: [ - { - name: "Compare", - title: "MACD", - bottom: ROLLING_WINDOWS_TO_1M.map((w) => - line({ - series: technical.macd[w.key].line, - name: w.name, - color: w.color, - unit: Unit.usd, - }), - ), - }, - ...ROLLING_WINDOWS_TO_1M.map((w) => ({ + name: "Stochastic", + tree: ROLLING_WINDOWS_TO_1M.map((w) => { + const rsi = technical.rsi[w.key]; + return { name: w.name, - title: `${w.title} MACD`, + title: `${w.title} Stochastic RSI`, bottom: [ - line({ - series: technical.macd[w.key].line, - name: "MACD", + ...indexRatio({ + pattern: rsi.stochRsiK, + name: "K", color: colors.indicator.fast, - unit: Unit.usd, }), - line({ - series: technical.macd[w.key].signal, - name: "Signal", + ...indexRatio({ + pattern: rsi.stochRsiD, + name: "D", color: colors.indicator.slow, - unit: Unit.usd, }), - histogram({ - series: technical.macd[w.key].histogram, - name: "Histogram", - unit: Unit.usd, + ...priceLines({ + unit: Unit.index, + numbers: [80, 20], }), ], - })), - ], + }; + }), }, ], }, + // MACD + { + name: "MACD", + tree: [ + { + name: "Compare", + title: "MACD", + bottom: ROLLING_WINDOWS_TO_1M.map((w) => + line({ + series: technical.macd[w.key].line, + name: w.name, + color: w.color, + unit: Unit.usd, + }), + ), + }, + ...ROLLING_WINDOWS_TO_1M.map((w) => ({ + name: w.name, + title: `${w.title} MACD`, + bottom: [ + line({ + series: technical.macd[w.key].line, + name: "MACD", + color: colors.indicator.fast, + unit: Unit.usd, + }), + line({ + series: technical.macd[w.key].signal, + name: "Signal", + color: colors.indicator.slow, + unit: Unit.usd, + }), + histogram({ + series: technical.macd[w.key].histogram, + name: "Histogram", + unit: Unit.usd, + }), + ], + })), + ], + }, + // Volatility { name: "Volatility", @@ -925,201 +922,190 @@ export function createMarketSection() { name: "Indicators", tree: [ { - name: "Envelope", - title: "Realized Envelope", - top: priceBands(percentileBands(indicators.realizedEnvelope), { - defaultActive: true, + name: "Rarity Meter", + tree: /** @type {const} */ ([ + { key: "full", name: "Full", title: "Rarity Meter" }, + { key: "local", name: "Local", title: "Local Rarity Meter" }, + { key: "cycle", name: "Cycle", title: "Cycle Rarity Meter" }, + ]).map((v) => { + const m = indicators.rarityMeter[v.key]; + return { + name: v.name, + title: v.title, + top: priceBands(percentileBands(m), { defaultActive: true }), + bottom: [ + histogram({ + series: m.index, + name: "Index", + unit: Unit.count, + colorFn: (v) => + /** @type {const} */ ([ + colors.ratioPct._0_5, + colors.ratioPct._1, + colors.ratioPct._2, + colors.ratioPct._5, + colors.transparent, + colors.ratioPct._95, + colors.ratioPct._98, + colors.ratioPct._99, + colors.ratioPct._99_5, + ])[v + 4], + }), + baseline({ + series: m.score, + name: "Score", + unit: Unit.count, + color: [colors.ratioPct._99, colors.ratioPct._1], + defaultActive: false, + }), + ], + }; }), + }, + { + name: "NVT", + title: "NVT Ratio", bottom: [ - histogram({ - series: indicators.realizedEnvelope.index, - name: "Index", - unit: Unit.count, - colorFn: (v) => - /** @type {const} */ ([ - colors.ratioPct._0_5, - colors.ratioPct._1, - colors.ratioPct._2, - colors.ratioPct._5, - colors.transparent, - colors.ratioPct._95, - colors.ratioPct._98, - colors.ratioPct._99, - colors.ratioPct._99_5, - ])[v + 4], + line({ + series: indicators.nvt.ratio, + name: "NVT", + color: colors.bitcoin, + unit: Unit.ratio, }), + ], + }, + { + name: "Thermocap Multiple", + title: "Thermocap Multiple", + bottom: [ + line({ + series: indicators.thermoCapMultiple.ratio, + name: "Thermocap", + color: colors.bitcoin, + unit: Unit.ratio, + }), + ], + }, + { + name: "Puell Multiple", + title: "Puell Multiple", + bottom: [ + line({ + series: indicators.puellMultiple.ratio, + name: "Puell", + color: colors.usd, + unit: Unit.ratio, + }), + ], + }, + { + name: "RHODL Ratio", + title: "RHODL Ratio", + bottom: [ + line({ + series: indicators.rhodlRatio.ratio, + name: "RHODL", + color: colors.bitcoin, + unit: Unit.ratio, + }), + ], + }, + { + name: "Stock-to-Flow", + title: "Stock-to-Flow", + bottom: [ + line({ + series: indicators.stockToFlow, + name: "S2F", + color: colors.bitcoin, + unit: Unit.ratio, + }), + ], + }, + { + name: "Pi Cycle", + title: "Pi Cycle", + top: [ + price({ + series: ma.sma._111d, + name: "111d SMA", + color: colors.indicator.upper, + }), + price({ + series: ma.sma._350d.x2, + name: "350d SMA x2", + color: colors.indicator.lower, + }), + ], + bottom: [ baseline({ - series: indicators.realizedEnvelope.score, - name: "Score", - unit: Unit.count, - color: [colors.ratioPct._99, colors.ratioPct._1], + series: technical.piCycle.ratio, + name: "Pi Cycle", + unit: Unit.ratio, + base: 1, + }), + ], + }, + { + name: "Dormancy", + title: "Dormancy", + bottom: [ + line({ + series: indicators.dormancy.supplyAdj, + name: "Supply Adjusted", + color: colors.bitcoin, + unit: Unit.ratio, + }), + line({ + series: indicators.dormancy.flow, + name: "Flow", + color: colors.usd, + unit: Unit.ratio, defaultActive: false, }), ], }, { - name: "Valuation", - tree: [ - { - name: "NVT", - title: "NVT Ratio", - bottom: [ - line({ - series: indicators.nvt.ratio, - name: "NVT", - color: colors.bitcoin, - unit: Unit.ratio, - }), - ], - }, - { - name: "Thermocap Multiple", - title: "Thermocap Multiple", - bottom: [ - line({ - series: indicators.thermoCapMultiple.ratio, - name: "Thermocap", - color: colors.bitcoin, - unit: Unit.ratio, - }), - ], - }, + name: "Seller Exhaustion", + title: "Seller Exhaustion Constant", + bottom: [ + line({ + series: indicators.sellerExhaustion, + name: "SEC", + color: colors.bitcoin, + unit: Unit.ratio, + }), ], }, { - name: "Cycle", + name: "Coin Destruction", tree: [ { - name: "Pi Cycle", - title: "Pi Cycle", - top: [ - price({ - series: ma.sma._111d, - name: "111d SMA", - color: colors.indicator.upper, - }), - price({ - series: ma.sma._350d.x2, - name: "350d SMA x2", - color: colors.indicator.lower, - }), - ], - bottom: [ - baseline({ - series: technical.piCycle.ratio, - name: "Pi Cycle", - unit: Unit.ratio, - base: 1, - }), - ], - }, - { - name: "Stock-to-Flow", - title: "Stock-to-Flow", - bottom: [ - line({ - series: indicators.stockToFlow, - name: "S2F", - color: colors.bitcoin, - unit: Unit.ratio, - }), - ], - }, - { - name: "Puell Multiple", - title: "Puell Multiple", - bottom: [ - line({ - series: indicators.puellMultiple.ratio, - name: "Puell", - color: colors.usd, - unit: Unit.ratio, - }), - ], - }, - { - name: "RHODL Ratio", - title: "RHODL Ratio", - bottom: [ - line({ - series: indicators.rhodlRatio.ratio, - name: "RHODL", - color: colors.bitcoin, - unit: Unit.ratio, - }), - ], - }, - ], - }, - { - name: "Activity", - tree: [ - { - name: "Dormancy", - title: "Dormancy", - bottom: [ - line({ - series: indicators.dormancy.supplyAdj, - name: "Supply Adjusted", - color: colors.bitcoin, - unit: Unit.ratio, - }), - line({ - series: indicators.dormancy.flow, - name: "Flow", - color: colors.usd, - unit: Unit.ratio, - defaultActive: false, - }), - ], - }, - { - name: "Seller Exhaustion", - title: "Seller Exhaustion Constant", - bottom: [ - line({ - series: indicators.sellerExhaustion, - name: "SEC", - color: colors.bitcoin, - unit: Unit.ratio, - }), - ], - }, - { - name: "CDD Supply Adjusted", + name: "CDD", title: "Coindays Destroyed (Supply Adjusted)", bottom: [ line({ series: indicators.coindaysDestroyedSupplyAdj, - name: "CDD SA", + name: "CDD", color: colors.bitcoin, unit: Unit.ratio, }), ], }, { - name: "CYD Supply Adjusted", + name: "CYD", title: "Coinyears Destroyed (Supply Adjusted)", bottom: [ line({ series: indicators.coinyearsDestroyedSupplyAdj, - name: "CYD SA", - color: colors.bitcoin, + name: "CYD", + color: colors.usd, unit: Unit.ratio, }), ], }, ], }, - { - name: "Gini", - title: "Gini Coefficient", - bottom: percentRatio({ - pattern: indicators.gini, - name: "Gini", - color: colors.loss, - }), - }, ], }, ], diff --git a/website/scripts/options/mining.js b/website/scripts/options/mining.js index f55a76252..ec6c888e1 100644 --- a/website/scripts/options/mining.js +++ b/website/scripts/options/mining.js @@ -190,9 +190,10 @@ export function createMiningSection() { /** * @param {string} groupTitle * @param {typeof majorPoolData} poolList + * @param {string} [name] */ - const createPoolCompare = (groupTitle, poolList) => ({ - name: "Compare", + const createPoolCompare = (groupTitle, poolList, name = "Compare") => ({ + name, tree: [ { name: "Dominance", @@ -602,7 +603,7 @@ export function createMiningSection() { { name: "Pools", tree: [ - createPoolCompare("Major Pools", featuredPools), + createPoolCompare("Major Pools", featuredPools, "Featured"), { name: "AntPool & Friends", tree: [ diff --git a/website/scripts/options/network.js b/website/scripts/options/network.js index a6cc319ca..916bdaaae 100644 --- a/website/scripts/options/network.js +++ b/website/scripts/options/network.js @@ -6,6 +6,7 @@ import { Unit } from "../utils/units.js"; import { entries } from "../utils/array.js"; import { line, + baseline, fromSupplyPattern, chartsFromFull, chartsFromFullPerBlock, @@ -14,6 +15,7 @@ import { chartsFromPercentCumulative, chartsFromPercentCumulativeEntries, chartsFromAggregatedPerBlock, + distributionWindowsTree, averagesArray, simpleDeltaTree, ROLLING_WINDOWS, @@ -91,39 +93,6 @@ export function createNetworkSection() { { key: "reactivated", name: "Reactivated" }, ]); - const countTypes = /** @type {const} */ ([ - { - name: "Funded", - title: "Address Count by Type", - /** @param {AddressableType} t */ - getSeries: (t) => addrs.funded[t], - }, - { - name: "Empty", - title: "Empty Address Count by Type", - /** @param {AddressableType} t */ - getSeries: (t) => addrs.empty[t], - }, - { - name: "Total", - title: "Total Address Count by Type", - /** @param {AddressableType} t */ - getSeries: (t) => addrs.total[t], - }, - { - name: "Funded Reused", - title: "Funded Reused Address Count by Type", - /** @param {AddressableType} t */ - getSeries: (t) => addrs.reused.count.funded[t], - }, - { - name: "Total Reused", - title: "Total Reused Address Count by Type", - /** @param {AddressableType} t */ - getSeries: (t) => addrs.reused.count.total[t], - }, - ]); - const countMetrics = /** @type {const} */ ([ { key: "funded", name: "Funded", color: undefined }, { key: "empty", name: "Empty", color: colors.gray }, @@ -543,6 +512,261 @@ export function createNetworkSection() { ]; }; + /** + * Mirror of the per-type singles tree, but every leaf is a cross-type + * comparison chart (same metric, same unit, one line per addr type). + * Structure parallels `createAddressSeriesTreeForType` section-by-section + * so users can compare anything they can view on a single type. + */ + const createAddressByTypeCompare = () => { + const typeLines = + /** + * @param {(t: (typeof addressTypes)[number]) => AnySeriesPattern} getSeries + * @param {Unit} [unit] + */ + (getSeries, unit = Unit.count) => + addressTypes.map((t) => + line({ + series: getSeries(t), + name: t.name, + color: t.color, + unit, + defaultActive: t.defaultActive, + }), + ); + + const typeBaselines = + /** + * @param {(t: (typeof addressTypes)[number]) => AnySeriesPattern} getSeries + * @param {Unit} [unit] + */ + (getSeries, unit = Unit.count) => + addressTypes.map((t) => + baseline({ + series: getSeries(t), + name: t.name, + color: t.color, + unit, + defaultActive: t.defaultActive, + }), + ); + + return { + name: "Compare", + tree: [ + // Count (lifetime Funded/Empty/Total) + { + name: "Count", + tree: countMetrics.map((m) => ({ + name: m.name, + title: `${m.name} Address Count by Type`, + bottom: typeLines((t) => addrs[m.key][t.key]), + })), + }, + + // New (rolling sums + cumulative) + { + name: "New", + tree: groupedWindowsCumulative({ + list: addressTypes, + title: (s) => s, + metricTitle: "New Addresses by Type", + getWindowSeries: (t, key) => addrs.new[t.key].sum[key], + getCumulativeSeries: (t) => addrs.new[t.key].cumulative, + seriesFn: line, + unit: Unit.count, + }), + }, + + // Change (rolling deltas, signed, baseline) + { + name: "Change", + tree: ROLLING_WINDOWS.map((w) => ({ + name: w.name, + title: `${w.title} Address Count Change by Type`, + bottom: typeBaselines( + (t) => addrs.delta[t.key].absolute[w.key], + ), + })), + }, + + // Growth Rate (rolling percent rates) + { + name: "Growth Rate", + tree: ROLLING_WINDOWS.map((w) => ({ + name: w.name, + title: `${w.title} Address Growth Rate by Type`, + bottom: typeLines( + (t) => addrs.delta[t.key].rate[w.key].percent, + Unit.percentage, + ), + })), + }, + + // Activity (per activity type, per window) + { + name: "Activity", + tree: activityTypes.map((a) => ({ + name: a.name, + tree: ROLLING_WINDOWS.map((w) => ({ + name: w.name, + title: `${w.title} ${a.name} Addresses by Type`, + bottom: typeLines( + (t) => addrs.activity[t.key][a.key][w.key], + ), + })), + })), + }, + + // Reused + { + name: "Reused", + tree: [ + { + name: "Funded", + title: "Funded Reused Address Count by Type", + bottom: typeLines((t) => addrs.reused.count.funded[t.key]), + }, + { + name: "Total", + title: "Total Reused Address Count by Type", + bottom: typeLines((t) => addrs.reused.count.total[t.key]), + }, + { + name: "Outputs", + tree: [ + { + name: "Count", + tree: groupedWindowsCumulative({ + list: addressTypes, + title: (s) => s, + metricTitle: + "Transaction Outputs to Reused Addresses by Type", + getWindowSeries: (t, key) => + addrs.reused.events.outputToReusedAddrCount[t.key].sum[ + key + ], + getCumulativeSeries: (t) => + addrs.reused.events.outputToReusedAddrCount[t.key] + .cumulative, + seriesFn: line, + unit: Unit.count, + }), + }, + { + name: "Share", + tree: [ + ...ROLLING_WINDOWS.map((w) => ({ + name: w.name, + title: `${w.title} Share of Transaction Outputs to Reused Addresses by Type`, + bottom: typeLines( + (t) => + addrs.reused.events.outputToReusedAddrShare[t.key][ + w.key + ].percent, + Unit.percentage, + ), + })), + { + name: "Cumulative", + title: + "Cumulative Share of Transaction Outputs to Reused Addresses by Type", + bottom: typeLines( + (t) => + addrs.reused.events.outputToReusedAddrShare[t.key] + .percent, + Unit.percentage, + ), + }, + ], + }, + ], + }, + { + name: "Inputs", + tree: [ + { + name: "Count", + tree: groupedWindowsCumulative({ + list: addressTypes, + title: (s) => s, + metricTitle: + "Transaction Inputs from Reused Addresses by Type", + getWindowSeries: (t, key) => + addrs.reused.events.inputFromReusedAddrCount[t.key].sum[ + key + ], + getCumulativeSeries: (t) => + addrs.reused.events.inputFromReusedAddrCount[t.key] + .cumulative, + seriesFn: line, + unit: Unit.count, + }), + }, + { + name: "Share", + tree: [ + ...ROLLING_WINDOWS.map((w) => ({ + name: w.name, + title: `${w.title} Share of Transaction Inputs from Reused Addresses by Type`, + bottom: typeLines( + (t) => + addrs.reused.events.inputFromReusedAddrShare[t.key][ + w.key + ].percent, + Unit.percentage, + ), + })), + { + name: "Cumulative", + title: + "Cumulative Share of Transaction Inputs from Reused Addresses by Type", + bottom: typeLines( + (t) => + addrs.reused.events.inputFromReusedAddrShare[t.key] + .percent, + Unit.percentage, + ), + }, + ], + }, + ], + }, + ], + }, + + // Exposed + { + name: "Exposed", + tree: [ + { + name: "Funded", + title: "Funded Exposed Address Count by Type", + bottom: typeLines((t) => addrs.exposed.count.funded[t.key]), + }, + { + name: "Total", + title: "Total Exposed Address Count by Type", + bottom: typeLines((t) => addrs.exposed.count.total[t.key]), + }, + { + name: "Supply", + title: "Supply in Exposed Addresses by Type", + bottom: addressTypes.flatMap((t) => + satsBtcUsd({ + pattern: addrs.exposed.supply[t.key], + name: t.name, + color: t.color, + defaultActive: t.defaultActive, + }), + ), + }, + ], + }, + ], + }; + }; + /** * Build a "By Type" subtree: Compare (count / tx count / tx %) plus a * per-type drill-down with the same three metrics. @@ -754,7 +978,7 @@ export function createNetworkSection() { name: "Unspendable", tree: [ { - name: "Total", + name: "All", title: "Unspendable Supply", bottom: satsBtcUsdFrom({ source: supply.burned, @@ -1002,19 +1226,74 @@ export function createNetworkSection() { tree: [ { name: "Count", - tree: chartsFromAggregatedPerBlock({ - pattern: outputs.count.total, - metric: "Output Count", - unit: Unit.count, - }), - }, - { - name: "Spendable", - tree: chartsFromCount({ - pattern: outputs.byType.spendableOutputCount, - metric: "Spendable Output Count", - unit: Unit.count, - }), + tree: [ + { + name: "Compare", + title: "Output Count", + bottom: ROLLING_WINDOWS.map((w) => + line({ + series: outputs.count.total.rolling.average[w.key], + name: w.name, + color: w.color, + unit: Unit.count, + }), + ), + }, + ...ROLLING_WINDOWS.map((w) => ({ + name: w.name, + title: `${w.title} Output Count`, + bottom: [ + line({ + series: outputs.count.total.rolling.sum[w.key], + name: "Total (Sum)", + color: w.color, + unit: Unit.count, + }), + line({ + series: outputs.byType.spendableOutputCount.sum[w.key], + name: "Spendable (Sum)", + color: colors.gray, + unit: Unit.count, + }), + line({ + series: outputs.count.total.rolling.average[w.key], + name: "Total (Avg)", + color: w.color, + unit: Unit.count, + defaultActive: false, + }), + line({ + series: outputs.byType.spendableOutputCount.average[w.key], + name: "Spendable (Avg)", + color: colors.gray, + unit: Unit.count, + defaultActive: false, + }), + ], + })), + { + name: "Cumulative", + title: "Cumulative Output Count", + bottom: [ + line({ + series: outputs.count.total.cumulative, + name: "Total", + unit: Unit.count, + }), + line({ + series: outputs.byType.spendableOutputCount.cumulative, + name: "Spendable", + color: colors.gray, + unit: Unit.count, + }), + ], + }, + distributionWindowsTree({ + pattern: outputs.count.total.rolling, + metric: "Output Count per Block", + unit: Unit.count, + }), + ], }, { name: "Per Second", @@ -1105,22 +1384,7 @@ export function createNetworkSection() { { name: "By Type", tree: [ - { - name: "Compare", - tree: countTypes.map((c) => ({ - name: c.name, - title: c.title, - bottom: addressTypes.map((t) => - line({ - series: c.getSeries(t.key), - name: t.name, - color: t.color, - unit: Unit.count, - defaultActive: t.defaultActive, - }), - ), - })), - }, + createAddressByTypeCompare(), ...addressTypes.map((t) => ({ name: t.name, tree: createAddressSeriesTreeForType(t.key, t.name), diff --git a/website/scripts/options/partial.js b/website/scripts/options/partial.js index 329caa9ba..0a0e50e51 100644 --- a/website/scripts/options/partial.js +++ b/website/scripts/options/partial.js @@ -18,6 +18,7 @@ import { createGroupedCohortFolderAddress, createGroupedAddressCohortFolder, createUtxoProfitabilitySection, + createAddressBalanceGiniLeaf, } from "./distribution/index.js"; import { createMarketSection } from "./market.js"; import { createNetworkSection } from "./network.js"; @@ -91,7 +92,7 @@ export function createPartialOptions() { name: "UTXO Age", tree: [ { - name: "Younger Than", + name: "Under", tree: [ createGroupedCohortFolderWithAdjusted({ name: "Compare", @@ -103,7 +104,7 @@ export function createPartialOptions() { ], }, { - name: "Older Than", + name: "Over", tree: [ createGroupedCohortFolderWithAdjusted({ name: "Compare", @@ -133,7 +134,7 @@ export function createPartialOptions() { name: "UTXO Size", tree: [ { - name: "Less Than", + name: "Under", tree: [ createGroupedCohortFolderBasicWithMarketCap({ name: "Compare", @@ -147,7 +148,7 @@ export function createPartialOptions() { ], }, { - name: "More Than", + name: "Over", tree: [ createGroupedCohortFolderBasicWithMarketCap({ name: "Compare", @@ -187,7 +188,7 @@ export function createPartialOptions() { name: "Address Balance", tree: [ { - name: "Less Than", + name: "Under", tree: [ createGroupedAddressCohortFolder({ name: "Compare", @@ -199,7 +200,7 @@ export function createPartialOptions() { ], }, { - name: "More Than", + name: "Over", tree: [ createGroupedAddressCohortFolder({ name: "Compare", @@ -222,6 +223,7 @@ export function createPartialOptions() { ...addressesAmountRange.map(createAddressCohortFolder), ], }, + createAddressBalanceGiniLeaf(), ], }, @@ -234,8 +236,25 @@ export function createPartialOptions() { list: typeAddressable, all: cohortAll, }), - ...typeAddressable.map(createCohortFolderAddress), - ...typeOther.map(createCohortFolderWithoutRelative), + .../** @satisfies {readonly SpendableType[]} */ ([ + "p2a", + "p2tr", + "p2wsh", + "p2wpkh", + "p2sh", + "p2ms", + "p2pkh", + "p2pk33", + "p2pk65", + "empty", + "unknown", + ]).flatMap((key) => { + const addr = typeAddressable.find((t) => t.key === key); + if (addr) return [createCohortFolderAddress(addr)]; + const other = typeOther.find((t) => t.key === key); + if (other) return [createCohortFolderWithoutRelative(other)]; + return []; + }), ], },