From 6bb1a2a3110d851fb0298fbef2e51c7d9f2a3ece Mon Sep 17 00:00:00 2001 From: nym21 Date: Fri, 16 Jan 2026 23:49:49 +0100 Subject: [PATCH] global: snapshot --- .gitignore | 4 +- Cargo.lock | 56 +- Cargo.toml | 4 +- crates/brk_bindgen/tests/catalog_test.rs | 6 +- crates/brk_cli/src/config.rs | 9 +- crates/brk_cli/src/main.rs | 27 +- crates/brk_cli/src/website.rs | 1 - crates/brk_client/src/lib.rs | 628 ++-- crates/brk_computer/src/price/compute.rs | 3 +- .../brk_computer/src/price/oracle/compute.rs | 928 +++++- .../brk_computer/src/price/oracle/import.rs | 66 + crates/brk_computer/src/price/oracle/mod.rs | 1 + .../brk_computer/src/price/oracle/phase_v2.rs | 296 ++ crates/brk_computer/src/price/oracle/vecs.rs | 49 +- crates/brk_server/README.md | 8 +- crates/brk_server/build.rs | 26 +- crates/brk_server/src/api/metrics/bulk.rs | 16 +- crates/brk_server/src/api/metrics/data.rs | 16 +- crates/brk_server/src/api/metrics/legacy.rs | 16 +- crates/brk_server/src/api/metrics/mod.rs | 13 +- crates/brk_server/src/api/mod.rs | 11 +- crates/brk_server/src/api/openapi/mod.rs | 2 +- crates/brk_server/src/api/scalar.html | 2 +- crates/brk_server/src/error.rs | 58 + crates/brk_server/src/files/file.rs | 46 +- crates/brk_server/src/lib.rs | 5 +- crates/brk_types/src/oracle_bins.rs | 154 + modules/brk-client/index.js | 713 ++--- packages/brk_client/brk_client/__init__.py | 327 ++- research/analyze_price_signals.py | 1081 ------- research/oracle_filter_analysis.md | 174 -- research/price_signal_analysis_report.txt | 2549 ----------------- research/test_phase_detection.py | 282 -- website/scripts/options/market/index.js | 94 +- 34 files changed, 2600 insertions(+), 5071 deletions(-) create mode 100644 crates/brk_computer/src/price/oracle/phase_v2.rs create mode 100644 crates/brk_server/src/error.rs delete mode 100644 research/analyze_price_signals.py delete mode 100644 research/oracle_filter_analysis.md delete mode 100644 research/price_signal_analysis_report.txt delete mode 100644 research/test_phase_detection.py diff --git a/.gitignore b/.gitignore index 345517cfb..7f734b3bf 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ target websites/dist bridge/ /ids.txt +rust_out # Copies *\ copy* @@ -15,9 +16,10 @@ _* !__*.py /*.md /*.py -/api.json /*.json /*.html +/research +/filter_* # Logs *.log* diff --git a/Cargo.lock b/Cargo.lock index 36934a0da..4a91e89ab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -41,9 +41,9 @@ dependencies = [ [[package]] name = "aide" -version = "0.16.0-alpha.1" +version = "0.16.0-alpha.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc156b16d6d8e9bb84a7cba8b93fc399c0654bfbb927832ea7ab326d5d6895e2" +checksum = "29e03dae8ee60626675f62a8d0c503fdc2721b01ae7feb2fd1e233b6bb16223a" dependencies = [ "axum", "bytes", @@ -831,9 +831,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" -version = "0.4.42" +version = "0.4.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" dependencies = [ "iana-time-zone", "js-sys", @@ -1957,9 +1957,9 @@ checksum = "00810f1d8b74be64b13dbf3db89ac67740615d6c891f0e7b6179326533011a07" [[package]] name = "js-sys" -version = "0.3.83" +version = "0.3.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" dependencies = [ "once_cell", "wasm-bindgen", @@ -2596,9 +2596,9 @@ dependencies = [ [[package]] name = "rawdb" -version = "0.5.10" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12f4f316acfc1844da8b3c84163aabd98f155df82d3b57ce4075dda550e1752d" +checksum = "133cd3a1d92510fe902efcdf70a7f45aa83e1cd47173d8cc013fef82af0b8f8e" dependencies = [ "libc", "log", @@ -2723,9 +2723,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.26" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" +checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" [[package]] name = "rustc-hash" @@ -3505,9 +3505,9 @@ checksum = "8f54a172d0620933a27a4360d3db3e2ae0dd6cceae9730751a036bbf182c4b23" [[package]] name = "vecdb" -version = "0.5.10" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d56bd525ca319c5772dcc91b40caa6947dfb6c56b63c4a9b2268ee24254c05" +checksum = "0a9b98950cd49f718ec32f3b91282b9d0c033ea15430436ddb0cc286c77aa5bb" dependencies = [ "ctrlc", "log", @@ -3526,9 +3526,9 @@ dependencies = [ [[package]] name = "vecdb_derive" -version = "0.5.10" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17c227a593e7a08c3d00babaa81aec624052d2c6a16ba127fb8f491f273c9751" +checksum = "043380b6bd5d519b63def192ff310f7a599feaca7f5b12cc86e5d9dd7eabf750" dependencies = [ "quote", "syn", @@ -3558,18 +3558,18 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.1+wasi-0.2.4" +version = "1.0.2+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" dependencies = [ "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.106" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" dependencies = [ "cfg-if", "once_cell", @@ -3580,9 +3580,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.106" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3590,9 +3590,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.106" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" dependencies = [ "bumpalo", "proc-macro2", @@ -3603,18 +3603,18 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.106" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" dependencies = [ "unicode-ident", ] [[package]] name = "web-sys" -version = "0.3.83" +version = "0.3.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" +checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" dependencies = [ "js-sys", "wasm-bindgen", @@ -3898,9 +3898,9 @@ dependencies = [ [[package]] name = "wit-bindgen" -version = "0.46.0" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" [[package]] name = "writeable" diff --git a/Cargo.toml b/Cargo.toml index b381b8fa4..2dd1b6867 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,7 +36,7 @@ inherits = "release" debug = true [workspace.dependencies] -aide = { version = "0.16.0-alpha.1", features = ["axum-json", "axum-query"] } +aide = { version = "0.16.0-alpha.2", features = ["axum-json", "axum-query"] } axum = "0.8.8" bitcoin = { version = "0.32.8", features = ["serde"] } bitcoincore-rpc = "0.19.0" @@ -78,7 +78,7 @@ serde_json = { version = "1.0.149", features = ["float_roundtrip", "preserve_ord smallvec = "1.15.1" tokio = { version = "1.49.0", features = ["rt-multi-thread"] } tracing = { version = "0.1", default-features = false, features = ["std"] } -vecdb = { version = "0.5.10", features = ["derive", "serde_json", "pco", "schemars"] } +vecdb = { version = "0.5.11", 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_bindgen/tests/catalog_test.rs b/crates/brk_bindgen/tests/catalog_test.rs index 181076450..96e96d53e 100644 --- a/crates/brk_bindgen/tests/catalog_test.rs +++ b/crates/brk_bindgen/tests/catalog_test.rs @@ -13,10 +13,10 @@ fn load_catalog() -> TreeNode { serde_json::from_str(&catalog_json).expect("Failed to parse catalog.json") } -/// Load OpenAPI spec from api.json. +/// Load OpenAPI spec from openapi.json. fn load_openapi_json() -> String { - let path = concat!(env!("CARGO_MANIFEST_DIR"), "/api.json"); - std::fs::read_to_string(path).expect("Failed to read api.json") + let path = concat!(env!("CARGO_MANIFEST_DIR"), "/openapi.json"); + std::fs::read_to_string(path).expect("Failed to read openapi.json") } /// Load metadata from the catalog. diff --git a/crates/brk_cli/src/config.rs b/crates/brk_cli/src/config.rs index 5f8be6109..5eb0bd544 100644 --- a/crates/brk_cli/src/config.rs +++ b/crates/brk_cli/src/config.rs @@ -11,7 +11,6 @@ use serde::{Deserialize, Deserializer, Serialize}; use crate::{default_brk_path, dot_brk_path, fix_user_path, website::Website}; - #[derive(Parser, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)] #[command(version, about)] pub struct Config { @@ -40,9 +39,9 @@ pub struct Config { #[arg(long, value_name = "BOOL")] exchanges: Option, - /// Website served by the server, default: default, saved + /// Website served by the server: true (default), false, or PATH, saved #[serde(default, deserialize_with = "default_on_error")] - #[arg(short, long)] + #[arg(short, long, value_name = "BOOL|PATH")] website: Option, /// Bitcoin RPC ip, default: localhost, saved @@ -232,9 +231,7 @@ Finally, you can run the program with '-h' for help." pub fn bitcoindir(&self) -> PathBuf { self.bitcoindir .as_ref() - .map_or_else(Client::default_bitcoin_path, |s| { - fix_user_path(s.as_ref()) - }) + .map_or_else(Client::default_bitcoin_path, |s| fix_user_path(s.as_ref())) } pub fn blocksdir(&self) -> PathBuf { diff --git a/crates/brk_cli/src/main.rs b/crates/brk_cli/src/main.rs index 4f7a4e859..3f3e32e9f 100644 --- a/crates/brk_cli/src/main.rs +++ b/crates/brk_cli/src/main.rs @@ -2,7 +2,6 @@ use std::{ fs, - path::PathBuf, thread::{self, sleep}, time::Duration, }; @@ -68,14 +67,17 @@ pub fn run() -> color_eyre::Result<()> { let data_path = config.brkdir(); let website_source = match config.website() { - Website::Enabled(false) => WebsiteSource::Disabled, - Website::Path(p) => WebsiteSource::Filesystem(p), + Website::Enabled(false) => { + info!("Website: disabled"); + WebsiteSource::Disabled + } + Website::Path(p) => { + info!("Website: filesystem ({})", p.display()); + WebsiteSource::Filesystem(p) + } Website::Enabled(true) => { - // Prefer local filesystem if available, otherwise use embedded - match find_local_website_dir() { - Some(path) => WebsiteSource::Filesystem(path), - None => WebsiteSource::Embedded, - } + info!("Website: embedded"); + WebsiteSource::Embedded } }; @@ -119,12 +121,3 @@ pub fn run() -> color_eyre::Result<()> { } } } - -/// Path to website directory relative to this crate (only valid at dev machine) -const DEV_WEBSITE_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../../website"); - -/// Returns local website path if it exists (dev mode) -fn find_local_website_dir() -> Option { - let path = PathBuf::from(DEV_WEBSITE_DIR); - path.exists().then_some(path) -} diff --git a/crates/brk_cli/src/website.rs b/crates/brk_cli/src/website.rs index c93b12039..9f4e5b1c5 100644 --- a/crates/brk_cli/src/website.rs +++ b/crates/brk_cli/src/website.rs @@ -21,7 +21,6 @@ impl Default for Website { } } - impl FromStr for Website { type Err = std::convert::Infallible; diff --git a/crates/brk_client/src/lib.rs b/crates/brk_client/src/lib.rs index 6945292a8..59af7d10c 100644 --- a/crates/brk_client/src/lib.rs +++ b/crates/brk_client/src/lib.rs @@ -1268,56 +1268,6 @@ impl Price111dSmaPattern { } } -/// Pattern struct for repeated tree structure. -pub struct PercentilesPattern { - pub pct05: MetricPattern4, - pub pct10: MetricPattern4, - pub pct15: MetricPattern4, - pub pct20: MetricPattern4, - pub pct25: MetricPattern4, - pub pct30: MetricPattern4, - pub pct35: MetricPattern4, - pub pct40: MetricPattern4, - pub pct45: MetricPattern4, - pub pct50: MetricPattern4, - pub pct55: MetricPattern4, - pub pct60: MetricPattern4, - pub pct65: MetricPattern4, - pub pct70: MetricPattern4, - pub pct75: MetricPattern4, - pub pct80: MetricPattern4, - pub pct85: MetricPattern4, - pub pct90: MetricPattern4, - pub pct95: MetricPattern4, -} - -impl PercentilesPattern { - /// Create a new pattern node with accumulated metric name. - pub fn new(client: Arc, acc: String) -> Self { - Self { - pct05: MetricPattern4::new(client.clone(), _m(&acc, "pct05")), - pct10: MetricPattern4::new(client.clone(), _m(&acc, "pct10")), - pct15: MetricPattern4::new(client.clone(), _m(&acc, "pct15")), - pct20: MetricPattern4::new(client.clone(), _m(&acc, "pct20")), - pct25: MetricPattern4::new(client.clone(), _m(&acc, "pct25")), - pct30: MetricPattern4::new(client.clone(), _m(&acc, "pct30")), - pct35: MetricPattern4::new(client.clone(), _m(&acc, "pct35")), - pct40: MetricPattern4::new(client.clone(), _m(&acc, "pct40")), - pct45: MetricPattern4::new(client.clone(), _m(&acc, "pct45")), - pct50: MetricPattern4::new(client.clone(), _m(&acc, "pct50")), - pct55: MetricPattern4::new(client.clone(), _m(&acc, "pct55")), - pct60: MetricPattern4::new(client.clone(), _m(&acc, "pct60")), - pct65: MetricPattern4::new(client.clone(), _m(&acc, "pct65")), - pct70: MetricPattern4::new(client.clone(), _m(&acc, "pct70")), - pct75: MetricPattern4::new(client.clone(), _m(&acc, "pct75")), - pct80: MetricPattern4::new(client.clone(), _m(&acc, "pct80")), - pct85: MetricPattern4::new(client.clone(), _m(&acc, "pct85")), - pct90: MetricPattern4::new(client.clone(), _m(&acc, "pct90")), - pct95: MetricPattern4::new(client.clone(), _m(&acc, "pct95")), - } - } -} - /// Pattern struct for repeated tree structure. pub struct ActivePriceRatioPattern { pub ratio: MetricPattern4, @@ -1368,6 +1318,56 @@ impl ActivePriceRatioPattern { } } +/// Pattern struct for repeated tree structure. +pub struct PercentilesPattern { + pub pct05: MetricPattern4, + pub pct10: MetricPattern4, + pub pct15: MetricPattern4, + pub pct20: MetricPattern4, + pub pct25: MetricPattern4, + pub pct30: MetricPattern4, + pub pct35: MetricPattern4, + pub pct40: MetricPattern4, + pub pct45: MetricPattern4, + pub pct50: MetricPattern4, + pub pct55: MetricPattern4, + pub pct60: MetricPattern4, + pub pct65: MetricPattern4, + pub pct70: MetricPattern4, + pub pct75: MetricPattern4, + pub pct80: MetricPattern4, + pub pct85: MetricPattern4, + pub pct90: MetricPattern4, + pub pct95: MetricPattern4, +} + +impl PercentilesPattern { + /// Create a new pattern node with accumulated metric name. + pub fn new(client: Arc, acc: String) -> Self { + Self { + pct05: MetricPattern4::new(client.clone(), _m(&acc, "pct05")), + pct10: MetricPattern4::new(client.clone(), _m(&acc, "pct10")), + pct15: MetricPattern4::new(client.clone(), _m(&acc, "pct15")), + pct20: MetricPattern4::new(client.clone(), _m(&acc, "pct20")), + pct25: MetricPattern4::new(client.clone(), _m(&acc, "pct25")), + pct30: MetricPattern4::new(client.clone(), _m(&acc, "pct30")), + pct35: MetricPattern4::new(client.clone(), _m(&acc, "pct35")), + pct40: MetricPattern4::new(client.clone(), _m(&acc, "pct40")), + pct45: MetricPattern4::new(client.clone(), _m(&acc, "pct45")), + pct50: MetricPattern4::new(client.clone(), _m(&acc, "pct50")), + pct55: MetricPattern4::new(client.clone(), _m(&acc, "pct55")), + pct60: MetricPattern4::new(client.clone(), _m(&acc, "pct60")), + pct65: MetricPattern4::new(client.clone(), _m(&acc, "pct65")), + pct70: MetricPattern4::new(client.clone(), _m(&acc, "pct70")), + pct75: MetricPattern4::new(client.clone(), _m(&acc, "pct75")), + pct80: MetricPattern4::new(client.clone(), _m(&acc, "pct80")), + pct85: MetricPattern4::new(client.clone(), _m(&acc, "pct85")), + pct90: MetricPattern4::new(client.clone(), _m(&acc, "pct90")), + pct95: MetricPattern4::new(client.clone(), _m(&acc, "pct95")), + } + } +} + /// Pattern struct for repeated tree structure. pub struct RelativePattern5 { pub neg_unrealized_loss_rel_to_market_cap: MetricPattern1, @@ -1668,38 +1668,6 @@ impl DollarsPattern { } } -/// Pattern struct for repeated tree structure. -pub struct RelativePattern2 { - pub neg_unrealized_loss_rel_to_own_market_cap: MetricPattern1, - pub neg_unrealized_loss_rel_to_own_total_unrealized_pnl: MetricPattern1, - pub net_unrealized_pnl_rel_to_own_market_cap: MetricPattern1, - pub net_unrealized_pnl_rel_to_own_total_unrealized_pnl: MetricPattern1, - pub supply_in_loss_rel_to_own_supply: MetricPattern1, - pub supply_in_profit_rel_to_own_supply: MetricPattern1, - pub unrealized_loss_rel_to_own_market_cap: MetricPattern1, - pub unrealized_loss_rel_to_own_total_unrealized_pnl: MetricPattern1, - pub unrealized_profit_rel_to_own_market_cap: MetricPattern1, - pub unrealized_profit_rel_to_own_total_unrealized_pnl: MetricPattern1, -} - -impl RelativePattern2 { - /// Create a new pattern node with accumulated metric name. - pub fn new(client: Arc, acc: String) -> Self { - Self { - neg_unrealized_loss_rel_to_own_market_cap: MetricPattern1::new(client.clone(), _m(&acc, "neg_unrealized_loss_rel_to_own_market_cap")), - neg_unrealized_loss_rel_to_own_total_unrealized_pnl: MetricPattern1::new(client.clone(), _m(&acc, "neg_unrealized_loss_rel_to_own_total_unrealized_pnl")), - net_unrealized_pnl_rel_to_own_market_cap: MetricPattern1::new(client.clone(), _m(&acc, "net_unrealized_pnl_rel_to_own_market_cap")), - net_unrealized_pnl_rel_to_own_total_unrealized_pnl: MetricPattern1::new(client.clone(), _m(&acc, "net_unrealized_pnl_rel_to_own_total_unrealized_pnl")), - supply_in_loss_rel_to_own_supply: MetricPattern1::new(client.clone(), _m(&acc, "supply_in_loss_rel_to_own_supply")), - supply_in_profit_rel_to_own_supply: MetricPattern1::new(client.clone(), _m(&acc, "supply_in_profit_rel_to_own_supply")), - unrealized_loss_rel_to_own_market_cap: MetricPattern1::new(client.clone(), _m(&acc, "unrealized_loss_rel_to_own_market_cap")), - unrealized_loss_rel_to_own_total_unrealized_pnl: MetricPattern1::new(client.clone(), _m(&acc, "unrealized_loss_rel_to_own_total_unrealized_pnl")), - unrealized_profit_rel_to_own_market_cap: MetricPattern1::new(client.clone(), _m(&acc, "unrealized_profit_rel_to_own_market_cap")), - unrealized_profit_rel_to_own_total_unrealized_pnl: MetricPattern1::new(client.clone(), _m(&acc, "unrealized_profit_rel_to_own_total_unrealized_pnl")), - } - } -} - /// Pattern struct for repeated tree structure. pub struct RelativePattern { pub neg_unrealized_loss_rel_to_market_cap: MetricPattern1, @@ -1732,6 +1700,38 @@ impl RelativePattern { } } +/// Pattern struct for repeated tree structure. +pub struct RelativePattern2 { + pub neg_unrealized_loss_rel_to_own_market_cap: MetricPattern1, + pub neg_unrealized_loss_rel_to_own_total_unrealized_pnl: MetricPattern1, + pub net_unrealized_pnl_rel_to_own_market_cap: MetricPattern1, + pub net_unrealized_pnl_rel_to_own_total_unrealized_pnl: MetricPattern1, + pub supply_in_loss_rel_to_own_supply: MetricPattern1, + pub supply_in_profit_rel_to_own_supply: MetricPattern1, + pub unrealized_loss_rel_to_own_market_cap: MetricPattern1, + pub unrealized_loss_rel_to_own_total_unrealized_pnl: MetricPattern1, + pub unrealized_profit_rel_to_own_market_cap: MetricPattern1, + pub unrealized_profit_rel_to_own_total_unrealized_pnl: MetricPattern1, +} + +impl RelativePattern2 { + /// Create a new pattern node with accumulated metric name. + pub fn new(client: Arc, acc: String) -> Self { + Self { + neg_unrealized_loss_rel_to_own_market_cap: MetricPattern1::new(client.clone(), _m(&acc, "neg_unrealized_loss_rel_to_own_market_cap")), + neg_unrealized_loss_rel_to_own_total_unrealized_pnl: MetricPattern1::new(client.clone(), _m(&acc, "neg_unrealized_loss_rel_to_own_total_unrealized_pnl")), + net_unrealized_pnl_rel_to_own_market_cap: MetricPattern1::new(client.clone(), _m(&acc, "net_unrealized_pnl_rel_to_own_market_cap")), + net_unrealized_pnl_rel_to_own_total_unrealized_pnl: MetricPattern1::new(client.clone(), _m(&acc, "net_unrealized_pnl_rel_to_own_total_unrealized_pnl")), + supply_in_loss_rel_to_own_supply: MetricPattern1::new(client.clone(), _m(&acc, "supply_in_loss_rel_to_own_supply")), + supply_in_profit_rel_to_own_supply: MetricPattern1::new(client.clone(), _m(&acc, "supply_in_profit_rel_to_own_supply")), + unrealized_loss_rel_to_own_market_cap: MetricPattern1::new(client.clone(), _m(&acc, "unrealized_loss_rel_to_own_market_cap")), + unrealized_loss_rel_to_own_total_unrealized_pnl: MetricPattern1::new(client.clone(), _m(&acc, "unrealized_loss_rel_to_own_total_unrealized_pnl")), + unrealized_profit_rel_to_own_market_cap: MetricPattern1::new(client.clone(), _m(&acc, "unrealized_profit_rel_to_own_market_cap")), + unrealized_profit_rel_to_own_total_unrealized_pnl: MetricPattern1::new(client.clone(), _m(&acc, "unrealized_profit_rel_to_own_total_unrealized_pnl")), + } + } +} + /// Pattern struct for repeated tree structure. pub struct CountPattern2 { pub average: MetricPattern1, @@ -1794,36 +1794,6 @@ impl AddrCountPattern { } } -/// Pattern struct for repeated tree structure. -pub struct FullnessPattern { - pub average: MetricPattern2, - pub base: MetricPattern11, - pub max: MetricPattern2, - pub median: MetricPattern6, - pub min: MetricPattern2, - pub pct10: MetricPattern6, - pub pct25: MetricPattern6, - pub pct75: MetricPattern6, - pub pct90: MetricPattern6, -} - -impl FullnessPattern { - /// Create a new pattern node with accumulated metric name. - pub fn new(client: Arc, acc: String) -> Self { - Self { - average: MetricPattern2::new(client.clone(), _m(&acc, "average")), - base: MetricPattern11::new(client.clone(), acc.clone()), - max: MetricPattern2::new(client.clone(), _m(&acc, "max")), - median: MetricPattern6::new(client.clone(), _m(&acc, "median")), - min: MetricPattern2::new(client.clone(), _m(&acc, "min")), - pct10: MetricPattern6::new(client.clone(), _m(&acc, "pct10")), - pct25: MetricPattern6::new(client.clone(), _m(&acc, "pct25")), - pct75: MetricPattern6::new(client.clone(), _m(&acc, "pct75")), - pct90: MetricPattern6::new(client.clone(), _m(&acc, "pct90")), - } - } -} - /// Pattern struct for repeated tree structure. pub struct FeeRatePattern { pub average: MetricPattern1, @@ -1854,6 +1824,36 @@ impl FeeRatePattern { } } +/// Pattern struct for repeated tree structure. +pub struct FullnessPattern { + pub average: MetricPattern2, + pub base: MetricPattern11, + pub max: MetricPattern2, + pub median: MetricPattern6, + pub min: MetricPattern2, + pub pct10: MetricPattern6, + pub pct25: MetricPattern6, + pub pct75: MetricPattern6, + pub pct90: MetricPattern6, +} + +impl FullnessPattern { + /// Create a new pattern node with accumulated metric name. + pub fn new(client: Arc, acc: String) -> Self { + Self { + average: MetricPattern2::new(client.clone(), _m(&acc, "average")), + base: MetricPattern11::new(client.clone(), acc.clone()), + max: MetricPattern2::new(client.clone(), _m(&acc, "max")), + median: MetricPattern6::new(client.clone(), _m(&acc, "median")), + min: MetricPattern2::new(client.clone(), _m(&acc, "min")), + pct10: MetricPattern6::new(client.clone(), _m(&acc, "pct10")), + pct25: MetricPattern6::new(client.clone(), _m(&acc, "pct25")), + pct75: MetricPattern6::new(client.clone(), _m(&acc, "pct75")), + pct90: MetricPattern6::new(client.clone(), _m(&acc, "pct90")), + } + } +} + /// Pattern struct for repeated tree structure. pub struct _0satsPattern { pub activity: ActivityPattern2, @@ -1910,6 +1910,84 @@ impl PhaseDailyCentsPattern { } } +/// Pattern struct for repeated tree structure. +pub struct PeriodCagrPattern { + pub _10y: MetricPattern4, + pub _2y: MetricPattern4, + pub _3y: MetricPattern4, + pub _4y: MetricPattern4, + pub _5y: MetricPattern4, + pub _6y: MetricPattern4, + pub _8y: MetricPattern4, +} + +impl PeriodCagrPattern { + /// Create a new pattern node with accumulated metric name. + pub fn new(client: Arc, acc: String) -> Self { + Self { + _10y: MetricPattern4::new(client.clone(), _p("10y", &acc)), + _2y: MetricPattern4::new(client.clone(), _p("2y", &acc)), + _3y: MetricPattern4::new(client.clone(), _p("3y", &acc)), + _4y: MetricPattern4::new(client.clone(), _p("4y", &acc)), + _5y: MetricPattern4::new(client.clone(), _p("5y", &acc)), + _6y: MetricPattern4::new(client.clone(), _p("6y", &acc)), + _8y: MetricPattern4::new(client.clone(), _p("8y", &acc)), + } + } +} + +/// Pattern struct for repeated tree structure. +pub struct _0satsPattern2 { + pub activity: ActivityPattern2, + pub cost_basis: CostBasisPattern, + pub outputs: OutputsPattern, + pub realized: RealizedPattern, + pub relative: RelativePattern4, + pub supply: SupplyPattern2, + pub unrealized: UnrealizedPattern, +} + +impl _0satsPattern2 { + /// Create a new pattern node with accumulated metric name. + pub fn new(client: Arc, acc: String) -> Self { + Self { + activity: ActivityPattern2::new(client.clone(), acc.clone()), + cost_basis: CostBasisPattern::new(client.clone(), acc.clone()), + outputs: OutputsPattern::new(client.clone(), _m(&acc, "utxo_count")), + realized: RealizedPattern::new(client.clone(), acc.clone()), + relative: RelativePattern4::new(client.clone(), _m(&acc, "supply_in")), + supply: SupplyPattern2::new(client.clone(), _m(&acc, "supply")), + unrealized: UnrealizedPattern::new(client.clone(), acc.clone()), + } + } +} + +/// Pattern struct for repeated tree structure. +pub struct _100btcPattern { + pub activity: ActivityPattern2, + pub cost_basis: CostBasisPattern, + pub outputs: OutputsPattern, + pub realized: RealizedPattern, + pub relative: RelativePattern, + pub supply: SupplyPattern2, + pub unrealized: UnrealizedPattern, +} + +impl _100btcPattern { + /// Create a new pattern node with accumulated metric name. + pub fn new(client: Arc, acc: String) -> Self { + Self { + activity: ActivityPattern2::new(client.clone(), acc.clone()), + cost_basis: CostBasisPattern::new(client.clone(), acc.clone()), + outputs: OutputsPattern::new(client.clone(), _m(&acc, "utxo_count")), + realized: RealizedPattern::new(client.clone(), acc.clone()), + relative: RelativePattern::new(client.clone(), acc.clone()), + supply: SupplyPattern2::new(client.clone(), _m(&acc, "supply")), + unrealized: UnrealizedPattern::new(client.clone(), acc.clone()), + } + } +} + /// Pattern struct for repeated tree structure. pub struct UnrealizedPattern { pub neg_unrealized_loss: MetricPattern1, @@ -1962,32 +2040,6 @@ impl _10yTo12yPattern { } } -/// Pattern struct for repeated tree structure. -pub struct PeriodCagrPattern { - pub _10y: MetricPattern4, - pub _2y: MetricPattern4, - pub _3y: MetricPattern4, - pub _4y: MetricPattern4, - pub _5y: MetricPattern4, - pub _6y: MetricPattern4, - pub _8y: MetricPattern4, -} - -impl PeriodCagrPattern { - /// Create a new pattern node with accumulated metric name. - pub fn new(client: Arc, acc: String) -> Self { - Self { - _10y: MetricPattern4::new(client.clone(), _p("10y", &acc)), - _2y: MetricPattern4::new(client.clone(), _p("2y", &acc)), - _3y: MetricPattern4::new(client.clone(), _p("3y", &acc)), - _4y: MetricPattern4::new(client.clone(), _p("4y", &acc)), - _5y: MetricPattern4::new(client.clone(), _p("5y", &acc)), - _6y: MetricPattern4::new(client.clone(), _p("6y", &acc)), - _8y: MetricPattern4::new(client.clone(), _p("8y", &acc)), - } - } -} - /// Pattern struct for repeated tree structure. pub struct _10yPattern { pub activity: ActivityPattern2, @@ -2014,58 +2066,6 @@ impl _10yPattern { } } -/// Pattern struct for repeated tree structure. -pub struct _100btcPattern { - pub activity: ActivityPattern2, - pub cost_basis: CostBasisPattern, - pub outputs: OutputsPattern, - pub realized: RealizedPattern, - pub relative: RelativePattern, - pub supply: SupplyPattern2, - pub unrealized: UnrealizedPattern, -} - -impl _100btcPattern { - /// Create a new pattern node with accumulated metric name. - pub fn new(client: Arc, acc: String) -> Self { - Self { - activity: ActivityPattern2::new(client.clone(), acc.clone()), - cost_basis: CostBasisPattern::new(client.clone(), acc.clone()), - outputs: OutputsPattern::new(client.clone(), _m(&acc, "utxo_count")), - realized: RealizedPattern::new(client.clone(), acc.clone()), - relative: RelativePattern::new(client.clone(), acc.clone()), - supply: SupplyPattern2::new(client.clone(), _m(&acc, "supply")), - unrealized: UnrealizedPattern::new(client.clone(), acc.clone()), - } - } -} - -/// Pattern struct for repeated tree structure. -pub struct _0satsPattern2 { - pub activity: ActivityPattern2, - pub cost_basis: CostBasisPattern, - pub outputs: OutputsPattern, - pub realized: RealizedPattern, - pub relative: RelativePattern4, - pub supply: SupplyPattern2, - pub unrealized: UnrealizedPattern, -} - -impl _0satsPattern2 { - /// Create a new pattern node with accumulated metric name. - pub fn new(client: Arc, acc: String) -> Self { - Self { - activity: ActivityPattern2::new(client.clone(), acc.clone()), - cost_basis: CostBasisPattern::new(client.clone(), acc.clone()), - outputs: OutputsPattern::new(client.clone(), _m(&acc, "utxo_count")), - realized: RealizedPattern::new(client.clone(), acc.clone()), - relative: RelativePattern4::new(client.clone(), _m(&acc, "supply_in")), - supply: SupplyPattern2::new(client.clone(), _m(&acc, "supply")), - unrealized: UnrealizedPattern::new(client.clone(), acc.clone()), - } - } -} - /// Pattern struct for repeated tree structure. pub struct ActivityPattern2 { pub coinblocks_destroyed: BlockCountPattern, @@ -2108,24 +2108,6 @@ impl SplitPattern2 { } } -/// Pattern struct for repeated tree structure. -pub struct _2015Pattern { - pub bitcoin: MetricPattern4, - pub dollars: MetricPattern4, - pub sats: MetricPattern4, -} - -impl _2015Pattern { - /// Create a new pattern node with accumulated metric name. - pub fn new(client: Arc, acc: String) -> Self { - Self { - bitcoin: MetricPattern4::new(client.clone(), _m(&acc, "btc")), - dollars: MetricPattern4::new(client.clone(), _m(&acc, "usd")), - sats: MetricPattern4::new(client.clone(), acc.clone()), - } - } -} - /// Pattern struct for repeated tree structure. pub struct ActiveSupplyPattern { pub bitcoin: MetricPattern1, @@ -2145,19 +2127,19 @@ impl ActiveSupplyPattern { } /// Pattern struct for repeated tree structure. -pub struct CostBasisPattern2 { - pub max: MetricPattern1, - pub min: MetricPattern1, - pub percentiles: PercentilesPattern, +pub struct _2015Pattern { + pub bitcoin: MetricPattern4, + pub dollars: MetricPattern4, + pub sats: MetricPattern4, } -impl CostBasisPattern2 { +impl _2015Pattern { /// Create a new pattern node with accumulated metric name. pub fn new(client: Arc, acc: String) -> Self { Self { - max: MetricPattern1::new(client.clone(), _m(&acc, "max_cost_basis")), - min: MetricPattern1::new(client.clone(), _m(&acc, "min_cost_basis")), - percentiles: PercentilesPattern::new(client.clone(), _m(&acc, "cost_basis")), + bitcoin: MetricPattern4::new(client.clone(), _m(&acc, "btc")), + dollars: MetricPattern4::new(client.clone(), _m(&acc, "usd")), + sats: MetricPattern4::new(client.clone(), acc.clone()), } } } @@ -2180,6 +2162,24 @@ impl CoinbasePattern2 { } } +/// Pattern struct for repeated tree structure. +pub struct CostBasisPattern2 { + pub max: MetricPattern1, + pub min: MetricPattern1, + pub percentiles: PercentilesPattern, +} + +impl CostBasisPattern2 { + /// Create a new pattern node with accumulated metric name. + pub fn new(client: Arc, acc: String) -> Self { + Self { + max: MetricPattern1::new(client.clone(), _m(&acc, "max_cost_basis")), + min: MetricPattern1::new(client.clone(), _m(&acc, "min_cost_basis")), + percentiles: PercentilesPattern::new(client.clone(), _m(&acc, "cost_basis")), + } + } +} + /// Pattern struct for repeated tree structure. pub struct SegwitAdoptionPattern { pub base: MetricPattern11, @@ -2198,24 +2198,6 @@ impl SegwitAdoptionPattern { } } -/// Pattern struct for repeated tree structure. -pub struct CoinbasePattern { - pub bitcoin: BitcoinPattern, - pub dollars: DollarsPattern, - pub sats: DollarsPattern, -} - -impl CoinbasePattern { - /// Create a new pattern node with accumulated metric name. - pub fn new(client: Arc, acc: String) -> Self { - Self { - bitcoin: BitcoinPattern::new(client.clone(), _m(&acc, "btc")), - dollars: DollarsPattern::new(client.clone(), _m(&acc, "usd")), - sats: DollarsPattern::new(client.clone(), acc.clone()), - } - } -} - /// Pattern struct for repeated tree structure. pub struct UnclaimedRewardsPattern { pub bitcoin: BitcoinPattern2, @@ -2235,17 +2217,19 @@ impl UnclaimedRewardsPattern { } /// Pattern struct for repeated tree structure. -pub struct SupplyPattern2 { - pub halved: ActiveSupplyPattern, - pub total: ActiveSupplyPattern, +pub struct CoinbasePattern { + pub bitcoin: BitcoinPattern, + pub dollars: DollarsPattern, + pub sats: DollarsPattern, } -impl SupplyPattern2 { +impl CoinbasePattern { /// Create a new pattern node with accumulated metric name. pub fn new(client: Arc, acc: String) -> Self { Self { - halved: ActiveSupplyPattern::new(client.clone(), _m(&acc, "halved")), - total: ActiveSupplyPattern::new(client.clone(), acc.clone()), + bitcoin: BitcoinPattern::new(client.clone(), _m(&acc, "btc")), + dollars: DollarsPattern::new(client.clone(), _m(&acc, "usd")), + sats: DollarsPattern::new(client.clone(), acc.clone()), } } } @@ -2266,22 +2250,6 @@ impl RelativePattern4 { } } -/// Pattern struct for repeated tree structure. -pub struct CostBasisPattern { - pub max: MetricPattern1, - pub min: MetricPattern1, -} - -impl CostBasisPattern { - /// Create a new pattern node with accumulated metric name. - pub fn new(client: Arc, acc: String) -> Self { - Self { - max: MetricPattern1::new(client.clone(), _m(&acc, "max_cost_basis")), - min: MetricPattern1::new(client.clone(), _m(&acc, "min_cost_basis")), - } - } -} - /// Pattern struct for repeated tree structure. pub struct _1dReturns1mSdPattern { pub sd: MetricPattern4, @@ -2299,17 +2267,33 @@ impl _1dReturns1mSdPattern { } /// Pattern struct for repeated tree structure. -pub struct BitcoinPattern2 { - pub cumulative: MetricPattern2, - pub sum: MetricPattern1, +pub struct CostBasisPattern { + pub max: MetricPattern1, + pub min: MetricPattern1, } -impl BitcoinPattern2 { +impl CostBasisPattern { /// Create a new pattern node with accumulated metric name. pub fn new(client: Arc, acc: String) -> Self { Self { - cumulative: MetricPattern2::new(client.clone(), _m(&acc, "cumulative")), - sum: MetricPattern1::new(client.clone(), acc.clone()), + max: MetricPattern1::new(client.clone(), _m(&acc, "max_cost_basis")), + min: MetricPattern1::new(client.clone(), _m(&acc, "min_cost_basis")), + } + } +} + +/// Pattern struct for repeated tree structure. +pub struct SupplyPattern2 { + pub halved: ActiveSupplyPattern, + pub total: ActiveSupplyPattern, +} + +impl SupplyPattern2 { + /// Create a new pattern node with accumulated metric name. + pub fn new(client: Arc, acc: String) -> Self { + Self { + halved: ActiveSupplyPattern::new(client.clone(), _m(&acc, "halved")), + total: ActiveSupplyPattern::new(client.clone(), acc.clone()), } } } @@ -2330,6 +2314,22 @@ impl BlockCountPattern { } } +/// Pattern struct for repeated tree structure. +pub struct BitcoinPattern2 { + pub cumulative: MetricPattern2, + pub sum: MetricPattern1, +} + +impl BitcoinPattern2 { + /// Create a new pattern node with accumulated metric name. + pub fn new(client: Arc, acc: String) -> Self { + Self { + cumulative: MetricPattern2::new(client.clone(), _m(&acc, "cumulative")), + sum: MetricPattern1::new(client.clone(), acc.clone()), + } + } +} + /// Pattern struct for repeated tree structure. pub struct SatsPattern { pub ohlc: MetricPattern1, @@ -2340,22 +2340,8 @@ impl SatsPattern { /// Create a new pattern node with accumulated metric name. pub fn new(client: Arc, acc: String) -> Self { Self { - ohlc: MetricPattern1::new(client.clone(), _m(&acc, "ohlc")), - split: SplitPattern2::new(client.clone(), acc.clone()), - } - } -} - -/// Pattern struct for repeated tree structure. -pub struct RealizedPriceExtraPattern { - pub ratio: MetricPattern4, -} - -impl RealizedPriceExtraPattern { - /// Create a new pattern node with accumulated metric name. - pub fn new(client: Arc, acc: String) -> Self { - Self { - ratio: MetricPattern4::new(client.clone(), acc.clone()), + ohlc: MetricPattern1::new(client.clone(), _m(&acc, "ohlc_sats")), + split: SplitPattern2::new(client.clone(), _m(&acc, "sats")), } } } @@ -2374,6 +2360,20 @@ impl OutputsPattern { } } +/// Pattern struct for repeated tree structure. +pub struct RealizedPriceExtraPattern { + pub ratio: MetricPattern4, +} + +impl RealizedPriceExtraPattern { + /// Create a new pattern node with accumulated metric name. + pub fn new(client: Arc, acc: String) -> Self { + Self { + ratio: MetricPattern4::new(client.clone(), acc.clone()), + } + } +} + // Metrics tree /// Metrics tree node. @@ -4942,8 +4942,8 @@ impl MetricsTree_Positions { pub struct MetricsTree_Price { pub cents: MetricsTree_Price_Cents, pub oracle: MetricsTree_Price_Oracle, - pub sats: MetricsTree_Price_Sats, - pub usd: SatsPattern, + pub sats: SatsPattern, + pub usd: MetricsTree_Price_Usd, } impl MetricsTree_Price { @@ -4951,8 +4951,8 @@ impl MetricsTree_Price { Self { cents: MetricsTree_Price_Cents::new(client.clone(), format!("{base_path}_cents")), oracle: MetricsTree_Price_Oracle::new(client.clone(), format!("{base_path}_oracle")), - sats: MetricsTree_Price_Sats::new(client.clone(), format!("{base_path}_sats")), - usd: SatsPattern::new(client.clone(), "price".to_string()), + sats: SatsPattern::new(client.clone(), "price".to_string()), + usd: MetricsTree_Price_Usd::new(client.clone(), format!("{base_path}_usd")), } } } @@ -5003,6 +5003,20 @@ pub struct MetricsTree_Price_Oracle { pub phase_daily_dollars: PhaseDailyCentsPattern, pub phase_histogram: MetricPattern11, pub phase_price_cents: MetricPattern11, + pub phase_v2_daily_cents: PhaseDailyCentsPattern, + pub phase_v2_daily_dollars: PhaseDailyCentsPattern, + pub phase_v2_histogram: MetricPattern11, + pub phase_v2_peak_daily_cents: PhaseDailyCentsPattern, + pub phase_v2_peak_daily_dollars: PhaseDailyCentsPattern, + pub phase_v2_peak_price_cents: MetricPattern11, + pub phase_v2_price_cents: MetricPattern11, + pub phase_v3_daily_cents: PhaseDailyCentsPattern, + pub phase_v3_daily_dollars: PhaseDailyCentsPattern, + pub phase_v3_histogram: MetricPattern11, + pub phase_v3_peak_daily_cents: PhaseDailyCentsPattern, + pub phase_v3_peak_daily_dollars: PhaseDailyCentsPattern, + pub phase_v3_peak_price_cents: MetricPattern11, + pub phase_v3_price_cents: MetricPattern11, pub price_cents: MetricPattern11, pub tx_count: MetricPattern6, } @@ -5020,6 +5034,20 @@ impl MetricsTree_Price_Oracle { phase_daily_dollars: PhaseDailyCentsPattern::new(client.clone(), "phase_daily_dollars".to_string()), phase_histogram: MetricPattern11::new(client.clone(), "phase_histogram".to_string()), phase_price_cents: MetricPattern11::new(client.clone(), "phase_price_cents".to_string()), + phase_v2_daily_cents: PhaseDailyCentsPattern::new(client.clone(), "phase_v2_daily".to_string()), + phase_v2_daily_dollars: PhaseDailyCentsPattern::new(client.clone(), "phase_v2_daily_dollars".to_string()), + phase_v2_histogram: MetricPattern11::new(client.clone(), "phase_v2_histogram".to_string()), + phase_v2_peak_daily_cents: PhaseDailyCentsPattern::new(client.clone(), "phase_v2_peak_daily".to_string()), + phase_v2_peak_daily_dollars: PhaseDailyCentsPattern::new(client.clone(), "phase_v2_peak_daily_dollars".to_string()), + phase_v2_peak_price_cents: MetricPattern11::new(client.clone(), "phase_v2_peak_price_cents".to_string()), + phase_v2_price_cents: MetricPattern11::new(client.clone(), "phase_v2_price_cents".to_string()), + phase_v3_daily_cents: PhaseDailyCentsPattern::new(client.clone(), "phase_v3_daily".to_string()), + phase_v3_daily_dollars: PhaseDailyCentsPattern::new(client.clone(), "phase_v3_daily_dollars".to_string()), + phase_v3_histogram: MetricPattern11::new(client.clone(), "phase_v3_histogram".to_string()), + phase_v3_peak_daily_cents: PhaseDailyCentsPattern::new(client.clone(), "phase_v3_peak_daily".to_string()), + phase_v3_peak_daily_dollars: PhaseDailyCentsPattern::new(client.clone(), "phase_v3_peak_daily_dollars".to_string()), + phase_v3_peak_price_cents: MetricPattern11::new(client.clone(), "phase_v3_peak_price_cents".to_string()), + phase_v3_price_cents: MetricPattern11::new(client.clone(), "phase_v3_price_cents".to_string()), price_cents: MetricPattern11::new(client.clone(), "oracle_price_cents".to_string()), tx_count: MetricPattern6::new(client.clone(), "oracle_tx_count".to_string()), } @@ -5027,16 +5055,16 @@ impl MetricsTree_Price_Oracle { } /// Metrics tree node. -pub struct MetricsTree_Price_Sats { - pub ohlc: MetricPattern1, - pub split: SplitPattern2, +pub struct MetricsTree_Price_Usd { + pub ohlc: MetricPattern1, + pub split: SplitPattern2, } -impl MetricsTree_Price_Sats { +impl MetricsTree_Price_Usd { pub fn new(client: Arc, base_path: String) -> Self { Self { - ohlc: MetricPattern1::new(client.clone(), "price_ohlc_sats".to_string()), - split: SplitPattern2::new(client.clone(), "price_sats".to_string()), + ohlc: MetricPattern1::new(client.clone(), "price_ohlc".to_string()), + split: SplitPattern2::new(client.clone(), "price".to_string()), } } } @@ -5422,24 +5450,15 @@ impl BrkClient { ) } - /// OpenAPI specification + /// Compact OpenAPI specification /// - /// Full OpenAPI 3.1 specification for this API. + /// Compact OpenAPI specification optimized for LLM consumption. Removes redundant fields while preserving essential API information. Full spec available at `/openapi.json`. /// /// Endpoint: `GET /api.json` - pub fn get_openapi(&self) -> Result { + pub fn get_api(&self) -> Result { self.base.get_json(&format!("/api.json")) } - /// Trimmed OpenAPI specification - /// - /// Compact OpenAPI specification optimized for LLM consumption. Removes redundant fields while preserving essential API information. - /// - /// Endpoint: `GET /api.trimmed.json` - pub fn get_openapi_trimmed(&self) -> Result { - self.base.get_json(&format!("/api.trimmed.json")) - } - /// Address information /// /// Retrieve address information including balance and transaction counts. Supports all standard Bitcoin address types (P2PKH, P2SH, P2WPKH, P2WSH, P2TR). @@ -5999,6 +6018,15 @@ impl BrkClient { self.base.get_json(&format!("/health")) } + /// OpenAPI specification + /// + /// Full OpenAPI 3.1 specification for this API. + /// + /// Endpoint: `GET /openapi.json` + pub fn get_openapi(&self) -> Result { + self.base.get_json(&format!("/openapi.json")) + } + /// API version /// /// Returns the current version of the API server diff --git a/crates/brk_computer/src/price/compute.rs b/crates/brk_computer/src/price/compute.rs index d1839ba73..4c6ef4fd3 100644 --- a/crates/brk_computer/src/price/compute.rs +++ b/crates/brk_computer/src/price/compute.rs @@ -26,7 +26,8 @@ impl Vecs { info!("Computing oracle prices..."); let i = Instant::now(); - self.oracle.compute(indexer, indexes, starting_indexes, exit)?; + self.oracle + .compute(indexer, indexes, &self.cents, starting_indexes, exit)?; info!("Computed oracle prices in {:?}", i.elapsed()); } diff --git a/crates/brk_computer/src/price/oracle/compute.rs b/crates/brk_computer/src/price/oracle/compute.rs index 19421669f..79b222243 100644 --- a/crates/brk_computer/src/price/oracle/compute.rs +++ b/crates/brk_computer/src/price/oracle/compute.rs @@ -53,8 +53,8 @@ use std::collections::VecDeque; use brk_error::Result; use brk_indexer::Indexer; use brk_types::{ - Cents, Close, Date, DateIndex, Height, High, Low, OHLCCents, Open, OracleBins, OutputType, - PHASE_BINS, PairOutputIndex, Sats, StoredU32, StoredU64, TxIndex, + Cents, Close, Date, DateIndex, Height, High, Low, OHLCCents, Open, OracleBins, OracleBinsV2, + OutputType, PHASE_BINS, PairOutputIndex, Sats, StoredU32, StoredU64, TxIndex, }; use tracing::info; use vecdb::{ @@ -66,9 +66,10 @@ use super::{ Vecs, config::OracleConfig, histogram::{Histogram, TOTAL_BINS}, + phase_v2::{PhaseHistogramV2, find_best_phase, phase_range_from_anchor, phase_to_price}, stencil::{find_best_price, is_round_sats, refine_price}, }; -use crate::{ComputeIndexes, indexes}; +use crate::{ComputeIndexes, indexes, price::cents}; /// Flush interval for periodic writes during oracle computation. const FLUSH_INTERVAL: usize = 10_000; @@ -79,6 +80,7 @@ impl Vecs { &mut self, indexer: &Indexer, indexes: &indexes::Vecs, + price_cents: ¢s::Vecs, starting_indexes: &ComputeIndexes, exit: &Exit, ) -> Result<()> { @@ -100,6 +102,32 @@ impl Vecs { // Step 7: Aggregate to daily OHLC self.compute_daily_ohlc(indexes, starting_indexes, exit)?; + // Step 8: Compute Phase Oracle V2 (round USD template matching) + // 8a: Per-block 200-bin histograms (uses ALL outputs, not pair-filtered) + self.compute_phase_v2_histograms(indexer, indexes, starting_indexes, exit)?; + + // 8b: Per-block prices using cross-correlation with weekly anchors + self.compute_phase_v2_prices(indexes, price_cents, starting_indexes, exit)?; + + // 8c: Per-block prices using direct peak finding (like V1) + self.compute_phase_v2_peak_prices(indexes, price_cents, starting_indexes, exit)?; + + // 8d: Daily distributions from per-block prices + self.compute_phase_v2_daily(indexes, starting_indexes, exit)?; + + // Step 9: Compute Phase Oracle V3 (BASE + uniqueVal filter) + // 9a: Per-block histograms with uniqueVal filtering (only outputs with unique values in tx) + self.compute_phase_v3_histograms(indexer, indexes, starting_indexes, exit)?; + + // 9b: Per-block prices using cross-correlation + self.compute_phase_v3_prices(indexes, price_cents, starting_indexes, exit)?; + + // 9c: Per-block prices using direct peak finding (like V1) + self.compute_phase_v3_peak_prices(indexes, price_cents, starting_indexes, exit)?; + + // 9d: Daily distributions from per-block prices + self.compute_phase_v3_daily(indexes, starting_indexes, exit)?; + Ok(()) } @@ -1091,4 +1119,898 @@ impl Vecs { Ok(()) } + + /// Compute Phase Oracle V2 - Step 1: Per-block 200-bin phase histograms + /// + /// Uses ALL outputs (like Python test), filtered only by sats range (1k-100k BTC). + /// This is different from the pair-filtered approach used by UTXOracle. + fn compute_phase_v2_histograms( + &mut self, + indexer: &Indexer, + indexes: &indexes::Vecs, + starting_indexes: &ComputeIndexes, + exit: &Exit, + ) -> Result<()> { + let source_version = indexer.vecs.outputs.value.version(); + self.phase_v2_histogram + .validate_computed_version_or_reset(source_version)?; + + let total_heights = indexer.vecs.blocks.timestamp.len(); + + let start_height = self + .phase_v2_histogram + .len() + .min(starting_indexes.height.to_usize()); + + self.phase_v2_histogram + .truncate_if_needed_at(start_height)?; + + if start_height >= total_heights { + return Ok(()); + } + + info!( + "Computing phase V2 histograms from height {} to {}", + start_height, total_heights + ); + + let mut height_to_first_txindex_iter = indexer.vecs.transactions.first_txindex.into_iter(); + let mut txindex_to_first_txoutindex_iter = + indexer.vecs.transactions.first_txoutindex.into_iter(); + let mut txindex_to_output_count_iter = indexes.txindex.output_count.iter(); + let mut txoutindex_to_value_iter = indexer.vecs.outputs.value.into_iter(); + + let total_txs = indexer.vecs.transactions.height.len(); + let mut last_progress = (start_height * 100 / total_heights.max(1)) as u8; + + for height in start_height..total_heights { + // Get transaction range for this block + let first_txindex = height_to_first_txindex_iter.get_at_unwrap(height); + let next_first_txindex = height_to_first_txindex_iter + .get_at(height + 1) + .unwrap_or(TxIndex::from(total_txs)); + + // Build phase histogram from ALL outputs in this block + let mut histogram = OracleBinsV2::ZERO; + + for txindex in first_txindex.to_usize()..next_first_txindex.to_usize() { + // Get output count and first output for this transaction + let first_txoutindex = txindex_to_first_txoutindex_iter.get_at_unwrap(txindex); + let output_count: StoredU64 = + txindex_to_output_count_iter.get_unwrap(TxIndex::from(txindex)); + + for i in 0..*output_count as usize { + let txoutindex = first_txoutindex.to_usize() + i; + let sats: Sats = txoutindex_to_value_iter.get_at_unwrap(txoutindex); + // OracleBinsV2::add already filters by sats range (1k to 100k BTC) + histogram.add(sats); + } + } + + self.phase_v2_histogram.push(histogram); + + // Progress logging + let progress = (height * 100 / total_heights.max(1)) as u8; + if progress > last_progress { + last_progress = progress; + info!("Phase V2 histogram computation: {}%", progress); + + let _lock = exit.lock(); + self.phase_v2_histogram.write()?; + } + } + + // Final write + { + let _lock = exit.lock(); + self.phase_v2_histogram.write()?; + } + + info!( + "Phase V2 histograms complete: {} blocks", + self.phase_v2_histogram.len() + ); + + Ok(()) + } + + /// Compute Phase Oracle V2 - Step 2: Per-block prices using cross-correlation + fn compute_phase_v2_prices( + &mut self, + indexes: &indexes::Vecs, + price_cents: ¢s::Vecs, + starting_indexes: &ComputeIndexes, + exit: &Exit, + ) -> Result<()> { + let source_version = self.phase_v2_histogram.version(); + self.phase_v2_price_cents + .validate_computed_version_or_reset(source_version)?; + + let total_heights = self.phase_v2_histogram.len(); + + let start_height = self + .phase_v2_price_cents + .len() + .min(starting_indexes.height.to_usize()); + + self.phase_v2_price_cents + .truncate_if_needed_at(start_height)?; + + if start_height >= total_heights { + return Ok(()); + } + + info!( + "Computing phase V2 prices from height {} to {}", + start_height, total_heights + ); + + let mut histogram_iter = self.phase_v2_histogram.iter()?; + let mut height_to_dateindex_iter = indexes.height.dateindex.iter(); + + // For weekly OHLC anchors + let mut price_ohlc_iter = price_cents.ohlc.dateindex.iter()?; + let mut dateindex_to_weekindex_iter = indexes.dateindex.weekindex.iter(); + let mut weekindex_to_first_dateindex_iter = indexes.weekindex.first_dateindex.iter(); + let mut weekindex_dateindex_count_iter = indexes.weekindex.dateindex_count.iter(); + + let mut last_progress = (start_height * 100 / total_heights.max(1)) as u8; + + // Track previous price for fallback + let mut prev_price_cents = if start_height > 0 { + self.phase_v2_price_cents + .iter()? + .get(Height::from(start_height - 1)) + .unwrap_or(Cents::from(10_000_000i64)) + } else { + Cents::from(10_000_000i64) // Default ~$100k + }; + + for height in start_height..total_heights { + let height_idx = Height::from(height); + let histogram: OracleBinsV2 = histogram_iter.get_unwrap(height_idx); + + // Get weekly anchor for this block's date + let dateindex = height_to_dateindex_iter.get(height_idx); + let weekly_bounds: Option<(f64, f64)> = dateindex.and_then(|di| { + let wi = dateindex_to_weekindex_iter.get(di)?; + let first_di = weekindex_to_first_dateindex_iter.get(wi)?; + let count = weekindex_dateindex_count_iter + .get(wi) + .map(|c| *c as usize)?; + + let mut low = Cents::from(i64::MAX); + let mut high = Cents::from(0i64); + + for i in 0..count { + let di = DateIndex::from(first_di.to_usize() + i); + if let Some(ohlc) = price_ohlc_iter.get(di) { + if *ohlc.low < low { + low = *ohlc.low; + } + if *ohlc.high > high { + high = *ohlc.high; + } + } + } + + if i64::from(low) > 0 && i64::from(high) > 0 { + Some(( + i64::from(low) as f64 / 100.0, + i64::from(high) as f64 / 100.0, + )) + } else { + None + } + }); + + // Compute price using cross-correlation + let price_cents = if histogram.total_count() >= 10 { + // Convert OracleBinsV2 to PhaseHistogramV2 + let mut phase_hist = PhaseHistogramV2::new(); + for (i, &count) in histogram.bins.iter().enumerate() { + if count > 0 { + let phase = (i as f64 + 0.5) / 200.0; + let log_sats = 6.0 + phase; + let sats = 10.0_f64.powf(log_sats); + for _ in 0..count { + phase_hist.add(Sats::from(sats as u64)); + } + } + } + + if let Some((low, high)) = weekly_bounds { + // Have weekly anchor - constrained search + let (phase_min, phase_max) = phase_range_from_anchor(low, high, 0.05); + let (best_phase, _corr) = + find_best_phase(&phase_hist, 2, Some(phase_min), Some(phase_max)); + let price = phase_to_price(best_phase, low, high); + Cents::from((price * 100.0) as i64) + } else { + // No anchor - use previous price as reference + let anchor_low = (i64::from(prev_price_cents) as f64 / 100.0) * 0.5; + let anchor_high = (i64::from(prev_price_cents) as f64 / 100.0) * 2.0; + let (best_phase, _corr) = find_best_phase(&phase_hist, 2, None, None); + let price = phase_to_price(best_phase, anchor_low, anchor_high); + Cents::from((price * 100.0) as i64) + } + } else { + // Too few outputs - use previous price + prev_price_cents + }; + + prev_price_cents = price_cents; + self.phase_v2_price_cents.push(price_cents); + + // Progress logging + let progress = (height * 100 / total_heights.max(1)) as u8; + if progress > last_progress { + last_progress = progress; + info!("Phase V2 price computation: {}%", progress); + + let _lock = exit.lock(); + self.phase_v2_price_cents.write()?; + } + } + + // Final write + { + let _lock = exit.lock(); + self.phase_v2_price_cents.write()?; + } + + info!( + "Phase V2 prices complete: {} blocks", + self.phase_v2_price_cents.len() + ); + + Ok(()) + } + + /// Compute Phase Oracle V2 - Peak prices using direct peak finding (like V1) + fn compute_phase_v2_peak_prices( + &mut self, + indexes: &indexes::Vecs, + price_cents: ¢s::Vecs, + starting_indexes: &ComputeIndexes, + exit: &Exit, + ) -> Result<()> { + let source_version = self.phase_v2_histogram.version(); + self.phase_v2_peak_price_cents + .validate_computed_version_or_reset(source_version)?; + + let total_heights = self.phase_v2_histogram.len(); + + let start_height = self + .phase_v2_peak_price_cents + .len() + .min(starting_indexes.height.to_usize()); + + self.phase_v2_peak_price_cents + .truncate_if_needed_at(start_height)?; + + if start_height >= total_heights { + return Ok(()); + } + + info!( + "Computing phase V2 peak prices from height {} to {}", + start_height, total_heights + ); + + let mut histogram_iter = self.phase_v2_histogram.iter()?; + let mut height_to_dateindex_iter = indexes.height.dateindex.iter(); + + // For weekly OHLC anchors + let mut price_ohlc_iter = price_cents.ohlc.dateindex.iter()?; + let mut dateindex_to_weekindex_iter = indexes.dateindex.weekindex.iter(); + let mut weekindex_to_first_dateindex_iter = indexes.weekindex.first_dateindex.iter(); + let mut weekindex_dateindex_count_iter = indexes.weekindex.dateindex_count.iter(); + + let mut last_progress = (start_height * 100 / total_heights.max(1)) as u8; + + // Track previous price for fallback + let mut prev_price_cents = if start_height > 0 { + self.phase_v2_peak_price_cents + .iter()? + .get(Height::from(start_height - 1)) + .unwrap_or(Cents::from(10_000_000i64)) + } else { + Cents::from(10_000_000i64) + }; + + for height in start_height..total_heights { + let height_idx = Height::from(height); + let histogram: OracleBinsV2 = histogram_iter.get_unwrap(height_idx); + + // Get weekly anchor for decade selection + let dateindex = height_to_dateindex_iter.get(height_idx); + let anchor_price: Option = dateindex.and_then(|di| { + let wi = dateindex_to_weekindex_iter.get(di)?; + let first_di = weekindex_to_first_dateindex_iter.get(wi)?; + let count = weekindex_dateindex_count_iter + .get(wi) + .map(|c| *c as usize)?; + + let mut sum = 0i64; + let mut cnt = 0; + for i in 0..count { + let di = DateIndex::from(first_di.to_usize() + i); + if let Some(ohlc) = price_ohlc_iter.get(di) { + sum += i64::from(*ohlc.close); + cnt += 1; + } + } + + if cnt > 0 { + Some(sum as f64 / cnt as f64 / 100.0) + } else { + None + } + }); + + // Use anchor or previous price for decade selection + let anchor = anchor_price.unwrap_or(i64::from(prev_price_cents) as f64 / 100.0); + + // Find peak bin directly (like V1) using 100 bins (downsample from 200) + let price_cents = if histogram.total_count() >= 10 { + // Downsample 200 bins to 100 bins + let mut bins100 = [0u32; 100]; + for i in 0..100 { + bins100[i] = histogram.bins[i * 2] as u32 + histogram.bins[i * 2 + 1] as u32; + } + + // Find peak bin, skipping bin 0 (round BTC amounts cluster there) + let peak_bin = bins100 + .iter() + .enumerate() + .filter(|(bin, _)| *bin != 0) + .max_by_key(|(_, count)| *count) + .map(|(bin, _)| bin) + .unwrap_or(0); + + // Convert bin to price using anchor for decade (100 bins) + let phase = (peak_bin as f64 + 0.5) / 100.0; + let base_price = 10.0_f64.powf(phase); + + // Find best decade + let mut best_price = base_price; + let mut best_dist = f64::MAX; + for decade in -2..=6 { + let candidate = base_price * 10.0_f64.powi(decade); + let dist = (candidate - anchor).abs(); + if dist < best_dist { + best_dist = dist; + best_price = candidate; + } + } + + Cents::from((best_price.clamp(0.01, 10_000_000.0) * 100.0) as i64) + } else { + prev_price_cents + }; + + prev_price_cents = price_cents; + self.phase_v2_peak_price_cents.push(price_cents); + + // Progress logging + let progress = (height * 100 / total_heights.max(1)) as u8; + if progress > last_progress { + last_progress = progress; + info!("Phase V2 peak price computation: {}%", progress); + + let _lock = exit.lock(); + self.phase_v2_peak_price_cents.write()?; + } + } + + // Final write + { + let _lock = exit.lock(); + self.phase_v2_peak_price_cents.write()?; + } + + info!( + "Phase V2 peak prices complete: {} blocks", + self.phase_v2_peak_price_cents.len() + ); + + Ok(()) + } + + /// Compute Phase Oracle V2 - Daily distributions from per-block prices + fn compute_phase_v2_daily( + &mut self, + indexes: &indexes::Vecs, + starting_indexes: &ComputeIndexes, + exit: &Exit, + ) -> Result<()> { + info!("Computing phase V2 daily distributions"); + + // Cross-correlation based + self.phase_v2_daily_cents.compute( + starting_indexes.dateindex, + &self.phase_v2_price_cents, + &indexes.dateindex.first_height, + &indexes.dateindex.height_count, + exit, + )?; + + // Peak-based + self.phase_v2_peak_daily_cents.compute( + starting_indexes.dateindex, + &self.phase_v2_peak_price_cents, + &indexes.dateindex.first_height, + &indexes.dateindex.height_count, + exit, + )?; + + info!( + "Phase V2 daily distributions complete: {} days", + self.phase_v2_daily_cents.len() + ); + + Ok(()) + } + + /// Compute Phase Oracle V3 - Step 1: Per-block histograms with uniqueVal filtering + /// + /// Filters: >= 1000 sats, only outputs with unique values within their transaction. + /// This reduces spurious peaks from exchange batched payouts and inscription spam. + fn compute_phase_v3_histograms( + &mut self, + indexer: &Indexer, + indexes: &indexes::Vecs, + starting_indexes: &ComputeIndexes, + exit: &Exit, + ) -> Result<()> { + let source_version = indexer.vecs.outputs.value.version(); + self.phase_v3_histogram + .validate_computed_version_or_reset(source_version)?; + + let total_heights = indexer.vecs.blocks.timestamp.len(); + + let start_height = self + .phase_v3_histogram + .len() + .min(starting_indexes.height.to_usize()); + + self.phase_v3_histogram + .truncate_if_needed_at(start_height)?; + + if start_height >= total_heights { + return Ok(()); + } + + info!( + "Computing phase V3 histograms from height {} to {}", + start_height, total_heights + ); + + let mut height_to_first_txindex_iter = indexer.vecs.transactions.first_txindex.into_iter(); + let mut txindex_to_first_txoutindex_iter = + indexer.vecs.transactions.first_txoutindex.into_iter(); + let mut txindex_to_output_count_iter = indexes.txindex.output_count.iter(); + let mut txoutindex_to_value_iter = indexer.vecs.outputs.value.into_iter(); + + let total_txs = indexer.vecs.transactions.height.len(); + let mut last_progress = (start_height * 100 / total_heights.max(1)) as u8; + + // Reusable buffer for collecting output values per transaction + let mut tx_values: Vec = Vec::with_capacity(16); + + for height in start_height..total_heights { + // Get transaction range for this block + let first_txindex = height_to_first_txindex_iter.get_at_unwrap(height); + let next_first_txindex = height_to_first_txindex_iter + .get_at(height + 1) + .unwrap_or(TxIndex::from(total_txs)); + + // Build phase histogram with uniqueVal filtering + let mut histogram = OracleBinsV2::ZERO; + + // Skip coinbase (first tx in block) + for txindex in (first_txindex.to_usize() + 1)..next_first_txindex.to_usize() { + // Get output count and first output for this transaction + let first_txoutindex = txindex_to_first_txoutindex_iter.get_at_unwrap(txindex); + let output_count: StoredU64 = + txindex_to_output_count_iter.get_unwrap(TxIndex::from(txindex)); + + // Collect all output values for this transaction + tx_values.clear(); + for i in 0..*output_count as usize { + let txoutindex = first_txoutindex.to_usize() + i; + let sats: Sats = txoutindex_to_value_iter.get_at_unwrap(txoutindex); + tx_values.push(sats); + } + + // Count occurrences of each value to determine uniqueness + // For small output counts, simple nested loop is faster than HashMap + for (i, &sats) in tx_values.iter().enumerate() { + // Skip if below minimum (BASE filter: >= 1000 sats) + if sats < Sats::_1K { + continue; + } + + // Check if this value is unique within the transaction + let mut is_unique = true; + for (j, &other_sats) in tx_values.iter().enumerate() { + if i != j && sats == other_sats { + is_unique = false; + break; + } + } + + // Only add unique values to histogram + if is_unique { + histogram.add(sats); + } + } + } + + self.phase_v3_histogram.push(histogram); + + // Progress logging + let progress = (height * 100 / total_heights.max(1)) as u8; + if progress > last_progress { + last_progress = progress; + info!("Phase V3 histogram computation: {}%", progress); + + let _lock = exit.lock(); + self.phase_v3_histogram.write()?; + } + } + + // Final write + { + let _lock = exit.lock(); + self.phase_v3_histogram.write()?; + } + + info!( + "Phase V3 histograms complete: {} blocks", + self.phase_v3_histogram.len() + ); + + Ok(()) + } + + /// Compute Phase Oracle V3 - Step 2: Per-block prices using cross-correlation + fn compute_phase_v3_prices( + &mut self, + indexes: &indexes::Vecs, + price_cents: ¢s::Vecs, + starting_indexes: &ComputeIndexes, + exit: &Exit, + ) -> Result<()> { + let source_version = self.phase_v3_histogram.version(); + self.phase_v3_price_cents + .validate_computed_version_or_reset(source_version)?; + + let total_heights = self.phase_v3_histogram.len(); + + let start_height = self + .phase_v3_price_cents + .len() + .min(starting_indexes.height.to_usize()); + + self.phase_v3_price_cents + .truncate_if_needed_at(start_height)?; + + if start_height >= total_heights { + return Ok(()); + } + + info!( + "Computing phase V3 prices from height {} to {}", + start_height, total_heights + ); + + let mut histogram_iter = self.phase_v3_histogram.iter()?; + let mut height_to_dateindex_iter = indexes.height.dateindex.iter(); + + // For weekly OHLC anchors + let mut price_ohlc_iter = price_cents.ohlc.dateindex.iter()?; + let mut dateindex_to_weekindex_iter = indexes.dateindex.weekindex.iter(); + let mut weekindex_to_first_dateindex_iter = indexes.weekindex.first_dateindex.iter(); + let mut weekindex_dateindex_count_iter = indexes.weekindex.dateindex_count.iter(); + + let mut last_progress = (start_height * 100 / total_heights.max(1)) as u8; + + // Track previous price for fallback + let mut prev_price_cents = if start_height > 0 { + self.phase_v3_price_cents + .iter()? + .get(Height::from(start_height - 1)) + .unwrap_or(Cents::from(10_000_000i64)) + } else { + Cents::from(10_000_000i64) // Default ~$100k + }; + + for height in start_height..total_heights { + let height_idx = Height::from(height); + let histogram: OracleBinsV2 = histogram_iter.get_unwrap(height_idx); + + // Get weekly anchor for this block's date + let dateindex = height_to_dateindex_iter.get(height_idx); + let weekly_bounds: Option<(f64, f64)> = dateindex.and_then(|di| { + let wi = dateindex_to_weekindex_iter.get(di)?; + let first_di = weekindex_to_first_dateindex_iter.get(wi)?; + let count = weekindex_dateindex_count_iter + .get(wi) + .map(|c| *c as usize)?; + + let mut low = Cents::from(i64::MAX); + let mut high = Cents::from(0i64); + + for i in 0..count { + let di = DateIndex::from(first_di.to_usize() + i); + if let Some(ohlc) = price_ohlc_iter.get(di) { + if *ohlc.low < low { + low = *ohlc.low; + } + if *ohlc.high > high { + high = *ohlc.high; + } + } + } + + if i64::from(low) > 0 && i64::from(high) > 0 { + Some(( + i64::from(low) as f64 / 100.0, + i64::from(high) as f64 / 100.0, + )) + } else { + None + } + }); + + // Compute price using cross-correlation + let price_cents = if histogram.total_count() >= 10 { + // Convert OracleBinsV2 to PhaseHistogramV2 + let mut phase_hist = PhaseHistogramV2::new(); + for (i, &count) in histogram.bins.iter().enumerate() { + if count > 0 { + let phase = (i as f64 + 0.5) / 200.0; + let log_sats = 6.0 + phase; + let sats = 10.0_f64.powf(log_sats); + for _ in 0..count { + phase_hist.add(Sats::from(sats as u64)); + } + } + } + + if let Some((low, high)) = weekly_bounds { + // Have weekly anchor - constrained search + let (phase_min, phase_max) = phase_range_from_anchor(low, high, 0.05); + let (best_phase, _corr) = + find_best_phase(&phase_hist, 2, Some(phase_min), Some(phase_max)); + let price = phase_to_price(best_phase, low, high); + Cents::from((price * 100.0) as i64) + } else { + // No anchor - use previous price as reference + let anchor_low = (i64::from(prev_price_cents) as f64 / 100.0) * 0.5; + let anchor_high = (i64::from(prev_price_cents) as f64 / 100.0) * 2.0; + let (best_phase, _corr) = find_best_phase(&phase_hist, 2, None, None); + let price = phase_to_price(best_phase, anchor_low, anchor_high); + Cents::from((price * 100.0) as i64) + } + } else { + // Too few outputs - use previous price + prev_price_cents + }; + + prev_price_cents = price_cents; + self.phase_v3_price_cents.push(price_cents); + + // Progress logging + let progress = (height * 100 / total_heights.max(1)) as u8; + if progress > last_progress { + last_progress = progress; + info!("Phase V3 price computation: {}%", progress); + + let _lock = exit.lock(); + self.phase_v3_price_cents.write()?; + } + } + + // Final write + { + let _lock = exit.lock(); + self.phase_v3_price_cents.write()?; + } + + info!( + "Phase V3 prices complete: {} blocks", + self.phase_v3_price_cents.len() + ); + + Ok(()) + } + + /// Compute Phase Oracle V3 - Peak prices using direct peak finding (like V1) + fn compute_phase_v3_peak_prices( + &mut self, + indexes: &indexes::Vecs, + price_cents: ¢s::Vecs, + starting_indexes: &ComputeIndexes, + exit: &Exit, + ) -> Result<()> { + let source_version = self.phase_v3_histogram.version(); + self.phase_v3_peak_price_cents + .validate_computed_version_or_reset(source_version)?; + + let total_heights = self.phase_v3_histogram.len(); + + let start_height = self + .phase_v3_peak_price_cents + .len() + .min(starting_indexes.height.to_usize()); + + self.phase_v3_peak_price_cents + .truncate_if_needed_at(start_height)?; + + if start_height >= total_heights { + return Ok(()); + } + + info!( + "Computing phase V3 peak prices from height {} to {}", + start_height, total_heights + ); + + let mut histogram_iter = self.phase_v3_histogram.iter()?; + let mut height_to_dateindex_iter = indexes.height.dateindex.iter(); + + // For weekly OHLC anchors + let mut price_ohlc_iter = price_cents.ohlc.dateindex.iter()?; + let mut dateindex_to_weekindex_iter = indexes.dateindex.weekindex.iter(); + let mut weekindex_to_first_dateindex_iter = indexes.weekindex.first_dateindex.iter(); + let mut weekindex_dateindex_count_iter = indexes.weekindex.dateindex_count.iter(); + + let mut last_progress = (start_height * 100 / total_heights.max(1)) as u8; + + // Track previous price for fallback + let mut prev_price_cents = if start_height > 0 { + self.phase_v3_peak_price_cents + .iter()? + .get(Height::from(start_height - 1)) + .unwrap_or(Cents::from(10_000_000i64)) + } else { + Cents::from(10_000_000i64) + }; + + for height in start_height..total_heights { + let height_idx = Height::from(height); + let histogram: OracleBinsV2 = histogram_iter.get_unwrap(height_idx); + + // Get weekly anchor for decade selection + let dateindex = height_to_dateindex_iter.get(height_idx); + let anchor_price: Option = dateindex.and_then(|di| { + let wi = dateindex_to_weekindex_iter.get(di)?; + let first_di = weekindex_to_first_dateindex_iter.get(wi)?; + let count = weekindex_dateindex_count_iter + .get(wi) + .map(|c| *c as usize)?; + + let mut sum = 0i64; + let mut cnt = 0; + for i in 0..count { + let di = DateIndex::from(first_di.to_usize() + i); + if let Some(ohlc) = price_ohlc_iter.get(di) { + sum += i64::from(*ohlc.close); + cnt += 1; + } + } + + if cnt > 0 { + Some(sum as f64 / cnt as f64 / 100.0) + } else { + None + } + }); + + // Use anchor or previous price for decade selection + let anchor = anchor_price.unwrap_or(i64::from(prev_price_cents) as f64 / 100.0); + + // Find peak bin directly (like V1) using 100 bins (downsample from 200) + let price_cents = if histogram.total_count() >= 10 { + // Downsample 200 bins to 100 bins + let mut bins100 = [0u32; 100]; + (0..100).for_each(|i| { + bins100[i] = histogram.bins[i * 2] as u32 + histogram.bins[i * 2 + 1] as u32; + }); + + // Find peak bin, skipping bin 0 (round BTC amounts cluster there) + let peak_bin = bins100 + .iter() + .enumerate() + .filter(|(bin, _)| *bin != 0) + .max_by_key(|(_, count)| *count) + .map(|(bin, _)| bin) + .unwrap_or(0); + + // Convert bin to price using anchor for decade (100 bins) + let phase = (peak_bin as f64 + 0.5) / 100.0; + let base_price = 10.0_f64.powf(phase); + + // Find best decade + let mut best_price = base_price; + let mut best_dist = f64::MAX; + for decade in -2..=6 { + let candidate = base_price * 10.0_f64.powi(decade); + let dist = (candidate - anchor).abs(); + if dist < best_dist { + best_dist = dist; + best_price = candidate; + } + } + + Cents::from((best_price.clamp(0.01, 10_000_000.0) * 100.0) as i64) + } else { + prev_price_cents + }; + + prev_price_cents = price_cents; + self.phase_v3_peak_price_cents.push(price_cents); + + // Progress logging + let progress = (height * 100 / total_heights.max(1)) as u8; + if progress > last_progress { + last_progress = progress; + info!("Phase V3 peak price computation: {}%", progress); + + let _lock = exit.lock(); + self.phase_v3_peak_price_cents.write()?; + } + } + + // Final write + { + let _lock = exit.lock(); + self.phase_v3_peak_price_cents.write()?; + } + + info!( + "Phase V3 peak prices complete: {} blocks", + self.phase_v3_peak_price_cents.len() + ); + + Ok(()) + } + + /// Compute Phase Oracle V3 - Daily distributions from per-block prices + fn compute_phase_v3_daily( + &mut self, + indexes: &indexes::Vecs, + starting_indexes: &ComputeIndexes, + exit: &Exit, + ) -> Result<()> { + info!("Computing phase V3 daily distributions"); + + // Cross-correlation based + self.phase_v3_daily_cents.compute( + starting_indexes.dateindex, + &self.phase_v3_price_cents, + &indexes.dateindex.first_height, + &indexes.dateindex.height_count, + exit, + )?; + + // Peak-based + self.phase_v3_peak_daily_cents.compute( + starting_indexes.dateindex, + &self.phase_v3_peak_price_cents, + &indexes.dateindex.first_height, + &indexes.dateindex.height_count, + exit, + )?; + + info!( + "Phase V3 daily distributions complete: {} days", + self.phase_v3_daily_cents.len() + ); + + Ok(()) + } } diff --git a/crates/brk_computer/src/price/oracle/import.rs b/crates/brk_computer/src/price/oracle/import.rs index 9e34a5a91..eb4ad6b0e 100644 --- a/crates/brk_computer/src/price/oracle/import.rs +++ b/crates/brk_computer/src/price/oracle/import.rs @@ -46,6 +46,58 @@ impl Vecs { |di: DateIndex, iter| iter.get(di).map(|o: OHLCCents| OHLCDollars::from(o)), ); + // Phase Oracle V2 (round USD template matching) + // v3: Peak prices use 100 bins (downsampled from 200) + let phase_v2_version = version + Version::new(3); + let phase_v2_histogram = + BytesVec::forced_import(db, "phase_v2_histogram", phase_v2_version)?; + let phase_v2_price_cents = + PcoVec::forced_import(db, "phase_v2_price_cents", phase_v2_version)?; + let phase_v2_peak_price_cents = + PcoVec::forced_import(db, "phase_v2_peak_price_cents", phase_v2_version)?; + let phase_v2_daily_cents = + Distribution::forced_import(db, "phase_v2_daily", phase_v2_version)?; + let phase_v2_daily_dollars = + LazyTransformDistribution::from_distribution::( + "phase_v2_daily_dollars", + phase_v2_version, + &phase_v2_daily_cents, + ); + let phase_v2_peak_daily_cents = + Distribution::forced_import(db, "phase_v2_peak_daily", phase_v2_version)?; + let phase_v2_peak_daily_dollars = + LazyTransformDistribution::from_distribution::( + "phase_v2_peak_daily_dollars", + phase_v2_version, + &phase_v2_peak_daily_cents, + ); + + // Phase Oracle V3 (BASE + uniqueVal filter) + // v4: Peak prices use 100 bins (downsampled from 200) + let phase_v3_version = version + Version::new(4); + let phase_v3_histogram = + BytesVec::forced_import(db, "phase_v3_histogram", phase_v3_version)?; + let phase_v3_price_cents = + PcoVec::forced_import(db, "phase_v3_price_cents", phase_v3_version)?; + let phase_v3_peak_price_cents = + PcoVec::forced_import(db, "phase_v3_peak_price_cents", phase_v3_version)?; + let phase_v3_daily_cents = + Distribution::forced_import(db, "phase_v3_daily", phase_v3_version)?; + let phase_v3_daily_dollars = + LazyTransformDistribution::from_distribution::( + "phase_v3_daily_dollars", + phase_v3_version, + &phase_v3_daily_cents, + ); + let phase_v3_peak_daily_cents = + Distribution::forced_import(db, "phase_v3_peak_daily", phase_v3_version)?; + let phase_v3_peak_daily_dollars = + LazyTransformDistribution::from_distribution::( + "phase_v3_peak_daily_dollars", + phase_v3_version, + &phase_v3_peak_daily_cents, + ); + Ok(Self { pairoutputindex_to_txindex, height_to_first_pairoutputindex, @@ -59,6 +111,20 @@ impl Vecs { ohlc_cents, ohlc_dollars, tx_count, + phase_v2_histogram, + phase_v2_price_cents, + phase_v2_peak_price_cents, + phase_v2_daily_cents, + phase_v2_daily_dollars, + phase_v2_peak_daily_cents, + phase_v2_peak_daily_dollars, + phase_v3_histogram, + phase_v3_price_cents, + phase_v3_peak_price_cents, + phase_v3_daily_cents, + phase_v3_daily_dollars, + phase_v3_peak_daily_cents, + phase_v3_peak_daily_dollars, }) } } diff --git a/crates/brk_computer/src/price/oracle/mod.rs b/crates/brk_computer/src/price/oracle/mod.rs index 34a5eab14..3a90f7f12 100644 --- a/crates/brk_computer/src/price/oracle/mod.rs +++ b/crates/brk_computer/src/price/oracle/mod.rs @@ -158,6 +158,7 @@ mod compute; mod config; mod histogram; mod import; +mod phase_v2; mod stencil; mod vecs; diff --git a/crates/brk_computer/src/price/oracle/phase_v2.rs b/crates/brk_computer/src/price/oracle/phase_v2.rs new file mode 100644 index 000000000..c1814e44a --- /dev/null +++ b/crates/brk_computer/src/price/oracle/phase_v2.rs @@ -0,0 +1,296 @@ +//! Phase Oracle V2 - Round USD Template Cross-Correlation +//! +//! Detects Bitcoin prices by finding where round USD amounts ($1, $5, $10, etc.) +//! cluster in the phase histogram. Uses weekly OHLC anchors to constrain search. +//! +//! ## Algorithm +//! +//! 1. Build 200-bin phase histogram: bin = frac(log10(sats)) * 200 +//! 2. Cross-correlate with weighted round USD template +//! 3. Use weekly OHLC anchor to constrain phase search range +//! 4. Return best-matching phase, convert to price +//! +//! ## Key Insight +//! +//! Round USD amounts create a fixed "fingerprint" pattern in phase space: +//! - $1, $10, $100, $1000 → phase 0.00 (weight 10) +//! - $5, $50, $500 → phase 0.70 (weight 9) +//! - $2, $20, $200 → phase 0.30 (weight 7) +//! - etc. +//! +//! The pattern shifts based on price: sats_phase = usd_phase - price_phase (mod 1) +//! Finding the shift that best matches the template reveals the price phase. + +use brk_types::Sats; + +/// Number of phase bins (0.5% resolution) +pub const PHASE_BINS_V2: usize = 200; + +/// Round USD template: (phase, weight) pairs +/// Phase = frac(log10(usd_cents)) for round USD values +/// Weight reflects expected popularity (higher = more common) +pub const ROUND_USD_TEMPLATE: [(f64, u32); 11] = [ + (0.00, 10), // $1, $10, $100, $1000 - VERY common + (0.18, 3), // $1.50, $15, $150 - uncommon + (0.30, 7), // $2, $20, $200 - common + (0.40, 4), // $2.50, $25, $250 - moderate + (0.48, 5), // $3, $30, $300 - moderate + (0.60, 4), // $4, $40, $400 - moderate + (0.70, 9), // $5, $50, $500 - VERY common + (0.78, 2), // $6, $60, $600 - rare + (0.85, 2), // $7, $70, $700 - rare + (0.90, 2), // $8, $80, $800 - rare + (0.95, 2), // $9, $90, $900 - rare +]; + +/// Pre-computed template bins: (bin_index, weight) +pub fn template_bins() -> Vec<(usize, u32)> { + ROUND_USD_TEMPLATE + .iter() + .map(|&(phase, weight)| { + let bin = ((phase * PHASE_BINS_V2 as f64) as usize) % PHASE_BINS_V2; + (bin, weight) + }) + .collect() +} + +/// Phase histogram for V2 oracle (200 bins) +#[derive(Clone)] +pub struct PhaseHistogramV2 { + bins: [u32; PHASE_BINS_V2], + total: u32, +} + +impl Default for PhaseHistogramV2 { + fn default() -> Self { + Self::new() + } +} + +impl PhaseHistogramV2 { + pub fn new() -> Self { + Self { + bins: [0; PHASE_BINS_V2], + total: 0, + } + } + + /// Convert sats value to phase bin index + /// Filters: min 1k sats, max 100k BTC + #[inline] + pub fn sats_to_bin(sats: Sats) -> Option { + if sats < Sats::_1K || sats > Sats::_100K_BTC { + return None; + } + let log_sats = f64::from(sats).log10(); + let phase = log_sats.fract(); + let phase = if phase < 0.0 { phase + 1.0 } else { phase }; + Some(((phase * PHASE_BINS_V2 as f64) as usize).min(PHASE_BINS_V2 - 1)) + } + + /// Add a sats value to the histogram + #[inline] + pub fn add(&mut self, sats: Sats) { + if let Some(bin) = Self::sats_to_bin(sats) { + self.bins[bin] = self.bins[bin].saturating_add(1); + self.total += 1; + } + } + + /// Add another histogram to this one + pub fn add_histogram(&mut self, other: &PhaseHistogramV2) { + for (i, &count) in other.bins.iter().enumerate() { + self.bins[i] = self.bins[i].saturating_add(count); + } + self.total = self.total.saturating_add(other.total); + } + + /// Get total count + pub fn total(&self) -> u32 { + self.total + } + + /// Get bins array + pub fn bins(&self) -> &[u32; PHASE_BINS_V2] { + &self.bins + } + + /// Clear the histogram + pub fn clear(&mut self) { + self.bins.fill(0); + self.total = 0; + } +} + +/// Find the best price phase using cross-correlation with weighted template +/// +/// # Arguments +/// * `histogram` - Phase histogram to analyze +/// * `tolerance_bins` - Number of bins tolerance for template matching (e.g., 4 = ±2%) +/// * `phase_min` - Optional minimum phase from anchor (0.0-1.0) +/// * `phase_max` - Optional maximum phase from anchor (0.0-1.0) +/// +/// # Returns +/// * `(best_phase, best_correlation)` - Best matching phase (0.0-1.0) and correlation score +pub fn find_best_phase( + histogram: &PhaseHistogramV2, + tolerance_bins: usize, + phase_min: Option, + phase_max: Option, +) -> (f64, u64) { + let template = template_bins(); + let bins = histogram.bins(); + + let mut best_phase = 0.0; + let mut best_corr: u64 = 0; + + // Determine valid shifts based on anchor constraints + let valid_shifts: Vec = if let (Some(p_min), Some(p_max)) = (phase_min, phase_max) { + let min_bin = ((p_min * PHASE_BINS_V2 as f64) as usize) % PHASE_BINS_V2; + let max_bin = ((p_max * PHASE_BINS_V2 as f64) as usize) % PHASE_BINS_V2; + + if min_bin <= max_bin { + (min_bin..=max_bin).collect() + } else { + // Wraps around + (min_bin..PHASE_BINS_V2) + .chain(0..=max_bin) + .collect() + } + } else { + (0..PHASE_BINS_V2).collect() + }; + + // Cross-correlation: slide template across histogram + for shift in valid_shifts { + let mut corr: u64 = 0; + + for &(template_bin, weight) in &template { + // Where would this template bin appear at this price phase shift? + let expected_bin = (template_bin + PHASE_BINS_V2 - shift) % PHASE_BINS_V2; + + // Sum bins within tolerance, weighted + for t in 0..=(2 * tolerance_bins) { + let check_bin = (expected_bin + PHASE_BINS_V2 - tolerance_bins + t) % PHASE_BINS_V2; + corr += bins[check_bin] as u64 * weight as u64; + } + } + + if corr > best_corr { + best_corr = corr; + best_phase = shift as f64 / PHASE_BINS_V2 as f64; + } + } + + (best_phase, best_corr) +} + +/// Get phase range from price anchor (low, high) +/// +/// Returns (phase_min, phase_max) with tolerance added +pub fn phase_range_from_anchor(price_low: f64, price_high: f64, tolerance_pct: f64) -> (f64, f64) { + let low_adj = price_low * (1.0 - tolerance_pct); + let high_adj = price_high * (1.0 + tolerance_pct); + + let phase_low = low_adj.log10().fract(); + let phase_high = high_adj.log10().fract(); + + let phase_low = if phase_low < 0.0 { + phase_low + 1.0 + } else { + phase_low + }; + let phase_high = if phase_high < 0.0 { + phase_high + 1.0 + } else { + phase_high + }; + + (phase_low, phase_high) +} + +/// Convert detected phase to price using anchor for decade selection +/// +/// The phase alone is ambiguous ($6.3, $63, $630, $6300 all have same phase). +/// Use the anchor price range to select the correct decade. +pub fn phase_to_price(phase: f64, anchor_low: f64, anchor_high: f64) -> f64 { + // Base price from phase (arbitrary decade, we'll adjust) + // phase = frac(log10(price)), so price = 10^(decade + phase) + // Start with decade 0 (prices 1-10) + let base_price = 10.0_f64.powf(phase); + + // Find which decade puts us in the anchor range + let anchor_mid = (anchor_low + anchor_high) / 2.0; + + // Try decades -2 to 6 ($0.01 to $1,000,000) + let mut best_price = base_price; + let mut best_dist = f64::MAX; + + for decade in -2..=6 { + let candidate = base_price * 10.0_f64.powi(decade); + let dist = (candidate - anchor_mid).abs(); + if dist < best_dist { + best_dist = dist; + best_price = candidate; + } + } + + // Clamp to reasonable range + best_price.clamp(0.01, 10_000_000.0) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_template_bins() { + let template = template_bins(); + assert_eq!(template.len(), 11); + + // Check $1/$10/$100 maps to bin 0 + assert_eq!(template[0].0, 0); + assert_eq!(template[0].1, 10); + + // Check $5/$50 maps to bin 140 (0.70 * 200) + assert_eq!(template[6].0, 140); + assert_eq!(template[6].1, 9); + } + + #[test] + fn test_sats_to_bin() { + // 1 BTC = 100M sats, log10(100M) = 8.0, frac = 0.0 → bin 0 + let bin = PhaseHistogramV2::sats_to_bin(Sats::_1BTC).unwrap(); + assert_eq!(bin, 0); + + // 10M sats, log10(10M) = 7.0, frac = 0.0 → bin 0 + let bin = PhaseHistogramV2::sats_to_bin(Sats::_10M).unwrap(); + assert_eq!(bin, 0); + + // 5M sats, log10(5M) ≈ 6.699, frac ≈ 0.699 → bin ~140 + let bin = PhaseHistogramV2::sats_to_bin(Sats::from(5_000_000u64)).unwrap(); + assert!((138..=142).contains(&bin), "5M sats bin = {}", bin); + } + + #[test] + fn test_phase_range_from_anchor() { + // $6000-$8000 range + let (p_min, p_max) = phase_range_from_anchor(6000.0, 8000.0, 0.05); + + // $6000 → log10 = 3.778, phase = 0.778 + // $8000 → log10 = 3.903, phase = 0.903 + assert!(p_min > 0.7 && p_min < 0.8, "p_min = {}", p_min); + assert!(p_max > 0.85 && p_max < 0.95, "p_max = {}", p_max); + } + + #[test] + fn test_phase_to_price() { + // Phase 0.0 with anchor $50-150 should give ~$100 + let price = phase_to_price(0.0, 50.0, 150.0); + assert!(price > 80.0 && price < 120.0, "price = {}", price); + + // Phase 0.70 with anchor $4000-6000 should give ~$5000 + let price = phase_to_price(0.70, 4000.0, 6000.0); + assert!(price > 4000.0 && price < 6000.0, "price = {}", price); + } +} diff --git a/crates/brk_computer/src/price/oracle/vecs.rs b/crates/brk_computer/src/price/oracle/vecs.rs index 655fee85c..7be99fb4c 100644 --- a/crates/brk_computer/src/price/oracle/vecs.rs +++ b/crates/brk_computer/src/price/oracle/vecs.rs @@ -1,7 +1,7 @@ use brk_traversable::Traversable; use brk_types::{ - Cents, DateIndex, Dollars, Height, OHLCCents, OHLCDollars, OracleBins, PairOutputIndex, Sats, - StoredU32, TxIndex, + Cents, DateIndex, Dollars, Height, OHLCCents, OHLCDollars, OracleBins, OracleBinsV2, + PairOutputIndex, Sats, StoredU32, TxIndex, }; use vecdb::{BytesVec, LazyVecFrom1, PcoVec}; @@ -55,4 +55,49 @@ pub struct Vecs { /// Number of qualifying transactions per day (for confidence) pub tx_count: PcoVec, + + // ========== Phase Oracle V2 (round USD template matching) ========== + /// Per-block 200-bin phase histogram + pub phase_v2_histogram: BytesVec, + + /// Per-block price in cents from phase oracle V2 (cross-correlation with round USD template) + pub phase_v2_price_cents: PcoVec, + + /// Per-block price in cents using direct peak finding (like V1) + pub phase_v2_peak_price_cents: PcoVec, + + /// Daily distribution (min, max, average, percentiles) from phase oracle V2 + pub phase_v2_daily_cents: Distribution, + + /// Daily distribution in dollars (lazy conversion from cents) + pub phase_v2_daily_dollars: LazyTransformDistribution, + + /// Daily distribution from peak-based prices + pub phase_v2_peak_daily_cents: Distribution, + + /// Daily distribution in dollars (lazy conversion from cents) + pub phase_v2_peak_daily_dollars: LazyTransformDistribution, + + // ========== Phase Oracle V3 (BASE + uniqueVal filter) ========== + /// Per-block 200-bin phase histogram with uniqueVal filtering + /// Only includes outputs with unique values within their transaction + pub phase_v3_histogram: BytesVec, + + /// Per-block price in cents from phase oracle V3 (cross-correlation) + pub phase_v3_price_cents: PcoVec, + + /// Per-block price in cents using direct peak finding (like V1) + pub phase_v3_peak_price_cents: PcoVec, + + /// Daily distribution from phase oracle V3 + pub phase_v3_daily_cents: Distribution, + + /// Daily distribution in dollars (lazy conversion from cents) + pub phase_v3_daily_dollars: LazyTransformDistribution, + + /// Daily distribution from peak-based prices + pub phase_v3_peak_daily_cents: Distribution, + + /// Daily distribution in dollars (lazy conversion from cents) + pub phase_v3_peak_daily_dollars: LazyTransformDistribution, } diff --git a/crates/brk_server/README.md b/crates/brk_server/README.md index ec00ea6c4..3463d44f6 100644 --- a/crates/brk_server/README.md +++ b/crates/brk_server/README.md @@ -4,8 +4,8 @@ HTTP API server for Bitcoin on-chain analytics. ## Features -- **OpenAPI spec**: Auto-generated docs at `/api` with full spec at `/api.json` -- **LLM-optimized**: Compact spec at `/api.trimmed.json` for AI tools +- **OpenAPI spec**: Auto-generated docs at `/api` with full spec at `/openapi.json` +- **LLM-optimized**: Compact spec at `/api.json` for AI tools - **Response caching**: ETag-based with LRU cache (5000 entries) - **Compression**: Brotli, gzip, deflate, zstd - **Static files**: Optional web interface hosting @@ -23,8 +23,8 @@ server.serve().await?; | Path | Description | |------|-------------| | `/api` | Interactive API documentation | -| `/api.json` | Full OpenAPI specification | -| `/api.trimmed.json` | Compact OpenAPI for LLMs | +| `/openapi.json` | Full OpenAPI specification | +| `/api.json` | Compact OpenAPI for LLMs | | `/api/address/{address}` | Address stats, transactions, UTXOs | | `/api/block/{hash}` | Block info, transactions, status | | `/api/block-height/{height}` | Block by height | diff --git a/crates/brk_server/build.rs b/crates/brk_server/build.rs index 3edd508b1..889caf7f0 100644 --- a/crates/brk_server/build.rs +++ b/crates/brk_server/build.rs @@ -5,19 +5,35 @@ fn main() { // Generate importmap for website (updates index.html in place) let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); - let website_path = Path::new(&manifest_dir).join("../../website"); + // Use ./website (symlink in repo, real dir in published crate) + let website_path = Path::new(&manifest_dir).join("website"); - println!("cargo:rerun-if-changed=../../website"); + println!("cargo:rerun-if-changed=website"); + println!("cargo::warning=build.rs: website_path={website_path:?}, exists={}", website_path.exists()); if website_path.exists() { // Skip importmap hashing in dev mode (files change often) let map = if is_dev { + println!("cargo::warning=build.rs: dev mode, skipping importmap"); importmap::ImportMap::empty() } else { - importmap::ImportMap::scan(&website_path, "") - .unwrap_or_else(|_| importmap::ImportMap::empty()) + match importmap::ImportMap::scan(&website_path, "") { + Ok(map) => { + println!("cargo::warning=build.rs: importmap scanned {} entries", map.imports.len()); + map + } + Err(e) => { + println!("cargo::warning=build.rs: importmap scan failed: {e}"); + importmap::ImportMap::empty() + } + } }; - let _ = map.update_html_file(&website_path.join("index.html")); + let index_path = website_path.join("index.html"); + if let Err(e) = map.update_html_file(&index_path) { + println!("cargo::warning=build.rs: failed to update index.html: {e}"); + } + } else { + println!("cargo::warning=build.rs: website path does not exist!"); } } diff --git a/crates/brk_server/src/api/metrics/bulk.rs b/crates/brk_server/src/api/metrics/bulk.rs index 84241fe13..8744cafef 100644 --- a/crates/brk_server/src/api/metrics/bulk.rs +++ b/crates/brk_server/src/api/metrics/bulk.rs @@ -6,11 +6,11 @@ use axum::{ http::{HeaderMap, StatusCode, Uri}, response::{IntoResponse, Response}, }; -use brk_error::Result; use brk_types::{Format, MetricSelection, Output}; use quick_cache::sync::GuardResult; use crate::{ + Result, api::metrics::{CACHE_CONTROL, MAX_WEIGHT}, extended::HeaderMapExtended, }; @@ -18,22 +18,10 @@ use crate::{ use super::AppState; pub async fn handler( - uri: Uri, - headers: HeaderMap, - query: Query, - State(state): State, -) -> Response { - match req_to_response_res(uri, headers, query, state).await { - Ok(response) => response, - Err(error) => (StatusCode::INTERNAL_SERVER_ERROR, error.to_string()).into_response(), - } -} - -async fn req_to_response_res( uri: Uri, headers: HeaderMap, Query(params): Query, - AppState { query, cache, .. }: AppState, + State(AppState { query, cache, .. }): State, ) -> Result { // Phase 1: Search and resolve metadata (cheap) let resolved = query.run(move |q| q.resolve(params, MAX_WEIGHT)).await?; diff --git a/crates/brk_server/src/api/metrics/data.rs b/crates/brk_server/src/api/metrics/data.rs index 236055b94..d6ca06795 100644 --- a/crates/brk_server/src/api/metrics/data.rs +++ b/crates/brk_server/src/api/metrics/data.rs @@ -6,11 +6,11 @@ use axum::{ http::{HeaderMap, StatusCode, Uri}, response::{IntoResponse, Response}, }; -use brk_error::Result; use brk_types::{Format, MetricSelection, Output}; use quick_cache::sync::GuardResult; use crate::{ + Result, api::metrics::{CACHE_CONTROL, MAX_WEIGHT}, extended::HeaderMapExtended, }; @@ -18,22 +18,10 @@ use crate::{ use super::AppState; pub async fn handler( - uri: Uri, - headers: HeaderMap, - query: Query, - State(state): State, -) -> Response { - match req_to_response_res(uri, headers, query, state).await { - Ok(response) => response, - Err(error) => (StatusCode::INTERNAL_SERVER_ERROR, error.to_string()).into_response(), - } -} - -async fn req_to_response_res( uri: Uri, headers: HeaderMap, Query(params): Query, - AppState { query, cache, .. }: AppState, + State(AppState { query, cache, .. }): State, ) -> Result { // Phase 1: Search and resolve metadata (cheap) let resolved = query.run(move |q| q.resolve(params, MAX_WEIGHT)).await?; diff --git a/crates/brk_server/src/api/metrics/legacy.rs b/crates/brk_server/src/api/metrics/legacy.rs index 643b7ed4d..bffd1fbef 100644 --- a/crates/brk_server/src/api/metrics/legacy.rs +++ b/crates/brk_server/src/api/metrics/legacy.rs @@ -6,11 +6,11 @@ use axum::{ http::{HeaderMap, StatusCode, Uri}, response::{IntoResponse, Response}, }; -use brk_error::Result; use brk_types::{Format, MetricSelection, OutputLegacy}; use quick_cache::sync::GuardResult; use crate::{ + Result, api::metrics::{CACHE_CONTROL, MAX_WEIGHT}, extended::HeaderMapExtended, }; @@ -18,22 +18,10 @@ use crate::{ use super::AppState; pub async fn handler( - uri: Uri, - headers: HeaderMap, - query: Query, - State(state): State, -) -> Response { - match req_to_response_res(uri, headers, query, state).await { - Ok(response) => response, - Err(error) => (StatusCode::INTERNAL_SERVER_ERROR, error.to_string()).into_response(), - } -} - -async fn req_to_response_res( uri: Uri, headers: HeaderMap, Query(params): Query, - AppState { query, cache, .. }: AppState, + State(AppState { query, cache, .. }): State, ) -> Result { // Phase 1: Search and resolve metadata (cheap) let resolved = query.run(move |q| q.resolve(params, MAX_WEIGHT)).await?; diff --git a/crates/brk_server/src/api/metrics/mod.rs b/crates/brk_server/src/api/metrics/mod.rs index ff27c7c2a..dfb714d55 100644 --- a/crates/brk_server/src/api/metrics/mod.rs +++ b/crates/brk_server/src/api/metrics/mod.rs @@ -170,6 +170,7 @@ impl ApiMetricsRoutes for ApiRouter { state, ) .await + .into_response() }, |op| op .id("get_metric") @@ -188,7 +189,9 @@ impl ApiMetricsRoutes for ApiRouter { .api_route( "/api/metrics/bulk", get_with( - bulk::handler, + |uri, headers, query, state| async move { + bulk::handler(uri, headers, query, state).await.into_response() + }, |op| op .id("get_metrics") .metrics_tag() @@ -225,7 +228,9 @@ impl ApiMetricsRoutes for ApiRouter { Metrics::from(split.collect::>().join(separator)), range, )); - legacy::handler(uri, headers, Query(params), state).await + legacy::handler(uri, headers, Query(params), state) + .await + .into_response() }, |op| op .metrics_tag() @@ -250,7 +255,9 @@ impl ApiMetricsRoutes for ApiRouter { state: State| -> Response { let params: MetricSelection = params.into(); - legacy::handler(uri, headers, Query(params), state).await + legacy::handler(uri, headers, Query(params), state) + .await + .into_response() }, |op| op .metrics_tag() diff --git a/crates/brk_server/src/api/mod.rs b/crates/brk_server/src/api/mod.rs index a6bd4c31a..2c793e030 100644 --- a/crates/brk_server/src/api/mod.rs +++ b/crates/brk_server/src/api/mod.rs @@ -48,7 +48,7 @@ impl ApiRoutes for ApiRouter { .add_server_routes() .route("/api/server", get(Redirect::temporary("/api#tag/server"))) .api_route( - "/api.json", + "/openapi.json", get_with( async |headers: HeaderMap, Extension(api): Extension>| @@ -62,7 +62,7 @@ impl ApiRoutes for ApiRouter { ), ) .api_route( - "/api.trimmed.json", + "/api.json", get_with( async |headers: HeaderMap, Extension(api_trimmed): Extension>| @@ -72,12 +72,13 @@ impl ApiRoutes for ApiRouter { Response::static_json(&headers, &value) }, |op| { - op.id("get_openapi_trimmed") + op.id("get_api") .server_tag() - .summary("Trimmed OpenAPI specification") + .summary("Compact OpenAPI specification") .description( "Compact OpenAPI specification optimized for LLM consumption. \ - Removes redundant fields while preserving essential API information.", + Removes redundant fields while preserving essential API information. \ + Full spec available at `/openapi.json`.", ) .ok_response::() }, diff --git a/crates/brk_server/src/api/openapi/mod.rs b/crates/brk_server/src/api/openapi/mod.rs index 6c368de2c..315122ad2 100644 --- a/crates/brk_server/src/api/openapi/mod.rs +++ b/crates/brk_server/src/api/openapi/mod.rs @@ -29,7 +29,7 @@ pub fn create_openapi() -> OpenApi { - **Metrics**: Thousands of time-series metrics across multiple indexes (date, block height, etc.) - **[Mempool.space](https://mempool.space/docs/api/rest) compatible** (WIP): Most non-metrics endpoints follow the mempool.space API format - **Multiple formats**: JSON and CSV output -- **LLM-optimized**: Compact OpenAPI spec at [`/api.trimmed.json`](/api.trimmed.json) for AI tools +- **LLM-optimized**: Compact OpenAPI spec at [`/api.json`](/api.json) for AI tools (full spec at [`/openapi.json`](/openapi.json)) ### Client Libraries diff --git a/crates/brk_server/src/api/scalar.html b/crates/brk_server/src/api/scalar.html index ae66caf68..2c9fb15e5 100644 --- a/crates/brk_server/src/api/scalar.html +++ b/crates/brk_server/src/api/scalar.html @@ -18,7 +18,7 @@