From 967d2c7f35def198ca45c9d69b42db3ed9181e4d Mon Sep 17 00:00:00 2001 From: nym21 Date: Thu, 15 Jan 2026 23:34:43 +0100 Subject: [PATCH] global: snapshot --- Cargo.lock | 197 +- Cargo.toml | 1 - crates/brk/Cargo.toml | 3 - crates/brk/README.md | 1 - crates/brk/src/lib.rs | 4 - crates/brk_bindgen/Cargo.toml | 1 + crates/brk_bindgen/README.md | 2 +- crates/brk_cli/README.md | 2 +- crates/brk_cli/src/main.rs | 15 +- crates/brk_client/src/lib.rs | 483 +- .../single/lazy_transform/distribution.rs | 31 +- .../single/transform/cents_to_dollars.rs | 11 + .../src/internal/single/transform/mod.rs | 2 + .../brk_computer/src/price/oracle/compute.rs | 702 +- .../brk_computer/src/price/oracle/import.rs | 38 +- crates/brk_computer/src/price/oracle/vecs.rs | 49 +- crates/brk_logger/README.md | 2 +- crates/brk_logger/src/lib.rs | 12 +- crates/brk_mcp/Cargo.toml | 23 - crates/brk_mcp/README.md | 34 - crates/brk_mcp/src/lib.rs | 110 - crates/brk_mcp/src/route.rs | 26 - crates/brk_query/README.md | 9 +- crates/brk_query/src/impl/metrics.rs | 198 +- crates/brk_query/src/impl/metrics_legacy.rs | 92 +- crates/brk_query/src/lib.rs | 9 +- crates/brk_query/src/resolved.rs | 23 + crates/brk_server/Cargo.toml | 1 - crates/brk_server/README.md | 55 +- crates/brk_server/examples/server.rs | 2 +- crates/brk_server/src/api/blk_reader.rs | 59 - crates/brk_server/src/api/blocks/mod.rs | 10 +- crates/brk_server/src/api/metrics/bulk.rs | 48 +- crates/brk_server/src/api/metrics/data.rs | 48 +- crates/brk_server/src/api/metrics/legacy.rs | 45 +- crates/brk_server/src/api/metrics/mod.rs | 12 +- crates/brk_server/src/api/mining/mod.rs | 20 +- crates/brk_server/src/api/mod.rs | 43 +- .../src/api/{openapi.rs => openapi/mod.rs} | 26 +- crates/brk_server/src/api/openapi/trim.rs | 447 + crates/brk_server/src/cache.rs | 73 +- crates/brk_server/src/extended/response.rs | 16 +- crates/brk_server/src/lib.rs | 13 +- crates/brk_types/src/cents.rs | 8 +- crates/brk_types/src/datarange.rs | 5 - crates/brk_types/src/etag.rs | 59 + crates/brk_types/src/index.rs | 16 +- crates/brk_types/src/lib.rs | 10 + crates/brk_types/src/metricdata.rs | 17 +- crates/brk_types/src/metricoutput.rs | 33 + crates/brk_types/src/metrics.rs | 3 +- crates/brk_types/src/oracle_bins.rs | 150 + crates/{brk_query => brk_types}/src/output.rs | 8 +- crates/brk_types/src/pairoutputindex.rs | 121 + crates/brk_types/src/sats.rs | 34 + docker/.env.example | 3 - docker/README.md | 2 - docker/docker-compose.yml | 1 - docs/README.md | 2 +- docs/TODO.md | 1 - modules/brk-client/index.js | 555 +- .../5.1.0/dist/typings.d.ts | 7667 +++++++++-------- modules/tsconfig.json | 15 + packages/brk_client/brk_client/__init__.py | 335 +- scripts/rust-publish.sh | 1 - website/scripts/options/market/index.js | 12 + website/scripts/resources.js | 8 +- 67 files changed, 6854 insertions(+), 5210 deletions(-) create mode 100644 crates/brk_computer/src/internal/single/transform/cents_to_dollars.rs delete mode 100644 crates/brk_mcp/Cargo.toml delete mode 100644 crates/brk_mcp/README.md delete mode 100644 crates/brk_mcp/src/lib.rs delete mode 100644 crates/brk_mcp/src/route.rs create mode 100644 crates/brk_query/src/resolved.rs delete mode 100644 crates/brk_server/src/api/blk_reader.rs rename crates/brk_server/src/api/{openapi.rs => openapi/mod.rs} (94%) create mode 100644 crates/brk_server/src/api/openapi/trim.rs create mode 100644 crates/brk_types/src/etag.rs create mode 100644 crates/brk_types/src/metricoutput.rs create mode 100644 crates/brk_types/src/oracle_bins.rs rename crates/{brk_query => brk_types}/src/output.rs (90%) create mode 100644 crates/brk_types/src/pairoutputindex.rs create mode 100644 modules/tsconfig.json diff --git a/Cargo.lock b/Cargo.lock index 36b68ef29..b197207e1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -247,12 +247,6 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - [[package]] name = "bech32" version = "0.11.1" @@ -396,7 +390,6 @@ dependencies = [ "brk_indexer", "brk_iterator", "brk_logger", - "brk_mcp", "brk_mempool", "brk_query", "brk_reader", @@ -442,6 +435,7 @@ dependencies = [ "oas3", "serde", "serde_json", + "tracing", ] [[package]] @@ -597,19 +591,6 @@ dependencies = [ "tracing-subscriber", ] -[[package]] -name = "brk_mcp" -version = "0.1.0-alpha.3" -dependencies = [ - "axum", - "brk_rmcp", - "minreq", - "schemars", - "serde", - "serde_json", - "tracing", -] - [[package]] name = "brk_mempool" version = "0.1.0-alpha.3" @@ -660,49 +641,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "brk_rmcp" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe0eee22d5688b9b4bbd0071a8d8b540243a8df014eeff4bfe6f90e774e83755" -dependencies = [ - "base64 0.22.1", - "brk_rmcp-macros", - "bytes", - "chrono", - "futures", - "http", - "http-body", - "http-body-util", - "paste", - "pin-project-lite", - "rand 0.9.2", - "schemars", - "serde", - "serde_json", - "sse-stream", - "thiserror 2.0.17", - "tokio", - "tokio-stream", - "tokio-util", - "tower-service", - "tracing", - "uuid", -] - -[[package]] -name = "brk_rmcp-macros" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aac2141d6358651fcdcabd43fa4dae4525cbba376bc9303a821cb170c2909f4b" -dependencies = [ - "darling", - "proc-macro2", - "quote", - "serde_json", - "syn", -] - [[package]] name = "brk_rpc" version = "0.1.0-alpha.3" @@ -728,7 +666,6 @@ dependencies = [ "brk_fetcher", "brk_indexer", "brk_logger", - "brk_mcp", "brk_mempool", "brk_query", "brk_reader", @@ -901,7 +838,6 @@ dependencies = [ "iana-time-zone", "js-sys", "num-traits", - "serde", "wasm-bindgen", "windows-link", ] @@ -1181,41 +1117,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b365fabc795046672053e29c954733ec3b05e4be654ab130fe8f1f94d7051f35" -[[package]] -name = "darling" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" -dependencies = [ - "darling_core", - "darling_macro", -] - -[[package]] -name = "darling_core" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn", -] - -[[package]] -name = "darling_macro" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" -dependencies = [ - "darling_core", - "quote", - "syn", -] - [[package]] name = "dashmap" version = "6.1.0" @@ -1446,12 +1347,6 @@ dependencies = [ "spin", ] -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - [[package]] name = "font-kit" version = "0.14.3" @@ -1889,12 +1784,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - [[package]] name = "idna" version = "1.1.0" @@ -2082,7 +1971,7 @@ version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3662a38d341d77efecb73caf01420cfa5aa63c0253fd7bc05289ef9f6616e1bf" dependencies = [ - "base64 0.13.1", + "base64", "minreq", "serde", "serde_json", @@ -2450,12 +2339,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - [[package]] name = "pathfinder_geometry" version = "0.5.1" @@ -2670,18 +2553,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - -[[package]] -name = "rand" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" -dependencies = [ - "rand_chacha 0.9.0", - "rand_core 0.9.5", + "rand_chacha", + "rand_core", ] [[package]] @@ -2691,17 +2564,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core 0.9.5", + "rand_core", ] [[package]] @@ -2713,22 +2576,13 @@ dependencies = [ "getrandom 0.2.17", ] -[[package]] -name = "rand_core" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" -dependencies = [ - "getrandom 0.3.4", -] - [[package]] name = "rand_xoshiro" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa" dependencies = [ - "rand_core 0.6.4", + "rand_core", ] [[package]] @@ -2950,7 +2804,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54e910108742c57a770f492731f99be216a52fadd361b06c8fb59d74ccc267d2" dependencies = [ - "chrono", "dyn-clone", "indexmap", "ref-cast", @@ -2994,7 +2847,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113" dependencies = [ "bitcoin_hashes", - "rand 0.8.5", + "rand", "secp256k1-sys", "serde", ] @@ -3193,19 +3046,6 @@ dependencies = [ "lock_api", ] -[[package]] -name = "sse-stream" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb4dc4d33c68ec1f27d386b5610a351922656e1fdf5c05bbaad930cd1519479a" -dependencies = [ - "bytes", - "futures-util", - "http-body", - "http-body-util", - "pin-project-lite", -] - [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -3376,7 +3216,6 @@ version = "1.49.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" dependencies = [ - "bytes", "libc", "mio", "pin-project-lite", @@ -3396,17 +3235,6 @@ dependencies = [ "syn", ] -[[package]] -name = "tokio-stream" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" -dependencies = [ - "futures-core", - "pin-project-lite", - "tokio", -] - [[package]] name = "tokio-util" version = "0.7.18" @@ -3661,17 +3489,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" -[[package]] -name = "uuid" -version = "1.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" -dependencies = [ - "getrandom 0.3.4", - "js-sys", - "wasm-bindgen", -] - [[package]] name = "valuable" version = "0.1.1" diff --git a/Cargo.toml b/Cargo.toml index 1a7e404ba..b381b8fa4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,7 +52,6 @@ brk_fetcher = { version = "0.1.0-alpha.3", path = "crates/brk_fetcher" } brk_indexer = { version = "0.1.0-alpha.3", path = "crates/brk_indexer" } brk_iterator = { version = "0.1.0-alpha.3", path = "crates/brk_iterator" } brk_logger = { version = "0.1.0-alpha.3", path = "crates/brk_logger" } -brk_mcp = { version = "0.1.0-alpha.3", path = "crates/brk_mcp" } brk_mempool = { version = "0.1.0-alpha.3", path = "crates/brk_mempool" } brk_query = { version = "0.1.0-alpha.3", path = "crates/brk_query", features = ["tokio"] } brk_reader = { version = "0.1.0-alpha.3", path = "crates/brk_reader" } diff --git a/crates/brk/Cargo.toml b/crates/brk/Cargo.toml index 3569c0224..fb7667f7a 100644 --- a/crates/brk/Cargo.toml +++ b/crates/brk/Cargo.toml @@ -19,7 +19,6 @@ full = [ "indexer", "iterator", "logger", - "mcp", "mempool", "query", "reader", @@ -39,7 +38,6 @@ cohort = ["brk_cohort"] indexer = ["brk_indexer"] iterator = ["brk_iterator"] logger = ["brk_logger"] -mcp = ["brk_mcp"] mempool = ["brk_mempool"] query = ["brk_query"] reader = ["brk_reader"] @@ -60,7 +58,6 @@ brk_cohort = { workspace = true, optional = true } brk_indexer = { workspace = true, optional = true } brk_iterator = { workspace = true, optional = true } brk_logger = { workspace = true, optional = true } -brk_mcp = { workspace = true, optional = true } brk_mempool = { workspace = true, optional = true } brk_query = { workspace = true, optional = true } brk_reader = { workspace = true, optional = true } diff --git a/crates/brk/README.md b/crates/brk/README.md index 0818e68de..a3ea54cdb 100644 --- a/crates/brk/README.md +++ b/crates/brk/README.md @@ -51,7 +51,6 @@ Feature flags match crate names without the `brk_` prefix. Use `full` to enable |-------|-------------| | [brk_client](https://docs.rs/brk_client) | Generated Rust API client | | [brk_bindgen](https://docs.rs/brk_bindgen) | Generate typed clients (Rust, JavaScript, Python) | -| [brk_mcp](https://docs.rs/brk_mcp) | Model Context Protocol server for LLM integration | **Internal** diff --git a/crates/brk/src/lib.rs b/crates/brk/src/lib.rs index f212b329e..7c8b8248c 100644 --- a/crates/brk/src/lib.rs +++ b/crates/brk/src/lib.rs @@ -40,10 +40,6 @@ pub use brk_iterator as iterator; #[doc(inline)] pub use brk_logger as logger; -#[cfg(feature = "mcp")] -#[doc(inline)] -pub use brk_mcp as mcp; - #[cfg(feature = "mempool")] #[doc(inline)] pub use brk_mempool as mempool; diff --git a/crates/brk_bindgen/Cargo.toml b/crates/brk_bindgen/Cargo.toml index dec77bf70..544472f62 100644 --- a/crates/brk_bindgen/Cargo.toml +++ b/crates/brk_bindgen/Cargo.toml @@ -14,3 +14,4 @@ brk_types = { workspace = true } oas3 = "0.20" serde = { workspace = true } serde_json = { workspace = true } +tracing = { workspace = true } diff --git a/crates/brk_bindgen/README.md b/crates/brk_bindgen/README.md index 23b11c115..26bd6315a 100644 --- a/crates/brk_bindgen/README.md +++ b/crates/brk_bindgen/README.md @@ -4,7 +4,7 @@ Code generation for BRK client libraries. ## What It Enables -Generate typed client libraries for Rust, JavaScript/TypeScript, and Python from the OpenAPI specification. Keeps frontend code in sync with available metrics and API endpoints without manual maintenance. +Generate typed client libraries for Rust, JavaScript, and Python from the OpenAPI specification. Keeps frontend code in sync with available metrics and API endpoints without manual maintenance. ## Key Features diff --git a/crates/brk_cli/README.md b/crates/brk_cli/README.md index 67a76cbfc..45a787930 100644 --- a/crates/brk_cli/README.md +++ b/crates/brk_cli/README.md @@ -75,7 +75,7 @@ brk --help 1. **Indexer**: Processes blocks into queryable indexes 2. **Computer**: Derives 1000+ on-chain metrics 3. **Mempool**: Real-time fee estimation -4. **Server**: REST API + MCP endpoint +4. **Server**: REST API with OpenAPI docs 5. **Bundler**: JS bundling for web interface (if enabled) ## Performance diff --git a/crates/brk_cli/src/main.rs b/crates/brk_cli/src/main.rs index babe81892..4f7a4e859 100644 --- a/crates/brk_cli/src/main.rs +++ b/crates/brk_cli/src/main.rs @@ -54,19 +54,6 @@ pub fn run() -> color_eyre::Result<()> { let mut indexer = Indexer::forced_import(&config.brkdir())?; - #[cfg(not(debug_assertions))] - { - // Pre-run indexer if too far behind, then drop and reimport to reduce memory - let chain_height = client.get_last_height()?; - let indexed_height = indexer.vecs.starting_height(); - if chain_height.saturating_sub(*indexed_height) > 1000 { - indexer.index(&blocks, &client, &exit)?; - drop(indexer); - Mimalloc::collect(); - indexer = Indexer::forced_import(&config.brkdir())?; - } - } - let mut computer = Computer::forced_import(&config.brkdir(), &indexer, config.fetcher())?; let mempool = Mempool::new(&client); @@ -96,7 +83,7 @@ pub fn run() -> color_eyre::Result<()> { let server = Server::new(&query, data_path, website_source); tokio::spawn(async move { - server.serve(true).await.unwrap(); + server.serve().await.unwrap(); }); Ok(()) as Result<()> diff --git a/crates/brk_client/src/lib.rs b/crates/brk_client/src/lib.rs index c58efb408..30511d28d 100644 --- a/crates/brk_client/src/lib.rs +++ b/crates/brk_client/src/lib.rs @@ -370,6 +370,7 @@ const _I29: &[Index] = &[Index::WeekIndex]; const _I30: &[Index] = &[Index::YearIndex]; const _I31: &[Index] = &[Index::LoadedAddressIndex]; const _I32: &[Index] = &[Index::EmptyAddressIndex]; +const _I33: &[Index] = &[Index::PairOutputIndex]; #[inline] fn _ep(c: &Arc, n: &Arc, i: Index) -> MetricEndpointBuilder { @@ -855,6 +856,20 @@ impl MetricPattern32 { impl AnyMetricPattern for MetricPattern32 { fn name(&self) -> &str { &self.name } fn indexes(&self) -> &'static [Index] { _I32 } } impl MetricPattern for MetricPattern32 { fn get(&self, index: Index) -> Option> { _I32.contains(&index).then(|| _ep(&self.by.client, &self.by.name, index)) } } +pub struct MetricPattern33By { client: Arc, name: Arc, _marker: std::marker::PhantomData } +impl MetricPattern33By { + pub fn pairoutputindex(&self) -> MetricEndpointBuilder { _ep(&self.client, &self.name, Index::PairOutputIndex) } +} + +pub struct MetricPattern33 { name: Arc, pub by: MetricPattern33By } +impl MetricPattern33 { + pub fn new(client: Arc, name: String) -> Self { let name: Arc = name.into(); Self { name: name.clone(), by: MetricPattern33By { client, name, _marker: std::marker::PhantomData } } } + pub fn name(&self) -> &str { &self.name } +} + +impl AnyMetricPattern for MetricPattern33 { fn name(&self) -> &str { &self.name } fn indexes(&self) -> &'static [Index] { _I33 } } +impl MetricPattern for MetricPattern33 { fn get(&self, index: Index) -> Option> { _I33.contains(&index).then(|| _ep(&self.by.client, &self.by.name, index)) } } + // Reusable pattern structs /// Pattern struct for repeated tree structure. @@ -1253,56 +1268,6 @@ impl Price111dSmaPattern { } } -/// Pattern struct for repeated tree structure. -pub struct ActivePriceRatioPattern { - pub ratio: MetricPattern4, - pub ratio_1m_sma: MetricPattern4, - pub ratio_1w_sma: MetricPattern4, - pub ratio_1y_sd: Ratio1ySdPattern, - pub ratio_2y_sd: Ratio1ySdPattern, - pub ratio_4y_sd: Ratio1ySdPattern, - pub ratio_pct1: MetricPattern4, - pub ratio_pct1_usd: MetricPattern4, - pub ratio_pct2: MetricPattern4, - pub ratio_pct2_usd: MetricPattern4, - pub ratio_pct5: MetricPattern4, - pub ratio_pct5_usd: MetricPattern4, - pub ratio_pct95: MetricPattern4, - pub ratio_pct95_usd: MetricPattern4, - pub ratio_pct98: MetricPattern4, - pub ratio_pct98_usd: MetricPattern4, - pub ratio_pct99: MetricPattern4, - pub ratio_pct99_usd: MetricPattern4, - pub ratio_sd: Ratio1ySdPattern, -} - -impl ActivePriceRatioPattern { - /// 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()), - ratio_1m_sma: MetricPattern4::new(client.clone(), _m(&acc, "1m_sma")), - ratio_1w_sma: MetricPattern4::new(client.clone(), _m(&acc, "1w_sma")), - ratio_1y_sd: Ratio1ySdPattern::new(client.clone(), _m(&acc, "1y")), - ratio_2y_sd: Ratio1ySdPattern::new(client.clone(), _m(&acc, "2y")), - ratio_4y_sd: Ratio1ySdPattern::new(client.clone(), _m(&acc, "4y")), - ratio_pct1: MetricPattern4::new(client.clone(), _m(&acc, "pct1")), - ratio_pct1_usd: MetricPattern4::new(client.clone(), _m(&acc, "pct1_usd")), - ratio_pct2: MetricPattern4::new(client.clone(), _m(&acc, "pct2")), - ratio_pct2_usd: MetricPattern4::new(client.clone(), _m(&acc, "pct2_usd")), - ratio_pct5: MetricPattern4::new(client.clone(), _m(&acc, "pct5")), - ratio_pct5_usd: MetricPattern4::new(client.clone(), _m(&acc, "pct5_usd")), - ratio_pct95: MetricPattern4::new(client.clone(), _m(&acc, "pct95")), - ratio_pct95_usd: MetricPattern4::new(client.clone(), _m(&acc, "pct95_usd")), - ratio_pct98: MetricPattern4::new(client.clone(), _m(&acc, "pct98")), - ratio_pct98_usd: MetricPattern4::new(client.clone(), _m(&acc, "pct98_usd")), - ratio_pct99: MetricPattern4::new(client.clone(), _m(&acc, "pct99")), - ratio_pct99_usd: MetricPattern4::new(client.clone(), _m(&acc, "pct99_usd")), - ratio_sd: Ratio1ySdPattern::new(client.clone(), acc.clone()), - } - } -} - /// Pattern struct for repeated tree structure. pub struct PercentilesPattern { pub pct05: MetricPattern4, @@ -1353,6 +1318,56 @@ impl PercentilesPattern { } } +/// Pattern struct for repeated tree structure. +pub struct ActivePriceRatioPattern { + pub ratio: MetricPattern4, + pub ratio_1m_sma: MetricPattern4, + pub ratio_1w_sma: MetricPattern4, + pub ratio_1y_sd: Ratio1ySdPattern, + pub ratio_2y_sd: Ratio1ySdPattern, + pub ratio_4y_sd: Ratio1ySdPattern, + pub ratio_pct1: MetricPattern4, + pub ratio_pct1_usd: MetricPattern4, + pub ratio_pct2: MetricPattern4, + pub ratio_pct2_usd: MetricPattern4, + pub ratio_pct5: MetricPattern4, + pub ratio_pct5_usd: MetricPattern4, + pub ratio_pct95: MetricPattern4, + pub ratio_pct95_usd: MetricPattern4, + pub ratio_pct98: MetricPattern4, + pub ratio_pct98_usd: MetricPattern4, + pub ratio_pct99: MetricPattern4, + pub ratio_pct99_usd: MetricPattern4, + pub ratio_sd: Ratio1ySdPattern, +} + +impl ActivePriceRatioPattern { + /// 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()), + ratio_1m_sma: MetricPattern4::new(client.clone(), _m(&acc, "1m_sma")), + ratio_1w_sma: MetricPattern4::new(client.clone(), _m(&acc, "1w_sma")), + ratio_1y_sd: Ratio1ySdPattern::new(client.clone(), _m(&acc, "1y")), + ratio_2y_sd: Ratio1ySdPattern::new(client.clone(), _m(&acc, "2y")), + ratio_4y_sd: Ratio1ySdPattern::new(client.clone(), _m(&acc, "4y")), + ratio_pct1: MetricPattern4::new(client.clone(), _m(&acc, "pct1")), + ratio_pct1_usd: MetricPattern4::new(client.clone(), _m(&acc, "pct1_usd")), + ratio_pct2: MetricPattern4::new(client.clone(), _m(&acc, "pct2")), + ratio_pct2_usd: MetricPattern4::new(client.clone(), _m(&acc, "pct2_usd")), + ratio_pct5: MetricPattern4::new(client.clone(), _m(&acc, "pct5")), + ratio_pct5_usd: MetricPattern4::new(client.clone(), _m(&acc, "pct5_usd")), + ratio_pct95: MetricPattern4::new(client.clone(), _m(&acc, "pct95")), + ratio_pct95_usd: MetricPattern4::new(client.clone(), _m(&acc, "pct95_usd")), + ratio_pct98: MetricPattern4::new(client.clone(), _m(&acc, "pct98")), + ratio_pct98_usd: MetricPattern4::new(client.clone(), _m(&acc, "pct98_usd")), + ratio_pct99: MetricPattern4::new(client.clone(), _m(&acc, "pct99")), + ratio_pct99_usd: MetricPattern4::new(client.clone(), _m(&acc, "pct99_usd")), + ratio_sd: Ratio1ySdPattern::new(client.clone(), acc.clone()), + } + } +} + /// Pattern struct for repeated tree structure. pub struct RelativePattern5 { pub neg_unrealized_loss_rel_to_market_cap: MetricPattern1, @@ -1779,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, @@ -1839,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, @@ -1868,27 +1883,29 @@ impl _0satsPattern { } /// 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, +pub struct PhaseDailyCentsPattern { + pub average: MetricPattern6, + pub max: MetricPattern6, + pub median: MetricPattern6, + pub min: MetricPattern6, + pub pct10: MetricPattern6, + pub pct25: MetricPattern6, + pub pct75: MetricPattern6, + pub pct90: MetricPattern6, } -impl _100btcPattern { +impl PhaseDailyCentsPattern { /// 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()), + average: MetricPattern6::new(client.clone(), _m(&acc, "average")), + max: MetricPattern6::new(client.clone(), _m(&acc, "max")), + median: MetricPattern6::new(client.clone(), _m(&acc, "median")), + min: MetricPattern6::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")), } } } @@ -1919,32 +1936,6 @@ impl PeriodCagrPattern { } } -/// Pattern struct for repeated tree structure. -pub struct UnrealizedPattern { - pub neg_unrealized_loss: MetricPattern1, - pub net_unrealized_pnl: MetricPattern1, - pub supply_in_loss: ActiveSupplyPattern, - pub supply_in_profit: ActiveSupplyPattern, - pub total_unrealized_pnl: MetricPattern1, - pub unrealized_loss: MetricPattern1, - pub unrealized_profit: MetricPattern1, -} - -impl UnrealizedPattern { - /// Create a new pattern node with accumulated metric name. - pub fn new(client: Arc, acc: String) -> Self { - Self { - neg_unrealized_loss: MetricPattern1::new(client.clone(), _m(&acc, "neg_unrealized_loss")), - net_unrealized_pnl: MetricPattern1::new(client.clone(), _m(&acc, "net_unrealized_pnl")), - supply_in_loss: ActiveSupplyPattern::new(client.clone(), _m(&acc, "supply_in_loss")), - supply_in_profit: ActiveSupplyPattern::new(client.clone(), _m(&acc, "supply_in_profit")), - total_unrealized_pnl: MetricPattern1::new(client.clone(), _m(&acc, "total_unrealized_pnl")), - unrealized_loss: MetricPattern1::new(client.clone(), _m(&acc, "unrealized_loss")), - unrealized_profit: MetricPattern1::new(client.clone(), _m(&acc, "unrealized_profit")), - } - } -} - /// Pattern struct for repeated tree structure. pub struct _10yTo12yPattern { pub activity: ActivityPattern2, @@ -1971,6 +1962,32 @@ impl _10yTo12yPattern { } } +/// Pattern struct for repeated tree structure. +pub struct UnrealizedPattern { + pub neg_unrealized_loss: MetricPattern1, + pub net_unrealized_pnl: MetricPattern1, + pub supply_in_loss: ActiveSupplyPattern, + pub supply_in_profit: ActiveSupplyPattern, + pub total_unrealized_pnl: MetricPattern1, + pub unrealized_loss: MetricPattern1, + pub unrealized_profit: MetricPattern1, +} + +impl UnrealizedPattern { + /// Create a new pattern node with accumulated metric name. + pub fn new(client: Arc, acc: String) -> Self { + Self { + neg_unrealized_loss: MetricPattern1::new(client.clone(), _m(&acc, "neg_unrealized_loss")), + net_unrealized_pnl: MetricPattern1::new(client.clone(), _m(&acc, "net_unrealized_pnl")), + supply_in_loss: ActiveSupplyPattern::new(client.clone(), _m(&acc, "supply_in_loss")), + supply_in_profit: ActiveSupplyPattern::new(client.clone(), _m(&acc, "supply_in_profit")), + total_unrealized_pnl: MetricPattern1::new(client.clone(), _m(&acc, "total_unrealized_pnl")), + unrealized_loss: MetricPattern1::new(client.clone(), _m(&acc, "unrealized_loss")), + unrealized_profit: MetricPattern1::new(client.clone(), _m(&acc, "unrealized_profit")), + } + } +} + /// Pattern struct for repeated tree structure. pub struct _0satsPattern2 { pub activity: ActivityPattern2, @@ -1997,6 +2014,32 @@ impl _0satsPattern2 { } } +/// 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 _10yPattern { pub activity: ActivityPattern2, @@ -2066,19 +2109,19 @@ impl SplitPattern2 { } /// Pattern struct for repeated tree structure. -pub struct _2015Pattern { - pub bitcoin: MetricPattern4, - pub dollars: MetricPattern4, - pub sats: MetricPattern4, +pub struct CoinbasePattern2 { + pub bitcoin: BlockCountPattern, + pub dollars: BlockCountPattern, + pub sats: BlockCountPattern, } -impl _2015Pattern { +impl CoinbasePattern2 { /// 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()), + bitcoin: BlockCountPattern::new(client.clone(), _m(&acc, "btc")), + dollars: BlockCountPattern::new(client.clone(), _m(&acc, "usd")), + sats: BlockCountPattern::new(client.clone(), acc.clone()), } } } @@ -2101,24 +2144,6 @@ impl CoinbasePattern { } } -/// Pattern struct for repeated tree structure. -pub struct CoinbasePattern2 { - pub bitcoin: BlockCountPattern, - pub dollars: BlockCountPattern, - pub sats: BlockCountPattern, -} - -impl CoinbasePattern2 { - /// Create a new pattern node with accumulated metric name. - pub fn new(client: Arc, acc: String) -> Self { - Self { - bitcoin: BlockCountPattern::new(client.clone(), _m(&acc, "btc")), - dollars: BlockCountPattern::new(client.clone(), _m(&acc, "usd")), - sats: BlockCountPattern::new(client.clone(), acc.clone()), - } - } -} - /// Pattern struct for repeated tree structure. pub struct SegwitAdoptionPattern { pub base: MetricPattern11, @@ -2138,19 +2163,19 @@ impl SegwitAdoptionPattern { } /// 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()), } } } @@ -2173,6 +2198,24 @@ impl ActiveSupplyPattern { } } +/// 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 UnclaimedRewardsPattern { pub bitcoin: BitcoinPattern2, @@ -2191,6 +2234,22 @@ impl UnclaimedRewardsPattern { } } +/// Pattern struct for repeated tree structure. +pub struct RelativePattern4 { + pub supply_in_loss_rel_to_own_supply: MetricPattern1, + pub supply_in_profit_rel_to_own_supply: MetricPattern1, +} + +impl RelativePattern4 { + /// Create a new pattern node with accumulated metric name. + pub fn new(client: Arc, acc: String) -> Self { + Self { + supply_in_loss_rel_to_own_supply: MetricPattern1::new(client.clone(), _m(&acc, "loss_rel_to_own_supply")), + supply_in_profit_rel_to_own_supply: MetricPattern1::new(client.clone(), _m(&acc, "profit_rel_to_own_supply")), + } + } +} + /// Pattern struct for repeated tree structure. pub struct CostBasisPattern { pub max: MetricPattern1, @@ -2223,22 +2282,6 @@ impl SupplyPattern2 { } } -/// Pattern struct for repeated tree structure. -pub struct RelativePattern4 { - pub supply_in_loss_rel_to_own_supply: MetricPattern1, - pub supply_in_profit_rel_to_own_supply: MetricPattern1, -} - -impl RelativePattern4 { - /// Create a new pattern node with accumulated metric name. - pub fn new(client: Arc, acc: String) -> Self { - Self { - supply_in_loss_rel_to_own_supply: MetricPattern1::new(client.clone(), _m(&acc, "loss_rel_to_own_supply")), - supply_in_profit_rel_to_own_supply: MetricPattern1::new(client.clone(), _m(&acc, "profit_rel_to_own_supply")), - } - } -} - /// Pattern struct for repeated tree structure. pub struct _1dReturns1mSdPattern { pub sd: MetricPattern4, @@ -2255,6 +2298,22 @@ impl _1dReturns1mSdPattern { } } +/// Pattern struct for repeated tree structure. +pub struct SatsPattern { + pub ohlc: MetricPattern1, + pub split: SplitPattern2, +} + +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 BlockCountPattern { pub cumulative: MetricPattern1, @@ -2288,17 +2347,15 @@ impl BitcoinPattern2 { } /// Pattern struct for repeated tree structure. -pub struct SatsPattern { - pub ohlc: MetricPattern1, - pub split: SplitPattern2, +pub struct OutputsPattern { + pub utxo_count: MetricPattern1, } -impl SatsPattern { +impl OutputsPattern { /// 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_sats")), - split: SplitPattern2::new(client.clone(), _m(&acc, "sats")), + utxo_count: MetricPattern1::new(client.clone(), acc.clone()), } } } @@ -2317,20 +2374,6 @@ impl RealizedPriceExtraPattern { } } -/// Pattern struct for repeated tree structure. -pub struct OutputsPattern { - pub utxo_count: MetricPattern1, -} - -impl OutputsPattern { - /// Create a new pattern node with accumulated metric name. - pub fn new(client: Arc, acc: String) -> Self { - Self { - utxo_count: MetricPattern1::new(client.clone(), acc.clone()), - } - } -} - // Metrics tree /// Metrics tree node. @@ -4899,8 +4942,8 @@ impl MetricsTree_Positions { pub struct MetricsTree_Price { pub cents: MetricsTree_Price_Cents, pub oracle: MetricsTree_Price_Oracle, - pub sats: SatsPattern, - pub usd: MetricsTree_Price_Usd, + pub sats: MetricsTree_Price_Sats, + pub usd: SatsPattern, } impl MetricsTree_Price { @@ -4908,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: SatsPattern::new(client.clone(), "price".to_string()), - usd: MetricsTree_Price_Usd::new(client.clone(), format!("{base_path}_usd")), + sats: MetricsTree_Price_Sats::new(client.clone(), format!("{base_path}_sats")), + usd: SatsPattern::new(client.clone(), "price".to_string()), } } } @@ -4950,8 +4993,16 @@ impl MetricsTree_Price_Cents_Split { /// Metrics tree node. pub struct MetricsTree_Price_Oracle { + pub height_to_first_pairoutputindex: MetricPattern11, pub ohlc_cents: MetricPattern6, pub ohlc_dollars: MetricPattern6, + pub output0_value: MetricPattern33, + pub output1_value: MetricPattern33, + pub pairoutputindex_to_txindex: MetricPattern33, + pub phase_daily_cents: PhaseDailyCentsPattern, + pub phase_daily_dollars: PhaseDailyCentsPattern, + pub phase_histogram: MetricPattern11, + pub phase_price_cents: MetricPattern11, pub price_cents: MetricPattern11, pub tx_count: MetricPattern6, } @@ -4959,8 +5010,16 @@ pub struct MetricsTree_Price_Oracle { impl MetricsTree_Price_Oracle { pub fn new(client: Arc, base_path: String) -> Self { Self { + height_to_first_pairoutputindex: MetricPattern11::new(client.clone(), "height_to_first_pairoutputindex".to_string()), ohlc_cents: MetricPattern6::new(client.clone(), "oracle_ohlc_cents".to_string()), ohlc_dollars: MetricPattern6::new(client.clone(), "oracle_ohlc".to_string()), + output0_value: MetricPattern33::new(client.clone(), "pair_output0_value".to_string()), + output1_value: MetricPattern33::new(client.clone(), "pair_output1_value".to_string()), + pairoutputindex_to_txindex: MetricPattern33::new(client.clone(), "pairoutputindex_to_txindex".to_string()), + phase_daily_cents: PhaseDailyCentsPattern::new(client.clone(), "phase_daily".to_string()), + 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()), price_cents: MetricPattern11::new(client.clone(), "oracle_price_cents".to_string()), tx_count: MetricPattern6::new(client.clone(), "oracle_tx_count".to_string()), } @@ -4968,16 +5027,16 @@ impl MetricsTree_Price_Oracle { } /// Metrics tree node. -pub struct MetricsTree_Price_Usd { - pub ohlc: MetricPattern1, - pub split: SplitPattern2, +pub struct MetricsTree_Price_Sats { + pub ohlc: MetricPattern1, + pub split: SplitPattern2, } -impl MetricsTree_Price_Usd { +impl MetricsTree_Price_Sats { pub fn new(client: Arc, base_path: String) -> Self { Self { - ohlc: MetricPattern1::new(client.clone(), "price_ohlc".to_string()), - split: SplitPattern2::new(client.clone(), "price".to_string()), + ohlc: MetricPattern1::new(client.clone(), "price_ohlc_sats".to_string()), + split: SplitPattern2::new(client.clone(), "price_sats".to_string()), } } } @@ -5323,7 +5382,7 @@ pub struct BrkClient { impl BrkClient { /// Client version. - pub const VERSION: &'static str = "v0.1.0-alpha.2"; + pub const VERSION: &'static str = "v0.1.0-alpha.3"; /// Create a new client with the given base URL. pub fn new(base_url: impl Into) -> Self { @@ -5363,6 +5422,24 @@ impl BrkClient { ) } + /// OpenAPI specification + /// + /// Full OpenAPI 3.1 specification for this API. + /// + /// Endpoint: `GET /api.json` + pub fn get_openapi(&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). diff --git a/crates/brk_computer/src/internal/single/lazy_transform/distribution.rs b/crates/brk_computer/src/internal/single/lazy_transform/distribution.rs index cb5bfc111..7e0cf648d 100644 --- a/crates/brk_computer/src/internal/single/lazy_transform/distribution.rs +++ b/crates/brk_computer/src/internal/single/lazy_transform/distribution.rs @@ -7,7 +7,7 @@ use brk_types::Version; use schemars::JsonSchema; use vecdb::{LazyVecFrom1, UnaryTransform, VecIndex}; -use crate::internal::{ComputedVecValue, Full}; +use crate::internal::{ComputedVecValue, Distribution, Full}; use super::LazyPercentiles; @@ -61,4 +61,33 @@ where ), } } + + pub fn from_distribution>( + name: &str, + version: Version, + source: &Distribution, + ) -> Self { + Self { + average: LazyVecFrom1::transformed::( + &format!("{name}_average"), + version, + source.boxed_average(), + ), + min: LazyVecFrom1::transformed::( + &format!("{name}_min"), + version, + source.boxed_min(), + ), + max: LazyVecFrom1::transformed::( + &format!("{name}_max"), + version, + source.boxed_max(), + ), + percentiles: LazyPercentiles::from_percentiles::( + name, + version, + &source.percentiles, + ), + } + } } diff --git a/crates/brk_computer/src/internal/single/transform/cents_to_dollars.rs b/crates/brk_computer/src/internal/single/transform/cents_to_dollars.rs new file mode 100644 index 000000000..b041d5c4e --- /dev/null +++ b/crates/brk_computer/src/internal/single/transform/cents_to_dollars.rs @@ -0,0 +1,11 @@ +use brk_types::{Cents, Dollars}; +use vecdb::UnaryTransform; + +pub struct CentsToDollars; + +impl UnaryTransform for CentsToDollars { + #[inline(always)] + fn apply(cents: Cents) -> Dollars { + Dollars::from(cents) + } +} diff --git a/crates/brk_computer/src/internal/single/transform/mod.rs b/crates/brk_computer/src/internal/single/transform/mod.rs index 5d7ceccf6..5b63f73e0 100644 --- a/crates/brk_computer/src/internal/single/transform/mod.rs +++ b/crates/brk_computer/src/internal/single/transform/mod.rs @@ -1,3 +1,4 @@ +mod cents_to_dollars; mod close_price_times_ratio; mod close_price_times_sats; mod difference_f32; @@ -37,6 +38,7 @@ mod volatility_sqrt365; mod volatility_sqrt7; mod weight_to_fullness; +pub use cents_to_dollars::*; pub use close_price_times_ratio::*; pub use close_price_times_sats::*; pub use difference_f32::*; diff --git a/crates/brk_computer/src/price/oracle/compute.rs b/crates/brk_computer/src/price/oracle/compute.rs index 1be3ce6ab..7ed016ba5 100644 --- a/crates/brk_computer/src/price/oracle/compute.rs +++ b/crates/brk_computer/src/price/oracle/compute.rs @@ -1,10 +1,50 @@ +//! # Phase Oracle - On-chain Price Discovery +//! +//! Uses `frac(log10(sats))` to bin outputs into 100 bins per block. +//! The peak bin indicates the price decade (cyclical: $6.3, $63, $630, $6300 all map to same bin). +//! Monthly/yearly calibration anchors resolve the decade ambiguity. +//! +//! ## What Worked +//! +//! **Transaction filters (in `compute_pair_index`):** +//! - `output_count == 2` - payment + change pattern +//! - `input_count <= 5` - matches Python UTXOracle +//! - `witness_size <= 2500` bytes total +//! - No OP_RETURN outputs +//! - No P2TR (taproot) outputs - significantly cleaned up 2021+ data +//! - No P2MS, Empty, Unknown outputs - allowlist approach +//! - No same-day spends - inputs must spend outputs confirmed on earlier days +//! - No both-outputs-round - skip tx if both outputs are round BTC amounts (±0.1%) +//! +//! **Output filters (in `OracleBins::sats_to_bin`):** +//! - Per-output min/max: 1k sats to 100k BTC (matches Python's 1e-5 to 1e5 BTC) +//! +//! **Peak finding:** +//! - Skip bin 0 when finding peak - round BTC amounts (0.001, 0.01, 0.1, 1.0 BTC) cluster there +//! +//! **Anchors:** +//! - Monthly anchors 2010-2020 for better decade selection in volatile early years +//! - Yearly anchors 2021+ when prices are more stable +//! +//! ## What Didn't Work +//! +//! - **Skip all round bins (0, 10, 20, ..., 90) before 2020** - made results worse, not better +//! - **Top-N tie-breaking with prev_price** - caused drift +//! - **50% margin threshold for round bin avoidance** - still had issues +//! - **Transaction-level min sats filter** - Python filters per-output, not per-tx +//! +//! ## Known Limitations +//! +//! - Pre-2017 data is noisy due to low transaction volume (weak signal) +//! - 2017 SegWit activation era has some spikes + use std::collections::VecDeque; use brk_error::Result; use brk_indexer::Indexer; use brk_types::{ - Cents, Close, Date, DateIndex, Height, High, Low, OHLCCents, Open, OutputType, Sats, StoredU32, - StoredU64, TxIndex, + Cents, Close, Date, DateIndex, Height, High, Low, OHLCCents, Open, OracleBins, OutputType, + PHASE_BINS, PairOutputIndex, Sats, StoredU32, StoredU64, TxIndex, }; use tracing::info; use vecdb::{ @@ -31,6 +71,653 @@ impl Vecs { indexes: &indexes::Vecs, starting_indexes: &ComputeIndexes, exit: &Exit, + ) -> Result<()> { + // Step 1: Compute pair output index (all 2-output transactions) + self.compute_pair_index(indexer, indexes, starting_indexes, exit)?; + + // Step 2: Compute phase histograms (Layer 4) + self.compute_phase_histograms(starting_indexes, exit)?; + + // Step 3: Compute phase oracle prices (Layer 5) + self.compute_phase_prices(starting_indexes, exit)?; + + // Step 4: Compute phase daily average + self.compute_phase_daily_average(indexes, starting_indexes, exit)?; + + // Step 6: Compute UTXOracle prices (Python port) + self.compute_prices(indexer, indexes, starting_indexes, exit)?; + + // Step 7: Aggregate to daily OHLC + self.compute_daily_ohlc(indexes, starting_indexes, exit)?; + + Ok(()) + } + + /// Compute the pair output index: all transactions with exactly 2 outputs + /// + /// This is Layer 1 of the oracle computation - identifies all candidate + /// transactions for the payment+change pattern. + fn compute_pair_index( + &mut self, + indexer: &Indexer, + indexes: &indexes::Vecs, + starting_indexes: &ComputeIndexes, + exit: &Exit, + ) -> Result<()> { + // Validate version - combine all source vec versions + let source_version = indexes.txindex.output_count.version() + + indexes.txindex.input_count.version() + + indexer.vecs.transactions.base_size.version() + + indexer.vecs.transactions.total_size.version() + + indexer.vecs.outputs.outputtype.version() + + indexer.vecs.outputs.value.version() + + indexer.vecs.inputs.outpoint.version() + + indexes.height.dateindex.version(); + self.pairoutputindex_to_txindex + .validate_computed_version_or_reset(source_version)?; + self.height_to_first_pairoutputindex + .validate_computed_version_or_reset(source_version)?; + self.output0_value + .validate_computed_version_or_reset(source_version)?; + self.output1_value + .validate_computed_version_or_reset(source_version)?; + + let total_heights = indexer.vecs.blocks.timestamp.len(); + let total_txs = indexer.vecs.transactions.height.len(); + + // Determine starting height (handle rollback + sync) + let start_height = self + .height_to_first_pairoutputindex + .len() + .min(starting_indexes.height.to_usize()); + + // Truncation point for pair vecs: first_pairoutputindex of start_height block + // (i.e., keep all pairs from blocks before start_height) + let pair_truncate_len = + if start_height > 0 && start_height <= self.height_to_first_pairoutputindex.len() { + self.height_to_first_pairoutputindex + .iter()? + .get(Height::from(start_height)) + .map(|idx| idx.to_usize()) + .unwrap_or(self.pairoutputindex_to_txindex.len()) + } else if start_height == 0 { + 0 + } else { + self.pairoutputindex_to_txindex.len() + } + .min(self.pairoutputindex_to_txindex.len()) + .min(self.output0_value.len()) + .min(self.output1_value.len()); + + // Truncate all vecs together + self.height_to_first_pairoutputindex + .truncate_if_needed_at(start_height)?; + self.pairoutputindex_to_txindex + .truncate_if_needed_at(pair_truncate_len)?; + self.output0_value + .truncate_if_needed_at(pair_truncate_len)?; + self.output1_value + .truncate_if_needed_at(pair_truncate_len)?; + + if start_height >= total_heights { + return Ok(()); + } + + info!( + "Computing pair index 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_output_count_iter = indexes.txindex.output_count.iter(); + let mut txindex_to_input_count_iter = indexes.txindex.input_count.iter(); + let mut txindex_to_base_size_iter = indexer.vecs.transactions.base_size.into_iter(); + let mut txindex_to_total_size_iter = indexer.vecs.transactions.total_size.into_iter(); + let mut txindex_to_first_txoutindex_iter = + indexer.vecs.transactions.first_txoutindex.into_iter(); + let mut txindex_to_first_txinindex_iter = + indexer.vecs.transactions.first_txinindex.into_iter(); + let mut txoutindex_to_outputtype_iter = indexer.vecs.outputs.outputtype.into_iter(); + let mut txoutindex_to_value_iter = indexer.vecs.outputs.value.into_iter(); + let mut txinindex_to_outpoint_iter = indexer.vecs.inputs.outpoint.into_iter(); + let mut height_to_dateindex_iter = indexes.height.dateindex.iter(); + let mut dateindex_to_first_height_iter = indexes.dateindex.first_height.iter(); + + // Track current date for same-day spend check + let mut current_dateindex = DateIndex::from(0usize); + let mut current_date_first_txindex = TxIndex::from(0usize); + + let mut last_progress = (start_height * 100 / total_heights.max(1)) as u8; + + for height in start_height..total_heights { + // Record first pairoutputindex for this block + let first_pairoutputindex = + PairOutputIndex::from(self.pairoutputindex_to_txindex.len()); + self.height_to_first_pairoutputindex + .push(first_pairoutputindex); + + // 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)); + + // Update current date tracking for same-day spend check + let block_dateindex = height_to_dateindex_iter.get_unwrap(Height::from(height)); + if block_dateindex != current_dateindex { + current_dateindex = block_dateindex; + if let Some(first_height) = dateindex_to_first_height_iter.get(block_dateindex) { + current_date_first_txindex = height_to_first_txindex_iter + .get_at(first_height.to_usize()) + .unwrap_or(first_txindex); + } + } + + // Skip coinbase (first tx in block) + let tx_start = first_txindex.to_usize() + 1; + let tx_end = next_first_txindex.to_usize(); + + for txindex in tx_start..tx_end { + // Check output count first (most common filter) + let output_count: StoredU64 = + txindex_to_output_count_iter.get_unwrap(TxIndex::from(txindex)); + if *output_count != 2 { + continue; + } + + // Filter: 1-5 inputs (same as UTXOracle) + let input_count: StoredU64 = + txindex_to_input_count_iter.get_unwrap(TxIndex::from(txindex)); + if *input_count == 0 || *input_count > 5 { + continue; + } + + // Filter: max 2500 bytes total witness size + let base_size: StoredU32 = txindex_to_base_size_iter.get_at_unwrap(txindex); + let total_size: StoredU32 = txindex_to_total_size_iter.get_at_unwrap(txindex); + let witness_size = *total_size - *base_size; + if witness_size > 2500 { + continue; + } + + // Filter: only standard payment types (no OP_RETURN, P2TR, P2MS, Empty, Unknown) + let first_txoutindex = txindex_to_first_txoutindex_iter.get_at_unwrap(txindex); + let out0_type = + txoutindex_to_outputtype_iter.get_at_unwrap(first_txoutindex.to_usize()); + let out1_type = + txoutindex_to_outputtype_iter.get_at_unwrap(first_txoutindex.to_usize() + 1); + if !matches!( + out0_type, + OutputType::P2PK65 + | OutputType::P2PK33 + | OutputType::P2PKH + | OutputType::P2SH + | OutputType::P2WPKH + | OutputType::P2WSH + | OutputType::P2A + ) || !matches!( + out1_type, + OutputType::P2PK65 + | OutputType::P2PK33 + | OutputType::P2PKH + | OutputType::P2SH + | OutputType::P2WPKH + | OutputType::P2WSH + | OutputType::P2A + ) { + continue; + } + + // Filter: no same-day spends (input spending output confirmed today) + let first_txinindex = txindex_to_first_txinindex_iter.get_at_unwrap(txindex); + let mut has_same_day_spend = false; + for i in 0..*input_count as usize { + let txinindex = first_txinindex.to_usize() + i; + let outpoint = txinindex_to_outpoint_iter.get_at_unwrap(txinindex); + if !outpoint.is_coinbase() && outpoint.txindex() >= current_date_first_txindex { + has_same_day_spend = true; + break; + } + } + if has_same_day_spend { + continue; + } + + // Get output values (Layer 3) + let value0: Sats = + txoutindex_to_value_iter.get_at_unwrap(first_txoutindex.to_usize()); + let value1: Sats = + txoutindex_to_value_iter.get_at_unwrap(first_txoutindex.to_usize() + 1); + + // Filter: skip if BOTH outputs are round BTC amounts (not price-related) + if value0.is_round_btc() && value1.is_round_btc() { + continue; + } + + // Store Layer 1 & 3 data + // Note: min/max sats filtering done per-output in OracleBins::sats_to_bin + self.pairoutputindex_to_txindex.push(TxIndex::from(txindex)); + self.output0_value.push(value0); + self.output1_value.push(value1); + } + + // Log and flush every 1% + let progress = (height * 100 / total_heights.max(1)) as u8; + if progress > last_progress { + last_progress = progress; + info!("Pair index computation: {}%", progress); + + let _lock = exit.lock(); + self.pairoutputindex_to_txindex.write()?; + self.height_to_first_pairoutputindex.write()?; + self.output0_value.write()?; + self.output1_value.write()?; + } + } + + // Final write + { + let _lock = exit.lock(); + self.pairoutputindex_to_txindex.write()?; + self.height_to_first_pairoutputindex.write()?; + self.output0_value.write()?; + self.output1_value.write()?; + } + + info!( + "Pair index complete: {} pairs across {} blocks", + self.pairoutputindex_to_txindex.len(), + self.height_to_first_pairoutputindex.len() + ); + + Ok(()) + } + + /// Compute phase histograms per block (Layer 4) + /// + /// Bins output values by frac(log10(sats)) into 100 bins per block. + fn compute_phase_histograms( + &mut self, + starting_indexes: &ComputeIndexes, + exit: &Exit, + ) -> Result<()> { + let source_version = self.pairoutputindex_to_txindex.version(); + self.phase_histogram + .validate_computed_version_or_reset(source_version)?; + + let total_heights = self.height_to_first_pairoutputindex.len(); + let total_pairs = self.pairoutputindex_to_txindex.len(); + + let start_height = self + .phase_histogram + .len() + .min(starting_indexes.height.to_usize()); + + self.phase_histogram.truncate_if_needed_at(start_height)?; + + if start_height >= total_heights { + return Ok(()); + } + + info!( + "Computing phase histograms from height {} to {}", + start_height, total_heights + ); + + let mut output0_iter = self.output0_value.iter()?; + let mut output1_iter = self.output1_value.iter()?; + let mut height_to_first_pair_iter = self.height_to_first_pairoutputindex.iter()?; + + let mut last_progress = (start_height * 100 / total_heights.max(1)) as u8; + + for height in start_height..total_heights { + // Get pair range for this block + let first_pair = height_to_first_pair_iter + .get_unwrap(Height::from(height)) + .to_usize(); + let next_first_pair = height_to_first_pair_iter + .get(Height::from(height + 1)) + .map(|p| p.to_usize()) + .unwrap_or(total_pairs); + + // Build phase histogram + let mut histogram = OracleBins::ZERO; + + for pair_idx in first_pair..next_first_pair { + let pair_idx = PairOutputIndex::from(pair_idx); + + let sats0: Sats = output0_iter.get_unwrap(pair_idx); + let sats1: Sats = output1_iter.get_unwrap(pair_idx); + + histogram.add(sats0); + histogram.add(sats1); + } + + self.phase_histogram.push(histogram); + + // Progress logging + let progress = (height * 100 / total_heights.max(1)) as u8; + if progress > last_progress { + last_progress = progress; + info!("Phase histogram computation: {}%", progress); + + let _lock = exit.lock(); + self.phase_histogram.write()?; + } + } + + // Final write + { + let _lock = exit.lock(); + self.phase_histogram.write()?; + } + + info!( + "Phase histograms complete: {} blocks", + self.phase_histogram.len() + ); + + Ok(()) + } + + /// Compute phase oracle prices (Layer 5) + /// + /// Derives prices from phase histograms using peak finding. + /// Uses monthly calibration anchors (2010-2020) then yearly (2021+). + fn compute_phase_prices( + &mut self, + starting_indexes: &ComputeIndexes, + exit: &Exit, + ) -> Result<()> { + /// Monthly calibration anchors 2010-2020, then yearly 2021+ + /// Format: (first_height_of_period, open_price) + const ANCHORS: [(u32, f64); 129] = [ + // 2010 (monthly from Oct) + (82_998, 0.06), // 2010-10-01 + (88_893, 0.19), // 2010-11-01 + (94_802, 0.20), // 2010-12-01 + // 2011 + (100_410, 0.30), // 2011-01-01 + (105_571, 0.55), // 2011-02-01 + (111_137, 0.86), // 2011-03-01 + (116_039, 0.78), // 2011-04-01 + (121_127, 3.50), // 2011-05-01 + (127_866, 8.74), // 2011-06-01 + (134_122, 16.10), // 2011-07-01 + (139_036, 13.35), // 2011-08-01 + (143_409, 8.19), // 2011-09-01 + (147_566, 5.14), // 2011-10-01 + (151_315, 3.24), // 2011-11-01 + (155_452, 2.97), // 2011-12-01 + // 2012 + (160_037, 4.72), // 2012-01-01 + (164_781, 5.48), // 2012-02-01 + (169_136, 4.86), // 2012-03-01 + (173_805, 4.90), // 2012-04-01 + (178_015, 4.94), // 2012-05-01 + (182_429, 5.18), // 2012-06-01 + (186_964, 6.68), // 2012-07-01 + (191_737, 9.35), // 2012-08-01 + (196_616, 10.16), // 2012-09-01 + (201_311, 12.40), // 2012-10-01 + (205_919, 11.20), // 2012-11-01 + (210_350, 12.56), // 2012-12-01 + // 2013 + (214_563, 13.51), // 2013-01-01 + (219_007, 20.41), // 2013-02-01 + (223_665, 33.38), // 2013-03-01 + (229_008, 93.03), // 2013-04-01 + (233_975, 139.22), // 2013-05-01 + (238_952, 128.81), // 2013-06-01 + (244_160, 97.51), // 2013-07-01 + (249_525, 106.21), // 2013-08-01 + (255_362, 141.00), // 2013-09-01 + (260_989, 141.89), // 2013-10-01 + (267_188, 211.17), // 2013-11-01 + (272_375, 1205.80), // 2013-12-01 + // 2014 + (277_996, 739.28), // 2014-01-01 + (283_468, 805.22), // 2014-02-01 + (288_370, 549.99), // 2014-03-01 + (293_483, 456.98), // 2014-04-01 + (298_513, 449.02), // 2014-05-01 + (303_552, 626.21), // 2014-06-01 + (308_672, 640.79), // 2014-07-01 + (313_404, 580.00), // 2014-08-01 + (318_531, 477.81), // 2014-09-01 + (323_269, 387.00), // 2014-10-01 + (327_939, 336.82), // 2014-11-01 + (332_363, 379.89), // 2014-12-01 + // 2015 + (336_861, 322.30), // 2015-01-01 + (341_392, 215.80), // 2015-02-01 + (345_611, 255.70), // 2015-03-01 + (350_162, 244.51), // 2015-04-01 + (354_416, 236.11), // 2015-05-01 + (358_881, 228.70), // 2015-06-01 + (363_263, 262.89), // 2015-07-01 + (367_846, 284.45), // 2015-08-01 + (372_441, 231.35), // 2015-09-01 + (376_910, 236.49), // 2015-10-01 + (381_470, 316.00), // 2015-11-01 + (386_119, 376.88), // 2015-12-01 + // 2016 + (391_182, 429.02), // 2016-01-01 + (396_049, 365.52), // 2016-02-01 + (400_601, 438.99), // 2016-03-01 + (405_179, 416.02), // 2016-04-01 + (409_638, 446.60), // 2016-05-01 + (414_258, 530.69), // 2016-06-01 + (418_723, 671.91), // 2016-07-01 + (423_088, 624.22), // 2016-08-01 + (427_737, 573.80), // 2016-09-01 + (432_284, 609.67), // 2016-10-01 + (436_828, 697.69), // 2016-11-01 + (441_341, 742.33), // 2016-12-01 + // 2017 + (446_033, 970.41), // 2017-01-01 + (450_945, 968.74), // 2017-02-01 + (455_200, 1190.37), // 2017-03-01 + (459_832, 1080.82), // 2017-04-01 + (464_270, 1362.02), // 2017-05-01 + (469_122, 2299.05), // 2017-06-01 + (473_593, 2455.42), // 2017-07-01 + (478_479, 2865.02), // 2017-08-01 + (482_885, 4737.93), // 2017-09-01 + (487_740, 4334.18), // 2017-10-01 + (492_558, 6439.52), // 2017-11-01 + (496_932, 9968.39), // 2017-12-01 + // 2018 + (501_961, 13888.32), // 2018-01-01 + (507_016, 10115.79), // 2018-02-01 + (511_385, 10306.80), // 2018-03-01 + (516_040, 6922.18), // 2018-04-01 + (520_650, 9243.39), // 2018-05-01 + (525_367, 7486.93), // 2018-06-01 + (529_967, 6386.45), // 2018-07-01 + (534_613, 7725.93), // 2018-08-01 + (539_416, 7016.31), // 2018-09-01 + (543_835, 6565.64), // 2018-10-01 + (548_214, 6305.13), // 2018-11-01 + (552_084, 3971.61), // 2018-12-01 + // 2019 + (556_459, 3692.35), // 2019-01-01 + (560_984, 3411.57), // 2019-02-01 + (565_109, 3792.17), // 2019-03-01 + (569_659, 4095.32), // 2019-04-01 + (573_997, 5269.55), // 2019-05-01 + (578_718, 8542.59), // 2019-06-01 + (583_237, 10754.91), // 2019-07-01 + (588_007, 10085.57), // 2019-08-01 + (592_683, 9600.93), // 2019-09-01 + (597_318, 8303.79), // 2019-10-01 + (601_842, 9152.56), // 2019-11-01 + (606_088, 7554.92), // 2019-12-01 + // 2020 + (610_691, 7167.07), // 2020-01-01 + (615_428, 9333.17), // 2020-02-01 + (619_582, 8526.76), // 2020-03-01 + (623_837, 6424.03), // 2020-04-01 + (628_350, 8627.93), // 2020-05-01 + (632_542, 9448.95), // 2020-06-01 + (637_091, 9134.01), // 2020-07-01 + (641_680, 11354.08), // 2020-08-01 + (646_201, 11657.26), // 2020-09-01 + (650_732, 10779.19), // 2020-10-01 + (654_933, 13809.85), // 2020-11-01 + (658_977, 19698.14), // 2020-12-01 + // 2021+ (yearly) + (663_913, 28_980.45), // 2021-01-01 + (716_599, 46_195.56), // 2022-01-01 + (769_787, 16_528.89), // 2023-01-01 + (823_786, 42_241.10), // 2024-01-01 + (877_259, 93_576.00), // 2025-01-01 + (930_341, 87_648.22), // 2026-01-01 + ]; + + /// Find the calibration price for a given height + fn anchor_price_for_height(height: usize) -> Option { + let mut result = None; + for &(anchor_height, price) in &ANCHORS { + if height >= anchor_height as usize { + result = Some(price); + } else { + break; + } + } + result + } + + let source_version = self.phase_histogram.version(); + self.phase_price_cents + .validate_computed_version_or_reset(source_version)?; + + let total_heights = self.phase_histogram.len(); + + let start_height = self + .phase_price_cents + .len() + .min(starting_indexes.height.to_usize()); + + self.phase_price_cents.truncate_if_needed_at(start_height)?; + + if start_height >= total_heights { + return Ok(()); + } + + info!( + "Computing phase prices from height {} to {}", + start_height, total_heights + ); + + let mut histogram_iter = self.phase_histogram.iter()?; + let mut last_progress = (start_height * 100 / total_heights.max(1)) as u8; + + // Fixed exponent calibrated for ~$63,000 (ceil(log10(63000)) = 5) + const EXPONENT: f64 = 5.0; + + /// Convert a bin to price using anchor for decade selection + fn bin_to_price(bin: usize, anchor_price: f64) -> f64 { + let peak = (bin as f64 + 0.5) / PHASE_BINS as f64; + let raw_price = 10.0_f64.powf(EXPONENT - peak); + let decade_ratio = (anchor_price / raw_price).log10().round(); + raw_price * 10.0_f64.powf(decade_ratio) + } + + for height in start_height..total_heights { + // Before first anchor (pre-Oct 2010), output 0 + let anchor_price = match anchor_price_for_height(height) { + Some(price) => price, + None => { + self.phase_price_cents.push(Cents::ZERO); + continue; + } + }; + + let histogram = histogram_iter.get_unwrap(Height::from(height)); + + // Skip empty histograms, use anchor price + if histogram.total_count() == 0 { + let price_cents = Cents::from((anchor_price * 100.0) as i64); + self.phase_price_cents.push(price_cents); + continue; + } + + // Find peak bin, skipping bin 0 (round BTC amounts cluster there) + let peak_bin = histogram + .bins + .iter() + .enumerate() + .filter(|(bin, _)| *bin != 0) + .max_by_key(|(_, count)| *count) + .map(|(bin, _)| bin) + .unwrap_or(0); + + let price = bin_to_price(peak_bin, anchor_price); + + // Clamp to reasonable range ($0.001 to $10M) + let price = price.clamp(0.001, 10_000_000.0); + + let price_cents = Cents::from((price * 100.0) as i64); + self.phase_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 price computation: {}%", progress); + + let _lock = exit.lock(); + self.phase_price_cents.write()?; + } + } + + // Final write + { + let _lock = exit.lock(); + self.phase_price_cents.write()?; + } + + info!( + "Phase prices complete: {} blocks", + self.phase_price_cents.len() + ); + + Ok(()) + } + + /// Compute daily distribution (min, max, average, percentiles) from phase oracle prices + fn compute_phase_daily_average( + &mut self, + indexes: &indexes::Vecs, + starting_indexes: &ComputeIndexes, + exit: &Exit, + ) -> Result<()> { + info!("Computing phase daily distribution"); + + self.phase_daily_cents.compute( + starting_indexes.dateindex, + &self.phase_price_cents, + &indexes.dateindex.first_height, + &indexes.dateindex.height_count, + exit, + )?; + + info!( + "Phase daily distribution complete: {} days", + self.phase_daily_cents.len() + ); + + Ok(()) + } + + /// Compute oracle prices from on-chain data (UTXOracle port) + fn compute_prices( + &mut self, + indexer: &Indexer, + indexes: &indexes::Vecs, + starting_indexes: &ComputeIndexes, + exit: &Exit, ) -> Result<()> { // Validate versions self.price_cents @@ -92,8 +779,8 @@ impl Vecs { }; // Progress tracking - let total_blocks = last_height.to_usize() - start_height.to_usize(); - let mut last_progress = 0u8; + let mut last_progress = + (start_height.to_usize() * 100 / last_height.to_usize().max(1)) as u8; let total_txs = indexer.vecs.transactions.height.len(); // Sparse entries for current block (reused buffer) @@ -109,8 +796,7 @@ impl Vecs { let height = Height::from(height); // Log progress every 1% - let progress = - ((height.to_usize() - start_height.to_usize()) * 100 / total_blocks.max(1)) as u8; + let progress = (height.to_usize() * 100 / last_height.to_usize().max(1)) as u8; if progress > last_progress { last_progress = progress; info!("Oracle price computation: {}%", progress); @@ -174,14 +860,14 @@ impl Vecs { // Check outputs: no OP_RETURN, collect values let mut has_opreturn = false; let mut values: [Sats; 2] = [Sats::ZERO; 2]; - for i in 0..2usize { + for (i, value) in values.iter_mut().enumerate() { let txoutindex = first_txoutindex.to_usize() + i; let outputtype = txoutindex_to_outputtype_iter.get_at_unwrap(txoutindex); if outputtype == OutputType::OpReturn { has_opreturn = true; break; } - values[i] = txoutindex_to_value_iter.get_at_unwrap(txoutindex); + *value = txoutindex_to_value_iter.get_at_unwrap(txoutindex); } if has_opreturn { continue; diff --git a/crates/brk_computer/src/price/oracle/import.rs b/crates/brk_computer/src/price/oracle/import.rs index 1df260d89..8f78cf8af 100644 --- a/crates/brk_computer/src/price/oracle/import.rs +++ b/crates/brk_computer/src/price/oracle/import.rs @@ -3,12 +3,38 @@ use brk_types::{DateIndex, OHLCCents, OHLCDollars, Version}; use vecdb::{BytesVec, Database, ImportableVec, IterableCloneableVec, LazyVecFrom1, PcoVec}; use super::Vecs; +use crate::internal::{CentsToDollars, Distribution, LazyTransformDistribution}; impl Vecs { pub fn forced_import(db: &Database, parent_version: Version) -> Result { - // v2: Fixed spike stencil positions and Gaussian center to match Python's empirical data - let version = parent_version + Version::TWO; + // v12: Add both-outputs-round filter + let version = parent_version + Version::new(12); + // Layer 1: Pair output index + let pairoutputindex_to_txindex = + PcoVec::forced_import(db, "pairoutputindex_to_txindex", version)?; + let height_to_first_pairoutputindex = + PcoVec::forced_import(db, "height_to_first_pairoutputindex", version)?; + + // Layer 3: Output values + let output0_value = PcoVec::forced_import(db, "pair_output0_value", version)?; + let output1_value = PcoVec::forced_import(db, "pair_output1_value", version)?; + + // Layer 4: Phase histograms (depends on Layer 1) + let phase_histogram = BytesVec::forced_import(db, "phase_histogram", version)?; + + // Layer 5: Phase Oracle prices + // v20: Skip only bin 0 (reverted round bin skip) + let phase_version = version + Version::new(13); + let phase_price_cents = PcoVec::forced_import(db, "phase_price_cents", phase_version)?; + let phase_daily_cents = Distribution::forced_import(db, "phase_daily", phase_version)?; + let phase_daily_dollars = LazyTransformDistribution::from_distribution::( + "phase_daily_dollars", + phase_version, + &phase_daily_cents, + ); + + // UTXOracle (Python port) let price_cents = PcoVec::forced_import(db, "oracle_price_cents", version)?; let ohlc_cents = BytesVec::forced_import(db, "oracle_ohlc_cents", version)?; let tx_count = PcoVec::forced_import(db, "oracle_tx_count", version)?; @@ -21,6 +47,14 @@ impl Vecs { ); Ok(Self { + pairoutputindex_to_txindex, + height_to_first_pairoutputindex, + output0_value, + output1_value, + phase_histogram, + phase_price_cents, + phase_daily_cents, + phase_daily_dollars, price_cents, ohlc_cents, ohlc_dollars, diff --git a/crates/brk_computer/src/price/oracle/vecs.rs b/crates/brk_computer/src/price/oracle/vecs.rs index 0ea0be795..655fee85c 100644 --- a/crates/brk_computer/src/price/oracle/vecs.rs +++ b/crates/brk_computer/src/price/oracle/vecs.rs @@ -1,16 +1,53 @@ use brk_traversable::Traversable; -use brk_types::{Cents, DateIndex, Height, OHLCCents, OHLCDollars, StoredU32}; +use brk_types::{ + Cents, DateIndex, Dollars, Height, OHLCCents, OHLCDollars, OracleBins, PairOutputIndex, Sats, + StoredU32, TxIndex, +}; use vecdb::{BytesVec, LazyVecFrom1, PcoVec}; -/// Vectors storing UTXOracle-derived price data +use crate::internal::{Distribution, LazyTransformDistribution}; + +/// Vectors storing oracle-derived price data #[derive(Clone, Traversable)] pub struct Vecs { - /// Per-block price estimate in cents - /// This enables OHLC derivation for any time period + // ========== Layer 1: Pair identification (requires chain scan) ========== + /// Maps PairOutputIndex to TxIndex for all 2-output transactions + /// This is the base index for oracle candidates (~400M entries) + pub pairoutputindex_to_txindex: PcoVec, + + /// Maps Height to first PairOutputIndex in that block + /// Enables efficient per-block iteration over pairs + pub height_to_first_pairoutputindex: PcoVec, + + // ========== Layer 3: Output values (enables any price algorithm) ========== + /// First output value for each pair (index 0) + pub output0_value: PcoVec, + + /// Second output value for each pair (index 1) + pub output1_value: PcoVec, + + // ========== Layer 4: Phase histograms (per block) ========== + /// Phase histogram per block: frac(log10(sats)) binned into 100 bins + /// ~200 bytes per block, ~175 MB total + pub phase_histogram: BytesVec, + + // ========== Layer 5: Phase Oracle prices (derived from histograms) ========== + /// Per-block price in cents from phase histogram analysis + /// Calibrated at block 840,000 (~$63,000) + /// TODO: Add interpolation for sub-bin precision + pub phase_price_cents: PcoVec, + + /// Daily distribution (min, max, average, percentiles) from phase oracle in cents + pub phase_daily_cents: Distribution, + + /// Daily distribution in dollars (lazy conversion from cents) + pub phase_daily_dollars: LazyTransformDistribution, + + // ========== UTXOracle (Python port) ========== + /// Per-block price estimate in cents (sliding window + stencil matching) pub price_cents: PcoVec, - /// Daily OHLC derived from height_to_price - /// Uses BytesVec because OHLCCents is a complex type + /// Daily OHLC derived from price_cents pub ohlc_cents: BytesVec, /// Daily OHLC in dollars (lazy conversion from cents) diff --git a/crates/brk_logger/README.md b/crates/brk_logger/README.md index 31b5be57c..d7ea5748c 100644 --- a/crates/brk_logger/README.md +++ b/crates/brk_logger/README.md @@ -4,7 +4,7 @@ Colorized, timestamped logging with optional file output and hooks. ## What It Enables -Drop-in logging initialization that silences noisy dependencies (bitcoin, fjall, rolldown, rmcp) while keeping your logs readable with color-coded levels and local timestamps. +Drop-in logging initialization that silences noisy dependencies (bitcoin, fjall, rolldown, ...) while keeping your logs readable with color-coded levels and local timestamps. ## Key Features diff --git a/crates/brk_logger/src/lib.rs b/crates/brk_logger/src/lib.rs index b219b67ad..a40e6c29c 100644 --- a/crates/brk_logger/src/lib.rs +++ b/crates/brk_logger/src/lib.rs @@ -20,7 +20,7 @@ type LogHook = Box; static GUARD: OnceLock = OnceLock::new(); static LOG_HOOK: OnceLock = OnceLock::new(); -const MAX_LOG_FILES: u64 = 2; +const MAX_LOG_FILES: u64 = 5; const MAX_FILE_SIZE_MB: u64 = 42; // Don't remove, used to know the target of unwanted logs @@ -117,9 +117,13 @@ impl tracing::field::Visit for FieldVisitor { match name { "uri" => self.uri = Some(format!("{value:?}")), "latency" => self.latency = Some(format!("{value:?}")), - "message" => { let _ = write!(self.result, "{value:?}"); } + "message" => { + let _ = write!(self.result, "{value:?}"); + } _ if name.starts_with("log.") => {} - _ => { let _ = write!(self.result, "{}={:?} ", name, value); } + _ => { + let _ = write!(self.result, "{}={:?} ", name, value); + } } } } @@ -214,7 +218,7 @@ pub fn init(path: Option<&Path>) -> io::Result<()> { const DEFAULT_LEVEL: &str = "info"; let default_filter = format!( - "{DEFAULT_LEVEL},bitcoin=off,bitcoincore-rpc=off,fjall=off,brk_fjall=off,lsm_tree=off,brk_rolldown=off,rolldown=off,rmcp=off,brk_rmcp=off,tracing=off,aide=off,rustls=off,notify=off,oxc_resolver=off,tower_http=off" + "{DEFAULT_LEVEL},bitcoin=off,bitcoincore-rpc=off,fjall=off,brk_fjall=off,lsm_tree=off,brk_rolldown=off,rolldown=off,tracing=off,aide=off,rustls=off,notify=off,oxc_resolver=off,tower_http=off" ); let filter = diff --git a/crates/brk_mcp/Cargo.toml b/crates/brk_mcp/Cargo.toml deleted file mode 100644 index c632fc349..000000000 --- a/crates/brk_mcp/Cargo.toml +++ /dev/null @@ -1,23 +0,0 @@ -[package] -name = "brk_mcp" -description = "A bridge for LLMs to access BRK" -version.workspace = true -edition.workspace = true -license.workspace = true -homepage.workspace = true -repository.workspace = true - -[dependencies] -axum = { workspace = true } -brk_rmcp = { version = "0.8.0", features = [ - "transport-worker", - "transport-streamable-http-server", -] } -tracing = { workspace = true } -minreq = { workspace = true } -schemars = { workspace = true } -serde = { workspace = true } -serde_json = { workspace = true } - -[package.metadata.cargo-machete] -ignored = ["serde_json"] diff --git a/crates/brk_mcp/README.md b/crates/brk_mcp/README.md deleted file mode 100644 index 318b91e95..000000000 --- a/crates/brk_mcp/README.md +++ /dev/null @@ -1,34 +0,0 @@ -# brk_mcp - -Model Context Protocol (MCP) server for Bitcoin on-chain data. - -## What It Enables - -Expose BRK's REST API to AI assistants via MCP. The LLM reads the OpenAPI spec and calls any endpoint through a generic fetch tool. - -## Available Tools - -| Tool | Description | -|------|-------------| -| `get_openapi` | Get the OpenAPI specification for all REST endpoints | -| `fetch` | Call any REST API endpoint by path and query | - -## Workflow - -1. LLM calls `get_openapi` to discover available endpoints -2. LLM calls `fetch` with the desired path and query parameters - -## Usage - -```rust,ignore -let mcp = MCP::new("http://127.0.0.1:3110", openapi_json); -``` - -## Integration - -The MCP server is integrated into `brk_server` and exposed at `/mcp` endpoint. - -## Built On - -- `brk_rmcp` for MCP protocol implementation -- `minreq` for HTTP requests diff --git a/crates/brk_mcp/src/lib.rs b/crates/brk_mcp/src/lib.rs deleted file mode 100644 index ded07af64..000000000 --- a/crates/brk_mcp/src/lib.rs +++ /dev/null @@ -1,110 +0,0 @@ -#![doc = include_str!("../README.md")] - -use std::sync::Arc; - -use brk_rmcp::{ - ErrorData as McpError, RoleServer, ServerHandler, - handler::server::{router::tool::ToolRouter, wrapper::Parameters}, - model::*, - service::RequestContext, - tool, tool_handler, tool_router, -}; -use schemars::JsonSchema; -use serde::Deserialize; -use tracing::info; - -pub mod route; - -#[derive(Clone)] -pub struct MCP { - base_url: Arc, - openapi_json: Arc, - tool_router: ToolRouter, -} - -/// Parameters for fetching from the REST API. -#[derive(Deserialize, JsonSchema)] -pub struct FetchParams { - /// API path (e.g., "/api/blocks" or "/api/metrics/list") - pub path: String, - /// Optional query string (e.g., "page=0" or "from=-1&to=-10") - pub query: Option, -} - -#[tool_router] -impl MCP { - pub fn new(base_url: impl Into, openapi_json: impl Into) -> Self { - Self { - base_url: Arc::new(base_url.into()), - openapi_json: Arc::new(openapi_json.into()), - tool_router: Self::tool_router(), - } - } - - #[tool( - description = "Get the OpenAPI specification describing all available REST API endpoints." - )] - async fn get_openapi(&self) -> Result { - info!("mcp: get_openapi"); - Ok(CallToolResult::success(vec![Content::text( - self.openapi_json.as_str(), - )])) - } - - #[tool( - description = "Call a REST API endpoint. Use get_openapi first to discover available endpoints." - )] - async fn fetch( - &self, - Parameters(params): Parameters, - ) -> Result { - info!("mcp: fetch {}", params.path); - - let url = match ¶ms.query { - Some(q) if !q.is_empty() => format!("{}{}?{}", self.base_url, params.path, q), - _ => format!("{}{}", self.base_url, params.path), - }; - - match minreq::get(&url).with_timeout(30).send() { - Ok(response) => { - let body = response.as_str().unwrap_or("").to_string(); - Ok(CallToolResult::success(vec![Content::text(body)])) - } - Err(e) => Err(McpError::internal_error( - format!("HTTP request failed: {e}"), - None, - )), - } - } -} - -#[tool_handler] -impl ServerHandler for MCP { - fn get_info(&self) -> ServerInfo { - ServerInfo { - protocol_version: ProtocolVersion::LATEST, - capabilities: ServerCapabilities::builder().enable_tools().build(), - server_info: Implementation::from_build_env(), - instructions: Some( - " -Bitcoin Research Kit (BRK) - Bitcoin on-chain metrics and market data. - -Workflow: -1. Call get_openapi to get the full API specification -2. Use fetch to call any endpoint described in the spec - -Example: fetch with path=\"/api/metrics/list\" to list metrics. -" - .to_string(), - ), - } - } - - async fn initialize( - &self, - _request: InitializeRequestParam, - _context: RequestContext, - ) -> Result { - Ok(self.get_info()) - } -} diff --git a/crates/brk_mcp/src/route.rs b/crates/brk_mcp/src/route.rs deleted file mode 100644 index d5f9b7f25..000000000 --- a/crates/brk_mcp/src/route.rs +++ /dev/null @@ -1,26 +0,0 @@ -use std::sync::Arc; - -use axum::Router; -use brk_rmcp::transport::{ - StreamableHttpServerConfig, - streamable_http_server::{StreamableHttpService, session::local::LocalSessionManager}, -}; -use tracing::info; - -use crate::MCP; - -/// Create an MCP service router. -pub fn mcp_router(base_url: String, openapi_json: Arc) -> Router { - info!("Setting up MCP..."); - - let service = StreamableHttpService::new( - move || Ok(MCP::new(base_url.clone(), openapi_json.as_str())), - LocalSessionManager::default().into(), - StreamableHttpServerConfig { - stateful_mode: false, - ..Default::default() - }, - ); - - Router::new().nest_service("/mcp", service) -} diff --git a/crates/brk_query/README.md b/crates/brk_query/README.md index 529ebed1b..b44da23a0 100644 --- a/crates/brk_query/README.md +++ b/crates/brk_query/README.md @@ -23,12 +23,13 @@ let query = Query::build(&reader, &indexer, &computer, Some(mempool)); // Current height let height = query.height(); -// Metric queries -let data = query.search_and_format(MetricSelection { +// Metric queries (two-phase: resolve then format) +let resolved = query.resolve(MetricSelection { metrics: vec!["supply".into()], index: Index::Height, range: DataRangeFormat::default(), -})?; +}, usize::MAX)?; +let data = query.format(resolved)?; // Block queries let info = query.block_by_height(Height::new(840_000))?; @@ -44,7 +45,7 @@ let stats = query.address(address)?; | Domain | Methods | |--------|---------| -| Metrics | `metrics`, `search_and_format`, `metric_to_indexes` | +| Metrics | `metrics`, `resolve`, `format`, `metric_to_indexes` | | Blocks | `block`, `block_by_height`, `blocks`, `block_txs`, `block_status`, `block_by_timestamp` | | Transactions | `transaction`, `transaction_status`, `transaction_hex`, `outspend`, `outspends` | | Addresses | `address`, `address_txids`, `address_utxos` | diff --git a/crates/brk_query/src/impl/metrics.rs b/crates/brk_query/src/impl/metrics.rs index 894b8494f..b40d8db2d 100644 --- a/crates/brk_query/src/impl/metrics.rs +++ b/crates/brk_query/src/impl/metrics.rs @@ -3,12 +3,15 @@ use std::collections::BTreeMap; use brk_error::{Error, Result}; use brk_traversable::TreeNode; use brk_types::{ - DetailedMetricCount, Format, Index, IndexInfo, Limit, Metric, MetricData, PaginatedMetrics, - Pagination, PaginationIndex, + DetailedMetricCount, Format, Index, IndexInfo, Limit, Metric, MetricData, MetricOutput, + MetricSelection, Output, PaginatedMetrics, Pagination, PaginationIndex, }; use vecdb::AnyExportableVec; -use crate::{vecs::{IndexToVec, MetricToVec}, DataRangeFormat, MetricSelection, Output, Query}; +use crate::{ + Query, ResolvedQuery, + vecs::{IndexToVec, MetricToVec}, +}; /// Estimated bytes per column header const CSV_HEADER_BYTES_PER_COL: usize = 10; @@ -33,19 +36,25 @@ impl Query { // Metric doesn't exist, suggest alternatives Error::MetricNotFound { metric: metric.to_string(), - suggestion: self.match_metric(metric, Limit::MIN).first().map(|s| s.to_string()), + suggestion: self + .match_metric(metric, Limit::MIN) + .first() + .map(|s| s.to_string()), } } pub(crate) fn columns_to_csv( columns: &[&dyn AnyExportableVec], - from: Option, - to: Option, + start: usize, + end: usize, ) -> Result { if columns.is_empty() { return Ok(String::new()); } + let from = Some(start as i64); + let to = Some(end as i64); + let num_rows = columns[0].range_count(from, to); let num_cols = columns.len(); @@ -79,82 +88,6 @@ impl Query { Ok(csv) } - /// Format single metric - returns `MetricData` - pub fn format( - &self, - metric: &dyn AnyExportableVec, - params: &DataRangeFormat, - ) -> Result { - let len = metric.len(); - let from = params.start().map(|start| metric.i64_to_usize(start)); - let to = params.end_for_len(len).map(|end| metric.i64_to_usize(end)); - - Ok(match params.format() { - Format::CSV => Output::CSV(Self::columns_to_csv( - &[metric], - from.map(|v| v as i64), - to.map(|v| v as i64), - )?), - Format::JSON => { - let mut buf = Vec::new(); - MetricData::serialize(metric, from, to, &mut buf)?; - Output::Json(buf) - } - }) - } - - /// Format multiple metrics - returns `Vec` - pub fn format_bulk( - &self, - metrics: &[&dyn AnyExportableVec], - params: &DataRangeFormat, - ) -> Result { - // Use min length across metrics for consistent count resolution - let min_len = metrics.iter().map(|v| v.len()).min().unwrap_or(0); - - let from = params.start().map(|start| { - metrics - .iter() - .map(|v| v.i64_to_usize(start)) - .min() - .unwrap_or_default() - }); - - let to = params.end_for_len(min_len).map(|end| { - metrics - .iter() - .map(|v| v.i64_to_usize(end)) - .min() - .unwrap_or_default() - }); - - let format = params.format(); - - Ok(match format { - Format::CSV => Output::CSV(Self::columns_to_csv( - metrics, - from.map(|v| v as i64), - to.map(|v| v as i64), - )?), - Format::JSON => { - if metrics.is_empty() { - return Ok(Output::default(format)); - } - - let mut buf = Vec::new(); - buf.push(b'['); - for (i, vec) in metrics.iter().enumerate() { - if i > 0 { - buf.push(b','); - } - MetricData::serialize(*vec, from, to, &mut buf)?; - } - buf.push(b']'); - Output::Json(buf) - } - }) - } - /// Search for vecs matching the given metrics and index. /// Returns error if no metrics requested or any requested metric is not found. pub fn search(&self, params: &MetricSelection) -> Result> { @@ -185,22 +118,34 @@ impl Query { .sum() } - /// Search and format single metric - pub fn search_and_format(&self, params: MetricSelection) -> Result { - self.search_and_format_checked(params, usize::MAX) - } - - /// Search and format single metric with weight limit - pub fn search_and_format_checked( + /// Resolve query metadata without formatting (cheap). + /// Use with `format` for lazy formatting after ETag check. + pub fn resolve( &self, params: MetricSelection, max_weight: usize, - ) -> Result { + ) -> Result { let vecs = self.search(¶ms)?; - let metric = vecs.first().expect("search guarantees non-empty on success"); + let total = vecs.iter().map(|v| v.len()).min().unwrap_or(0); + let version: u64 = vecs.iter().map(|v| u64::from(v.version())).sum(); - let weight = Self::weight(&vecs, params.start(), params.end_for_len(metric.len())); + let start = params + .start() + .map(|s| vecs.iter().map(|v| v.i64_to_usize(s)).min().unwrap_or(0)) + .unwrap_or(0); + + let end = params + .end_for_len(total) + .map(|e| { + vecs.iter() + .map(|v| v.i64_to_usize(e)) + .min() + .unwrap_or(total) + }) + .unwrap_or(total); + + let weight = Self::weight(&vecs, Some(start as i64), Some(end as i64)); if weight > max_weight { return Err(Error::WeightExceeded { requested: weight, @@ -208,32 +153,57 @@ impl Query { }); } - self.format(*metric, ¶ms.range) + Ok(ResolvedQuery { + vecs, + format: params.format(), + version, + total, + start, + end, + }) } - /// Search and format bulk metrics - pub fn search_and_format_bulk(&self, params: MetricSelection) -> Result { - self.search_and_format_bulk_checked(params, usize::MAX) - } + /// Format a resolved query (expensive). + /// Call after ETag/cache checks to avoid unnecessary work. + pub fn format(&self, resolved: ResolvedQuery) -> Result { + let ResolvedQuery { + vecs, + format, + version, + total, + start, + end, + } = resolved; - /// Search and format bulk metrics with weight limit (for DDoS prevention) - pub fn search_and_format_bulk_checked( - &self, - params: MetricSelection, - max_weight: usize, - ) -> Result { - let vecs = self.search(¶ms)?; + let output = match format { + Format::CSV => Output::CSV(Self::columns_to_csv(&vecs, start, end)?), + Format::JSON => { + if vecs.len() == 1 { + let mut buf = Vec::new(); + MetricData::serialize(vecs[0], start, end, &mut buf)?; + Output::Json(buf) + } else { + let mut buf = Vec::new(); + buf.push(b'['); + for (i, vec) in vecs.iter().enumerate() { + if i > 0 { + buf.push(b','); + } + MetricData::serialize(*vec, start, end, &mut buf)?; + } + buf.push(b']'); + Output::Json(buf) + } + } + }; - let min_len = vecs.iter().map(|v| v.len()).min().expect("search guarantees non-empty"); - let weight = Self::weight(&vecs, params.start(), params.end_for_len(min_len)); - if weight > max_weight { - return Err(Error::WeightExceeded { - requested: weight, - max: max_weight, - }); - } - - self.format_bulk(&vecs, ¶ms.range) + Ok(MetricOutput { + output, + version, + total, + start, + end, + }) } pub fn metric_to_index_to_vec(&self) -> &BTreeMap<&str, IndexToVec<'_>> { diff --git a/crates/brk_query/src/impl/metrics_legacy.rs b/crates/brk_query/src/impl/metrics_legacy.rs index 6c8886861..89d9ba02c 100644 --- a/crates/brk_query/src/impl/metrics_legacy.rs +++ b/crates/brk_query/src/impl/metrics_legacy.rs @@ -1,73 +1,65 @@ -use brk_error::{Error, Result}; -use brk_types::Format; -use vecdb::AnyExportableVec; +use brk_error::Result; +use brk_types::{Format, LegacyValue, MetricOutputLegacy, OutputLegacy}; -use crate::{DataRangeFormat, LegacyValue, MetricSelection, OutputLegacy, Query}; +use crate::{Query, ResolvedQuery}; impl Query { - /// Deprecated - raw data without MetricData wrapper - pub fn format_legacy(&self, metrics: &[&dyn AnyExportableVec], params: &DataRangeFormat) -> Result { - let min_len = metrics.iter().map(|v| v.len()).min().unwrap_or(0); + /// Deprecated - format a resolved query as legacy output (expensive). + pub fn format_legacy(&self, resolved: ResolvedQuery) -> Result { + let ResolvedQuery { + vecs, + format, + version, + total, + start, + end, + } = resolved; - let from = params - .start() - .map(|start| metrics.iter().map(|v| v.i64_to_usize(start)).min().unwrap_or_default()); + if vecs.is_empty() { + return Ok(MetricOutputLegacy { + output: OutputLegacy::default(format), + version: 0, + total: 0, + start: 0, + end: 0, + }); + } - let to = params - .end_for_len(min_len) - .map(|end| metrics.iter().map(|v| v.i64_to_usize(end)).min().unwrap_or_default()); + let from = Some(start as i64); + let to = Some(end as i64); - let format = params.format(); - - Ok(match format { - Format::CSV => OutputLegacy::CSV(Self::columns_to_csv(metrics, from.map(|v| v as i64), to.map(|v| v as i64))?), + let output = match format { + Format::CSV => OutputLegacy::CSV(Self::columns_to_csv(&vecs, start, end)?), Format::JSON => { - if metrics.is_empty() { - return Ok(OutputLegacy::default(format)); - } - - if metrics.len() == 1 { - let metric = metrics[0]; - let count = metric.range_count(from.map(|v| v as i64), to.map(|v| v as i64)); + if vecs.len() == 1 { + let metric = vecs[0]; + let count = metric.range_count(from, to); let mut buf = Vec::new(); if count == 1 { - metric.write_json_value(from, &mut buf)?; + metric.write_json_value(Some(start), &mut buf)?; OutputLegacy::Json(LegacyValue::Value(buf)) } else { - metric.write_json(from, to, &mut buf)?; + metric.write_json(Some(start), Some(end), &mut buf)?; OutputLegacy::Json(LegacyValue::List(buf)) } } else { - let mut values = Vec::with_capacity(metrics.len()); - for vec in metrics { + let mut values = Vec::with_capacity(vecs.len()); + for vec in &vecs { let mut buf = Vec::new(); - vec.write_json(from, to, &mut buf)?; + vec.write_json(Some(start), Some(end), &mut buf)?; values.push(buf); } OutputLegacy::Json(LegacyValue::Matrix(values)) } } + }; + + Ok(MetricOutputLegacy { + output, + version, + total, + start, + end, }) } - - /// Deprecated - use search_and_format instead - pub fn search_and_format_legacy(&self, params: MetricSelection) -> Result { - self.search_and_format_legacy_checked(params, usize::MAX) - } - - /// Deprecated - use search_and_format_checked instead - pub fn search_and_format_legacy_checked(&self, params: MetricSelection, max_weight: usize) -> Result { - let vecs = self.search(¶ms)?; - - let min_len = vecs.iter().map(|v| v.len()).min().expect("search guarantees non-empty"); - let weight = Self::weight(&vecs, params.start(), params.end_for_len(min_len)); - if weight > max_weight { - return Err(Error::WeightExceeded { - requested: weight, - max: max_weight, - }); - } - - self.format_legacy(&vecs, ¶ms.range) - } } diff --git a/crates/brk_query/src/lib.rs b/crates/brk_query/src/lib.rs index e87c96e10..b170d2ed1 100644 --- a/crates/brk_query/src/lib.rs +++ b/crates/brk_query/src/lib.rs @@ -13,20 +13,15 @@ use vecdb::AnyStoredVec; #[cfg(feature = "tokio")] mod r#async; -mod output; +mod resolved; mod vecs; mod r#impl; #[cfg(feature = "tokio")] pub use r#async::*; -pub use brk_types::{ - DataRange, DataRangeFormat, MetricSelection, MetricSelectionLegacy, PaginatedMetrics, - Pagination, PaginationIndex, -}; pub use r#impl::BLOCK_TXS_PAGE_SIZE; -pub use output::{LegacyValue, Output, OutputLegacy}; - +use resolved::ResolvedQuery; pub use vecs::Vecs; #[derive(Clone)] diff --git a/crates/brk_query/src/resolved.rs b/crates/brk_query/src/resolved.rs new file mode 100644 index 000000000..c3310ea4a --- /dev/null +++ b/crates/brk_query/src/resolved.rs @@ -0,0 +1,23 @@ +use brk_types::{Etag, Format}; +use vecdb::AnyExportableVec; + +/// A resolved metric query ready for formatting. +/// Contains the vecs and metadata needed to build an ETag or format the output. +pub struct ResolvedQuery { + pub(crate) vecs: Vec<&'static dyn AnyExportableVec>, + pub(crate) format: Format, + pub(crate) version: u64, + pub(crate) total: usize, + pub(crate) start: usize, + pub(crate) end: usize, +} + +impl ResolvedQuery { + pub fn etag(&self) -> Etag { + Etag::from_metric(self.version, self.total, self.start, self.end) + } + + pub fn format(&self) -> Format { + self.format + } +} diff --git a/crates/brk_server/Cargo.toml b/crates/brk_server/Cargo.toml index 3ea3498ab..be2c9edcf 100644 --- a/crates/brk_server/Cargo.toml +++ b/crates/brk_server/Cargo.toml @@ -18,7 +18,6 @@ brk_error = { workspace = true } brk_fetcher = { workspace = true } brk_indexer = { workspace = true } brk_logger = { workspace = true } -brk_mcp = { workspace = true } brk_query = { workspace = true } brk_reader = { workspace = true } brk_rpc = { workspace = true } diff --git a/crates/brk_server/README.md b/crates/brk_server/README.md index 9d143fb51..ec00ea6c4 100644 --- a/crates/brk_server/README.md +++ b/crates/brk_server/README.md @@ -2,57 +2,50 @@ HTTP API server for Bitcoin on-chain analytics. -## What It Enables +## Features -Serve BRK data via REST API with OpenAPI documentation, response caching, MCP endpoint, and optional static file hosting for web interfaces. - -## Key Features - -- **OpenAPI spec**: Auto-generated documentation at `/api.json` (interactive docs at `/api`) -- **Response caching**: LRU cache with 5000 entries for repeated queries -- **Compression**: Brotli, gzip, deflate, zstd support +- **OpenAPI spec**: Auto-generated docs at `/api` with full spec at `/api.json` +- **LLM-optimized**: Compact spec at `/api.trimmed.json` for AI tools +- **Response caching**: ETag-based with LRU cache (5000 entries) +- **Compression**: Brotli, gzip, deflate, zstd - **Static files**: Optional web interface hosting -- **Request logging**: Colorized status/latency logging -## Core API +## Usage ```rust,ignore let server = Server::new(&async_query, data_path, WebsiteSource::Filesystem(files_path)); // Or WebsiteSource::Embedded, or WebsiteSource::Disabled -server.serve(true).await?; // true enables MCP endpoint +server.serve().await?; ``` -## API Endpoints +## Endpoints | Path | Description | |------|-------------| -| `/api/block-height/{height}` | Block by height | +| `/api` | Interactive API documentation | +| `/api.json` | Full OpenAPI specification | +| `/api.trimmed.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 | | `/api/tx/{txid}` | Transaction details, status, hex | -| `/api/address/{addr}` | Address stats, transactions, UTXOs | +| `/api/mempool` | Fee estimates, mempool stats | | `/api/metrics` | Metric catalog and data queries | -| `/api/v1/mining/*` | Hashrate, difficulty, pools, rewards | -| `/api/mempool/*` | Fee estimates, projected blocks | -| `/mcp` | MCP endpoint (if enabled) | +| `/api/v1/mining/...` | Hashrate, difficulty, pools | ## Caching -Uses ETag-based caching with must-revalidate semantics: -- Height-indexed data: Cache until height changes -- Date-indexed data: Cache with longer TTL -- Mempool data: Short TTL, frequent updates +Uses ETag-based caching with `must-revalidate`: +- **Height-indexed**: Invalidates when new block arrives +- **Immutable**: 1-year cache for deeply-confirmed blocks/txs (6+ confirmations) +- **Mempool**: Short max-age, no ETag ## Configuration -Server binds to port 3110 by default, auto-incrementing if busy (up to 3210). +Binds to port 3110, auto-incrementing up to 3210 if busy. -## Recommended: mimalloc v3 +## Dependencies -Use [mimalloc v3](https://crates.io/crates/mimalloc) as the global allocator to reduce memory usage. - -## Built On - -- `brk_query` for data access -- `brk_mcp` for MCP protocol -- `aide` + `axum` for HTTP routing and OpenAPI -- `tower-http` for compression and tracing +- `brk_query` - data access +- `aide` + `axum` - HTTP routing and OpenAPI +- `tower-http` - compression and tracing diff --git a/crates/brk_server/examples/server.rs b/crates/brk_server/examples/server.rs index 653b95c9f..33c79bc25 100644 --- a/crates/brk_server/examples/server.rs +++ b/crates/brk_server/examples/server.rs @@ -56,7 +56,7 @@ fn run() -> Result<()> { runtime.block_on(async move { let server = Server::new(&query, outputs_dir, WebsiteSource::Disabled); - let handle = tokio::spawn(async move { server.serve(true).await }); + let handle = tokio::spawn(async move { server.serve().await }); // Await the handle to catch both panics and errors match handle.await { diff --git a/crates/brk_server/src/api/blk_reader.rs b/crates/brk_server/src/api/blk_reader.rs deleted file mode 100644 index 138d8d107..000000000 --- a/crates/brk_server/src/api/blk_reader.rs +++ /dev/null @@ -1,59 +0,0 @@ -// use lru::LruCache; -// use std::num::NonZeroUsize; - -// struct SmartBlkReader { -// // LRU cache of recently accessed files (memory mapped) -// mmap_cache: LruCache, -// // Fallback to direct file I/O for cache misses -// max_cached_files: usize, -// } - -// impl SmartBlkReader { -// fn new(max_cached: usize) -> Self { -// Self { -// mmap_cache: LruCache::new(NonZeroUsize::new(max_cached).unwrap()), -// max_cached_files: max_cached, -// } -// } - -// fn get_transaction( -// &mut self, -// file_path: &str, -// offset: u64, -// length: usize, -// ) -> Result> { -// // Try cache first -// if let Some(mmap) = self.mmap_cache.get(file_path) { -// let tx_data = &mmap[offset as usize..(offset as usize + length)]; -// let mut cursor = std::io::Cursor::new(tx_data); -// return Ok(bitcoin::consensus::Decodable::consensus_decode( -// &mut cursor, -// )?); -// } - -// // Cache miss - use direct I/O and potentially cache the file -// let mut file = File::open(file_path)?; -// file.seek(SeekFrom::Start(offset))?; - -// let mut buffer = vec![0u8; length]; -// file.read_exact(&mut buffer)?; - -// // Optionally add to cache (based on access patterns) -// if self.should_cache_file(file_path) { -// let file_for_mmap = File::open(file_path)?; -// if let Ok(mmap) = unsafe { memmap2::MmapOptions::new().map(&file_for_mmap) } { -// self.mmap_cache.put(file_path.to_string(), mmap); -// } -// } - -// let mut cursor = std::io::Cursor::new(&buffer); -// Ok(bitcoin::consensus::Decodable::consensus_decode( -// &mut cursor, -// )?) -// } - -// fn should_cache_file(&self, _file_path: &str) -> bool { -// // Implement logic: recent files, frequently accessed files, etc. -// true -// } -// } diff --git a/crates/brk_server/src/api/blocks/mod.rs b/crates/brk_server/src/api/blocks/mod.rs index 14a004977..d56414993 100644 --- a/crates/brk_server/src/api/blocks/mod.rs +++ b/crates/brk_server/src/api/blocks/mod.rs @@ -44,7 +44,7 @@ impl BlockRoutes for ApiRouter { async |headers: HeaderMap, Path(path): Path, State(state): State| { - state.cached_json(&headers, CacheStrategy::Height, move |q| q.block(&path.hash)).await + state.cached_json(&headers, CacheStrategy::Static, move |q| q.block(&path.hash)).await }, |op| { op.id("get_block") @@ -135,7 +135,7 @@ impl BlockRoutes for ApiRouter { async |headers: HeaderMap, Path(path): Path, State(state): State| { - state.cached_json(&headers, CacheStrategy::Height, move |q| q.block_txids(&path.hash)).await + state.cached_json(&headers, CacheStrategy::Static, move |q| q.block_txids(&path.hash)).await }, |op| { op.id("get_block_txids") @@ -158,7 +158,7 @@ impl BlockRoutes for ApiRouter { async |headers: HeaderMap, Path(path): Path, State(state): State| { - state.cached_json(&headers, CacheStrategy::Height, move |q| q.block_txs(&path.hash, path.start_index)).await + state.cached_json(&headers, CacheStrategy::Static, move |q| q.block_txs(&path.hash, path.start_index)).await }, |op| { op.id("get_block_txs") @@ -182,7 +182,7 @@ impl BlockRoutes for ApiRouter { async |headers: HeaderMap, Path(path): Path, State(state): State| { - state.cached_text(&headers, CacheStrategy::Height, move |q| q.block_txid_at_index(&path.hash, path.index).map(|t| t.to_string())).await + state.cached_text(&headers, CacheStrategy::Static, move |q| q.block_txid_at_index(&path.hash, path.index).map(|t| t.to_string())).await }, |op| { op.id("get_block_txid") @@ -226,7 +226,7 @@ impl BlockRoutes for ApiRouter { async |headers: HeaderMap, Path(path): Path, State(state): State| { - state.cached_bytes(&headers, CacheStrategy::Height, move |q| q.block_raw(&path.hash)).await + state.cached_bytes(&headers, CacheStrategy::Static, move |q| q.block_raw(&path.hash)).await }, |op| { op.id("get_block_raw") diff --git a/crates/brk_server/src/api/metrics/bulk.rs b/crates/brk_server/src/api/metrics/bulk.rs index d96558397..bb703ec05 100644 --- a/crates/brk_server/src/api/metrics/bulk.rs +++ b/crates/brk_server/src/api/metrics/bulk.rs @@ -6,12 +6,12 @@ use axum::{ http::{HeaderMap, StatusCode, Uri}, response::{IntoResponse, Response}, }; -use brk_query::{MetricSelection, Output}; -use brk_types::Format; +use brk_types::{Format, MetricSelection, Output}; use quick_cache::sync::GuardResult; use crate::{ - CacheStrategy, api::metrics::MAX_WEIGHT, cache::CacheParams, extended::HeaderMapExtended, + api::metrics::{CACHE_CONTROL, MAX_WEIGHT}, + extended::HeaderMapExtended, }; use super::AppState; @@ -39,35 +39,32 @@ async fn req_to_response_res( Query(params): Query, AppState { query, cache, .. }: AppState, ) -> brk_error::Result { - let format = params.format(); - let height = query.sync(|q| q.height()); + // Phase 1: Search and resolve metadata (cheap) + let resolved = query + .run(move |q| q.resolve(params, MAX_WEIGHT)) + .await?; - let cache_params = - CacheParams::resolve(&CacheStrategy::height_with(params.etag_suffix()), || { - height.into() - }); + let format = resolved.format(); + let etag = resolved.etag(); - if cache_params.matches_etag(&headers) { + // Check if client has fresh cache + if headers.has_etag(etag.as_str()) { let mut response = (StatusCode::NOT_MODIFIED, "").into_response(); response.headers_mut().insert_cors(); return Ok(response); } - let cache_key = format!( - "{}{}{}", - uri.path(), - uri.query().unwrap_or(""), - cache_params.etag_str() - ); + // Check server-side cache + let cache_key = format!("bulk-{}{}{}", uri.path(), uri.query().unwrap_or(""), etag); let guard_res = cache.get_value_or_guard(&cache_key, Some(Duration::from_millis(50))); let mut response = if let GuardResult::Value(v) = guard_res { Response::new(Body::from(v)) } else { - match query - .run(move |q| q.search_and_format_bulk_checked(params, MAX_WEIGHT)) - .await? - { + // Phase 2: Format (expensive, only on cache miss) + let metric_output = query.run(move |q| q.format(resolved)).await?; + + match metric_output.output { Output::CSV(s) => { if let GuardResult::Guard(g) = guard_res { let _ = g.insert(s.clone().into()); @@ -75,21 +72,18 @@ async fn req_to_response_res( s.into_response() } Output::Json(v) => { - let json = v.to_vec(); if let GuardResult::Guard(g) = guard_res { - let _ = g.insert(json.clone().into()); + let _ = g.insert(v.clone().into()); } - json.into_response() + Response::new(Body::from(v)) } } }; let headers = response.headers_mut(); headers.insert_cors(); - if let Some(etag) = &cache_params.etag { - headers.insert_etag(etag); - } - headers.insert_cache_control(&cache_params.cache_control); + headers.insert_etag(etag.as_str()); + headers.insert_cache_control(CACHE_CONTROL); match format { Format::CSV => { diff --git a/crates/brk_server/src/api/metrics/data.rs b/crates/brk_server/src/api/metrics/data.rs index d41ab5006..0dd4ba3d7 100644 --- a/crates/brk_server/src/api/metrics/data.rs +++ b/crates/brk_server/src/api/metrics/data.rs @@ -6,12 +6,12 @@ use axum::{ http::{HeaderMap, StatusCode, Uri}, response::{IntoResponse, Response}, }; -use brk_query::{MetricSelection, Output}; -use brk_types::Format; +use brk_types::{Format, MetricSelection, Output}; use quick_cache::sync::GuardResult; use crate::{ - CacheStrategy, api::metrics::MAX_WEIGHT, cache::CacheParams, extended::HeaderMapExtended, + api::metrics::{CACHE_CONTROL, MAX_WEIGHT}, + extended::HeaderMapExtended, }; use super::AppState; @@ -39,35 +39,32 @@ async fn req_to_response_res( Query(params): Query, AppState { query, cache, .. }: AppState, ) -> brk_error::Result { - let format = params.format(); - let height = query.sync(|q| q.height()); + // Phase 1: Search and resolve metadata (cheap) + let resolved = query + .run(move |q| q.resolve(params, MAX_WEIGHT)) + .await?; - let cache_params = - CacheParams::resolve(&CacheStrategy::height_with(params.etag_suffix()), || { - height.into() - }); + let format = resolved.format(); + let etag = resolved.etag(); - if cache_params.matches_etag(&headers) { + // Check if client has fresh cache + if headers.has_etag(etag.as_str()) { let mut response = (StatusCode::NOT_MODIFIED, "").into_response(); response.headers_mut().insert_cors(); return Ok(response); } - let cache_key = format!( - "single-{}{}{}", - uri.path(), - uri.query().unwrap_or(""), - cache_params.etag_str() - ); + // Check server-side cache + let cache_key = format!("single-{}{}{}", uri.path(), uri.query().unwrap_or(""), etag); let guard_res = cache.get_value_or_guard(&cache_key, Some(Duration::from_millis(50))); let mut response = if let GuardResult::Value(v) = guard_res { Response::new(Body::from(v)) } else { - match query - .run(move |q| q.search_and_format_checked(params, MAX_WEIGHT)) - .await? - { + // Phase 2: Format (expensive, only on cache miss) + let metric_output = query.run(move |q| q.format(resolved)).await?; + + match metric_output.output { Output::CSV(s) => { if let GuardResult::Guard(g) = guard_res { let _ = g.insert(s.clone().into()); @@ -75,21 +72,18 @@ async fn req_to_response_res( s.into_response() } Output::Json(v) => { - let json = v.to_vec(); if let GuardResult::Guard(g) = guard_res { - let _ = g.insert(json.clone().into()); + let _ = g.insert(v.clone().into()); } - json.into_response() + Response::new(Body::from(v)) } } }; let headers = response.headers_mut(); headers.insert_cors(); - if let Some(etag) = &cache_params.etag { - headers.insert_etag(etag); - } - headers.insert_cache_control(&cache_params.cache_control); + headers.insert_etag(etag.as_str()); + headers.insert_cache_control(CACHE_CONTROL); match format { Format::CSV => { diff --git a/crates/brk_server/src/api/metrics/legacy.rs b/crates/brk_server/src/api/metrics/legacy.rs index b3349345a..4ced894a1 100644 --- a/crates/brk_server/src/api/metrics/legacy.rs +++ b/crates/brk_server/src/api/metrics/legacy.rs @@ -6,12 +6,12 @@ use axum::{ http::{HeaderMap, StatusCode, Uri}, response::{IntoResponse, Response}, }; -use brk_query::{MetricSelection, OutputLegacy}; -use brk_types::Format; +use brk_types::{Format, MetricSelection, OutputLegacy}; use quick_cache::sync::GuardResult; use crate::{ - CacheStrategy, api::metrics::MAX_WEIGHT, cache::CacheParams, extended::HeaderMapExtended, + api::metrics::{CACHE_CONTROL, MAX_WEIGHT}, + extended::HeaderMapExtended, }; use super::AppState; @@ -39,35 +39,34 @@ async fn req_to_response_res( Query(params): Query, AppState { query, cache, .. }: AppState, ) -> brk_error::Result { - let format = params.format(); - let height = query.sync(|q| q.height()); + // Phase 1: Search and resolve metadata (cheap) + let resolved = query + .run(move |q| q.resolve(params, MAX_WEIGHT)) + .await?; - let cache_params = - CacheParams::resolve(&CacheStrategy::height_with(params.etag_suffix()), || { - height.into() - }); + let format = resolved.format(); + let etag = resolved.etag(); - if cache_params.matches_etag(&headers) { + // Check if client has fresh cache + if headers.has_etag(etag.as_str()) { let mut response = (StatusCode::NOT_MODIFIED, "").into_response(); response.headers_mut().insert_cors(); return Ok(response); } - let cache_key = format!( - "legacy-{}{}{}", - uri.path(), - uri.query().unwrap_or(""), - cache_params.etag_str() - ); + // Check server-side cache + let cache_key = format!("legacy-{}{}{}", uri.path(), uri.query().unwrap_or(""), etag); let guard_res = cache.get_value_or_guard(&cache_key, Some(Duration::from_millis(50))); let mut response = if let GuardResult::Value(v) = guard_res { Response::new(Body::from(v)) } else { - match query - .run(move |q| q.search_and_format_legacy_checked(params, MAX_WEIGHT)) - .await? - { + // Phase 2: Format (expensive, only on cache miss) + let metric_output = query + .run(move |q| q.format_legacy(resolved)) + .await?; + + match metric_output.output { OutputLegacy::CSV(s) => { if let GuardResult::Guard(g) = guard_res { let _ = g.insert(s.clone().into()); @@ -86,10 +85,8 @@ async fn req_to_response_res( let headers = response.headers_mut(); headers.insert_cors(); - if let Some(etag) = &cache_params.etag { - headers.insert_etag(etag); - } - headers.insert_cache_control(&cache_params.cache_control); + headers.insert_etag(etag.as_str()); + headers.insert_cache_control(CACHE_CONTROL); match format { Format::CSV => { diff --git a/crates/brk_server/src/api/metrics/mod.rs b/crates/brk_server/src/api/metrics/mod.rs index 126d94707..ff27c7c2a 100644 --- a/crates/brk_server/src/api/metrics/mod.rs +++ b/crates/brk_server/src/api/metrics/mod.rs @@ -4,13 +4,10 @@ use axum::{ http::{HeaderMap, Uri}, response::{IntoResponse, Response}, }; -use brk_query::{ - DataRangeFormat, MetricSelection, MetricSelectionLegacy, PaginatedMetrics, Pagination, -}; use brk_traversable::TreeNode; use brk_types::{ - Index, IndexInfo, LimitParam, Metric, MetricCount, MetricData, MetricParam, MetricWithIndex, - Metrics, + DataRangeFormat, Index, IndexInfo, LimitParam, Metric, MetricCount, MetricData, MetricParam, + MetricSelection, MetricSelectionLegacy, MetricWithIndex, Metrics, PaginatedMetrics, Pagination, }; use crate::{CacheStrategy, extended::TransformResponseExtended}; @@ -23,6 +20,8 @@ mod legacy; /// Maximum allowed request weight in bytes (650KB) const MAX_WEIGHT: usize = 65 * 10_000; +/// Cache control header for metric data responses +const CACHE_CONTROL: &str = "public, max-age=1, must-revalidate"; pub trait ApiMetricsRoutes { fn add_metrics_routes(self) -> Self; @@ -250,7 +249,8 @@ impl ApiMetricsRoutes for ApiRouter { Query(params): Query, state: State| -> Response { - legacy::handler(uri, headers, Query(params.into()), state).await + let params: MetricSelection = params.into(); + legacy::handler(uri, headers, Query(params), state).await }, |op| op .metrics_tag() diff --git a/crates/brk_server/src/api/mining/mod.rs b/crates/brk_server/src/api/mining/mod.rs index b9d02b6ab..9701df3ad 100644 --- a/crates/brk_server/src/api/mining/mod.rs +++ b/crates/brk_server/src/api/mining/mod.rs @@ -64,7 +64,7 @@ impl MiningRoutes for ApiRouter { "/api/v1/mining/pools/{time_period}", get_with( async |headers: HeaderMap, Path(path): Path, State(state): State| { - state.cached_json(&headers, CacheStrategy::height_with(format!("{:?}", path.time_period)), move |q| q.mining_pools(path.time_period)).await + state.cached_json(&headers, CacheStrategy::Height, move |q| q.mining_pools(path.time_period)).await }, |op| { op.id("get_pool_stats") @@ -81,7 +81,7 @@ impl MiningRoutes for ApiRouter { "/api/v1/mining/pool/{slug}", get_with( async |headers: HeaderMap, Path(path): Path, State(state): State| { - state.cached_json(&headers, CacheStrategy::height_with(path.slug), move |q| q.pool_detail(path.slug)).await + state.cached_json(&headers, CacheStrategy::Height, move |q| q.pool_detail(path.slug)).await }, |op| { op.id("get_pool") @@ -99,7 +99,7 @@ impl MiningRoutes for ApiRouter { "/api/v1/mining/hashrate", get_with( async |headers: HeaderMap, State(state): State| { - state.cached_json(&headers, CacheStrategy::height_with("hashrate"), |q| q.hashrate(None)).await + state.cached_json(&headers, CacheStrategy::Height, |q| q.hashrate(None)).await }, |op| { op.id("get_hashrate") @@ -116,7 +116,7 @@ impl MiningRoutes for ApiRouter { "/api/v1/mining/hashrate/{time_period}", get_with( async |headers: HeaderMap, Path(path): Path, State(state): State| { - state.cached_json(&headers, CacheStrategy::height_with(format!("hashrate-{:?}", path.time_period)), move |q| q.hashrate(Some(path.time_period))).await + state.cached_json(&headers, CacheStrategy::Height, move |q| q.hashrate(Some(path.time_period))).await }, |op| { op.id("get_hashrate_by_period") @@ -133,7 +133,7 @@ impl MiningRoutes for ApiRouter { "/api/v1/mining/difficulty-adjustments", get_with( async |headers: HeaderMap, State(state): State| { - state.cached_json(&headers, CacheStrategy::height_with("diff-adj"), |q| q.difficulty_adjustments(None)).await + state.cached_json(&headers, CacheStrategy::Height, |q| q.difficulty_adjustments(None)).await }, |op| { op.id("get_difficulty_adjustments") @@ -150,7 +150,7 @@ impl MiningRoutes for ApiRouter { "/api/v1/mining/difficulty-adjustments/{time_period}", get_with( async |headers: HeaderMap, Path(path): Path, State(state): State| { - state.cached_json(&headers, CacheStrategy::height_with(format!("diff-adj-{:?}", path.time_period)), move |q| q.difficulty_adjustments(Some(path.time_period))).await + state.cached_json(&headers, CacheStrategy::Height, move |q| q.difficulty_adjustments(Some(path.time_period))).await }, |op| { op.id("get_difficulty_adjustments_by_period") @@ -167,7 +167,7 @@ impl MiningRoutes for ApiRouter { "/api/v1/mining/blocks/fees/{time_period}", get_with( async |headers: HeaderMap, Path(path): Path, State(state): State| { - state.cached_json(&headers, CacheStrategy::height_with(format!("fees-{:?}", path.time_period)), move |q| q.block_fees(path.time_period)).await + state.cached_json(&headers, CacheStrategy::Height, move |q| q.block_fees(path.time_period)).await }, |op| { op.id("get_block_fees") @@ -184,7 +184,7 @@ impl MiningRoutes for ApiRouter { "/api/v1/mining/blocks/rewards/{time_period}", get_with( async |headers: HeaderMap, Path(path): Path, State(state): State| { - state.cached_json(&headers, CacheStrategy::height_with(format!("rewards-{:?}", path.time_period)), move |q| q.block_rewards(path.time_period)).await + state.cached_json(&headers, CacheStrategy::Height, move |q| q.block_rewards(path.time_period)).await }, |op| { op.id("get_block_rewards") @@ -219,7 +219,7 @@ impl MiningRoutes for ApiRouter { "/api/v1/mining/blocks/sizes-weights/{time_period}", get_with( async |headers: HeaderMap, Path(path): Path, State(state): State| { - state.cached_json(&headers, CacheStrategy::height_with(format!("sizes-{:?}", path.time_period)), move |q| q.block_sizes_weights(path.time_period)).await + state.cached_json(&headers, CacheStrategy::Height, move |q| q.block_sizes_weights(path.time_period)).await }, |op| { op.id("get_block_sizes_weights") @@ -236,7 +236,7 @@ impl MiningRoutes for ApiRouter { "/api/v1/mining/reward-stats/{block_count}", get_with( async |headers: HeaderMap, Path(path): Path, State(state): State| { - state.cached_json(&headers, CacheStrategy::height_with(format!("reward-stats-{}", path.block_count)), move |q| q.reward_stats(path.block_count)).await + state.cached_json(&headers, CacheStrategy::Height, move |q| q.reward_stats(path.block_count)).await }, |op| { op.id("get_reward_stats") diff --git a/crates/brk_server/src/api/mod.rs b/crates/brk_server/src/api/mod.rs index d25a3f41f..a6bd4c31a 100644 --- a/crates/brk_server/src/api/mod.rs +++ b/crates/brk_server/src/api/mod.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use aide::{ - axum::ApiRouter, + axum::{ApiRouter, routing::get_with}, openapi::OpenApi, }; use axum::{ @@ -12,13 +12,12 @@ use axum::{ }; use crate::{ - VERSION, api::{ addresses::AddressRoutes, blocks::BlockRoutes, mempool::MempoolRoutes, metrics::ApiMetricsRoutes, mining::MiningRoutes, server::ServerRoutes, transactions::TxRoutes, }, - extended::{HeaderMapExtended, ResponseExtended}, + extended::{ResponseExtended, TransformResponseExtended}, }; use super::AppState; @@ -48,19 +47,39 @@ impl ApiRoutes for ApiRouter { .add_metrics_routes() .add_server_routes() .route("/api/server", get(Redirect::temporary("/api#tag/server"))) - .route( + .api_route( "/api.json", - get( + get_with( async |headers: HeaderMap, Extension(api): Extension>| + -> Response { Response::static_json(&headers, &*api) }, + |op| { + op.id("get_openapi") + .server_tag() + .summary("OpenAPI specification") + .description("Full OpenAPI 3.1 specification for this API.") + }, + ), + ) + .api_route( + "/api.trimmed.json", + get_with( + async |headers: HeaderMap, + Extension(api_trimmed): Extension>| -> Response { - let etag = VERSION; - - if headers.has_etag(etag) { - return Response::new_not_modified(); - } - - Response::new_json(&api, etag) + let value: serde_json::Value = + serde_json::from_str(&api_trimmed).unwrap(); + Response::static_json(&headers, &value) + }, + |op| { + op.id("get_openapi_trimmed") + .server_tag() + .summary("Trimmed OpenAPI specification") + .description( + "Compact OpenAPI specification optimized for LLM consumption. \ + Removes redundant fields while preserving essential API information.", + ) + .ok_response::() }, ), ) diff --git a/crates/brk_server/src/api/openapi.rs b/crates/brk_server/src/api/openapi/mod.rs similarity index 94% rename from crates/brk_server/src/api/openapi.rs rename to crates/brk_server/src/api/openapi/mod.rs index 7718990f3..6c368de2c 100644 --- a/crates/brk_server/src/api/openapi.rs +++ b/crates/brk_server/src/api/openapi/mod.rs @@ -1,5 +1,3 @@ -use aide::openapi::{Contact, Info, License, OpenApi, Tag}; - // // https://docs.rs/schemars/latest/schemars/derive.JsonSchema.html // @@ -12,6 +10,12 @@ use aide::openapi::{Contact, Info, License, OpenApi, Tag}; // - https://api.supabase.com/api/v1 // +mod trim; + +pub use trim::trim_openapi_json; + +use aide::openapi::{Contact, Info, License, OpenApi, Tag}; + use crate::VERSION; pub fn create_openapi() -> OpenApi { @@ -25,10 +29,11 @@ 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 ### Client Libraries -- [JavaScript/TypeScript](https://www.npmjs.com/package/brk-client) +- [JavaScript](https://www.npmjs.com/package/brk-client) - [Python](https://pypi.org/project/brk-client/) - [Rust](https://crates.io/crates/brk_client) @@ -56,6 +61,13 @@ pub fn create_openapi() -> OpenApi { }; let tags = vec![ + Tag { + name: "Server".to_string(), + description: Some( + "API metadata, health monitoring, and OpenAPI specifications.".to_string(), + ), + ..Default::default() + }, Tag { name: "Metrics".to_string(), description: Some( @@ -115,14 +127,6 @@ pub fn create_openapi() -> OpenApi { ), ..Default::default() }, - Tag { - name: "Server".to_string(), - description: Some( - "API metadata and health monitoring. Version information and service status." - .to_string(), - ), - ..Default::default() - }, ]; OpenApi { diff --git a/crates/brk_server/src/api/openapi/trim.rs b/crates/brk_server/src/api/openapi/trim.rs new file mode 100644 index 000000000..f1f783a78 --- /dev/null +++ b/crates/brk_server/src/api/openapi/trim.rs @@ -0,0 +1,447 @@ +use serde_json::{Map, Value}; + +/// Trims an OpenAPI spec JSON to reduce size for LLM consumption. +/// Removes redundant fields while preserving essential API information. +/// +/// Transformations applied (in order): +/// 1. Remove error responses (304, 400, 404, 500) +/// 2. Compact responses to "returns": "Type" +/// 3. Remove per-endpoint tags and style +/// 4. Simplify parameter schema to type, remove param descriptions +/// 5. Remove summary +/// 6. Remove examples, replace $ref with type +/// 7. Flatten single-item allOf +/// 8. Flatten anyOf to type array +/// 9. Remove format +/// 10. Remove property descriptions +/// 11. Simplify properties to direct types +pub fn trim_openapi_json(json: &str) -> String { + let mut spec: Value = serde_json::from_str(json).expect("Invalid OpenAPI JSON"); + trim_value(&mut spec); + serde_json::to_string(&spec).unwrap() +} + +fn trim_value(value: &mut Value) { + match value { + Value::Object(obj) => { + // Step 1: Remove error responses + if let Some(Value::Object(responses)) = obj.get_mut("responses") { + for code in &["304", "400", "404", "500"] { + responses.remove(*code); + } + } + + // Step 2: Compact responses to "returns": "Type" + if let Some(Value::Object(responses)) = obj.remove("responses") + && let Some(returns) = extract_return_type(&responses) + { + obj.insert("returns".to_string(), Value::String(returns)); + } + + // Step 3: Remove per-endpoint tags and style + // (only remove "tags" if it's an array, not if it's the top-level tags definition) + if let Some(Value::Array(_)) = obj.get("tags") { + // This is a per-endpoint tags array like ["Addresses"], remove it + obj.remove("tags"); + } + obj.remove("style"); + + // Step 4: Simplify parameters (schema to type, remove descriptions) + if let Some(Value::Array(params)) = obj.get_mut("parameters") { + for param in params { + simplify_parameter(param); + } + } + + // Step 5: Remove summary + obj.remove("summary"); + + // Step 6: Remove examples, replace $ref with type + obj.remove("example"); + obj.remove("examples"); + if let Some(Value::String(ref_path)) = obj.remove("$ref") { + let type_name = ref_path.split('/').next_back().unwrap_or("any"); + obj.insert("type".to_string(), Value::String(type_name.to_string())); + } + + // Step 7: Flatten single-item allOf + if let Some(Value::Array(all_of)) = obj.remove("allOf") + && all_of.len() == 1 + && let Some(Value::Object(inner)) = all_of.into_iter().next() + { + for (k, v) in inner { + obj.insert(k, v); + } + } + + // Step 8: Flatten anyOf to type array + if let Some(Value::Array(any_of)) = obj.remove("anyOf") { + let types: Vec = any_of + .into_iter() + .filter_map(|item| { + if let Value::Object(o) = item { + if let Some(Value::String(ref_path)) = o.get("$ref") { + return Some(Value::String( + ref_path.split('/').next_back().unwrap_or("any").to_string(), + )); + } + o.get("type").cloned() + } else { + None + } + }) + .collect(); + if !types.is_empty() { + obj.insert("type".to_string(), Value::Array(types)); + } + } + + // Step 9: Remove format + obj.remove("format"); + + // Step 10 & 11: Simplify properties (remove descriptions, simplify to direct types) + if let Some(Value::Object(props)) = obj.get_mut("properties") { + simplify_properties(props); + } + + // Recurse into remaining values + for (_, v) in obj.iter_mut() { + trim_value(v); + } + } + Value::Array(arr) => { + for item in arr { + trim_value(item); + } + } + _ => {} + } +} + +fn extract_return_type(responses: &Map) -> Option { + let resp_200 = responses.get("200")?; + let content = resp_200.get("content")?; + let json_content = content.get("application/json")?; + let schema = json_content.get("schema")?; + Some(schema_to_type_string(schema)) +} + +fn schema_to_type_string(schema: &Value) -> String { + if let Some(Value::String(ref_path)) = schema.get("$ref") { + return ref_path.split('/').next_back().unwrap_or("any").to_string(); + } + if let Some(Value::String(t)) = schema.get("type") { + if t == "array" + && let Some(items) = schema.get("items") + { + return format!("array[{}]", schema_to_type_string(items)); + } + return t.clone(); + } + "any".to_string() +} + +fn simplify_parameter(param: &mut Value) { + if let Value::Object(obj) = param { + // Remove description + obj.remove("description"); + + // Extract type from schema + if let Some(schema) = obj.remove("schema") { + let type_val = extract_type_from_schema(&schema); + obj.insert("type".to_string(), type_val); + } + } +} + +fn extract_type_from_schema(schema: &Value) -> Value { + if let Value::Object(obj) = schema { + // Handle anyOf (optional fields) + if let Some(Value::Array(any_of)) = obj.get("anyOf") { + let types: Vec = any_of + .iter() + .filter_map(|item| { + if let Value::Object(o) = item { + if let Some(Value::String(ref_path)) = o.get("$ref") { + return Some(Value::String( + ref_path.split('/').next_back().unwrap_or("any").to_string(), + )); + } + o.get("type").cloned() + } else { + None + } + }) + .collect(); + if types.len() == 1 { + return types.into_iter().next().unwrap(); + } + return Value::Array(types); + } + + // Handle $ref + if let Some(Value::String(ref_path)) = obj.get("$ref") { + return Value::String(ref_path.split('/').next_back().unwrap_or("any").to_string()); + } + + // Handle type + if let Some(t) = obj.get("type") { + return t.clone(); + } + } + Value::String("any".to_string()) +} + +fn simplify_properties(props: &mut Map) { + let keys: Vec = props.keys().cloned().collect(); + for key in keys { + if let Some(prop_value) = props.get_mut(&key) + && let Value::Object(prop_obj) = prop_value + { + // Remove description + prop_obj.remove("description"); + + // Check if we can simplify to just the type + let simplified = simplify_property_value(prop_obj); + *prop_value = simplified; + } + } +} + +fn simplify_property_value(obj: &mut Map) -> Value { + // Remove validation constraints + for key in &["default", "minItems", "maxItems", "uniqueItems"] { + obj.remove(*key); + } + + // Handle $ref - convert to type (runs before recursion would) + if let Some(Value::String(ref_path)) = obj.remove("$ref") { + let type_name = ref_path.split('/').next_back().unwrap_or("any"); + return Value::String(type_name.to_string()); + } + + // Handle single-item allOf - flatten and extract type + if let Some(Value::Array(all_of)) = obj.remove("allOf") + && all_of.len() == 1 + && let Some(Value::Object(inner)) = all_of.into_iter().next() + { + if let Some(Value::String(ref_path)) = inner.get("$ref") { + let type_name = ref_path.split('/').next_back().unwrap_or("any"); + return Value::String(type_name.to_string()); + } + if let Some(t) = inner.get("type") { + return t.clone(); + } + } + + // Handle anyOf - flatten to type array (runs before recursion would) + if let Some(Value::Array(any_of)) = obj.remove("anyOf") { + let types: Vec = any_of + .into_iter() + .filter_map(|item| { + if let Value::Object(o) = item { + if let Some(Value::String(ref_path)) = o.get("$ref") { + return Some(Value::String( + ref_path.split('/').next_back().unwrap_or("any").to_string(), + )); + } + o.get("type").cloned() + } else { + None + } + }) + .collect(); + return Value::Array(types); + } + + // If only "type" remains, return just the type value + if obj.len() == 1 + && let Some(t) = obj.get("type") + { + return t.clone(); + } + + // Handle array with items + if obj.get("type") == Some(&Value::String("array".to_string())) + && let Some(items) = obj.get("items") + && let Value::Object(items_obj) = items + && items_obj.len() == 1 + { + // Items can have either "type" or "$ref" + if let Some(Value::String(item_type)) = items_obj.get("type") { + return Value::String(format!("array[{}]", item_type)); + } + if let Some(Value::String(ref_path)) = items_obj.get("$ref") { + let type_name = ref_path.split('/').next_back().unwrap_or("any"); + return Value::String(format!("array[{}]", type_name)); + } + } + + Value::Object(obj.clone()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_trim_property_anyof() { + let input = r##"{ + "type": "object", + "properties": { + "index": { + "anyOf": [ + {"type": "TxIndex"}, + {"type": "null"} + ] + } + } + }"##; + + let result = trim_openapi_json(input); + let parsed: Value = serde_json::from_str(&result).unwrap(); + + // Property should be simplified to array, not {"type": [...]} + let index = &parsed["properties"]["index"]; + assert!(index.is_array(), "Expected array, got: {}", index); + assert_eq!(index[0], "TxIndex"); + assert_eq!(index[1], "null"); + } + + #[test] + fn test_trim_parameter_anyof() { + let input = r##"{ + "parameters": [{ + "in": "query", + "name": "after_txid", + "schema": { + "anyOf": [ + {"$ref": "#/components/schemas/Txid"}, + {"type": "null"} + ] + } + }] + }"##; + + let result = trim_openapi_json(input); + let parsed: Value = serde_json::from_str(&result).unwrap(); + + // Parameter should have type array including null + let param = &parsed["parameters"][0]; + assert_eq!(param["name"], "after_txid"); + assert_eq!(param["type"][0], "Txid"); + assert_eq!(param["type"][1], "null"); + } + + #[test] + fn test_trim_property_ref() { + let input = r##"{ + "type": "object", + "properties": { + "txid": { + "$ref": "#/components/schemas/Txid" + } + } + }"##; + + let result = trim_openapi_json(input); + let parsed: Value = serde_json::from_str(&result).unwrap(); + + // Property with $ref should be simplified to just the type name + assert_eq!(parsed["properties"]["txid"], "Txid"); + } + + #[test] + fn test_trim_nested_component_schema() { + // This matches the actual API structure: components > schemas > Type > properties + let input = r##"{ + "components": { + "schemas": { + "AddressStats": { + "type": "object", + "properties": { + "address": { + "$ref": "#/components/schemas/Address" + }, + "chain_stats": { + "$ref": "#/components/schemas/AddressChainStats" + } + } + } + } + } + }"##; + + let result = trim_openapi_json(input); + let parsed: Value = serde_json::from_str(&result).unwrap(); + + let props = &parsed["components"]["schemas"]["AddressStats"]["properties"]; + assert_eq!(props["address"], "Address", "address should be simplified"); + assert_eq!(props["chain_stats"], "AddressChainStats", "chain_stats should be simplified"); + } + + #[test] + fn test_trim_property_allof_with_ref() { + // Real API uses allOf wrapper around $ref + let input = r##"{ + "type": "object", + "properties": { + "address": { + "description": "Bitcoin address string", + "allOf": [ + {"$ref": "#/components/schemas/Address"} + ] + } + } + }"##; + + let result = trim_openapi_json(input); + let parsed: Value = serde_json::from_str(&result).unwrap(); + + assert_eq!(parsed["properties"]["address"], "Address"); + } + + #[test] + fn test_trim_property_array_with_ref() { + let input = r##"{ + "type": "object", + "properties": { + "vin": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TxIn" + } + } + } + }"##; + + let result = trim_openapi_json(input); + let parsed: Value = serde_json::from_str(&result).unwrap(); + + // Array with $ref items should be simplified to "array[Type]" + assert_eq!(parsed["properties"]["vin"], "array[TxIn]"); + } + + #[test] + fn test_trim_responses_to_returns() { + let input = r##"{ + "responses": { + "200": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Block"} + } + } + }, + "400": {"description": "Bad request"}, + "500": {"description": "Error"} + } + }"##; + + let result = trim_openapi_json(input); + let parsed: Value = serde_json::from_str(&result).unwrap(); + + assert_eq!(parsed["returns"], "Block"); + assert!(parsed.get("responses").is_none()); + } +} diff --git a/crates/brk_server/src/cache.rs b/crates/brk_server/src/cache.rs index 4ea01dfe7..4dfb34a22 100644 --- a/crates/brk_server/src/cache.rs +++ b/crates/brk_server/src/cache.rs @@ -2,44 +2,14 @@ use axum::http::HeaderMap; use crate::{VERSION, extended::HeaderMapExtended}; -/// Minimum confirmations before data is considered immutable -pub const MIN_CONFIRMATIONS: u32 = 6; - /// Cache strategy for HTTP responses. -/// -/// # Future optimization: Immutable caching for blocks/txs -/// -/// The `Immutable` variant supports caching deeply-confirmed blocks/txs forever -/// (1 year, `immutable` directive). To use it, you need the confirmation count: -/// -/// ```ignore -/// // Example: cache block by hash as immutable if deeply confirmed -/// let confirmations = current_height - block_height + 1; -/// let prefix = *BlockHashPrefix::from(&hash); -/// state.cached_json(&headers, CacheStrategy::immutable(prefix, confirmations), |q| q.block(&hash)).await -/// ``` -/// -/// Currently all block/tx handlers use `Height` for simplicity since determining -/// confirmations requires knowing the block height upfront (an extra lookup). -/// This could be optimized by either: -/// 1. Including confirmation count in the response type -/// 2. Doing a lightweight height lookup before the main query pub enum CacheStrategy { - /// Immutable data (blocks by hash with 6+ confirmations) - /// Etag = VERSION-{prefix:x}, Cache-Control: immutable, 1yr - /// Falls back to Height if < 6 confirmations - Immutable { prefix: u64, confirmations: u32 }, - - /// Data that changes with each new block (addresses, block-by-height) + /// Data that changes with each new block (addresses, mining stats, txs, outspends) /// Etag = VERSION-{height}, Cache-Control: must-revalidate Height, - /// Data that changes with height + depends on parameter - /// Etag = VERSION-{height}-{suffix}, Cache-Control: must-revalidate - HeightWith(String), - - /// Static data (validate-address, metrics catalog) - /// Etag = VERSION only, Cache-Control: 1hr + /// Static/immutable data (blocks by hash, validate-address, metrics catalog) + /// Etag = VERSION only, Cache-Control: must-revalidate Static, /// Volatile data (mempool) - no etag, just max-age @@ -47,21 +17,6 @@ pub enum CacheStrategy { MaxAge(u64), } -impl CacheStrategy { - /// Create Immutable strategy - pass *prefix (deref BlockHashPrefix/TxidPrefix to u64) - pub fn immutable(prefix: u64, confirmations: u32) -> Self { - Self::Immutable { - prefix, - confirmations, - } - } - - /// Create HeightWith from any Display type - pub fn height_with(suffix: impl std::fmt::Display) -> Self { - Self::HeightWith(suffix.to_string()) - } -} - /// Resolved cache parameters pub struct CacheParams { pub etag: Option, @@ -69,32 +24,28 @@ pub struct CacheParams { } impl CacheParams { + /// Cache params using VERSION as etag + pub fn version() -> Self { + Self::resolve(&CacheStrategy::Static, || unreachable!()) + } + pub fn etag_str(&self) -> &str { self.etag.as_deref().unwrap_or("") } pub fn matches_etag(&self, headers: &HeaderMap) -> bool { - self.etag.as_ref().is_some_and(|etag| headers.has_etag(etag)) + self.etag + .as_ref() + .is_some_and(|etag| headers.has_etag(etag)) } pub fn resolve(strategy: &CacheStrategy, height: impl FnOnce() -> u32) -> Self { use CacheStrategy::*; match strategy { - Immutable { - prefix, - confirmations, - } if *confirmations >= MIN_CONFIRMATIONS => Self { - etag: Some(format!("{VERSION}-{prefix:x}")), - cache_control: "public, max-age=31536000, immutable".into(), - }, - Immutable { .. } | Height => Self { + Height => Self { etag: Some(format!("{VERSION}-{}", height())), cache_control: "public, max-age=1, must-revalidate".into(), }, - HeightWith(suffix) => Self { - etag: Some(format!("{VERSION}-{}-{suffix}", height())), - cache_control: "public, max-age=1, must-revalidate".into(), - }, Static => Self { etag: Some(VERSION.to_string()), cache_control: "public, max-age=1, must-revalidate".into(), diff --git a/crates/brk_server/src/extended/response.rs b/crates/brk_server/src/extended/response.rs index ae85d0a1d..2895812fc 100644 --- a/crates/brk_server/src/extended/response.rs +++ b/crates/brk_server/src/extended/response.rs @@ -1,6 +1,6 @@ use axum::{ body::Body, - http::{Response, StatusCode}, + http::{HeaderMap, Response, StatusCode}, response::IntoResponse, }; use serde::Serialize; @@ -20,6 +20,9 @@ where where T: Serialize; fn new_json_cached(value: T, params: &CacheParams) -> Self + where + T: Serialize; + fn static_json(headers: &HeaderMap, value: T) -> Self where T: Serialize; fn new_text(value: &str, etag: &str) -> Self; @@ -108,6 +111,17 @@ impl ResponseExtended for Response { response } + fn static_json(headers: &HeaderMap, value: T) -> Self + where + T: Serialize, + { + let params = CacheParams::version(); + if params.matches_etag(headers) { + return Self::new_not_modified(); + } + Self::new_json_cached(value, ¶ms) + } + fn new_text_cached(value: &str, params: &CacheParams) -> Self { let mut response = Response::builder() .body(value.to_string().into()) diff --git a/crates/brk_server/src/lib.rs b/crates/brk_server/src/lib.rs index dd6fb7ae9..da27107e2 100644 --- a/crates/brk_server/src/lib.rs +++ b/crates/brk_server/src/lib.rs @@ -13,7 +13,6 @@ use axum::{ serve, }; use brk_error::Result; -use brk_mcp::route::mcp_router; use brk_query::AsyncQuery; use include_dir::{include_dir, Dir}; use quick_cache::sync::Cache; @@ -67,7 +66,7 @@ impl Server { }) } - pub async fn serve(self, mcp: bool) -> Result<()> { + pub async fn serve(self) -> Result<()> { let state = self.0; let compression_layer = CompressionLayer::new() @@ -159,6 +158,8 @@ impl Server { .python(workspace_root.join("packages/brk_client/brk_client/__init__.py")); let openapi_json = Arc::new(serde_json::to_string(&openapi).unwrap()); + let openapi_trimmed = Arc::new(trim_openapi_json(&openapi_json)); + let result = panic::catch_unwind(panic::AssertUnwindSafe(|| { brk_bindgen::generate_clients(vecs, &openapi_json, &output_paths) })); @@ -169,17 +170,11 @@ impl Server { Err(_) => error!("Client generation panicked"), } - let router = if mcp { - let base_url = format!("http://127.0.0.1:{port}"); - router.merge(mcp_router(base_url, openapi_json)) - } else { - router - }; - serve( listener, router .layer(Extension(Arc::new(openapi))) + .layer(Extension(openapi_trimmed)) .into_make_service(), ) .await?; diff --git a/crates/brk_types/src/cents.rs b/crates/brk_types/src/cents.rs index ff6a46762..3da3de800 100644 --- a/crates/brk_types/src/cents.rs +++ b/crates/brk_types/src/cents.rs @@ -1,4 +1,4 @@ -use std::ops::{Add, Div, Mul, Sub}; +use std::ops::{Add, AddAssign, Div, Mul, Sub}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -115,6 +115,12 @@ impl Add for Cents { } } +impl AddAssign for Cents { + fn add_assign(&mut self, rhs: Self) { + self.0 += rhs.0; + } +} + impl Sub for Cents { type Output = Self; fn sub(self, rhs: Self) -> Self::Output { diff --git a/crates/brk_types/src/datarange.rs b/crates/brk_types/src/datarange.rs index 97c675471..257deeb6f 100644 --- a/crates/brk_types/src/datarange.rs +++ b/crates/brk_types/src/datarange.rs @@ -55,11 +55,6 @@ impl DataRange { }) } - /// Returns a string for etag/cache key generation that captures all range parameters - pub fn etag_suffix(&self) -> String { - format!("{:?}{:?}{:?}", self.start, self.end, self.limit) - } - fn resolve_index(&self, idx: Option, len: usize, default: usize) -> usize { match idx { None => default, diff --git a/crates/brk_types/src/etag.rs b/crates/brk_types/src/etag.rs new file mode 100644 index 000000000..4992050af --- /dev/null +++ b/crates/brk_types/src/etag.rs @@ -0,0 +1,59 @@ +use std::fmt; + +/// HTTP ETag value. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Etag(String); + +impl Etag { + /// Create from raw string + pub fn new(s: impl Into) -> Self { + Self(s.into()) + } + + /// Get inner string reference + pub fn as_str(&self) -> &str { + &self.0 + } + + /// Consume and return inner string + pub fn into_string(self) -> String { + self.0 + } +} + +impl fmt::Display for Etag { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +impl From for Etag { + fn from(s: String) -> Self { + Self(s) + } +} + +impl From<&str> for Etag { + fn from(s: &str) -> Self { + Self(s.to_string()) + } +} + +impl Etag { + /// Create ETag from metric data response info. + /// + /// Format varies based on whether the slice touches the end: + /// - Slice ends before total: `{version:x}-{start}-{end}` (len irrelevant, data won't change if metric grows) + /// - Slice reaches the end: `{version:x}-{start}-{total}` (len matters, new data would change results) + /// + /// `version` is the metric version for single queries, or the sum of versions for bulk queries. + pub fn from_metric(version: u64, total: usize, start: usize, end: usize) -> Self { + if end < total { + // Fixed window not at the end - len doesn't matter + Self(format!("{version:x}-{start}-{end}")) + } else { + // Fetching up to current end - len matters + Self(format!("{version:x}-{start}-{total}")) + } + } +} diff --git a/crates/brk_types/src/index.rs b/crates/brk_types/src/index.rs index fc0d3a24f..b7da171e5 100644 --- a/crates/brk_types/src/index.rs +++ b/crates/brk_types/src/index.rs @@ -5,12 +5,14 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use vecdb::PrintableIndex; +use crate::PairOutputIndex; + use super::{ DateIndex, DecadeIndex, DifficultyEpoch, EmptyAddressIndex, EmptyOutputIndex, HalvingEpoch, - Height, TxInIndex, LoadedAddressIndex, MonthIndex, OpReturnIndex, TxOutIndex, - P2AAddressIndex, P2MSOutputIndex, P2PK33AddressIndex, P2PK65AddressIndex, P2PKHAddressIndex, - P2SHAddressIndex, P2TRAddressIndex, P2WPKHAddressIndex, P2WSHAddressIndex, QuarterIndex, - SemesterIndex, TxIndex, UnknownOutputIndex, WeekIndex, YearIndex, + Height, LoadedAddressIndex, MonthIndex, OpReturnIndex, P2AAddressIndex, P2MSOutputIndex, + P2PK33AddressIndex, P2PK65AddressIndex, P2PKHAddressIndex, P2SHAddressIndex, P2TRAddressIndex, + P2WPKHAddressIndex, P2WSHAddressIndex, QuarterIndex, SemesterIndex, TxInIndex, TxIndex, + TxOutIndex, UnknownOutputIndex, WeekIndex, YearIndex, }; /// Aggregation dimension for querying metrics. Includes time-based (date, week, month, year), @@ -46,10 +48,11 @@ pub enum Index { YearIndex, LoadedAddressIndex, EmptyAddressIndex, + PairOutputIndex, } impl Index { - pub const fn all() -> [Self; 27] { + pub const fn all() -> [Self; 28] { [ Self::DateIndex, Self::DecadeIndex, @@ -78,6 +81,7 @@ impl Index { Self::YearIndex, Self::LoadedAddressIndex, Self::EmptyAddressIndex, + Self::PairOutputIndex, ] } @@ -110,6 +114,7 @@ impl Index { Self::YearIndex => YearIndex::to_possible_strings(), Self::LoadedAddressIndex => LoadedAddressIndex::to_possible_strings(), Self::EmptyAddressIndex => EmptyAddressIndex::to_possible_strings(), + Self::PairOutputIndex => PairOutputIndex::to_possible_strings(), } } @@ -188,6 +193,7 @@ impl TryFrom<&str> for Index { v if (Self::EmptyAddressIndex).possible_values().contains(&v) => { Self::EmptyAddressIndex } + v if (Self::PairOutputIndex).possible_values().contains(&v) => Self::PairOutputIndex, _ => return Err(Error::Parse(format!("Invalid index: {value}"))), }) } diff --git a/crates/brk_types/src/lib.rs b/crates/brk_types/src/lib.rs index 4ae4105c1..8786bbf28 100644 --- a/crates/brk_types/src/lib.rs +++ b/crates/brk_types/src/lib.rs @@ -52,6 +52,7 @@ mod dollars; mod emptyaddressdata; mod emptyaddressindex; mod emptyoutputindex; +mod etag; mod feerate; mod feeratepercentiles; mod format; @@ -78,16 +79,20 @@ mod metriccount; mod metricdata; mod metricparam; mod metrics; +mod metricoutput; mod metricselection; mod metricselectionlegacy; mod metricspaginated; mod metricwithindex; mod monthindex; mod ohlc; +mod oracle_bins; mod opreturnindex; mod option_ext; mod outpoint; +mod output; mod outputtype; +mod pairoutputindex; mod p2aaddressindex; mod p2abytes; mod p2msoutputindex; @@ -216,6 +221,7 @@ pub use dollars::*; pub use emptyaddressdata::*; pub use emptyaddressindex::*; pub use emptyoutputindex::*; +pub use etag::*; pub use feerate::*; pub use feeratepercentiles::*; pub use format::*; @@ -242,16 +248,20 @@ pub use metriccount::*; pub use metricdata::*; pub use metricparam::*; pub use metrics::*; +pub use metricoutput::*; pub use metricselection::*; pub use metricselectionlegacy::*; pub use metricspaginated::*; pub use metricwithindex::*; pub use monthindex::*; pub use ohlc::*; +pub use oracle_bins::*; pub use opreturnindex::*; pub use option_ext::*; pub use outpoint::*; +pub use output::*; pub use outputtype::*; +pub use pairoutputindex::*; pub use p2aaddressindex::*; pub use p2abytes::*; pub use p2msoutputindex::*; diff --git a/crates/brk_types/src/metricdata.rs b/crates/brk_types/src/metricdata.rs index e72af3b8d..59f81eae0 100644 --- a/crates/brk_types/src/metricdata.rs +++ b/crates/brk_types/src/metricdata.rs @@ -11,6 +11,8 @@ use vecdb::AnySerializableVec; /// This type is not instantiated - use `MetricData::serialize()` to write JSON bytes directly. #[derive(Debug, JsonSchema, Deserialize)] pub struct MetricData { + /// Version of the metric data + pub version: u64, /// Total number of data points in the metric pub total: usize, /// Start index (inclusive) of the returned range @@ -22,22 +24,23 @@ pub struct MetricData { } impl MetricData { - /// Write metric data as JSON to buffer: `{"total":N,"start":N,"end":N,"data":[...]}` + /// Write metric data as JSON to buffer: `{"version":N,"total":N,"start":N,"end":N,"data":[...]}` pub fn serialize( vec: &dyn AnySerializableVec, - start: Option, - end: Option, + start: usize, + end: usize, buf: &mut Vec, ) -> vecdb::Result<()> { + let version = u64::from(vec.version()); let total = vec.len(); - let start_idx = start.unwrap_or(0); - let end_idx = end.unwrap_or(total).min(total); + let end = end.min(total); + let start = start.min(end); write!( buf, - r#"{{"total":{total},"start":{start_idx},"end":{end_idx},"data":"# + r#"{{"version":{version},"total":{total},"start":{start},"end":{end},"data":"#, )?; - vec.write_json(start, end, buf)?; + vec.write_json(Some(start), Some(end), buf)?; buf.push(b'}'); Ok(()) } diff --git a/crates/brk_types/src/metricoutput.rs b/crates/brk_types/src/metricoutput.rs new file mode 100644 index 000000000..37e5e16fc --- /dev/null +++ b/crates/brk_types/src/metricoutput.rs @@ -0,0 +1,33 @@ +use crate::{Etag, Output, OutputLegacy}; + +/// Metric output with metadata for caching. +#[derive(Debug)] +pub struct MetricOutput { + pub output: Output, + pub version: u64, + pub total: usize, + pub start: usize, + pub end: usize, +} + +impl MetricOutput { + pub fn etag(&self) -> Etag { + Etag::from_metric(self.version, self.total, self.start, self.end) + } +} + +/// Deprecated: Legacy metric output with metadata for caching. +#[derive(Debug)] +pub struct MetricOutputLegacy { + pub output: OutputLegacy, + pub version: u64, + pub total: usize, + pub start: usize, + pub end: usize, +} + +impl MetricOutputLegacy { + pub fn etag(&self) -> Etag { + Etag::from_metric(self.version, self.total, self.start, self.end) + } +} diff --git a/crates/brk_types/src/metrics.rs b/crates/brk_types/src/metrics.rs index ea4f75178..860b6a1c6 100644 --- a/crates/brk_types/src/metrics.rs +++ b/crates/brk_types/src/metrics.rs @@ -10,9 +10,10 @@ use super::Metric; #[derive(Debug, Deref, JsonSchema)] #[schemars( with = "String", + example = &"date,price_close", example = &"price_close", example = &"price_close,market_cap", - example = &"realized_price,nvt_ratio,mvrv" + example = &"realized_price,market_cap,mvrv" )] pub struct Metrics(Vec); diff --git a/crates/brk_types/src/oracle_bins.rs b/crates/brk_types/src/oracle_bins.rs new file mode 100644 index 000000000..715e8b185 --- /dev/null +++ b/crates/brk_types/src/oracle_bins.rs @@ -0,0 +1,150 @@ +use std::{fmt::Display, mem::size_of}; + +use schemars::JsonSchema; +use serde::{Deserialize, Deserializer, Serialize, Serializer, de::SeqAccess, de::Visitor}; +use vecdb::{Bytes, Formattable}; + +use crate::Sats; + +/// Number of bins for the phase histogram +pub const PHASE_BINS: usize = 100; + +/// Phase histogram: counts per bin for frac(log10(sats)) +/// +/// Used for on-chain price discovery. Each bin represents 1% of the +/// log10 fractional range [0, 1). Values are u16 (max 65535 per bin). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct OracleBins { + pub bins: [u16; PHASE_BINS], +} + +impl Default for OracleBins { + fn default() -> Self { + Self::ZERO + } +} + +impl Display for OracleBins { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "OracleBins(peak={})", self.peak_bin()) + } +} + +impl Serialize for OracleBins { + fn serialize(&self, serializer: S) -> Result { + self.bins.as_slice().serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for OracleBins { + fn deserialize>(deserializer: D) -> Result { + struct BinsVisitor; + + impl<'de> Visitor<'de> for BinsVisitor { + type Value = OracleBins; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(formatter, "an array of {} u16 values", PHASE_BINS) + } + + fn visit_seq>(self, mut seq: A) -> Result { + let mut bins = [0u16; PHASE_BINS]; + for (i, bin) in bins.iter_mut().enumerate() { + *bin = seq + .next_element()? + .ok_or_else(|| serde::de::Error::invalid_length(i, &self))?; + } + Ok(OracleBins { bins }) + } + } + + deserializer.deserialize_seq(BinsVisitor) + } +} + +impl JsonSchema for OracleBins { + fn schema_name() -> std::borrow::Cow<'static, str> { + "OracleBins".into() + } + + fn json_schema(_gen: &mut schemars::SchemaGenerator) -> schemars::Schema { + // Represent as array of u16 values + Vec::::json_schema(_gen) + } +} + +impl OracleBins { + pub const ZERO: Self = Self { + bins: [0; PHASE_BINS], + }; + + /// Get the bin index for a sats value + /// Filters: min 1k sats, max 100k BTC (matches Python 1e-5 to 1e5 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 as f64) as usize).min(PHASE_BINS - 1)) + } + + /// Add a count to the bin for this sats value + #[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); + } + } + + /// Find the peak bin (index with highest count) + pub fn peak_bin(&self) -> usize { + self.bins + .iter() + .enumerate() + .max_by_key(|(_, count)| *count) + .map(|(idx, _)| idx) + .unwrap_or(0) + } + + /// Get total count across all bins + pub fn total_count(&self) -> u32 { + self.bins.iter().map(|&c| c as u32).sum() + } +} + +impl Bytes for OracleBins { + type Array = [u8; size_of::()]; + + fn to_bytes(&self) -> Self::Array { + let mut arr = [0u8; size_of::()]; + for (i, &count) in self.bins.iter().enumerate() { + let bytes = count.to_le_bytes(); + arr[i * 2] = bytes[0]; + arr[i * 2 + 1] = bytes[1]; + } + arr + } + + fn from_bytes(bytes: &[u8]) -> vecdb::Result { + if bytes.len() < size_of::() { + return Err(vecdb::Error::WrongLength { + received: bytes.len(), + expected: size_of::(), + }); + } + let mut bins = [0u16; PHASE_BINS]; + for (i, bin) in bins.iter_mut().enumerate() { + *bin = u16::from_le_bytes([bytes[i * 2], bytes[i * 2 + 1]]); + } + Ok(Self { bins }) + } +} + +impl Formattable for OracleBins { + fn may_need_escaping() -> bool { + false + } +} diff --git a/crates/brk_query/src/output.rs b/crates/brk_types/src/output.rs similarity index 90% rename from crates/brk_query/src/output.rs rename to crates/brk_types/src/output.rs index 7e5da2451..0b81be7a1 100644 --- a/crates/brk_query/src/output.rs +++ b/crates/brk_types/src/output.rs @@ -1,6 +1,6 @@ -use brk_types::Format; +use crate::Format; -/// New format with MetricData metadata wrapper +/// Metric data output format #[derive(Debug)] pub enum Output { Json(Vec), @@ -19,7 +19,9 @@ impl Output { pub fn default(format: Format) -> Self { match format { Format::CSV => Output::CSV(String::new()), - Format::JSON => Output::Json(br#"{"len":0,"from":0,"to":0,"data":[]}"#.to_vec()), + Format::JSON => { + Output::Json(br#"{"version":0,"total":0,"start":0,"end":0,"data":[]}"#.to_vec()) + } } } } diff --git a/crates/brk_types/src/pairoutputindex.rs b/crates/brk_types/src/pairoutputindex.rs new file mode 100644 index 000000000..7df520adb --- /dev/null +++ b/crates/brk_types/src/pairoutputindex.rs @@ -0,0 +1,121 @@ +use std::ops::Add; + +use derive_more::{Deref, DerefMut}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use vecdb::{CheckedSub, Formattable, Pco, PrintableIndex}; + +/// Index for 2-output transactions (oracle pair candidates) +/// +/// This indexes all transactions with exactly 2 outputs, which are +/// candidates for the UTXOracle algorithm (payment + change pattern). +#[derive( + Debug, + PartialEq, + Eq, + PartialOrd, + Ord, + Clone, + Copy, + Deref, + DerefMut, + Default, + Serialize, + Deserialize, + Pco, + JsonSchema, + Hash, +)] +pub struct PairOutputIndex(u32); + +impl PairOutputIndex { + pub const ZERO: Self = Self(0); + + pub fn new(index: u32) -> Self { + Self(index) + } + + pub fn incremented(self) -> Self { + Self(*self + 1) + } +} + +impl Add for PairOutputIndex { + type Output = Self; + fn add(self, rhs: usize) -> Self::Output { + Self(self.0 + rhs as u32) + } +} + +impl CheckedSub for PairOutputIndex { + fn checked_sub(self, rhs: PairOutputIndex) -> Option { + self.0.checked_sub(rhs.0).map(PairOutputIndex::from) + } +} + +impl From for PairOutputIndex { + #[inline] + fn from(value: u32) -> Self { + Self(value) + } +} + +impl From for u32 { + #[inline] + fn from(value: PairOutputIndex) -> Self { + value.0 + } +} + +impl From for PairOutputIndex { + #[inline] + fn from(value: u64) -> Self { + Self(value as u32) + } +} + +impl From for u64 { + #[inline] + fn from(value: PairOutputIndex) -> Self { + value.0 as u64 + } +} + +impl From for PairOutputIndex { + #[inline] + fn from(value: usize) -> Self { + Self(value as u32) + } +} + +impl From for usize { + #[inline] + fn from(value: PairOutputIndex) -> Self { + value.0 as usize + } +} + +impl PrintableIndex for PairOutputIndex { + fn to_string() -> &'static str { + "pairoutputindex" + } + + fn to_possible_strings() -> &'static [&'static str] { + &["pairoutput", "pairoutputindex"] + } +} + +impl std::fmt::Display for PairOutputIndex { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut buf = itoa::Buffer::new(); + let str = buf.format(self.0); + f.write_str(str) + } +} + +impl Formattable for PairOutputIndex { + #[inline(always)] + fn may_need_escaping() -> bool { + false + } +} diff --git a/crates/brk_types/src/sats.rs b/crates/brk_types/src/sats.rs index d19435dab..424c9658d 100644 --- a/crates/brk_types/src/sats.rs +++ b/crates/brk_types/src/sats.rs @@ -69,6 +69,40 @@ impl Sats { pub fn is_max(&self) -> bool { *self == Self::MAX } + + /// Check if value is a "round" BTC amount (±0.1% of common round values). + /// Used to filter out non-price-related transactions. + /// Round amounts: 1k, 10k, 20k, 30k, 50k, 100k, 200k, 300k, 500k sats, + /// 0.01, 0.02, 0.03, 0.05, 0.1, 0.2, 0.3, 0.5, 1, 10 BTC + pub fn is_round_btc(&self) -> bool { + const ROUND_SATS: [u64; 19] = [ + 1_000, // 1k sats + 10_000, // 10k sats + 20_000, // 20k sats + 30_000, // 30k sats + 50_000, // 50k sats + 100_000, // 100k sats (0.001 BTC) + 200_000, // 200k sats + 300_000, // 300k sats + 500_000, // 500k sats + 1_000_000, // 0.01 BTC + 2_000_000, // 0.02 BTC + 3_000_000, // 0.03 BTC + 5_000_000, // 0.05 BTC + 10_000_000, // 0.1 BTC + 20_000_000, // 0.2 BTC + 30_000_000, // 0.3 BTC + 50_000_000, // 0.5 BTC + 100_000_000, // 1 BTC + 1_000_000_000, // 10 BTC + ]; + const TOLERANCE: f64 = 0.001; // 0.1% + + let v = self.0 as f64; + ROUND_SATS + .iter() + .any(|&r| (v - r as f64).abs() <= r as f64 * TOLERANCE) + } } impl Add for Sats { diff --git a/docker/.env.example b/docker/.env.example index c5687df65..47c1a2a28 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -17,9 +17,6 @@ BTC_RPC_PORT=8332 # Enable price fetching from exchanges BRK_FETCH=true -# Enable Model Context Protocol (MCP) for AI/LLM integration -BRK_MCP=true - # BRK data storage options # Option 1: Use a Docker named volume (default, recommended) # This is the default configuration - no changes needed. diff --git a/docker/README.md b/docker/README.md index f0d411bde..53b79fb9d 100644 --- a/docker/README.md +++ b/docker/README.md @@ -63,7 +63,6 @@ cd docker && docker compose up -d | `BTC_RPC_PASSWORD` | Bitcoin RPC password | - | | `BRK_DATA_VOLUME` | Docker volume name for BRK data | `brk-data` | | `BRK_FETCH` | Enable price fetching | `true` | -| `BRK_MCP` | Enable MCP for AI/LLM | `true` | ### Example .env File @@ -80,7 +79,6 @@ BTC_RPC_PASSWORD=your_password # BRK settings BRK_FETCH=true -BRK_MCP=true ``` ### Connecting to Bitcoin Core diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index b449c46c0..aeedc06b2 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -38,7 +38,6 @@ services: # BRK configuration - BRKDIR=/home/brk/.brk - FETCH=${BRK_FETCH:-true} - - MCP=${BRK_MCP:-true} command: - --bitcoindir - /bitcoin diff --git a/docs/README.md b/docs/README.md index d95955823..0576e205e 100644 --- a/docs/README.md +++ b/docs/README.md @@ -26,7 +26,7 @@ Browse metrics and charts visually. Use it free at [Bitview](https://bitview.spa Query thousands of metrics and blockchain data in JSON or CSV. Freely accessible at [Bitview](https://bitview.space/api). -[Documentation](https://bitview.space/api) · Clients: [JavaScript](https://www.npmjs.com/package/brk-client), [Python](https://pypi.org/project/brk-client), [Rust](https://crates.io/crates/brk_client) · [MCP](https://modelcontextprotocol.io) +[Documentation](https://bitview.space/api) · [JavaScript](https://www.npmjs.com/package/brk-client) · [Python](https://pypi.org/project/brk-client) · [Rust](https://crates.io/crates/brk_client) ### Self-host diff --git a/docs/TODO.md b/docs/TODO.md index 20ad0abc1..6d86a9c4a 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -67,7 +67,6 @@ - example: from -10,000 count 10, won’t work if underlying vec isn’t 10k or more long - _LOGGER_ - BUG: remove colors from file - - _MCP_ - _PARSER_ - _SERVER_ - api diff --git a/modules/brk-client/index.js b/modules/brk-client/index.js index 0092b1235..3aa049f3b 100644 --- a/modules/brk-client/index.js +++ b/modules/brk-client/index.js @@ -308,7 +308,7 @@ * Aggregation dimension for querying metrics. Includes time-based (date, week, month, year), * block-based (height, txindex), and address/output type indexes. * - * @typedef {("dateindex"|"decadeindex"|"difficultyepoch"|"emptyoutputindex"|"halvingepoch"|"height"|"txinindex"|"monthindex"|"opreturnindex"|"txoutindex"|"p2aaddressindex"|"p2msoutputindex"|"p2pk33addressindex"|"p2pk65addressindex"|"p2pkhaddressindex"|"p2shaddressindex"|"p2traddressindex"|"p2wpkhaddressindex"|"p2wshaddressindex"|"quarterindex"|"semesterindex"|"txindex"|"unknownoutputindex"|"weekindex"|"yearindex"|"loadedaddressindex"|"emptyaddressindex")} Index + * @typedef {("dateindex"|"decadeindex"|"difficultyepoch"|"emptyoutputindex"|"halvingepoch"|"height"|"txinindex"|"monthindex"|"opreturnindex"|"txoutindex"|"p2aaddressindex"|"p2msoutputindex"|"p2pk33addressindex"|"p2pk65addressindex"|"p2pkhaddressindex"|"p2shaddressindex"|"p2traddressindex"|"p2wpkhaddressindex"|"p2wshaddressindex"|"quarterindex"|"semesterindex"|"txindex"|"unknownoutputindex"|"weekindex"|"yearindex"|"loadedaddressindex"|"emptyaddressindex"|"pairoutputindex")} Index */ /** * Information about an available index and its query aliases @@ -455,6 +455,7 @@ * * @typedef {Cents} Open */ +/** @typedef {number[]} OracleBins */ /** @typedef {number} OutPoint */ /** * Type (P2PKH, P2WPKH, P2SH, P2TR, etc.) @@ -492,6 +493,14 @@ * @typedef {Object} Pagination * @property {?number=} page - Pagination index */ +/** + * Index for 2-output transactions (oracle pair candidates) + * + * This indexes all transactions with exactly 2 outputs, which are + * candidates for the UTXOracle algorithm (payment + change pattern). + * + * @typedef {number} PairOutputIndex + */ /** * Block counts for different time periods * @@ -1092,6 +1101,7 @@ const _i29 = /** @type {const} */ (["weekindex"]); const _i30 = /** @type {const} */ (["yearindex"]); const _i31 = /** @type {const} */ (["loadedaddressindex"]); const _i32 = /** @type {const} */ (["emptyaddressindex"]); +const _i33 = /** @type {const} */ (["pairoutputindex"]); /** * Generic metric pattern factory. @@ -1214,6 +1224,9 @@ function createMetricPattern31(client, name) { return _mp(client, name, _i31); } /** @template T @typedef {{ name: string, by: { readonly emptyaddressindex: MetricEndpointBuilder }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder|undefined }} MetricPattern32 */ /** @template T @param {BrkClientBase} client @param {string} name @returns {MetricPattern32} */ function createMetricPattern32(client, name) { return _mp(client, name, _i32); } +/** @template T @typedef {{ name: string, by: { readonly pairoutputindex: MetricEndpointBuilder }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder|undefined }} MetricPattern33 */ +/** @template T @param {BrkClientBase} client @param {string} name @returns {MetricPattern33} */ +function createMetricPattern33(client, name) { return _mp(client, name, _i33); } // Reusable structural pattern factories @@ -1631,59 +1644,6 @@ function createPrice111dSmaPattern(client, acc) { }; } -/** - * @typedef {Object} ActivePriceRatioPattern - * @property {MetricPattern4} ratio - * @property {MetricPattern4} ratio1mSma - * @property {MetricPattern4} ratio1wSma - * @property {Ratio1ySdPattern} ratio1ySd - * @property {Ratio1ySdPattern} ratio2ySd - * @property {Ratio1ySdPattern} ratio4ySd - * @property {MetricPattern4} ratioPct1 - * @property {MetricPattern4} ratioPct1Usd - * @property {MetricPattern4} ratioPct2 - * @property {MetricPattern4} ratioPct2Usd - * @property {MetricPattern4} ratioPct5 - * @property {MetricPattern4} ratioPct5Usd - * @property {MetricPattern4} ratioPct95 - * @property {MetricPattern4} ratioPct95Usd - * @property {MetricPattern4} ratioPct98 - * @property {MetricPattern4} ratioPct98Usd - * @property {MetricPattern4} ratioPct99 - * @property {MetricPattern4} ratioPct99Usd - * @property {Ratio1ySdPattern} ratioSd - */ - -/** - * Create a ActivePriceRatioPattern pattern node - * @param {BrkClientBase} client - * @param {string} acc - Accumulated metric name - * @returns {ActivePriceRatioPattern} - */ -function createActivePriceRatioPattern(client, acc) { - return { - ratio: createMetricPattern4(client, acc), - ratio1mSma: createMetricPattern4(client, _m(acc, '1m_sma')), - ratio1wSma: createMetricPattern4(client, _m(acc, '1w_sma')), - ratio1ySd: createRatio1ySdPattern(client, _m(acc, '1y')), - ratio2ySd: createRatio1ySdPattern(client, _m(acc, '2y')), - ratio4ySd: createRatio1ySdPattern(client, _m(acc, '4y')), - ratioPct1: createMetricPattern4(client, _m(acc, 'pct1')), - ratioPct1Usd: createMetricPattern4(client, _m(acc, 'pct1_usd')), - ratioPct2: createMetricPattern4(client, _m(acc, 'pct2')), - ratioPct2Usd: createMetricPattern4(client, _m(acc, 'pct2_usd')), - ratioPct5: createMetricPattern4(client, _m(acc, 'pct5')), - ratioPct5Usd: createMetricPattern4(client, _m(acc, 'pct5_usd')), - ratioPct95: createMetricPattern4(client, _m(acc, 'pct95')), - ratioPct95Usd: createMetricPattern4(client, _m(acc, 'pct95_usd')), - ratioPct98: createMetricPattern4(client, _m(acc, 'pct98')), - ratioPct98Usd: createMetricPattern4(client, _m(acc, 'pct98_usd')), - ratioPct99: createMetricPattern4(client, _m(acc, 'pct99')), - ratioPct99Usd: createMetricPattern4(client, _m(acc, 'pct99_usd')), - ratioSd: createRatio1ySdPattern(client, acc), - }; -} - /** * @typedef {Object} PercentilesPattern * @property {MetricPattern4} pct05 @@ -1737,6 +1697,59 @@ function createPercentilesPattern(client, acc) { }; } +/** + * @typedef {Object} ActivePriceRatioPattern + * @property {MetricPattern4} ratio + * @property {MetricPattern4} ratio1mSma + * @property {MetricPattern4} ratio1wSma + * @property {Ratio1ySdPattern} ratio1ySd + * @property {Ratio1ySdPattern} ratio2ySd + * @property {Ratio1ySdPattern} ratio4ySd + * @property {MetricPattern4} ratioPct1 + * @property {MetricPattern4} ratioPct1Usd + * @property {MetricPattern4} ratioPct2 + * @property {MetricPattern4} ratioPct2Usd + * @property {MetricPattern4} ratioPct5 + * @property {MetricPattern4} ratioPct5Usd + * @property {MetricPattern4} ratioPct95 + * @property {MetricPattern4} ratioPct95Usd + * @property {MetricPattern4} ratioPct98 + * @property {MetricPattern4} ratioPct98Usd + * @property {MetricPattern4} ratioPct99 + * @property {MetricPattern4} ratioPct99Usd + * @property {Ratio1ySdPattern} ratioSd + */ + +/** + * Create a ActivePriceRatioPattern pattern node + * @param {BrkClientBase} client + * @param {string} acc - Accumulated metric name + * @returns {ActivePriceRatioPattern} + */ +function createActivePriceRatioPattern(client, acc) { + return { + ratio: createMetricPattern4(client, acc), + ratio1mSma: createMetricPattern4(client, _m(acc, '1m_sma')), + ratio1wSma: createMetricPattern4(client, _m(acc, '1w_sma')), + ratio1ySd: createRatio1ySdPattern(client, _m(acc, '1y')), + ratio2ySd: createRatio1ySdPattern(client, _m(acc, '2y')), + ratio4ySd: createRatio1ySdPattern(client, _m(acc, '4y')), + ratioPct1: createMetricPattern4(client, _m(acc, 'pct1')), + ratioPct1Usd: createMetricPattern4(client, _m(acc, 'pct1_usd')), + ratioPct2: createMetricPattern4(client, _m(acc, 'pct2')), + ratioPct2Usd: createMetricPattern4(client, _m(acc, 'pct2_usd')), + ratioPct5: createMetricPattern4(client, _m(acc, 'pct5')), + ratioPct5Usd: createMetricPattern4(client, _m(acc, 'pct5_usd')), + ratioPct95: createMetricPattern4(client, _m(acc, 'pct95')), + ratioPct95Usd: createMetricPattern4(client, _m(acc, 'pct95_usd')), + ratioPct98: createMetricPattern4(client, _m(acc, 'pct98')), + ratioPct98Usd: createMetricPattern4(client, _m(acc, 'pct98_usd')), + ratioPct99: createMetricPattern4(client, _m(acc, 'pct99')), + ratioPct99Usd: createMetricPattern4(client, _m(acc, 'pct99_usd')), + ratioSd: createRatio1ySdPattern(client, acc), + }; +} + /** * @typedef {Object} RelativePattern5 * @property {MetricPattern1} negUnrealizedLossRelToMarketCap @@ -2209,41 +2222,6 @@ function createAddrCountPattern(client, acc) { }; } -/** - * @template T - * @typedef {Object} FullnessPattern - * @property {MetricPattern2} average - * @property {MetricPattern11} base - * @property {MetricPattern2} max - * @property {MetricPattern6} median - * @property {MetricPattern2} min - * @property {MetricPattern6} pct10 - * @property {MetricPattern6} pct25 - * @property {MetricPattern6} pct75 - * @property {MetricPattern6} pct90 - */ - -/** - * Create a FullnessPattern pattern node - * @template T - * @param {BrkClientBase} client - * @param {string} acc - Accumulated metric name - * @returns {FullnessPattern} - */ -function createFullnessPattern(client, acc) { - return { - average: createMetricPattern2(client, _m(acc, 'average')), - base: createMetricPattern11(client, acc), - max: createMetricPattern2(client, _m(acc, 'max')), - median: createMetricPattern6(client, _m(acc, 'median')), - min: createMetricPattern2(client, _m(acc, 'min')), - pct10: createMetricPattern6(client, _m(acc, 'pct10')), - pct25: createMetricPattern6(client, _m(acc, 'pct25')), - pct75: createMetricPattern6(client, _m(acc, 'pct75')), - pct90: createMetricPattern6(client, _m(acc, 'pct90')), - }; -} - /** * @template T * @typedef {Object} FeeRatePattern @@ -2279,6 +2257,41 @@ function createFeeRatePattern(client, acc) { }; } +/** + * @template T + * @typedef {Object} FullnessPattern + * @property {MetricPattern2} average + * @property {MetricPattern11} base + * @property {MetricPattern2} max + * @property {MetricPattern6} median + * @property {MetricPattern2} min + * @property {MetricPattern6} pct10 + * @property {MetricPattern6} pct25 + * @property {MetricPattern6} pct75 + * @property {MetricPattern6} pct90 + */ + +/** + * Create a FullnessPattern pattern node + * @template T + * @param {BrkClientBase} client + * @param {string} acc - Accumulated metric name + * @returns {FullnessPattern} + */ +function createFullnessPattern(client, acc) { + return { + average: createMetricPattern2(client, _m(acc, 'average')), + base: createMetricPattern11(client, acc), + max: createMetricPattern2(client, _m(acc, 'max')), + median: createMetricPattern6(client, _m(acc, 'median')), + min: createMetricPattern2(client, _m(acc, 'min')), + pct10: createMetricPattern6(client, _m(acc, 'pct10')), + pct25: createMetricPattern6(client, _m(acc, 'pct25')), + pct75: createMetricPattern6(client, _m(acc, 'pct75')), + pct90: createMetricPattern6(client, _m(acc, 'pct90')), + }; +} + /** * @typedef {Object} _0satsPattern * @property {ActivityPattern2} activity @@ -2311,31 +2324,35 @@ function create_0satsPattern(client, acc) { } /** - * @typedef {Object} _100btcPattern - * @property {ActivityPattern2} activity - * @property {CostBasisPattern} costBasis - * @property {OutputsPattern} outputs - * @property {RealizedPattern} realized - * @property {RelativePattern} relative - * @property {SupplyPattern2} supply - * @property {UnrealizedPattern} unrealized + * @template T + * @typedef {Object} PhaseDailyCentsPattern + * @property {MetricPattern6} average + * @property {MetricPattern6} max + * @property {MetricPattern6} median + * @property {MetricPattern6} min + * @property {MetricPattern6} pct10 + * @property {MetricPattern6} pct25 + * @property {MetricPattern6} pct75 + * @property {MetricPattern6} pct90 */ /** - * Create a _100btcPattern pattern node + * Create a PhaseDailyCentsPattern pattern node + * @template T * @param {BrkClientBase} client * @param {string} acc - Accumulated metric name - * @returns {_100btcPattern} + * @returns {PhaseDailyCentsPattern} */ -function create_100btcPattern(client, acc) { +function createPhaseDailyCentsPattern(client, acc) { return { - activity: createActivityPattern2(client, acc), - costBasis: createCostBasisPattern(client, acc), - outputs: createOutputsPattern(client, _m(acc, 'utxo_count')), - realized: createRealizedPattern(client, acc), - relative: createRelativePattern(client, acc), - supply: createSupplyPattern2(client, _m(acc, 'supply')), - unrealized: createUnrealizedPattern(client, acc), + average: createMetricPattern6(client, _m(acc, 'average')), + max: createMetricPattern6(client, _m(acc, 'max')), + median: createMetricPattern6(client, _m(acc, 'median')), + min: createMetricPattern6(client, _m(acc, 'min')), + pct10: createMetricPattern6(client, _m(acc, 'pct10')), + pct25: createMetricPattern6(client, _m(acc, 'pct25')), + pct75: createMetricPattern6(client, _m(acc, 'pct75')), + pct90: createMetricPattern6(client, _m(acc, 'pct90')), }; } @@ -2368,35 +2385,6 @@ function createPeriodCagrPattern(client, acc) { }; } -/** - * @typedef {Object} UnrealizedPattern - * @property {MetricPattern1} negUnrealizedLoss - * @property {MetricPattern1} netUnrealizedPnl - * @property {ActiveSupplyPattern} supplyInLoss - * @property {ActiveSupplyPattern} supplyInProfit - * @property {MetricPattern1} totalUnrealizedPnl - * @property {MetricPattern1} unrealizedLoss - * @property {MetricPattern1} unrealizedProfit - */ - -/** - * Create a UnrealizedPattern pattern node - * @param {BrkClientBase} client - * @param {string} acc - Accumulated metric name - * @returns {UnrealizedPattern} - */ -function createUnrealizedPattern(client, acc) { - return { - negUnrealizedLoss: createMetricPattern1(client, _m(acc, 'neg_unrealized_loss')), - netUnrealizedPnl: createMetricPattern1(client, _m(acc, 'net_unrealized_pnl')), - supplyInLoss: createActiveSupplyPattern(client, _m(acc, 'supply_in_loss')), - supplyInProfit: createActiveSupplyPattern(client, _m(acc, 'supply_in_profit')), - totalUnrealizedPnl: createMetricPattern1(client, _m(acc, 'total_unrealized_pnl')), - unrealizedLoss: createMetricPattern1(client, _m(acc, 'unrealized_loss')), - unrealizedProfit: createMetricPattern1(client, _m(acc, 'unrealized_profit')), - }; -} - /** * @typedef {Object} _10yTo12yPattern * @property {ActivityPattern2} activity @@ -2426,6 +2414,35 @@ function create_10yTo12yPattern(client, acc) { }; } +/** + * @typedef {Object} UnrealizedPattern + * @property {MetricPattern1} negUnrealizedLoss + * @property {MetricPattern1} netUnrealizedPnl + * @property {ActiveSupplyPattern} supplyInLoss + * @property {ActiveSupplyPattern} supplyInProfit + * @property {MetricPattern1} totalUnrealizedPnl + * @property {MetricPattern1} unrealizedLoss + * @property {MetricPattern1} unrealizedProfit + */ + +/** + * Create a UnrealizedPattern pattern node + * @param {BrkClientBase} client + * @param {string} acc - Accumulated metric name + * @returns {UnrealizedPattern} + */ +function createUnrealizedPattern(client, acc) { + return { + negUnrealizedLoss: createMetricPattern1(client, _m(acc, 'neg_unrealized_loss')), + netUnrealizedPnl: createMetricPattern1(client, _m(acc, 'net_unrealized_pnl')), + supplyInLoss: createActiveSupplyPattern(client, _m(acc, 'supply_in_loss')), + supplyInProfit: createActiveSupplyPattern(client, _m(acc, 'supply_in_profit')), + totalUnrealizedPnl: createMetricPattern1(client, _m(acc, 'total_unrealized_pnl')), + unrealizedLoss: createMetricPattern1(client, _m(acc, 'unrealized_loss')), + unrealizedProfit: createMetricPattern1(client, _m(acc, 'unrealized_profit')), + }; +} + /** * @typedef {Object} _0satsPattern2 * @property {ActivityPattern2} activity @@ -2455,6 +2472,35 @@ function create_0satsPattern2(client, acc) { }; } +/** + * @typedef {Object} _100btcPattern + * @property {ActivityPattern2} activity + * @property {CostBasisPattern} costBasis + * @property {OutputsPattern} outputs + * @property {RealizedPattern} realized + * @property {RelativePattern} relative + * @property {SupplyPattern2} supply + * @property {UnrealizedPattern} unrealized + */ + +/** + * Create a _100btcPattern pattern node + * @param {BrkClientBase} client + * @param {string} acc - Accumulated metric name + * @returns {_100btcPattern} + */ +function create_100btcPattern(client, acc) { + return { + activity: createActivityPattern2(client, acc), + costBasis: createCostBasisPattern(client, acc), + outputs: createOutputsPattern(client, _m(acc, 'utxo_count')), + realized: createRealizedPattern(client, acc), + relative: createRelativePattern(client, acc), + supply: createSupplyPattern2(client, _m(acc, 'supply')), + unrealized: createUnrealizedPattern(client, acc), + }; +} + /** * @typedef {Object} _10yPattern * @property {ActivityPattern2} activity @@ -2535,23 +2581,23 @@ function createSplitPattern2(client, acc) { } /** - * @typedef {Object} _2015Pattern - * @property {MetricPattern4} bitcoin - * @property {MetricPattern4} dollars - * @property {MetricPattern4} sats + * @typedef {Object} CoinbasePattern2 + * @property {BlockCountPattern} bitcoin + * @property {BlockCountPattern} dollars + * @property {BlockCountPattern} sats */ /** - * Create a _2015Pattern pattern node + * Create a CoinbasePattern2 pattern node * @param {BrkClientBase} client * @param {string} acc - Accumulated metric name - * @returns {_2015Pattern} + * @returns {CoinbasePattern2} */ -function create_2015Pattern(client, acc) { +function createCoinbasePattern2(client, acc) { return { - bitcoin: createMetricPattern4(client, _m(acc, 'btc')), - dollars: createMetricPattern4(client, _m(acc, 'usd')), - sats: createMetricPattern4(client, acc), + bitcoin: createBlockCountPattern(client, _m(acc, 'btc')), + dollars: createBlockCountPattern(client, _m(acc, 'usd')), + sats: createBlockCountPattern(client, acc), }; } @@ -2576,27 +2622,6 @@ function createCoinbasePattern(client, acc) { }; } -/** - * @typedef {Object} CoinbasePattern2 - * @property {BlockCountPattern} bitcoin - * @property {BlockCountPattern} dollars - * @property {BlockCountPattern} sats - */ - -/** - * Create a CoinbasePattern2 pattern node - * @param {BrkClientBase} client - * @param {string} acc - Accumulated metric name - * @returns {CoinbasePattern2} - */ -function createCoinbasePattern2(client, acc) { - return { - bitcoin: createBlockCountPattern(client, _m(acc, 'btc')), - dollars: createBlockCountPattern(client, _m(acc, 'usd')), - sats: createBlockCountPattern(client, acc), - }; -} - /** * @typedef {Object} SegwitAdoptionPattern * @property {MetricPattern11} base @@ -2619,23 +2644,23 @@ function createSegwitAdoptionPattern(client, acc) { } /** - * @typedef {Object} CostBasisPattern2 - * @property {MetricPattern1} max - * @property {MetricPattern1} min - * @property {PercentilesPattern} percentiles + * @typedef {Object} _2015Pattern + * @property {MetricPattern4} bitcoin + * @property {MetricPattern4} dollars + * @property {MetricPattern4} sats */ /** - * Create a CostBasisPattern2 pattern node + * Create a _2015Pattern pattern node * @param {BrkClientBase} client * @param {string} acc - Accumulated metric name - * @returns {CostBasisPattern2} + * @returns {_2015Pattern} */ -function createCostBasisPattern2(client, acc) { +function create_2015Pattern(client, acc) { return { - max: createMetricPattern1(client, _m(acc, 'max_cost_basis')), - min: createMetricPattern1(client, _m(acc, 'min_cost_basis')), - percentiles: createPercentilesPattern(client, _m(acc, 'cost_basis')), + bitcoin: createMetricPattern4(client, _m(acc, 'btc')), + dollars: createMetricPattern4(client, _m(acc, 'usd')), + sats: createMetricPattern4(client, acc), }; } @@ -2660,6 +2685,27 @@ function createActiveSupplyPattern(client, acc) { }; } +/** + * @typedef {Object} CostBasisPattern2 + * @property {MetricPattern1} max + * @property {MetricPattern1} min + * @property {PercentilesPattern} percentiles + */ + +/** + * Create a CostBasisPattern2 pattern node + * @param {BrkClientBase} client + * @param {string} acc - Accumulated metric name + * @returns {CostBasisPattern2} + */ +function createCostBasisPattern2(client, acc) { + return { + max: createMetricPattern1(client, _m(acc, 'max_cost_basis')), + min: createMetricPattern1(client, _m(acc, 'min_cost_basis')), + percentiles: createPercentilesPattern(client, _m(acc, 'cost_basis')), + }; +} + /** * @typedef {Object} UnclaimedRewardsPattern * @property {BitcoinPattern2} bitcoin @@ -2681,6 +2727,25 @@ function createUnclaimedRewardsPattern(client, acc) { }; } +/** + * @typedef {Object} RelativePattern4 + * @property {MetricPattern1} supplyInLossRelToOwnSupply + * @property {MetricPattern1} supplyInProfitRelToOwnSupply + */ + +/** + * Create a RelativePattern4 pattern node + * @param {BrkClientBase} client + * @param {string} acc - Accumulated metric name + * @returns {RelativePattern4} + */ +function createRelativePattern4(client, acc) { + return { + supplyInLossRelToOwnSupply: createMetricPattern1(client, _m(acc, 'loss_rel_to_own_supply')), + supplyInProfitRelToOwnSupply: createMetricPattern1(client, _m(acc, 'profit_rel_to_own_supply')), + }; +} + /** * @typedef {Object} CostBasisPattern * @property {MetricPattern1} max @@ -2719,25 +2784,6 @@ function createSupplyPattern2(client, acc) { }; } -/** - * @typedef {Object} RelativePattern4 - * @property {MetricPattern1} supplyInLossRelToOwnSupply - * @property {MetricPattern1} supplyInProfitRelToOwnSupply - */ - -/** - * Create a RelativePattern4 pattern node - * @param {BrkClientBase} client - * @param {string} acc - Accumulated metric name - * @returns {RelativePattern4} - */ -function createRelativePattern4(client, acc) { - return { - supplyInLossRelToOwnSupply: createMetricPattern1(client, _m(acc, 'loss_rel_to_own_supply')), - supplyInProfitRelToOwnSupply: createMetricPattern1(client, _m(acc, 'profit_rel_to_own_supply')), - }; -} - /** * @typedef {Object} _1dReturns1mSdPattern * @property {MetricPattern4} sd @@ -2757,6 +2803,27 @@ function create_1dReturns1mSdPattern(client, acc) { }; } +/** + * @template T + * @typedef {Object} SatsPattern + * @property {MetricPattern1} ohlc + * @property {SplitPattern2} split + */ + +/** + * Create a SatsPattern pattern node + * @template T + * @param {BrkClientBase} client + * @param {string} acc - Accumulated metric name + * @returns {SatsPattern} + */ +function createSatsPattern(client, acc) { + return { + ohlc: createMetricPattern1(client, _m(acc, 'ohlc')), + split: createSplitPattern2(client, acc), + }; +} + /** * @template T * @typedef {Object} BlockCountPattern @@ -2800,23 +2867,19 @@ function createBitcoinPattern2(client, acc) { } /** - * @template T - * @typedef {Object} SatsPattern - * @property {MetricPattern1} ohlc - * @property {SplitPattern2} split + * @typedef {Object} OutputsPattern + * @property {MetricPattern1} utxoCount */ /** - * Create a SatsPattern pattern node - * @template T + * Create a OutputsPattern pattern node * @param {BrkClientBase} client * @param {string} acc - Accumulated metric name - * @returns {SatsPattern} + * @returns {OutputsPattern} */ -function createSatsPattern(client, acc) { +function createOutputsPattern(client, acc) { return { - ohlc: createMetricPattern1(client, _m(acc, 'ohlc_sats')), - split: createSplitPattern2(client, _m(acc, 'sats')), + utxoCount: createMetricPattern1(client, acc), }; } @@ -2837,23 +2900,6 @@ function createRealizedPriceExtraPattern(client, acc) { }; } -/** - * @typedef {Object} OutputsPattern - * @property {MetricPattern1} utxoCount - */ - -/** - * Create a OutputsPattern pattern node - * @param {BrkClientBase} client - * @param {string} acc - Accumulated metric name - * @returns {OutputsPattern} - */ -function createOutputsPattern(client, acc) { - return { - utxoCount: createMetricPattern1(client, acc), - }; -} - // Catalog tree typedefs /** @@ -4009,8 +4055,8 @@ function createOutputsPattern(client, acc) { * @typedef {Object} MetricsTree_Price * @property {MetricsTree_Price_Cents} cents * @property {MetricsTree_Price_Oracle} oracle - * @property {SatsPattern} sats - * @property {MetricsTree_Price_Usd} usd + * @property {MetricsTree_Price_Sats} sats + * @property {SatsPattern} usd */ /** @@ -4029,16 +4075,24 @@ function createOutputsPattern(client, acc) { /** * @typedef {Object} MetricsTree_Price_Oracle + * @property {MetricPattern11} heightToFirstPairoutputindex * @property {MetricPattern6} ohlcCents * @property {MetricPattern6} ohlcDollars + * @property {MetricPattern33} output0Value + * @property {MetricPattern33} output1Value + * @property {MetricPattern33} pairoutputindexToTxindex + * @property {PhaseDailyCentsPattern} phaseDailyCents + * @property {PhaseDailyCentsPattern} phaseDailyDollars + * @property {MetricPattern11} phaseHistogram + * @property {MetricPattern11} phasePriceCents * @property {MetricPattern11} priceCents * @property {MetricPattern6} txCount */ /** - * @typedef {Object} MetricsTree_Price_Usd - * @property {MetricPattern1} ohlc - * @property {SplitPattern2} split + * @typedef {Object} MetricsTree_Price_Sats + * @property {MetricPattern1} ohlc + * @property {SplitPattern2} split */ /** @@ -4190,7 +4244,7 @@ function createOutputsPattern(client, acc) { * @extends BrkClientBase */ class BrkClient extends BrkClientBase { - VERSION = "v0.1.0-alpha.2"; + VERSION = "v0.1.0-alpha.3"; INDEXES = /** @type {const} */ ([ "dateindex", @@ -4219,7 +4273,8 @@ class BrkClient extends BrkClientBase { "weekindex", "yearindex", "loadedaddressindex", - "emptyaddressindex" + "emptyaddressindex", + "pairoutputindex" ]); POOL_ID_TO_POOL_NAME = /** @type {const} */ ({ @@ -5990,16 +6045,24 @@ class BrkClient extends BrkClientBase { }, }, oracle: { + heightToFirstPairoutputindex: createMetricPattern11(this, 'height_to_first_pairoutputindex'), ohlcCents: createMetricPattern6(this, 'oracle_ohlc_cents'), ohlcDollars: createMetricPattern6(this, 'oracle_ohlc'), + output0Value: createMetricPattern33(this, 'pair_output0_value'), + output1Value: createMetricPattern33(this, 'pair_output1_value'), + pairoutputindexToTxindex: createMetricPattern33(this, 'pairoutputindex_to_txindex'), + phaseDailyCents: createPhaseDailyCentsPattern(this, 'phase_daily'), + phaseDailyDollars: createPhaseDailyCentsPattern(this, 'phase_daily_dollars'), + phaseHistogram: createMetricPattern11(this, 'phase_histogram'), + phasePriceCents: createMetricPattern11(this, 'phase_price_cents'), priceCents: createMetricPattern11(this, 'oracle_price_cents'), txCount: createMetricPattern6(this, 'oracle_tx_count'), }, - sats: createSatsPattern(this, 'price'), - usd: { - ohlc: createMetricPattern1(this, 'price_ohlc'), - split: createSplitPattern2(this, 'price'), + sats: { + ohlc: createMetricPattern1(this, 'price_ohlc_sats'), + split: createSplitPattern2(this, 'price_sats'), }, + usd: createSatsPattern(this, 'price'), }, scripts: { count: { @@ -6120,6 +6183,30 @@ class BrkClient extends BrkClientBase { return _endpoint(this, metric, index); } + /** + * OpenAPI specification + * + * Full OpenAPI 3.1 specification for this API. + * + * Endpoint: `GET /api.json` + * @returns {Promise<*>} + */ + async getOpenapi() { + return this.getJson(`/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` + * @returns {Promise<*>} + */ + async getOpenapiTrimmed() { + return this.getJson(`/api.trimmed.json`); + } + /** * Address information * diff --git a/modules/lightweight-charts/5.1.0/dist/typings.d.ts b/modules/lightweight-charts/5.1.0/dist/typings.d.ts index 4c00ca8cd..7cdace64e 100644 --- a/modules/lightweight-charts/5.1.0/dist/typings.d.ts +++ b/modules/lightweight-charts/5.1.0/dist/typings.d.ts @@ -1,6 +1,6 @@ // Generated by dts-bundle-generator v9.5.1 -import { CanvasRenderingTarget2D } from 'fancy-canvas'; +type CanvasRenderingTarget2D = any; declare const areaSeries: SeriesDefinition<"Area">; declare const barSeries: SeriesDefinition<"Bar">; @@ -13,178 +13,178 @@ export declare const customSeriesDefaultOptions: CustomSeriesOptions; * Enumeration representing the sign of a marker. */ export declare const enum MarkerSign { - /** Represents a negative change (-1) */ - Negative = -1, - /** Represents no change (0) */ - Neutral = 0, - /** Represents a positive change (1) */ - Positive = 1 + /** Represents a negative change (-1) */ + Negative = -1, + /** Represents no change (0) */ + Neutral = 0, + /** Represents a positive change (1) */ + Positive = 1, } /** * Represents a type of color. */ export declare enum ColorType { - /** Solid color */ - Solid = "solid", - /** Vertical gradient color */ - VerticalGradient = "gradient" + /** Solid color */ + Solid = "solid", + /** Vertical gradient color */ + VerticalGradient = "gradient", } /** * Represents the crosshair mode. */ export declare enum CrosshairMode { - /** - * This mode allows crosshair to move freely on the chart. - */ - Normal = 0, - /** - * This mode sticks crosshair's horizontal line to the price value of a single-value series or to the close price of OHLC-based series. - */ - Magnet = 1, - /** - * This mode disables rendering of the crosshair. - */ - Hidden = 2, - /** - * This mode sticks crosshair's horizontal line to the price value of a single-value series or to the open/high/low/close price of OHLC-based series. - */ - MagnetOHLC = 3 + /** + * This mode allows crosshair to move freely on the chart. + */ + Normal = 0, + /** + * This mode sticks crosshair's horizontal line to the price value of a single-value series or to the close price of OHLC-based series. + */ + Magnet = 1, + /** + * This mode disables rendering of the crosshair. + */ + Hidden = 2, + /** + * This mode sticks crosshair's horizontal line to the price value of a single-value series or to the open/high/low/close price of OHLC-based series. + */ + MagnetOHLC = 3, } /** * Represents the type of the last price animation for series such as area or line. */ export declare enum LastPriceAnimationMode { - /** - * Animation is always disabled - */ - Disabled = 0, - /** - * Animation is always enabled. - */ - Continuous = 1, - /** - * Animation is active after new data. - */ - OnDataUpdate = 2 + /** + * Animation is always disabled + */ + Disabled = 0, + /** + * Animation is always enabled. + */ + Continuous = 1, + /** + * Animation is active after new data. + */ + OnDataUpdate = 2, } /** * Represents the possible line styles. */ export declare enum LineStyle { - /** - * A solid line. - */ - Solid = 0, - /** - * A dotted line. - */ - Dotted = 1, - /** - * A dashed line. - */ - Dashed = 2, - /** - * A dashed line with bigger dashes. - */ - LargeDashed = 3, - /** - * A dotted line with more space between dots. - */ - SparseDotted = 4 + /** + * A solid line. + */ + Solid = 0, + /** + * A dotted line. + */ + Dotted = 1, + /** + * A dashed line. + */ + Dashed = 2, + /** + * A dashed line with bigger dashes. + */ + LargeDashed = 3, + /** + * A dotted line with more space between dots. + */ + SparseDotted = 4, } /** * Represents the possible line types. */ export declare enum LineType { - /** - * A line. - */ - Simple = 0, - /** - * A stepped line. - */ - WithSteps = 1, - /** - * A curved line. - */ - Curved = 2 + /** + * A line. + */ + Simple = 0, + /** + * A stepped line. + */ + WithSteps = 1, + /** + * A curved line. + */ + Curved = 2, } /** * Search direction if no data found at provided index */ export declare enum MismatchDirection { - /** - * Search the nearest left item - */ - NearestLeft = -1, - /** - * Do not search - */ - None = 0, - /** - * Search the nearest right item - */ - NearestRight = 1 + /** + * Search the nearest left item + */ + NearestLeft = -1, + /** + * Do not search + */ + None = 0, + /** + * Search the nearest right item + */ + NearestRight = 1, } /** * Represents the source of data to be used for the horizontal price line. */ export declare enum PriceLineSource { - /** - * Use the last bar data. - */ - LastBar = 0, - /** - * Use the last visible data of the chart viewport. - */ - LastVisible = 1 + /** + * Use the last bar data. + */ + LastBar = 0, + /** + * Use the last visible data of the chart viewport. + */ + LastVisible = 1, } /** * Represents the price scale mode. */ export declare enum PriceScaleMode { - /** - * Price scale shows prices. Price range changes linearly. - */ - Normal = 0, - /** - * Price scale shows prices. Price range changes logarithmically. - */ - Logarithmic = 1, - /** - * Price scale shows percentage values according the first visible value of the price scale. - * The first visible value is 0% in this mode. - */ - Percentage = 2, - /** - * The same as percentage mode, but the first value is moved to 100. - */ - IndexedTo100 = 3 + /** + * Price scale shows prices. Price range changes linearly. + */ + Normal = 0, + /** + * Price scale shows prices. Price range changes logarithmically. + */ + Logarithmic = 1, + /** + * Price scale shows percentage values according the first visible value of the price scale. + * The first visible value is 0% in this mode. + */ + Percentage = 2, + /** + * The same as percentage mode, but the first value is moved to 100. + */ + IndexedTo100 = 3, } /** * Represents the type of a tick mark on the time axis. */ export declare enum TickMarkType { - /** - * The start of the year (e.g. it's the first tick mark in a year). - */ - Year = 0, - /** - * The start of the month (e.g. it's the first tick mark in a month). - */ - Month = 1, - /** - * A day of the month. - */ - DayOfMonth = 2, - /** - * A time without seconds. - */ - Time = 3, - /** - * A time with seconds. - */ - TimeWithSeconds = 4 + /** + * The start of the year (e.g. it's the first tick mark in a year). + */ + Year = 0, + /** + * The start of the month (e.g. it's the first tick mark in a month). + */ + Month = 1, + /** + * A day of the month. + */ + DayOfMonth = 2, + /** + * A time without seconds. + */ + Time = 3, + /** + * A time with seconds. + */ + TimeWithSeconds = 4, } /** * Determine how to exit the tracking mode. @@ -193,14 +193,14 @@ export declare enum TickMarkType { * Another press is required to activate the scroll, be able to move left/right, zoom, etc. */ export declare enum TrackingModeExitMode { - /** - * Tracking Mode will be deactivated on touch end event. - */ - OnTouchEnd = 0, - /** - * Tracking Mode will be deactivated on the next tap event. - */ - OnNextTap = 1 + /** + * Tracking Mode will be deactivated on touch end event. + */ + OnTouchEnd = 0, + /** + * Tracking Mode will be deactivated on the next tap event. + */ + OnNextTap = 1, } /** * This function is the simplified main entry point of the Lightweight Charting Library with time points for the horizontal scale. @@ -209,7 +209,10 @@ export declare enum TrackingModeExitMode { * @param options - Any subset of options to be applied at start. * @returns An interface to the created chart */ -export declare function createChart(container: string | HTMLElement, options?: DeepPartial): IChartApi; +export declare function createChart( + container: string | HTMLElement, + options?: DeepPartial, +): IChartApi; /** * This function is the main entry point of the Lightweight Charting Library. If you are using time values * for the horizontal scale then it is recommended that you rather use the {@link createChart} function. @@ -222,7 +225,14 @@ export declare function createChart(container: string | HTMLElement, options?: D * @param options - Any subset of options to be applied at start. * @returns An interface to the created chart */ -export declare function createChartEx>(container: string | HTMLElement, horzScaleBehavior: THorzScaleBehavior, options?: DeepPartial>): IChartApiBase; +export declare function createChartEx< + HorzScaleItem, + THorzScaleBehavior extends IHorzScaleBehavior, +>( + container: string | HTMLElement, + horzScaleBehavior: THorzScaleBehavior, + options?: DeepPartial>, +): IChartApiBase; /** * Creates an image watermark. * @@ -247,7 +257,11 @@ export declare function createChartEx(pane: IPaneApi, imageUrl: string, options: DeepPartial): IImageWatermarkPluginApi; +export declare function createImageWatermark( + pane: IPaneApi, + imageUrl: string, + options: DeepPartial, +): IImageWatermarkPluginApi; /** * Creates an 'options' chart with price values on the horizontal scale. * @@ -259,7 +273,10 @@ export declare function createImageWatermark(pane: IPaneApi, imageUrl: str * @param options - Optional configuration options for the price chart. * @returns An instance of IChartApiBase configured for price-based horizontal scaling. */ -export declare function createOptionsChart(container: string | HTMLElement, options?: DeepPartial): IChartApiBase; +export declare function createOptionsChart( + container: string | HTMLElement, + options?: DeepPartial, +): IChartApiBase; /** * A function to create a series markers primitive. * @@ -291,7 +308,11 @@ export declare function createOptionsChart(container: string | HTMLElement, opti * // `seriesMarkers.markers()` returns current markers * ``` */ -export declare function createSeriesMarkers(series: ISeriesApi, markers?: SeriesMarker[], options?: DeepPartial): ISeriesMarkersPluginApi; +export declare function createSeriesMarkers( + series: ISeriesApi, + markers?: SeriesMarker[], + options?: DeepPartial, +): ISeriesMarkersPluginApi; /** * Creates an image watermark. * @@ -330,7 +351,10 @@ export declare function createSeriesMarkers(series: ISeriesApi(pane: IPaneApi, options: DeepPartial): ITextWatermarkPluginApi; +export declare function createTextWatermark( + pane: IPaneApi, + options: DeepPartial, +): ITextWatermarkPluginApi; /** * Creates and attaches the Series Up Down Markers Plugin. * @@ -363,7 +387,10 @@ export declare function createTextWatermark(pane: IPaneApi, options: DeepP * upDownMarkers.detach(); * ``` */ -export declare function createUpDownMarkers(series: ISeriesApi, options?: Partial): ISeriesUpDownMarkerPluginApi; +export declare function createUpDownMarkers( + series: ISeriesApi, + options?: Partial, +): ISeriesUpDownMarkerPluginApi; /** * Creates a yield curve chart with the specified options. * @@ -377,7 +404,10 @@ export declare function createUpDownMarkers(series: ISeriesApi * @param options - The yield chart options. * @returns An interface to the created chart */ -export declare function createYieldCurveChart(container: string | HTMLElement, options?: DeepPartial): IYieldCurveChartApi; +export declare function createYieldCurveChart( + container: string | HTMLElement, + options?: DeepPartial, +): IYieldCurveChartApi; /** * Provides the default implementation of the horizontal scale (time-based) that can be used as a base for extending the horizontal scale with custom behavior. * This allows for the introduction of custom functionality without re-implementing the entire {@link IHorzScaleBehavior}<{@link Time}> interface. @@ -408,406 +438,409 @@ export declare function version(): string; /** * Structure describing a single item of data for area series */ -export interface AreaData extends SingleValueData { - /** - * Optional line color value for certain data item. If missed, color from options is used - */ - lineColor?: string; - /** - * Optional top color value for certain data item. If missed, color from options is used - */ - topColor?: string; - /** - * Optional bottom color value for certain data item. If missed, color from options is used - */ - bottomColor?: string; +export interface AreaData + extends SingleValueData { + /** + * Optional line color value for certain data item. If missed, color from options is used + */ + lineColor?: string; + /** + * Optional top color value for certain data item. If missed, color from options is used + */ + topColor?: string; + /** + * Optional bottom color value for certain data item. If missed, color from options is used + */ + bottomColor?: string; } /** * Represents style options for an area series. */ export interface AreaStyleOptions { - /** - * Color of the top part of the area. - * - * @defaultValue `'rgba( 46, 220, 135, 0.4)'` - */ - topColor: string; - /** - * Color of the bottom part of the area. - * - * @defaultValue `'rgba( 40, 221, 100, 0)'` - */ - bottomColor: string; - /** - * Gradient is relative to the base value and the currently visible range. - * If it is false, the gradient is relative to the top and bottom of the chart. - * - * @defaultValue `false` - */ - relativeGradient: boolean; - /** - * Invert the filled area. Fills the area above the line if set to true. - * - * @defaultValue `false` - */ - invertFilledArea: boolean; - /** - * Line color. - * - * @defaultValue `'#33D778'` - */ - lineColor: string; - /** - * Line style. - * - * @defaultValue {@link LineStyle.Solid} - */ - lineStyle: LineStyle; - /** - * Line width in pixels. - * - * @defaultValue `3` - */ - lineWidth: LineWidth; - /** - * Line type. - * - * @defaultValue {@link LineType.Simple} - */ - lineType: LineType; - /** - * Show series line. - * - * @defaultValue `true` - */ - lineVisible: boolean; - /** - * Show circle markers on each point. - * - * @defaultValue `false` - */ - pointMarkersVisible: boolean; - /** - * Circle markers radius in pixels. - * - * @defaultValue `undefined` - */ - pointMarkersRadius?: number; - /** - * Show the crosshair marker. - * - * @defaultValue `true` - */ - crosshairMarkerVisible: boolean; - /** - * Crosshair marker radius in pixels. - * - * @defaultValue `4` - */ - crosshairMarkerRadius: number; - /** - * Crosshair marker border color. An empty string falls back to the color of the series under the crosshair. - * - * @defaultValue `''` - */ - crosshairMarkerBorderColor: string; - /** - * The crosshair marker background color. An empty string falls back to the color of the series under the crosshair. - * - * @defaultValue `''` - */ - crosshairMarkerBackgroundColor: string; - /** - * Crosshair marker border width in pixels. - * - * @defaultValue `2` - */ - crosshairMarkerBorderWidth: number; - /** - * Last price animation mode. - * - * @defaultValue {@link LastPriceAnimationMode.Disabled} - */ - lastPriceAnimation: LastPriceAnimationMode; + /** + * Color of the top part of the area. + * + * @defaultValue `'rgba( 46, 220, 135, 0.4)'` + */ + topColor: string; + /** + * Color of the bottom part of the area. + * + * @defaultValue `'rgba( 40, 221, 100, 0)'` + */ + bottomColor: string; + /** + * Gradient is relative to the base value and the currently visible range. + * If it is false, the gradient is relative to the top and bottom of the chart. + * + * @defaultValue `false` + */ + relativeGradient: boolean; + /** + * Invert the filled area. Fills the area above the line if set to true. + * + * @defaultValue `false` + */ + invertFilledArea: boolean; + /** + * Line color. + * + * @defaultValue `'#33D778'` + */ + lineColor: string; + /** + * Line style. + * + * @defaultValue {@link LineStyle.Solid} + */ + lineStyle: LineStyle; + /** + * Line width in pixels. + * + * @defaultValue `3` + */ + lineWidth: LineWidth; + /** + * Line type. + * + * @defaultValue {@link LineType.Simple} + */ + lineType: LineType; + /** + * Show series line. + * + * @defaultValue `true` + */ + lineVisible: boolean; + /** + * Show circle markers on each point. + * + * @defaultValue `false` + */ + pointMarkersVisible: boolean; + /** + * Circle markers radius in pixels. + * + * @defaultValue `undefined` + */ + pointMarkersRadius?: number; + /** + * Show the crosshair marker. + * + * @defaultValue `true` + */ + crosshairMarkerVisible: boolean; + /** + * Crosshair marker radius in pixels. + * + * @defaultValue `4` + */ + crosshairMarkerRadius: number; + /** + * Crosshair marker border color. An empty string falls back to the color of the series under the crosshair. + * + * @defaultValue `''` + */ + crosshairMarkerBorderColor: string; + /** + * The crosshair marker background color. An empty string falls back to the color of the series under the crosshair. + * + * @defaultValue `''` + */ + crosshairMarkerBackgroundColor: string; + /** + * Crosshair marker border width in pixels. + * + * @defaultValue `2` + */ + crosshairMarkerBorderWidth: number; + /** + * Last price animation mode. + * + * @defaultValue {@link LastPriceAnimationMode.Disabled} + */ + lastPriceAnimation: LastPriceAnimationMode; } /** * Represents the margin used when updating a price scale. */ export interface AutoScaleMargins { - /** The number of pixels for bottom margin */ - below: number; - /** The number of pixels for top margin */ - above: number; + /** The number of pixels for bottom margin */ + below: number; + /** The number of pixels for top margin */ + above: number; } /** * Represents information used to update a price scale. */ export interface AutoscaleInfo { - /** - * Price range. - */ - priceRange: PriceRange | null; - /** - * Scale margins. - */ - margins?: AutoScaleMargins; + /** + * Price range. + */ + priceRange: PriceRange | null; + /** + * Scale margins. + */ + margins?: AutoScaleMargins; } /** * Represents options for how the time and price axes react to mouse double click. */ export interface AxisDoubleClickOptions { - /** - * Enable resetting scaling the time axis by double-clicking the left mouse button. - * - * @defaultValue `true` - */ - time: boolean; - /** - * Enable reseting scaling the price axis by by double-clicking the left mouse button. - * - * @defaultValue `true` - */ - price: boolean; + /** + * Enable resetting scaling the time axis by double-clicking the left mouse button. + * + * @defaultValue `true` + */ + time: boolean; + /** + * Enable reseting scaling the price axis by by double-clicking the left mouse button. + * + * @defaultValue `true` + */ + price: boolean; } /** * Represents options for how the time and price axes react to mouse movements. */ export interface AxisPressedMouseMoveOptions { - /** - * Enable scaling the time axis by holding down the left mouse button and moving the mouse. - * - * @defaultValue `true` - */ - time: boolean; - /** - * Enable scaling the price axis by holding down the left mouse button and moving the mouse. - * - * @defaultValue `true` - */ - price: boolean; + /** + * Enable scaling the time axis by holding down the left mouse button and moving the mouse. + * + * @defaultValue `true` + */ + time: boolean; + /** + * Enable scaling the price axis by holding down the left mouse button and moving the mouse. + * + * @defaultValue `true` + */ + price: boolean; } /** * Structure describing a single item of data for bar series */ export interface BarData extends OhlcData { - /** - * Optional color value for certain data item. If missed, color from options is used - */ - color?: string; + /** + * Optional color value for certain data item. If missed, color from options is used + */ + color?: string; } /** * Represents style options for a bar series. */ export interface BarStyleOptions { - /** - * Color of rising bars. - * - * @defaultValue `'#26a69a'` - */ - upColor: string; - /** - * Color of falling bars. - * - * @defaultValue `'#ef5350'` - */ - downColor: string; - /** - * Show open lines on bars. - * - * @defaultValue `true` - */ - openVisible: boolean; - /** - * Show bars as sticks. - * - * @defaultValue `true` - */ - thinBars: boolean; + /** + * Color of rising bars. + * + * @defaultValue `'#26a69a'` + */ + upColor: string; + /** + * Color of falling bars. + * + * @defaultValue `'#ef5350'` + */ + downColor: string; + /** + * Show open lines on bars. + * + * @defaultValue `true` + */ + openVisible: boolean; + /** + * Show bars as sticks. + * + * @defaultValue `true` + */ + thinBars: boolean; } /** * Represents a range of bars and the number of bars outside the range. */ -export interface BarsInfo extends Partial> { - /** - * The number of bars before the start of the range. - * Positive value means that there are some bars before (out of logical range from the left) the {@link IRange.from} logical index in the series. - * Negative value means that the first series' bar is inside the passed logical range, and between the first series' bar and the {@link IRange.from} logical index are some bars. - */ - barsBefore: number; - /** - * The number of bars after the end of the range. - * Positive value in the `barsAfter` field means that there are some bars after (out of logical range from the right) the {@link IRange.to} logical index in the series. - * Negative value means that the last series' bar is inside the passed logical range, and between the last series' bar and the {@link IRange.to} logical index are some bars. - */ - barsAfter: number; +export interface BarsInfo + extends Partial> { + /** + * The number of bars before the start of the range. + * Positive value means that there are some bars before (out of logical range from the left) the {@link IRange.from} logical index in the series. + * Negative value means that the first series' bar is inside the passed logical range, and between the first series' bar and the {@link IRange.from} logical index are some bars. + */ + barsBefore: number; + /** + * The number of bars after the end of the range. + * Positive value in the `barsAfter` field means that there are some bars after (out of logical range from the right) the {@link IRange.to} logical index in the series. + * Negative value means that the last series' bar is inside the passed logical range, and between the last series' bar and the {@link IRange.to} logical index are some bars. + */ + barsAfter: number; } /** * Represents a type of priced base value of baseline series type. */ export interface BaseValuePrice { - /** - * Distinguished type value. - */ - type: "price"; - /** - * Price value. - */ - price: number; + /** + * Distinguished type value. + */ + type: "price"; + /** + * Price value. + */ + price: number; } /** * Structure describing a single item of data for baseline series */ -export interface BaselineData extends SingleValueData { - /** - * Optional top area top fill color value for certain data item. If missed, color from options is used - */ - topFillColor1?: string; - /** - * Optional top area bottom fill color value for certain data item. If missed, color from options is used - */ - topFillColor2?: string; - /** - * Optional top area line color value for certain data item. If missed, color from options is used - */ - topLineColor?: string; - /** - * Optional bottom area top fill color value for certain data item. If missed, color from options is used - */ - bottomFillColor1?: string; - /** - * Optional bottom area bottom fill color value for certain data item. If missed, color from options is used - */ - bottomFillColor2?: string; - /** - * Optional bottom area line color value for certain data item. If missed, color from options is used - */ - bottomLineColor?: string; +export interface BaselineData + extends SingleValueData { + /** + * Optional top area top fill color value for certain data item. If missed, color from options is used + */ + topFillColor1?: string; + /** + * Optional top area bottom fill color value for certain data item. If missed, color from options is used + */ + topFillColor2?: string; + /** + * Optional top area line color value for certain data item. If missed, color from options is used + */ + topLineColor?: string; + /** + * Optional bottom area top fill color value for certain data item. If missed, color from options is used + */ + bottomFillColor1?: string; + /** + * Optional bottom area bottom fill color value for certain data item. If missed, color from options is used + */ + bottomFillColor2?: string; + /** + * Optional bottom area line color value for certain data item. If missed, color from options is used + */ + bottomLineColor?: string; } /** * Represents style options for a baseline series. */ export interface BaselineStyleOptions { - /** - * Base value of the series. - * - * @defaultValue `{ type: 'price', price: 0 }` - */ - baseValue: BaseValueType; - /** - * Gradient is relative to the base value and the currently visible range. - * If it is false, the gradient is relative to the top and bottom of the chart. - * - * @defaultValue `false` - */ - relativeGradient: boolean; - /** - * The first color of the top area. - * - * @defaultValue `'rgba(38, 166, 154, 0.28)'` - */ - topFillColor1: string; - /** - * The second color of the top area. - * - * @defaultValue `'rgba(38, 166, 154, 0.05)'` - */ - topFillColor2: string; - /** - * The line color of the top area. - * - * @defaultValue `'rgba(38, 166, 154, 1)'` - */ - topLineColor: string; - /** - * The first color of the bottom area. - * - * @defaultValue `'rgba(239, 83, 80, 0.05)'` - */ - bottomFillColor1: string; - /** - * The second color of the bottom area. - * - * @defaultValue `'rgba(239, 83, 80, 0.28)'` - */ - bottomFillColor2: string; - /** - * The line color of the bottom area. - * - * @defaultValue `'rgba(239, 83, 80, 1)'` - */ - bottomLineColor: string; - /** - * Line width. - * - * @defaultValue `3` - */ - lineWidth: LineWidth; - /** - * Line style. - * - * @defaultValue {@link LineStyle.Solid} - */ - lineStyle: LineStyle; - /** - * Line type. - * - * @defaultValue {@link LineType.Simple} - */ - lineType: LineType; - /** - * Show series line. - * - * @defaultValue `true` - */ - lineVisible: boolean; - /** - * Show circle markers on each point. - * - * @defaultValue `false` - */ - pointMarkersVisible: boolean; - /** - * Circle markers radius in pixels. - * - * @defaultValue `undefined` - */ - pointMarkersRadius?: number; - /** - * Show the crosshair marker. - * - * @defaultValue `true` - */ - crosshairMarkerVisible: boolean; - /** - * Crosshair marker radius in pixels. - * - * @defaultValue `4` - */ - crosshairMarkerRadius: number; - /** - * Crosshair marker border color. An empty string falls back to the color of the series under the crosshair. - * - * @defaultValue `''` - */ - crosshairMarkerBorderColor: string; - /** - * The crosshair marker background color. An empty string falls back to the color of the series under the crosshair. - * - * @defaultValue `''` - */ - crosshairMarkerBackgroundColor: string; - /** - * Crosshair marker border width in pixels. - * - * @defaultValue `2` - */ - crosshairMarkerBorderWidth: number; - /** - * Last price animation mode. - * - * @defaultValue {@link LastPriceAnimationMode.Disabled} - */ - lastPriceAnimation: LastPriceAnimationMode; + /** + * Base value of the series. + * + * @defaultValue `{ type: 'price', price: 0 }` + */ + baseValue: BaseValueType; + /** + * Gradient is relative to the base value and the currently visible range. + * If it is false, the gradient is relative to the top and bottom of the chart. + * + * @defaultValue `false` + */ + relativeGradient: boolean; + /** + * The first color of the top area. + * + * @defaultValue `'rgba(38, 166, 154, 0.28)'` + */ + topFillColor1: string; + /** + * The second color of the top area. + * + * @defaultValue `'rgba(38, 166, 154, 0.05)'` + */ + topFillColor2: string; + /** + * The line color of the top area. + * + * @defaultValue `'rgba(38, 166, 154, 1)'` + */ + topLineColor: string; + /** + * The first color of the bottom area. + * + * @defaultValue `'rgba(239, 83, 80, 0.05)'` + */ + bottomFillColor1: string; + /** + * The second color of the bottom area. + * + * @defaultValue `'rgba(239, 83, 80, 0.28)'` + */ + bottomFillColor2: string; + /** + * The line color of the bottom area. + * + * @defaultValue `'rgba(239, 83, 80, 1)'` + */ + bottomLineColor: string; + /** + * Line width. + * + * @defaultValue `3` + */ + lineWidth: LineWidth; + /** + * Line style. + * + * @defaultValue {@link LineStyle.Solid} + */ + lineStyle: LineStyle; + /** + * Line type. + * + * @defaultValue {@link LineType.Simple} + */ + lineType: LineType; + /** + * Show series line. + * + * @defaultValue `true` + */ + lineVisible: boolean; + /** + * Show circle markers on each point. + * + * @defaultValue `false` + */ + pointMarkersVisible: boolean; + /** + * Circle markers radius in pixels. + * + * @defaultValue `undefined` + */ + pointMarkersRadius?: number; + /** + * Show the crosshair marker. + * + * @defaultValue `true` + */ + crosshairMarkerVisible: boolean; + /** + * Crosshair marker radius in pixels. + * + * @defaultValue `4` + */ + crosshairMarkerRadius: number; + /** + * Crosshair marker border color. An empty string falls back to the color of the series under the crosshair. + * + * @defaultValue `''` + */ + crosshairMarkerBorderColor: string; + /** + * The crosshair marker background color. An empty string falls back to the color of the series under the crosshair. + * + * @defaultValue `''` + */ + crosshairMarkerBackgroundColor: string; + /** + * Crosshair marker border width in pixels. + * + * @defaultValue `2` + */ + crosshairMarkerBorderWidth: number; + /** + * Last price animation mode. + * + * @defaultValue {@link LastPriceAnimationMode.Disabled} + */ + lastPriceAnimation: LastPriceAnimationMode; } /** * Represents a time as a day/month/year. @@ -818,313 +851,320 @@ export interface BaselineStyleOptions { * ``` */ export interface BusinessDay { - /** - * The year. - */ - year: number; - /** - * The month. - */ - month: number; - /** - * The day. - */ - day: number; + /** + * The year. + */ + year: number; + /** + * The month. + */ + month: number; + /** + * The day. + */ + day: number; } /** * Structure describing a single item of data for candlestick series */ -export interface CandlestickData extends OhlcData { - /** - * Optional color value for certain data item. If missed, color from options is used - */ - color?: string; - /** - * Optional border color value for certain data item. If missed, color from options is used - */ - borderColor?: string; - /** - * Optional wick color value for certain data item. If missed, color from options is used - */ - wickColor?: string; +export interface CandlestickData + extends OhlcData { + /** + * Optional color value for certain data item. If missed, color from options is used + */ + color?: string; + /** + * Optional border color value for certain data item. If missed, color from options is used + */ + borderColor?: string; + /** + * Optional wick color value for certain data item. If missed, color from options is used + */ + wickColor?: string; } /** * Represents style options for a candlestick series. */ export interface CandlestickStyleOptions { - /** - * Color of rising candles. - * - * @defaultValue `'#26a69a'` - */ - upColor: string; - /** - * Color of falling candles. - * - * @defaultValue `'#ef5350'` - */ - downColor: string; - /** - * Enable high and low prices candle wicks. - * - * @defaultValue `true` - */ - wickVisible: boolean; - /** - * Enable candle borders. - * - * @defaultValue `true` - */ - borderVisible: boolean; - /** - * Border color. - * - * @defaultValue `'#378658'` - */ - borderColor: string; - /** - * Border color of rising candles. - * - * @defaultValue `'#26a69a'` - */ - borderUpColor: string; - /** - * Border color of falling candles. - * - * @defaultValue `'#ef5350'` - */ - borderDownColor: string; - /** - * Wick color. - * - * @defaultValue `'#737375'` - */ - wickColor: string; - /** - * Wick color of rising candles. - * - * @defaultValue `'#26a69a'` - */ - wickUpColor: string; - /** - * Wick color of falling candles. - * - * @defaultValue `'#ef5350'` - */ - wickDownColor: string; + /** + * Color of rising candles. + * + * @defaultValue `'#26a69a'` + */ + upColor: string; + /** + * Color of falling candles. + * + * @defaultValue `'#ef5350'` + */ + downColor: string; + /** + * Enable high and low prices candle wicks. + * + * @defaultValue `true` + */ + wickVisible: boolean; + /** + * Enable candle borders. + * + * @defaultValue `true` + */ + borderVisible: boolean; + /** + * Border color. + * + * @defaultValue `'#378658'` + */ + borderColor: string; + /** + * Border color of rising candles. + * + * @defaultValue `'#26a69a'` + */ + borderUpColor: string; + /** + * Border color of falling candles. + * + * @defaultValue `'#ef5350'` + */ + borderDownColor: string; + /** + * Wick color. + * + * @defaultValue `'#737375'` + */ + wickColor: string; + /** + * Wick color of rising candles. + * + * @defaultValue `'#26a69a'` + */ + wickUpColor: string; + /** + * Wick color of falling candles. + * + * @defaultValue `'#ef5350'` + */ + wickDownColor: string; } /** * Represents common chart options */ export interface ChartOptionsBase { - /** - * Width of the chart in pixels - * - * @defaultValue If `0` (default) or none value provided, then a size of the widget will be calculated based its container's size. - */ - width: number; - /** - * Height of the chart in pixels - * - * @defaultValue If `0` (default) or none value provided, then a size of the widget will be calculated based its container's size. - */ - height: number; - /** - * Setting this flag to `true` will make the chart watch the chart container's size and automatically resize the chart to fit its container whenever the size changes. - * - * This feature requires [`ResizeObserver`](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver) class to be available in the global scope. - * Note that calling code is responsible for providing a polyfill if required. If the global scope does not have `ResizeObserver`, a warning will appear and the flag will be ignored. - * - * Please pay attention that `autoSize` option and explicit sizes options `width` and `height` don't conflict with one another. - * If you specify `autoSize` flag, then `width` and `height` options will be ignored unless `ResizeObserver` has failed. If it fails then the values will be used as fallback. - * - * The flag `autoSize` could also be set with and unset with `applyOptions` function. - * ```js - * const chart = LightweightCharts.createChart(document.body, { - * autoSize: true, - * }); - * ``` - */ - autoSize: boolean; - /** - * Layout options - */ - layout: LayoutOptions; - /** - * Left price scale options - */ - leftPriceScale: VisiblePriceScaleOptions; - /** - * Right price scale options - */ - rightPriceScale: VisiblePriceScaleOptions; - /** - * Overlay price scale options - */ - overlayPriceScales: OverlayPriceScaleOptions; - /** - * Time scale options - */ - timeScale: HorzScaleOptions; - /** - * The crosshair shows the intersection of the price and time scale values at any point on the chart. - * - */ - crosshair: CrosshairOptions; - /** - * A grid is represented in the chart background as a vertical and horizontal lines drawn at the levels of visible marks of price and the time scales. - */ - grid: GridOptions; - /** - * Scroll options, or a boolean flag that enables/disables scrolling - */ - handleScroll: HandleScrollOptions | boolean; - /** - * Scale options, or a boolean flag that enables/disables scaling - */ - handleScale: HandleScaleOptions | boolean; - /** - * Kinetic scroll options - */ - kineticScroll: KineticScrollOptions; - /** @inheritDoc TrackingModeOptions - */ - trackingMode: TrackingModeOptions; - /** - * Basic localization options - */ - localization: LocalizationOptionsBase; - /** - * Whether to add a default pane to the chart - * Disable this option when you want to create a chart with no panes and add them manually - * @defaultValue `true` - */ - addDefaultPane: boolean; + /** + * Width of the chart in pixels + * + * @defaultValue If `0` (default) or none value provided, then a size of the widget will be calculated based its container's size. + */ + width: number; + /** + * Height of the chart in pixels + * + * @defaultValue If `0` (default) or none value provided, then a size of the widget will be calculated based its container's size. + */ + height: number; + /** + * Setting this flag to `true` will make the chart watch the chart container's size and automatically resize the chart to fit its container whenever the size changes. + * + * This feature requires [`ResizeObserver`](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver) class to be available in the global scope. + * Note that calling code is responsible for providing a polyfill if required. If the global scope does not have `ResizeObserver`, a warning will appear and the flag will be ignored. + * + * Please pay attention that `autoSize` option and explicit sizes options `width` and `height` don't conflict with one another. + * If you specify `autoSize` flag, then `width` and `height` options will be ignored unless `ResizeObserver` has failed. If it fails then the values will be used as fallback. + * + * The flag `autoSize` could also be set with and unset with `applyOptions` function. + * ```js + * const chart = LightweightCharts.createChart(document.body, { + * autoSize: true, + * }); + * ``` + */ + autoSize: boolean; + /** + * Layout options + */ + layout: LayoutOptions; + /** + * Left price scale options + */ + leftPriceScale: VisiblePriceScaleOptions; + /** + * Right price scale options + */ + rightPriceScale: VisiblePriceScaleOptions; + /** + * Overlay price scale options + */ + overlayPriceScales: OverlayPriceScaleOptions; + /** + * Time scale options + */ + timeScale: HorzScaleOptions; + /** + * The crosshair shows the intersection of the price and time scale values at any point on the chart. + * + */ + crosshair: CrosshairOptions; + /** + * A grid is represented in the chart background as a vertical and horizontal lines drawn at the levels of visible marks of price and the time scales. + */ + grid: GridOptions; + /** + * Scroll options, or a boolean flag that enables/disables scrolling + */ + handleScroll: HandleScrollOptions | boolean; + /** + * Scale options, or a boolean flag that enables/disables scaling + */ + handleScale: HandleScaleOptions | boolean; + /** + * Kinetic scroll options + */ + kineticScroll: KineticScrollOptions; + /** @inheritDoc TrackingModeOptions + */ + trackingMode: TrackingModeOptions; + /** + * Basic localization options + */ + localization: LocalizationOptionsBase; + /** + * Whether to add a default pane to the chart + * Disable this option when you want to create a chart with no panes and add them manually + * @defaultValue `true` + */ + addDefaultPane: boolean; } /** * Structure describing options of the chart. Series options are to be set separately */ export interface ChartOptionsImpl extends ChartOptionsBase { - /** - * Localization options. - */ - localization: LocalizationOptions; + /** + * Localization options. + */ + localization: LocalizationOptions; } /** Structure describing a crosshair line (vertical or horizontal) */ export interface CrosshairLineOptions { - /** - * Crosshair line color. - * - * @defaultValue `'#758696'` - */ - color: string; - /** - * Crosshair line width. - * - * @defaultValue `1` - */ - width: LineWidth; - /** - * Crosshair line style. - * - * @defaultValue {@link LineStyle.LargeDashed} - */ - style: LineStyle; - /** - * Display the crosshair line. - * - * Note that disabling crosshair lines does not disable crosshair marker on Line and Area series. - * It can be disabled by using `crosshairMarkerVisible` option of a relevant series. - * - * @see {@link LineStyleOptions.crosshairMarkerVisible} - * @see {@link AreaStyleOptions.crosshairMarkerVisible} - * @see {@link BaselineStyleOptions.crosshairMarkerVisible} - * @defaultValue `true` - */ - visible: boolean; - /** - * Display the crosshair label on the relevant scale. - * - * @defaultValue `true` - */ - labelVisible: boolean; - /** - * Crosshair label background color. - * - * @defaultValue `'#4c525e'` - */ - labelBackgroundColor: string; + /** + * Crosshair line color. + * + * @defaultValue `'#758696'` + */ + color: string; + /** + * Crosshair line width. + * + * @defaultValue `1` + */ + width: LineWidth; + /** + * Crosshair line style. + * + * @defaultValue {@link LineStyle.LargeDashed} + */ + style: LineStyle; + /** + * Display the crosshair line. + * + * Note that disabling crosshair lines does not disable crosshair marker on Line and Area series. + * It can be disabled by using `crosshairMarkerVisible` option of a relevant series. + * + * @see {@link LineStyleOptions.crosshairMarkerVisible} + * @see {@link AreaStyleOptions.crosshairMarkerVisible} + * @see {@link BaselineStyleOptions.crosshairMarkerVisible} + * @defaultValue `true` + */ + visible: boolean; + /** + * Display the crosshair label on the relevant scale. + * + * @defaultValue `true` + */ + labelVisible: boolean; + /** + * Crosshair label background color. + * + * @defaultValue `'#4c525e'` + */ + labelBackgroundColor: string; } /** Structure describing crosshair options */ export interface CrosshairOptions { - /** - * Crosshair mode - * - * @defaultValue {@link CrosshairMode.Magnet} - */ - mode: CrosshairMode; - /** - * Vertical line options. - */ - vertLine: CrosshairLineOptions; - /** - * Horizontal line options. - */ - horzLine: CrosshairLineOptions; - /** - * If set to `true`, the crosshair will not snap to the data points of hidden series. - * - * @defaultValue `false` - */ - doNotSnapToHiddenSeriesIndices: boolean; + /** + * Crosshair mode + * + * @defaultValue {@link CrosshairMode.Magnet} + */ + mode: CrosshairMode; + /** + * Vertical line options. + */ + vertLine: CrosshairLineOptions; + /** + * Horizontal line options. + */ + horzLine: CrosshairLineOptions; + /** + * If set to `true`, the crosshair will not snap to the data points of hidden series. + * + * @defaultValue `false` + */ + doNotSnapToHiddenSeriesIndices: boolean; } /** * Renderer data for an item within the custom series. */ -export interface CustomBarItemData = CustomData> { - /** - * Horizontal coordinate for the item. Measured from the left edge of the pane in pixels. - */ - x: number; - /** - * Time scale index for the item. This isn't the timestamp but rather the logical index. - */ - time: number; - /** - * Original data for the item. - */ - originalData: TData; - /** - * Color assigned for the item, typically used for price line and price scale label. - */ - barColor: string; +export interface CustomBarItemData< + HorzScaleItem, + TData extends CustomData = CustomData, +> { + /** + * Horizontal coordinate for the item. Measured from the left edge of the pane in pixels. + */ + x: number; + /** + * Time scale index for the item. This isn't the timestamp but rather the logical index. + */ + time: number; + /** + * Original data for the item. + */ + originalData: TData; + /** + * Color assigned for the item, typically used for price line and price scale label. + */ + barColor: string; } /** * Context object provided to custom series conflation reducers. * This wraps the internal SeriesPlotRow data while providing a user-friendly interface. */ -export interface CustomConflationContext = CustomData> { - /** - * The original custom data item provided by the user. - */ - readonly data: TData; - /** - * The time index of the data point in the series. - */ - readonly index: number; - /** - * The original time value provided by the user. - */ - readonly originalTime: HorzScaleItem; - /** - * The internal time point object. - */ - readonly time: unknown; - /** - * The computed price values for this data point (as returned by priceValueBuilder). - * The last value in this array is used as the current price. - */ - readonly priceValues: CustomSeriesPricePlotValues; +export interface CustomConflationContext< + HorzScaleItem = Time, + TData extends CustomData = CustomData, +> { + /** + * The original custom data item provided by the user. + */ + readonly data: TData; + /** + * The time index of the data point in the series. + */ + readonly index: number; + /** + * The original time value provided by the user. + */ + readonly originalTime: HorzScaleItem; + /** + * The internal time point object. + */ + readonly time: unknown; + /** + * The computed price values for this data point (as returned by priceValueBuilder). + * The last value in this array is used as the current price. + */ + readonly priceValues: CustomSeriesPricePlotValues; } /** * Base structure describing a single item of data for a custom series. @@ -1133,1576 +1173,1666 @@ export interface CustomConflationContext extends CustomSeriesWhitespaceData { - /** - * If defined then this color will be used for the price line and price scale line - * for this specific data item of the custom series. - */ - color?: string; +export interface CustomData + extends CustomSeriesWhitespaceData { + /** + * If defined then this color will be used for the price line and price scale line + * for this specific data item of the custom series. + */ + color?: string; } /** * Represents a whitespace data item, which is a data point without a value. */ export interface CustomSeriesWhitespaceData { - /** - * The time of the data. - */ - time: HorzScaleItem; - /** - * Additional custom values which will be ignored by the library, but - * could be used by plugins. - */ - customValues?: Record; + /** + * The time of the data. + */ + time: HorzScaleItem; + /** + * Additional custom values which will be ignored by the library, but + * could be used by plugins. + */ + customValues?: Record; } /** * Represents style options for a custom series. */ export interface CustomStyleOptions { - /** - * Color used for the price line and price scale label. - */ - color: string; + /** + * Color used for the price line and price scale label. + */ + color: string; } /** * Helper drawing utilities exposed by the library to a Primitive (a.k.a plugin). */ export interface DrawingUtils { - /** - * Drawing utility to change the line style on the canvas context to one of the - * built-in line styles. - * @param ctx - 2D rendering context for the target canvas. - * @param lineStyle - Built-in {@link LineStyle} to set on the canvas context. - */ - readonly setLineStyle: (ctx: CanvasRenderingContext2D, lineStyle: LineStyle) => void; + /** + * Drawing utility to change the line style on the canvas context to one of the + * built-in line styles. + * @param ctx - 2D rendering context for the target canvas. + * @param lineStyle - Built-in {@link LineStyle} to set on the canvas context. + */ + readonly setLineStyle: ( + ctx: CanvasRenderingContext2D, + lineStyle: LineStyle, + ) => void; } /** Grid line options. */ export interface GridLineOptions { - /** - * Line color. - * - * @defaultValue `'#D6DCDE'` - */ - color: string; - /** - * Line style. - * - * @defaultValue {@link LineStyle.Solid} - */ - style: LineStyle; - /** - * Display the lines. - * - * @defaultValue `true` - */ - visible: boolean; + /** + * Line color. + * + * @defaultValue `'#D6DCDE'` + */ + color: string; + /** + * Line style. + * + * @defaultValue {@link LineStyle.Solid} + */ + style: LineStyle; + /** + * Display the lines. + * + * @defaultValue `true` + */ + visible: boolean; } /** Structure describing grid options. */ export interface GridOptions { - /** - * Vertical grid line options. - */ - vertLines: GridLineOptions; - /** - * Horizontal grid line options. - */ - horzLines: GridLineOptions; + /** + * Vertical grid line options. + */ + vertLines: GridLineOptions; + /** + * Horizontal grid line options. + */ + horzLines: GridLineOptions; } /** * Represents options for how the chart is scaled by the mouse and touch gestures. */ export interface HandleScaleOptions { - /** - * Enable scaling with the mouse wheel. - * - * @defaultValue `true` - */ - mouseWheel: boolean; - /** - * Enable scaling with pinch/zoom gestures. - * - * @defaultValue `true` - */ - pinch: boolean; - /** - * Enable scaling the price and/or time scales by holding down the left mouse button and moving the mouse. - */ - axisPressedMouseMove: AxisPressedMouseMoveOptions | boolean; - /** - * Enable resetting scaling by double-clicking the left mouse button. - */ - axisDoubleClickReset: AxisDoubleClickOptions | boolean; + /** + * Enable scaling with the mouse wheel. + * + * @defaultValue `true` + */ + mouseWheel: boolean; + /** + * Enable scaling with pinch/zoom gestures. + * + * @defaultValue `true` + */ + pinch: boolean; + /** + * Enable scaling the price and/or time scales by holding down the left mouse button and moving the mouse. + */ + axisPressedMouseMove: AxisPressedMouseMoveOptions | boolean; + /** + * Enable resetting scaling by double-clicking the left mouse button. + */ + axisDoubleClickReset: AxisDoubleClickOptions | boolean; } /** * Represents options for how the chart is scrolled by the mouse and touch gestures. */ export interface HandleScrollOptions { - /** - * Enable scrolling with the mouse wheel. - * - * @defaultValue `true` - */ - mouseWheel: boolean; - /** - * Enable scrolling by holding down the left mouse button and moving the mouse. - * - * @defaultValue `true` - */ - pressedMouseMove: boolean; - /** - * Enable horizontal touch scrolling. - * - * When enabled the chart handles touch gestures that would normally scroll the webpage horizontally. - * - * @defaultValue `true` - */ - horzTouchDrag: boolean; - /** - * Enable vertical touch scrolling. - * - * When enabled the chart handles touch gestures that would normally scroll the webpage vertically. - * - * @defaultValue `true` - */ - vertTouchDrag: boolean; + /** + * Enable scrolling with the mouse wheel. + * + * @defaultValue `true` + */ + mouseWheel: boolean; + /** + * Enable scrolling by holding down the left mouse button and moving the mouse. + * + * @defaultValue `true` + */ + pressedMouseMove: boolean; + /** + * Enable horizontal touch scrolling. + * + * When enabled the chart handles touch gestures that would normally scroll the webpage horizontally. + * + * @defaultValue `true` + */ + horzTouchDrag: boolean; + /** + * Enable vertical touch scrolling. + * + * When enabled the chart handles touch gestures that would normally scroll the webpage vertically. + * + * @defaultValue `true` + */ + vertTouchDrag: boolean; } /** * Structure describing a single item of data for histogram series */ -export interface HistogramData extends SingleValueData { - /** - * Optional color value for certain data item. If missed, color from options is used - */ - color?: string; +export interface HistogramData + extends SingleValueData { + /** + * Optional color value for certain data item. If missed, color from options is used + */ + color?: string; } /** * Represents style options for a histogram series. */ export interface HistogramStyleOptions { - /** - * Column color. - * - * @defaultValue `'#26a69a'` - */ - color: string; - /** - * Initial level of histogram columns. - * - * @defaultValue `0` - */ - base: number; + /** + * Column color. + * + * @defaultValue `'#26a69a'` + */ + color: string; + /** + * Initial level of histogram columns. + * + * @defaultValue `0` + */ + base: number; } /** * Options for the time scale; the horizontal scale at the bottom of the chart that displays the time of data. */ export interface HorzScaleOptions { - /** - * The margin space in bars from the right side of the chart. - * - * @defaultValue `0` - */ - rightOffset: number; - /** - * The margin space in pixels from the right side of the chart. - * This option has priority over `rightOffset`. - * - * @defaultValue `undefined` - */ - rightOffsetPixels?: number; - /** - * The space between bars in pixels. - * - * @defaultValue `6` - */ - barSpacing: number; - /** - * The minimum space between bars in pixels. - * - * @defaultValue `0.5` - */ - minBarSpacing: number; - /** - * The maximum space between bars in pixels. - * - * Has no effect if value is set to `0`. - * - * @defaultValue `0` - */ - maxBarSpacing: number; - /** - * Prevent scrolling to the left of the first bar. - * - * @defaultValue `false` - */ - fixLeftEdge: boolean; - /** - * Prevent scrolling to the right of the most recent bar. - * - * @defaultValue `false` - */ - fixRightEdge: boolean; - /** - * Prevent changing the visible time range during chart resizing. - * - * @defaultValue `false` - */ - lockVisibleTimeRangeOnResize: boolean; - /** - * Prevent the hovered bar from moving when scrolling. - * - * @defaultValue `false` - */ - rightBarStaysOnScroll: boolean; - /** - * Show the time scale border. - * - * @defaultValue `true` - */ - borderVisible: boolean; - /** - * The time scale border color. - * - * @defaultValue `'#2B2B43'` - */ - borderColor: string; - /** - * Show the time scale. - * - * @defaultValue `true` - */ - visible: boolean; - /** - * Show the time, not just the date, in the time scale and vertical crosshair label. - * - * @defaultValue `false` - */ - timeVisible: boolean; - /** - * Show seconds in the time scale and vertical crosshair label in `hh:mm:ss` format for intraday data. - * - * @defaultValue `true` - */ - secondsVisible: boolean; - /** - * Shift the visible range to the right (into the future) by the number of new bars when new data is added. - * - * Note that this only applies when the last bar is visible. - * - * @defaultValue `true` - */ - shiftVisibleRangeOnNewBar: boolean; - /** - * Allow the visible range to be shifted to the right when a new bar is added which - * is replacing an existing whitespace time point on the chart. - * - * Note that this only applies when the last bar is visible & `shiftVisibleRangeOnNewBar` is enabled. - * - * @defaultValue `false` - */ - allowShiftVisibleRangeOnWhitespaceReplacement: boolean; - /** - * Draw small vertical line on time axis labels. - * - * @defaultValue `false` - */ - ticksVisible: boolean; - /** - * Maximum tick mark label length. Used to override the default 8 character maximum length. - * - * @defaultValue `undefined` - */ - tickMarkMaxCharacterLength?: number; - /** - * Changes horizontal scale marks generation. - * With this flag equal to `true`, marks of the same weight are either all drawn or none are drawn at all. - */ - uniformDistribution: boolean; - /** - * Define a minimum height for the time scale. - * Note: This value will be exceeded if the - * time scale needs more space to display it's contents. - * - * Setting a minimum height could be useful for ensuring that - * multiple charts positioned in a horizontal stack each have - * an identical time scale height, or for plugins which - * require a bit more space within the time scale pane. - * - * @defaultValue 0 - */ - minimumHeight: number; - /** - * Allow major time scale labels to be rendered in a bolder font weight. - * - * @defaultValue true - */ - allowBoldLabels: boolean; - /** - * Ignore time scale points containing only whitespace (for all series) when - * drawing grid lines, tick marks, and snapping the crosshair to time scale points. - * - * For the yield curve chart type it defaults to `true`. - * - * @defaultValue false - */ - ignoreWhitespaceIndices: boolean; - /** - * Enable data conflation for performance optimization when bar spacing is very small. - * When enabled, multiple data points are automatically combined into single points - * when they would be rendered in less than 0.5 pixels of screen space. - * This significantly improves rendering performance for large datasets when zoomed out. - * - * @defaultValue false - */ - enableConflation: boolean; - /** - * Smoothing factor for conflation thresholds. Controls how aggressively conflation is applied. - * This can be used to create smoother-looking charts, especially useful for sparklines and small charts. - * - * - 1.0 = conflate only when display can't show detail (default, performance-focused) - * - 2.0 = conflate at 2x the display threshold (moderate smoothing) - * - 4.0 = conflate at 4x the display threshold (strong smoothing) - * - 8.0+ = very aggressive smoothing for very small charts - * - * Higher values result in fewer data points being displayed, creating smoother but less detailed charts. - * This is particularly useful for sparklines and small charts where smooth appearance is prioritized over showing every data point. - * - * Note: Should be used with continuous series types (line, area, baseline) for best visual results. - * Candlestick and bar series may look less natural with high smoothing factors. - * - * @defaultValue 1.0 - */ - conflationThresholdFactor?: number; - /** - * Precompute conflation chunks for common levels right after data load. - * When enabled, the system will precompute conflation data in the background, - * which improves performance when zooming out but increases initial load time - * and memory usage. - * - * Performance impact: - * - Initial load: +100-500ms depending on dataset size - * - Memory usage: +20-50% of original dataset size - * - Zoom performance: Significant improvement (10-100x faster) - * - * Recommended for: Large datasets (\>10K points) on machines with sufficient memory - * @defaultValue false - */ - precomputeConflationOnInit: boolean; - /** - * Priority used for background precompute tasks when the Prioritized Task Scheduling API is available. - * - * Options: - * - 'background': Lowest priority, tasks run only when the browser is idle - * - 'user-visible': Medium priority, tasks run when they might affect visible content - * - 'user-blocking': Highest priority, tasks run immediately and may block user interaction - * - * Recommendation: Use 'background' for most cases to avoid impacting user experience. - * Only use higher priorities if conflation is critical for your application's functionality. - * @defaultValue 'background' - */ - precomputeConflationPriority: "background" | "user-visible" | "user-blocking"; + /** + * The margin space in bars from the right side of the chart. + * + * @defaultValue `0` + */ + rightOffset: number; + /** + * The margin space in pixels from the right side of the chart. + * This option has priority over `rightOffset`. + * + * @defaultValue `undefined` + */ + rightOffsetPixels?: number; + /** + * The space between bars in pixels. + * + * @defaultValue `6` + */ + barSpacing: number; + /** + * The minimum space between bars in pixels. + * + * @defaultValue `0.5` + */ + minBarSpacing: number; + /** + * The maximum space between bars in pixels. + * + * Has no effect if value is set to `0`. + * + * @defaultValue `0` + */ + maxBarSpacing: number; + /** + * Prevent scrolling to the left of the first bar. + * + * @defaultValue `false` + */ + fixLeftEdge: boolean; + /** + * Prevent scrolling to the right of the most recent bar. + * + * @defaultValue `false` + */ + fixRightEdge: boolean; + /** + * Prevent changing the visible time range during chart resizing. + * + * @defaultValue `false` + */ + lockVisibleTimeRangeOnResize: boolean; + /** + * Prevent the hovered bar from moving when scrolling. + * + * @defaultValue `false` + */ + rightBarStaysOnScroll: boolean; + /** + * Show the time scale border. + * + * @defaultValue `true` + */ + borderVisible: boolean; + /** + * The time scale border color. + * + * @defaultValue `'#2B2B43'` + */ + borderColor: string; + /** + * Show the time scale. + * + * @defaultValue `true` + */ + visible: boolean; + /** + * Show the time, not just the date, in the time scale and vertical crosshair label. + * + * @defaultValue `false` + */ + timeVisible: boolean; + /** + * Show seconds in the time scale and vertical crosshair label in `hh:mm:ss` format for intraday data. + * + * @defaultValue `true` + */ + secondsVisible: boolean; + /** + * Shift the visible range to the right (into the future) by the number of new bars when new data is added. + * + * Note that this only applies when the last bar is visible. + * + * @defaultValue `true` + */ + shiftVisibleRangeOnNewBar: boolean; + /** + * Allow the visible range to be shifted to the right when a new bar is added which + * is replacing an existing whitespace time point on the chart. + * + * Note that this only applies when the last bar is visible & `shiftVisibleRangeOnNewBar` is enabled. + * + * @defaultValue `false` + */ + allowShiftVisibleRangeOnWhitespaceReplacement: boolean; + /** + * Draw small vertical line on time axis labels. + * + * @defaultValue `false` + */ + ticksVisible: boolean; + /** + * Maximum tick mark label length. Used to override the default 8 character maximum length. + * + * @defaultValue `undefined` + */ + tickMarkMaxCharacterLength?: number; + /** + * Changes horizontal scale marks generation. + * With this flag equal to `true`, marks of the same weight are either all drawn or none are drawn at all. + */ + uniformDistribution: boolean; + /** + * Define a minimum height for the time scale. + * Note: This value will be exceeded if the + * time scale needs more space to display it's contents. + * + * Setting a minimum height could be useful for ensuring that + * multiple charts positioned in a horizontal stack each have + * an identical time scale height, or for plugins which + * require a bit more space within the time scale pane. + * + * @defaultValue 0 + */ + minimumHeight: number; + /** + * Allow major time scale labels to be rendered in a bolder font weight. + * + * @defaultValue true + */ + allowBoldLabels: boolean; + /** + * Ignore time scale points containing only whitespace (for all series) when + * drawing grid lines, tick marks, and snapping the crosshair to time scale points. + * + * For the yield curve chart type it defaults to `true`. + * + * @defaultValue false + */ + ignoreWhitespaceIndices: boolean; + /** + * Enable data conflation for performance optimization when bar spacing is very small. + * When enabled, multiple data points are automatically combined into single points + * when they would be rendered in less than 0.5 pixels of screen space. + * This significantly improves rendering performance for large datasets when zoomed out. + * + * @defaultValue false + */ + enableConflation: boolean; + /** + * Smoothing factor for conflation thresholds. Controls how aggressively conflation is applied. + * This can be used to create smoother-looking charts, especially useful for sparklines and small charts. + * + * - 1.0 = conflate only when display can't show detail (default, performance-focused) + * - 2.0 = conflate at 2x the display threshold (moderate smoothing) + * - 4.0 = conflate at 4x the display threshold (strong smoothing) + * - 8.0+ = very aggressive smoothing for very small charts + * + * Higher values result in fewer data points being displayed, creating smoother but less detailed charts. + * This is particularly useful for sparklines and small charts where smooth appearance is prioritized over showing every data point. + * + * Note: Should be used with continuous series types (line, area, baseline) for best visual results. + * Candlestick and bar series may look less natural with high smoothing factors. + * + * @defaultValue 1.0 + */ + conflationThresholdFactor?: number; + /** + * Precompute conflation chunks for common levels right after data load. + * When enabled, the system will precompute conflation data in the background, + * which improves performance when zooming out but increases initial load time + * and memory usage. + * + * Performance impact: + * - Initial load: +100-500ms depending on dataset size + * - Memory usage: +20-50% of original dataset size + * - Zoom performance: Significant improvement (10-100x faster) + * + * Recommended for: Large datasets (\>10K points) on machines with sufficient memory + * @defaultValue false + */ + precomputeConflationOnInit: boolean; + /** + * Priority used for background precompute tasks when the Prioritized Task Scheduling API is available. + * + * Options: + * - 'background': Lowest priority, tasks run only when the browser is idle + * - 'user-visible': Medium priority, tasks run when they might affect visible content + * - 'user-blocking': Highest priority, tasks run immediately and may block user interaction + * + * Recommendation: Use 'background' for most cases to avoid impacting user experience. + * Only use higher priorities if conflation is critical for your application's functionality. + * @defaultValue 'background' + */ + precomputeConflationPriority: "background" | "user-visible" | "user-blocking"; } /** * The main interface of a single chart using time for horizontal scale. */ export interface IChartApi extends IChartApiBase