Compare commits

...

93 Commits

Author SHA1 Message Date
nym21 b3031b3375 website: redesign part 33 2026-06-21 17:12:25 +02:00
nym21 2e401379a0 deps: bumped 2026-06-21 17:11:43 +02:00
nym21 45ab6ebf71 website: redesign part 32 2026-06-19 23:16:27 +02:00
nym21 00f7d69ea6 website: redesign part 31 2026-06-18 22:39:28 +02:00
nym21 408d83c350 global: private xpub support part 3 2026-06-17 21:23:26 +02:00
nym21 43df9e098c global: private xpub support part 2 2026-06-17 11:25:42 +02:00
nym21 0c7861071d global: private xpub support part 1 2026-06-16 23:37:03 +02:00
nym21 6f430bdb8c clients: bump versions 2026-06-15 16:24:56 +02:00
nym21 4b415b215d release: v0.3.4 2026-06-14 00:46:39 +02:00
nym21 8614e9eded docs: update generated docs 2026-06-14 00:46:07 +02:00
nym21 c85da92cbc global: add cohorts by entry 2026-06-14 00:40:18 +02:00
nym21 297fc3b855 website: redesign part 30 2026-06-12 12:25:06 +02:00
nym21 c9d5a62fcb website: redesign part 29 2026-06-09 17:01:28 +02:00
nym21 90b3b51c48 website: redesign part 28 2026-06-09 16:12:50 +02:00
nym21 5966ab05e4 website: redesign part 27 2026-06-09 13:35:21 +02:00
nym21 c3506339cd website: redesign part 26 2026-06-09 11:26:19 +02:00
nym21 e54843291e website: redesign part 25 2026-06-09 11:26:14 +02:00
nym21 b0b261fe9f website: redesign part 24 2026-06-08 16:37:53 +02:00
nym21 6786be296d website: redesign part 23 2026-06-08 16:08:52 +02:00
nym21 e5068bbbf3 website: redesign part 24 2026-06-07 23:11:18 +02:00
nym21 36cfe49b20 website: redesign part 23 2026-06-07 16:46:53 +02:00
nym21 33cc13708a website: redesign part 22 2026-06-07 16:11:42 +02:00
nym21 2389632812 website: redesign part 21 2026-06-07 13:10:23 +02:00
nym21 e0bcdb8105 website: redesign part 20 2026-06-07 12:22:49 +02:00
nym21 45e83c98b9 website: redesign part 19 2026-06-07 11:14:15 +02:00
nym21 753bbf3e7e website: redesign part 18 2026-06-07 11:07:15 +02:00
nym21 54cc0cb446 website: redesign part 17 2026-06-07 11:01:31 +02:00
nym21 d64dcb75a9 website: redesign part 16 2026-06-07 01:44:52 +02:00
nym21 f599115f6c website: redesign part 15 2026-06-07 01:38:38 +02:00
nym21 9fc45625ad website: redesign part 14 2026-06-07 01:05:04 +02:00
nym21 c68d1d1fda website: redesign part 13 2026-06-07 00:54:50 +02:00
nym21 6cbe09af23 release: v0.3.3 2026-06-06 22:37:40 +02:00
nym21 96d35d1d29 docs: update generated docs 2026-06-06 22:37:07 +02:00
nym21 e23554811b website: redesign part 12 2026-06-06 22:30:13 +02:00
nym21 041c542046 website: redesign part 11 2026-06-06 22:29:33 +02:00
nym21 66dc7cd8f5 website: redesign part 10 2026-06-06 11:03:55 +02:00
nym21 b00692249c website: redesign part 8 2026-06-05 18:12:46 +02:00
nym21 ff2c04a100 website: redesign part 7 2026-06-05 16:03:04 +02:00
nym21 7cee0e2c5a clients: bump versions 2026-06-04 19:12:58 +02:00
nym21 744032f1f1 release: v0.3.2 2026-06-04 18:56:31 +02:00
nym21 99b171bad6 docs: update generated docs 2026-06-04 18:56:00 +02:00
nym21 37e2b6eae2 changelog: updated 2026-06-04 18:50:35 +02:00
nym21 a967fe8f35 oracle: changes + changelog: updated 2026-06-04 18:35:48 +02:00
nym21 a3f3c54675 oracle: v4 2026-06-04 15:38:01 +02:00
nym21 f41874f438 website: redesign part 6 2026-06-03 18:07:11 +02:00
nym21 98bbfec525 website: redesign part 5 2026-06-03 16:50:52 +02:00
nym21 1bcf3235b6 website: redesign part 4 2026-06-03 16:37:00 +02:00
nym21 07734b8bab website: redesign part 3 2026-06-03 16:26:55 +02:00
nym21 a2fd1e03ad website: redesign part 2 2026-06-03 12:41:26 +02:00
nym21 90e8741fb7 website: redesign part 1 2026-06-03 12:34:05 +02:00
nym21 5f5563fece docs: renamed claude to ai 2026-06-02 12:06:50 +02:00
nym21 c7edfce481 changelog: updated 2026-06-02 09:27:56 +02:00
nym21 7b3dd83b93 clients: bump versions 2026-06-01 20:22:13 +02:00
nym21 cae16227fd release: v0.3.1 2026-06-01 19:19:13 +02:00
nym21 dc2ca0ca27 docs: update generated docs 2026-06-01 19:18:42 +02:00
nym21 d161462137 deps: bumped 2026-06-01 18:10:24 +02:00
nym21 be20633945 heatmaps: part 23 2026-06-01 18:03:41 +02:00
nym21 2bbc535b58 heatmaps: part 22 2026-06-01 17:54:42 +02:00
nym21 88c38e74f9 heatmaps: part 21 2026-06-01 14:25:21 +02:00
nym21 a61b76a4a5 heatmaps: part 20 2026-06-01 13:31:00 +02:00
nym21 46b888337c heatmaps: part 19 2026-06-01 13:20:34 +02:00
nym21 4b49a04186 heatmaps: part 18 2026-06-01 13:03:45 +02:00
nym21 15b0cd2445 heatmaps: part 17 2026-06-01 13:03:39 +02:00
nym21 76720434d7 heatmaps: part 16 2026-06-01 12:19:32 +02:00
nym21 200cd1011e heatmaps: part 15 2026-06-01 12:04:44 +02:00
nym21 cb9f277d49 heatmaps: part 14 2026-06-01 12:01:24 +02:00
nym21 102933b406 heatmaps: part 13 2026-06-01 11:17:00 +02:00
nym21 e64ffac8d1 heatmaps: part 12 2026-06-01 10:56:58 +02:00
nym21 a94d31dfdf heatmaps: part 12 2026-06-01 10:30:44 +02:00
nym21 087a3b6fd6 heatmaps: part 11 2026-06-01 01:04:14 +02:00
nym21 7181d59966 heatmaps: part 10 2026-06-01 00:38:12 +02:00
nym21 3b7734a61a heatmaps: part 9 2026-05-31 23:35:19 +02:00
nym21 7860c5a8bd heatmaps: part 8 2026-05-31 18:57:23 +02:00
nym21 5df399d2f7 heatmaps: part 7 2026-05-31 12:05:48 +02:00
nym21 b2345db279 heatmaps: part 6 2026-05-31 01:38:50 +02:00
nym21 7e2fc8b455 heatmaps: part 5 2026-05-30 15:43:59 +02:00
nym21 c1ff095e4b heatmaps: part 5 2026-05-30 13:16:22 +02:00
nym21 cc8fde59e8 heatmaps: part 4 2026-05-30 11:36:49 +02:00
nym21 e43b53b429 heatmaps: part 3 2026-05-30 11:36:46 +02:00
nym21 6938204a24 heatmaps: part 2 2026-05-29 23:17:39 +02:00
nym21 52883bbdba heatmaps: part 1 2026-05-28 21:58:54 +02:00
nym21 100495fdba deps: bumped 2026-05-27 19:41:37 +02:00
nym21 0ad5be6974 global: snap 2026-05-26 15:33:22 +02:00
nym21 66037c862f global: added support for oracle histograms 2026-05-25 16:44:09 +02:00
nym21 ee20175cbf oracle: cleanup + split lib.rs 2026-05-24 18:40:35 +02:00
nym21 7ad0adf659 oracle: start at 340k 2026-05-24 11:34:57 +02:00
nym21 6219d2301d oracle: snap pre 340k patch 2026-05-24 00:07:25 +02:00
nym21 0aaffc6c43 oracle: doc fixes 2026-05-23 12:18:34 +02:00
nym21 9c74881c5d oracle: snapshot + start at 508k 2026-05-23 00:45:37 +02:00
nym21 bf8de73541 oracle: cleanup 2026-05-22 11:02:56 +02:00
nym21 56e8103178 clients: bump versions 2026-05-22 11:02:12 +02:00
nym21 773c0d090b website: cleanup & fixes 2026-05-22 11:01:34 +02:00
nym21 d6f4c0ac19 changelog: updated 2026-05-22 11:01:07 +02:00
1077 changed files with 33480 additions and 297941 deletions
-15
View File
@@ -1,15 +0,0 @@
name: Check outdated dependencies
on:
schedule:
- cron: "0 9 * * 1"
workflow_dispatch:
jobs:
outdated:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- run: cargo install cargo-outdated
- run: cargo outdated --exit-code 1 --depth 1
+1
View File
@@ -18,6 +18,7 @@ _*
/*.py /*.py
/*.json /*.json
/*.html /*.html
!/btc-cycle-sim.html
/research /research
/filter_* /filter_*
/heatmaps* /heatmaps*
Generated
+205 -329
View File
File diff suppressed because it is too large Load Diff
+31 -31
View File
@@ -4,7 +4,7 @@ members = ["crates/*"]
package.description = "The Bitcoin Research Kit is a suite of tools designed to extract, compute and display data stored on a Bitcoin Core node" package.description = "The Bitcoin Research Kit is a suite of tools designed to extract, compute and display data stored on a Bitcoin Core node"
package.license = "MIT" package.license = "MIT"
package.edition = "2024" package.edition = "2024"
package.version = "0.3.0" package.version = "0.3.4"
package.homepage = "https://bitcoinresearchkit.org" package.homepage = "https://bitcoinresearchkit.org"
package.repository = "https://github.com/bitcoinresearchkit/brk" package.repository = "https://github.com/bitcoinresearchkit/brk"
package.readme = "README.md" package.readme = "README.md"
@@ -35,38 +35,38 @@ debug = true
[workspace.dependencies] [workspace.dependencies]
aide = { version = "0.16.0-alpha.4", features = ["axum-json", "axum-query"] } aide = { version = "0.16.0-alpha.4", features = ["axum-json", "axum-query"] }
axum = { version = "0.8.9", default-features = false, features = ["http1", "json", "query", "tokio", "tracing"] } axum = { version = "0.8.9", default-features = false, features = ["http1", "json", "query", "tokio", "tracing"] }
bitcoin = { version = "0.32.9", features = ["serde"] } bitcoin = { version = "0.32.10", features = ["serde"] }
brk_alloc = { version = "0.3.0", path = "crates/brk_alloc" } brk_alloc = { version = "0.3.4", path = "crates/brk_alloc" }
brk_bencher = { version = "0.3.0", path = "crates/brk_bencher" } brk_bencher = { version = "0.3.4", path = "crates/brk_bencher" }
brk_bindgen = { version = "0.3.0", path = "crates/brk_bindgen" } brk_bindgen = { version = "0.3.4", path = "crates/brk_bindgen" }
brk_cli = { version = "0.3.0", path = "crates/brk_cli" } brk_cli = { version = "0.3.4", path = "crates/brk_cli" }
brk_client = { version = "0.3.0", path = "crates/brk_client" } brk_client = { version = "0.3.4", path = "crates/brk_client" }
brk_cohort = { version = "0.3.0", path = "crates/brk_cohort" } brk_cohort = { version = "0.3.4", path = "crates/brk_cohort" }
brk_computer = { version = "0.3.0", path = "crates/brk_computer" } brk_computer = { version = "0.3.4", path = "crates/brk_computer" }
brk_error = { version = "0.3.0", path = "crates/brk_error" } brk_error = { version = "0.3.4", path = "crates/brk_error" }
brk_fetcher = { version = "0.3.0", path = "crates/brk_fetcher" } brk_fetcher = { version = "0.3.4", path = "crates/brk_fetcher" }
brk_indexer = { version = "0.3.0", path = "crates/brk_indexer" } brk_indexer = { version = "0.3.4", path = "crates/brk_indexer" }
brk_iterator = { version = "0.3.0", path = "crates/brk_iterator" } brk_iterator = { version = "0.3.4", path = "crates/brk_iterator" }
brk_logger = { version = "0.3.0", path = "crates/brk_logger" } brk_logger = { version = "0.3.4", path = "crates/brk_logger" }
brk_mempool = { version = "0.3.0", path = "crates/brk_mempool" } brk_mempool = { version = "0.3.4", path = "crates/brk_mempool" }
brk_oracle = { version = "0.3.0", path = "crates/brk_oracle" } brk_oracle = { version = "0.3.4", path = "crates/brk_oracle" }
brk_query = { version = "0.3.0", path = "crates/brk_query", features = ["tokio"] } brk_query = { version = "0.3.4", path = "crates/brk_query", features = ["tokio"] }
brk_reader = { version = "0.3.0", path = "crates/brk_reader" } brk_reader = { version = "0.3.4", path = "crates/brk_reader" }
brk_rpc = { version = "0.3.0", path = "crates/brk_rpc" } brk_rpc = { version = "0.3.4", path = "crates/brk_rpc" }
brk_server = { version = "0.3.0", path = "crates/brk_server" } brk_server = { version = "0.3.4", path = "crates/brk_server" }
brk_store = { version = "0.3.0", path = "crates/brk_store" } brk_store = { version = "0.3.4", path = "crates/brk_store" }
brk_traversable = { version = "0.3.0", path = "crates/brk_traversable", features = ["pco", "derive"] } brk_traversable = { version = "0.3.4", path = "crates/brk_traversable", features = ["pco", "derive"] }
brk_traversable_derive = { version = "0.3.0", path = "crates/brk_traversable_derive" } brk_traversable_derive = { version = "0.3.4", path = "crates/brk_traversable_derive" }
brk_types = { version = "0.3.0", path = "crates/brk_types" } brk_types = { version = "0.3.4", path = "crates/brk_types" }
brk_website = { version = "0.3.0", path = "crates/brk_website" } brk_website = { version = "0.3.4", path = "crates/brk_website" }
byteview = "0.10.1" byteview = "0.10.1"
color-eyre = "0.6.5" color-eyre = "0.6.5"
corepc-jsonrpc = { package = "jsonrpc", version = "0.19.0", features = ["simple_http"], default-features = false } corepc-jsonrpc = { package = "jsonrpc", version = "0.19.0", features = ["simple_http"], default-features = false }
corepc-types = { version = "0.13.0", features = ["std"], default-features = false } corepc-types = { version = "0.15.0", features = ["std"], default-features = false }
derive_more = { version = "2.1.1", features = ["deref", "deref_mut"] } derive_more = { version = "2.1.1", features = ["deref", "deref_mut"] }
fjall = "3.1.4" fjall = "3.1.5"
indexmap = { version = "2.14.0", features = ["serde"] } indexmap = { version = "2.14.0", features = ["serde"] }
jiff = { version = "0.2.24", features = ["perf-inline", "tz-system"], default-features = false } jiff = { version = "0.2.29", features = ["perf-inline", "tz-system"], default-features = false }
owo-colors = "4.3.0" owo-colors = "4.3.0"
parking_lot = "0.12.5" parking_lot = "0.12.5"
pco = "1.0.2" pco = "1.0.2"
@@ -76,10 +76,10 @@ schemars = { version = "1.2.1", features = ["indexmap2"] }
serde = "1.0.228" serde = "1.0.228"
serde_bytes = "0.11.19" serde_bytes = "0.11.19"
serde_derive = "1.0.228" serde_derive = "1.0.228"
serde_json = { version = "1.0.149", features = ["float_roundtrip", "preserve_order"] } serde_json = { version = "1.0.150", features = ["float_roundtrip", "preserve_order"] }
smallvec = "1.15.1" smallvec = "1.15.2"
tokio = { version = "1.52.3", features = ["rt-multi-thread"] } tokio = { version = "1.52.3", features = ["rt-multi-thread"] }
tower-http = { version = "0.6.11", features = ["catch-panic", "compression-br", "compression-gzip", "compression-zstd", "cors", "normalize-path", "timeout", "trace"] } tower-http = { version = "0.7.0", features = ["catch-panic", "compression-br", "compression-gzip", "compression-zstd", "cors", "normalize-path", "timeout", "trace"] }
tower-layer = "0.3" tower-layer = "0.3"
tracing = { version = "0.1", default-features = false, features = ["std"] } tracing = { version = "0.1", default-features = false, features = ["std"] }
ureq = { version = "3.3.0", features = ["json"] } ureq = { version = "3.3.0", features = ["json"] }
+2 -2
View File
@@ -8,5 +8,5 @@ homepage.workspace = true
repository.workspace = true repository.workspace = true
[dependencies] [dependencies]
libmimalloc-sys = { version = "0.1.47", features = ["extended"] } libmimalloc-sys = { version = "0.1.49", features = ["extended"] }
mimalloc = { version = "0.1.50" } mimalloc = { version = "0.1.52" }
+4 -3
View File
@@ -6,9 +6,9 @@
use std::collections::BTreeMap; use std::collections::BTreeMap;
use brk_cohort::{ use brk_cohort::{
AGE_RANGE_NAMES, AMOUNT_RANGE_NAMES, CLASS_NAMES, EPOCH_NAMES, LOSS_NAMES, OVER_AGE_NAMES, AGE_RANGE_NAMES, AMOUNT_RANGE_NAMES, CLASS_NAMES, ENTRY_NAMES, EPOCH_NAMES, LOSS_NAMES,
OVER_AMOUNT_NAMES, PROFIT_NAMES, PROFITABILITY_RANGE_NAMES, SPENDABLE_TYPE_NAMES, TERM_NAMES, OVER_AGE_NAMES, OVER_AMOUNT_NAMES, PROFIT_NAMES, PROFITABILITY_RANGE_NAMES,
UNDER_AGE_NAMES, UNDER_AMOUNT_NAMES, SPENDABLE_TYPE_NAMES, TERM_NAMES, UNDER_AGE_NAMES, UNDER_AMOUNT_NAMES,
}; };
use brk_types::{Index, pools}; use brk_types::{Index, pools};
use serde::Serialize; use serde::Serialize;
@@ -59,6 +59,7 @@ impl CohortConstants {
("TERM_NAMES", to_value(&TERM_NAMES)), ("TERM_NAMES", to_value(&TERM_NAMES)),
("EPOCH_NAMES", to_value(&EPOCH_NAMES)), ("EPOCH_NAMES", to_value(&EPOCH_NAMES)),
("CLASS_NAMES", to_value(&CLASS_NAMES)), ("CLASS_NAMES", to_value(&CLASS_NAMES)),
("ENTRY_NAMES", to_value(&ENTRY_NAMES)),
("SPENDABLE_TYPE_NAMES", to_value(&SPENDABLE_TYPE_NAMES)), ("SPENDABLE_TYPE_NAMES", to_value(&SPENDABLE_TYPE_NAMES)),
("AGE_RANGE_NAMES", to_value(&AGE_RANGE_NAMES)), ("AGE_RANGE_NAMES", to_value(&AGE_RANGE_NAMES)),
("UNDER_AGE_NAMES", to_value(&UNDER_AGE_NAMES)), ("UNDER_AGE_NAMES", to_value(&UNDER_AGE_NAMES)),
@@ -51,7 +51,7 @@ fn generate_get_method(output: &mut String, endpoint: &Endpoint) {
} }
writeln!( writeln!(
output, output,
" * @param {{{{ signal?: AbortSignal, onValue?: (value: {}) => void }}}} [options]", " * @param {{{{ signal?: AbortSignal, onValue?: (value: {}) => void, cache?: boolean }}}} [options]",
return_type return_type
) )
.unwrap(); .unwrap();
@@ -60,22 +60,22 @@ fn generate_get_method(output: &mut String, endpoint: &Endpoint) {
let params = build_method_params(endpoint); let params = build_method_params(endpoint);
let params_with_opts = if params.is_empty() { let params_with_opts = if params.is_empty() {
"{ signal, onValue } = {}".to_string() "{ signal, onValue, cache } = {}".to_string()
} else { } else {
format!("{}, {{ signal, onValue }} = {{}}", params) format!("{}, {{ signal, onValue, cache }} = {{}}", params)
}; };
writeln!(output, " async {}({}) {{", method_name, params_with_opts).unwrap(); writeln!(output, " async {}({}) {{", method_name, params_with_opts).unwrap();
let path = build_path_template(&endpoint.path, &endpoint.path_params); let path = build_path_template(&endpoint.path, &endpoint.path_params);
let fetch_call: String = if endpoint.returns_binary() { let fetch_call: String = if endpoint.returns_binary() {
"this.getBytes(path, { signal, onValue })".to_string() "this.getBytes(path, { signal, onValue, cache })".to_string()
} else if endpoint.returns_json() { } else if endpoint.returns_json() {
"this.getJson(path, { signal, onValue })".to_string() "this.getJson(path, { signal, onValue, cache })".to_string()
} else if endpoint.response_kind.text_is_numeric() { } else if endpoint.response_kind.text_is_numeric() {
"Number(await this.getText(path, { signal, onValue: onValue ? (v) => onValue(Number(v)) : undefined }))".to_string() "Number(await this.getText(path, { signal, cache, onValue: onValue ? (v) => onValue(Number(v)) : undefined }))".to_string()
} else { } else {
"this.getText(path, { signal, onValue })".to_string() "this.getText(path, { signal, onValue, cache })".to_string()
}; };
write_path_assignment(output, endpoint, &path); write_path_assignment(output, endpoint, &path);
@@ -83,7 +83,7 @@ fn generate_get_method(output: &mut String, endpoint: &Endpoint) {
if endpoint.supports_csv { if endpoint.supports_csv {
writeln!( writeln!(
output, output,
" if (format === 'csv') return this.getText(path, {{ signal, onValue }});" " if (format === 'csv') return this.getText(path, {{ signal, onValue, cache }});"
) )
.unwrap(); .unwrap();
} }
@@ -448,14 +448,17 @@ class BrkClientBase {{
/** /**
* @param {{string}} path * @param {{string}} path
* @param {{{{ signal?: AbortSignal }}}} [options] * @param {{{{ signal?: AbortSignal, cache?: boolean }}}} [options]
* @returns {{Promise<Response>}} * @returns {{Promise<Response>}}
*/ */
async get(path, {{ signal }} = {{}}) {{ async get(path, {{ signal, cache = true }} = {{}}) {{
const url = `${{this.baseUrl}}${{path}}`; const url = `${{this.baseUrl}}${{path}}`;
const signals = [AbortSignal.timeout(this.timeout)]; const signals = [AbortSignal.timeout(this.timeout)];
if (signal) signals.push(signal); if (signal) signals.push(signal);
const res = await fetch(url, {{ signal: AbortSignal.any(signals) }}); /** @type {{RequestInit}} */
const init = {{ signal: AbortSignal.any(signals) }};
if (!cache) init.cache = 'no-store';
const res = await fetch(url, init);
if (!res.ok) throw new BrkError(`HTTP ${{res.status}}: ${{url}}`, res.status); if (!res.ok) throw new BrkError(`HTTP ${{res.status}}: ${{url}}`, res.status);
return res; return res;
}} }}
@@ -475,14 +478,21 @@ class BrkClientBase {{
* @template T * @template T
* @param {{string}} path * @param {{string}} path
* @param {{(res: Response) => Promise<T>}} parse - Response body reader * @param {{(res: Response) => Promise<T>}} parse - Response body reader
* @param {{{{ onValue?: (value: T) => void, signal?: AbortSignal }}}} [options] * @param {{{{ onValue?: (value: T) => void, signal?: AbortSignal, cache?: boolean }}}} [options]
* @returns {{Promise<T>}} * @returns {{Promise<T>}}
*/ */
async _getCached(path, parse, {{ onValue, signal }} = {{}}) {{ async _getCached(path, parse, {{ onValue, signal, cache = true }} = {{}}) {{
if (!cache) {{
const res = await this.get(path, {{ signal, cache }});
const value = await parse(res);
if (onValue) onValue(value);
return value;
}}
const url = `${{this.baseUrl}}${{path}}`; const url = `${{this.baseUrl}}${{path}}`;
/** @type {{_MemEntry<T> | undefined}} */ /** @type {{_MemEntry<T> | undefined}} */
const memHit = this._memGet(url); const memHit = this._memGet(url);
const browserCache = this._browserCache ?? await this._browserCachePromise; const browserCache = this._browserCache;
// L1 fast path: deliver from memCache, revalidate via network. // L1 fast path: deliver from memCache, revalidate via network.
// ETag match → zero parse, zero clone, zero cache write, no second onValue fire. // ETag match → zero parse, zero clone, zero cache write, no second onValue fire.
@@ -497,8 +507,8 @@ class BrkClientBase {{
this._memSet(url, netEtag, value); this._memSet(url, netEtag, value);
if (onValue) onValue(value); if (onValue) onValue(value);
if (cloned && browserCache) {{ if (cloned && browserCache) {{
const cache = browserCache; const cacheStore = browserCache;
_runIdle(() => cache.put(url, cloned)); _runIdle(() => cacheStore.put(url, cloned));
}} }}
return value; return value;
}} catch {{ }} catch {{
@@ -531,8 +541,8 @@ class BrkClientBase {{
this._memSet(url, netEtag, value); this._memSet(url, netEtag, value);
if (onValue) onValue(value); if (onValue) onValue(value);
if (cloned && browserCache) {{ if (cloned && browserCache) {{
const cache = browserCache; const cacheStore = browserCache;
_runIdle(() => cache.put(url, cloned)); _runIdle(() => cacheStore.put(url, cloned));
}} }}
return value; return value;
}} catch (e) {{ }} catch (e) {{
@@ -546,7 +556,7 @@ class BrkClientBase {{
* Make a GET request expecting a JSON response. Cached and supports `onValue`. * Make a GET request expecting a JSON response. Cached and supports `onValue`.
* @template T * @template T
* @param {{string}} path * @param {{string}} path
* @param {{{{ onValue?: (value: T) => void, signal?: AbortSignal }}}} [options] * @param {{{{ onValue?: (value: T) => void, signal?: AbortSignal, cache?: boolean }}}} [options]
* @returns {{Promise<T>}} * @returns {{Promise<T>}}
*/ */
getJson(path, options) {{ getJson(path, options) {{
@@ -557,7 +567,7 @@ class BrkClientBase {{
* Make a GET request expecting a text response (text/plain, text/csv, ...). * Make a GET request expecting a text response (text/plain, text/csv, ...).
* Cached and supports `onValue`, same as `getJson`. * Cached and supports `onValue`, same as `getJson`.
* @param {{string}} path * @param {{string}} path
* @param {{{{ onValue?: (value: string) => void, signal?: AbortSignal }}}} [options] * @param {{{{ onValue?: (value: string) => void, signal?: AbortSignal, cache?: boolean }}}} [options]
* @returns {{Promise<string>}} * @returns {{Promise<string>}}
*/ */
getText(path, options) {{ getText(path, options) {{
@@ -568,7 +578,7 @@ class BrkClientBase {{
* Make a GET request expecting binary data (application/octet-stream). * Make a GET request expecting binary data (application/octet-stream).
* Cached and supports `onValue`, same as `getJson`. * Cached and supports `onValue`, same as `getJson`.
* @param {{string}} path * @param {{string}} path
* @param {{{{ onValue?: (value: Uint8Array) => void, signal?: AbortSignal }}}} [options] * @param {{{{ onValue?: (value: Uint8Array) => void, signal?: AbortSignal, cache?: boolean }}}} [options]
* @returns {{Promise<Uint8Array>}} * @returns {{Promise<Uint8Array>}}
*/ */
getBytes(path, options) {{ getBytes(path, options) {{
+1 -1
View File
@@ -17,7 +17,7 @@ fn main() -> brk_client::Result<()> {
// day1() returns DateMetricEndpointBuilder, so fetch() returns DateMetricData // day1() returns DateMetricEndpointBuilder, so fetch() returns DateMetricData
let price_close = client let price_close = client
.series() .series()
.prices .price
.split .split
.close .close
.usd .usd
+673 -34
View File
@@ -1191,6 +1191,22 @@ pub struct CapCapitalizedGrossLossMvrvNetPeakPriceProfitSellSoprPattern {
pub sopr: AdjustedRatioValuePattern, pub sopr: AdjustedRatioValuePattern,
} }
/// Pattern struct for repeated tree structure.
pub struct CapCapitalizedGrossLossMvrvNetPeakPriceProfitSellSoprPattern2 {
pub cap: CentsDeltaToUsdPattern,
pub capitalized: PricePattern,
pub gross_pnl: BlockCumulativeSumPattern,
pub loss: BlockCumulativeNegativeSumPattern,
pub mvrv: SeriesPattern1<StoredF32>,
pub net_pnl: BlockChangeCumulativeDeltaSumPattern,
pub peak_regret: BlockCumulativeSumPattern,
pub price: BpsCentsPercentilesRatioSatsSmaStdUsdPattern,
pub profit: BlockCumulativeSumPattern,
pub profit_to_loss_ratio: _1m1w1y24hPattern<StoredF64>,
pub sell_side_risk_ratio: _1m1w1y24hPattern8,
pub sopr: RatioValuePattern2,
}
/// Pattern struct for repeated tree structure. /// Pattern struct for repeated tree structure.
pub struct EmptyOpP2aP2msP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshUnknownPattern2 { pub struct EmptyOpP2aP2msP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshUnknownPattern2 {
pub empty: _1m1w1y24hBpsPercentRatioPattern, pub empty: _1m1w1y24hBpsPercentRatioPattern,
@@ -1658,6 +1674,17 @@ pub struct ActiveInputOutputSpendablePattern {
pub spendable_output_to_reused_addr_share: _1m1w1y24hBpsPercentRatioPattern, pub spendable_output_to_reused_addr_share: _1m1w1y24hBpsPercentRatioPattern,
} }
/// Pattern struct for repeated tree structure.
pub struct ActivityCostInvestedOutputsRealizedSupplyUnrealizedPattern2 {
pub activity: CoindaysCoinyearsDormancyTransferPattern,
pub cost_basis: InMaxMinPerSupplyPattern,
pub invested_capital: InPattern,
pub outputs: SpendingSpentUnspentPattern,
pub realized: CapCapitalizedGrossLossMvrvNetPeakPriceProfitSellSoprPattern2,
pub supply: DeltaDominanceHalfInTotalPattern2,
pub unrealized: CapitalizedGrossInvestedLossNetNuplProfitSentimentPattern2,
}
/// Pattern struct for repeated tree structure. /// Pattern struct for repeated tree structure.
pub struct CapLossMvrvNetPriceProfitSoprPattern { pub struct CapLossMvrvNetPriceProfitSoprPattern {
pub cap: CentsDeltaUsdPattern, pub cap: CentsDeltaUsdPattern,
@@ -3408,6 +3435,22 @@ impl PriceRatioPattern {
} }
} }
/// Pattern struct for repeated tree structure.
pub struct RatioValuePattern2 {
pub ratio: _1m1w1y24hPattern<StoredF64>,
pub value_destroyed: AverageBlockCumulativeSumPattern<Cents>,
}
impl RatioValuePattern2 {
/// Create a new pattern node with accumulated series name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
ratio: _1m1w1y24hPattern::new(client.clone(), _m(&acc, "sopr")),
value_destroyed: AverageBlockCumulativeSumPattern::new(client.clone(), _m(&acc, "value_destroyed")),
}
}
}
/// Pattern struct for repeated tree structure. /// Pattern struct for repeated tree structure.
pub struct RatioValuePattern { pub struct RatioValuePattern {
pub ratio: _24hPattern, pub ratio: _24hPattern,
@@ -3534,7 +3577,7 @@ pub struct SeriesTree {
pub investing: SeriesTree_Investing, pub investing: SeriesTree_Investing,
pub market: SeriesTree_Market, pub market: SeriesTree_Market,
pub pools: SeriesTree_Pools, pub pools: SeriesTree_Pools,
pub prices: SeriesTree_Prices, pub price: SeriesTree_Price,
pub supply: SeriesTree_Supply, pub supply: SeriesTree_Supply,
pub cohorts: SeriesTree_Cohorts, pub cohorts: SeriesTree_Cohorts,
} }
@@ -3556,7 +3599,7 @@ impl SeriesTree {
investing: SeriesTree_Investing::new(client.clone(), format!("{base_path}_investing")), investing: SeriesTree_Investing::new(client.clone(), format!("{base_path}_investing")),
market: SeriesTree_Market::new(client.clone(), format!("{base_path}_market")), market: SeriesTree_Market::new(client.clone(), format!("{base_path}_market")),
pools: SeriesTree_Pools::new(client.clone(), format!("{base_path}_pools")), pools: SeriesTree_Pools::new(client.clone(), format!("{base_path}_pools")),
prices: SeriesTree_Prices::new(client.clone(), format!("{base_path}_prices")), price: SeriesTree_Price::new(client.clone(), format!("{base_path}_price")),
supply: SeriesTree_Supply::new(client.clone(), format!("{base_path}_supply")), supply: SeriesTree_Supply::new(client.clone(), format!("{base_path}_supply")),
cohorts: SeriesTree_Cohorts::new(client.clone(), format!("{base_path}_cohorts")), cohorts: SeriesTree_Cohorts::new(client.clone(), format!("{base_path}_cohorts")),
} }
@@ -7063,31 +7106,31 @@ impl SeriesTree_Pools_Minor {
} }
/// Series tree node. /// Series tree node.
pub struct SeriesTree_Prices { pub struct SeriesTree_Price {
pub split: SeriesTree_Prices_Split, pub split: SeriesTree_Price_Split,
pub ohlc: SeriesTree_Prices_Ohlc, pub ohlc: SeriesTree_Price_Ohlc,
pub spot: SeriesTree_Prices_Spot, pub spot: SeriesTree_Price_Spot,
} }
impl SeriesTree_Prices { impl SeriesTree_Price {
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self { pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
Self { Self {
split: SeriesTree_Prices_Split::new(client.clone(), format!("{base_path}_split")), split: SeriesTree_Price_Split::new(client.clone(), format!("{base_path}_split")),
ohlc: SeriesTree_Prices_Ohlc::new(client.clone(), format!("{base_path}_ohlc")), ohlc: SeriesTree_Price_Ohlc::new(client.clone(), format!("{base_path}_ohlc")),
spot: SeriesTree_Prices_Spot::new(client.clone(), format!("{base_path}_spot")), spot: SeriesTree_Price_Spot::new(client.clone(), format!("{base_path}_spot")),
} }
} }
} }
/// Series tree node. /// Series tree node.
pub struct SeriesTree_Prices_Split { pub struct SeriesTree_Price_Split {
pub open: CentsSatsUsdPattern3, pub open: CentsSatsUsdPattern3,
pub high: CentsSatsUsdPattern3, pub high: CentsSatsUsdPattern3,
pub low: CentsSatsUsdPattern3, pub low: CentsSatsUsdPattern3,
pub close: CentsSatsUsdPattern3, pub close: CentsSatsUsdPattern3,
} }
impl SeriesTree_Prices_Split { impl SeriesTree_Price_Split {
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self { pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
Self { Self {
open: CentsSatsUsdPattern3::new(client.clone(), "price_open".to_string()), open: CentsSatsUsdPattern3::new(client.clone(), "price_open".to_string()),
@@ -7099,13 +7142,13 @@ impl SeriesTree_Prices_Split {
} }
/// Series tree node. /// Series tree node.
pub struct SeriesTree_Prices_Ohlc { pub struct SeriesTree_Price_Ohlc {
pub usd: SeriesPattern2<OHLCDollars>, pub usd: SeriesPattern2<OHLCDollars>,
pub cents: SeriesPattern2<OHLCCents>, pub cents: SeriesPattern2<OHLCCents>,
pub sats: SeriesPattern2<OHLCSats>, pub sats: SeriesPattern2<OHLCSats>,
} }
impl SeriesTree_Prices_Ohlc { impl SeriesTree_Price_Ohlc {
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self { pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
Self { Self {
usd: SeriesPattern2::new(client.clone(), "price_ohlc".to_string()), usd: SeriesPattern2::new(client.clone(), "price_ohlc".to_string()),
@@ -7116,13 +7159,13 @@ impl SeriesTree_Prices_Ohlc {
} }
/// Series tree node. /// Series tree node.
pub struct SeriesTree_Prices_Spot { pub struct SeriesTree_Price_Spot {
pub usd: SeriesPattern1<Dollars>, pub usd: SeriesPattern1<Dollars>,
pub cents: SeriesPattern1<Cents>, pub cents: SeriesPattern1<Cents>,
pub sats: SeriesPattern1<Sats>, pub sats: SeriesPattern1<Sats>,
} }
impl SeriesTree_Prices_Spot { impl SeriesTree_Price_Spot {
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self { pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
Self { Self {
usd: SeriesPattern1::new(client.clone(), "price".to_string()), usd: SeriesPattern1::new(client.clone(), "price".to_string()),
@@ -7199,6 +7242,7 @@ pub struct SeriesTree_Cohorts_Utxo {
pub over_age: SeriesTree_Cohorts_Utxo_OverAge, pub over_age: SeriesTree_Cohorts_Utxo_OverAge,
pub epoch: SeriesTree_Cohorts_Utxo_Epoch, pub epoch: SeriesTree_Cohorts_Utxo_Epoch,
pub class: SeriesTree_Cohorts_Utxo_Class, pub class: SeriesTree_Cohorts_Utxo_Class,
pub entry: SeriesTree_Cohorts_Utxo_Entry,
pub over_amount: SeriesTree_Cohorts_Utxo_OverAmount, pub over_amount: SeriesTree_Cohorts_Utxo_OverAmount,
pub amount_range: SeriesTree_Cohorts_Utxo_AmountRange, pub amount_range: SeriesTree_Cohorts_Utxo_AmountRange,
pub under_amount: SeriesTree_Cohorts_Utxo_UnderAmount, pub under_amount: SeriesTree_Cohorts_Utxo_UnderAmount,
@@ -7218,6 +7262,7 @@ impl SeriesTree_Cohorts_Utxo {
over_age: SeriesTree_Cohorts_Utxo_OverAge::new(client.clone(), format!("{base_path}_over_age")), over_age: SeriesTree_Cohorts_Utxo_OverAge::new(client.clone(), format!("{base_path}_over_age")),
epoch: SeriesTree_Cohorts_Utxo_Epoch::new(client.clone(), format!("{base_path}_epoch")), epoch: SeriesTree_Cohorts_Utxo_Epoch::new(client.clone(), format!("{base_path}_epoch")),
class: SeriesTree_Cohorts_Utxo_Class::new(client.clone(), format!("{base_path}_class")), class: SeriesTree_Cohorts_Utxo_Class::new(client.clone(), format!("{base_path}_class")),
entry: SeriesTree_Cohorts_Utxo_Entry::new(client.clone(), format!("{base_path}_entry")),
over_amount: SeriesTree_Cohorts_Utxo_OverAmount::new(client.clone(), format!("{base_path}_over_amount")), over_amount: SeriesTree_Cohorts_Utxo_OverAmount::new(client.clone(), format!("{base_path}_over_amount")),
amount_range: SeriesTree_Cohorts_Utxo_AmountRange::new(client.clone(), format!("{base_path}_amount_range")), amount_range: SeriesTree_Cohorts_Utxo_AmountRange::new(client.clone(), format!("{base_path}_amount_range")),
under_amount: SeriesTree_Cohorts_Utxo_UnderAmount::new(client.clone(), format!("{base_path}_under_amount")), under_amount: SeriesTree_Cohorts_Utxo_UnderAmount::new(client.clone(), format!("{base_path}_under_amount")),
@@ -7999,7 +8044,7 @@ pub struct SeriesTree_Cohorts_Utxo_Lth_Realized {
pub price: SeriesTree_Cohorts_Utxo_Lth_Realized_Price, pub price: SeriesTree_Cohorts_Utxo_Lth_Realized_Price,
pub mvrv: SeriesPattern1<StoredF32>, pub mvrv: SeriesPattern1<StoredF32>,
pub net_pnl: BlockChangeCumulativeDeltaSumPattern, pub net_pnl: BlockChangeCumulativeDeltaSumPattern,
pub sopr: SeriesTree_Cohorts_Utxo_Lth_Realized_Sopr, pub sopr: RatioValuePattern2,
pub gross_pnl: BlockCumulativeSumPattern, pub gross_pnl: BlockCumulativeSumPattern,
pub sell_side_risk_ratio: _1m1w1y24hPattern8, pub sell_side_risk_ratio: _1m1w1y24hPattern8,
pub peak_regret: BlockCumulativeSumPattern, pub peak_regret: BlockCumulativeSumPattern,
@@ -8016,7 +8061,7 @@ impl SeriesTree_Cohorts_Utxo_Lth_Realized {
price: SeriesTree_Cohorts_Utxo_Lth_Realized_Price::new(client.clone(), format!("{base_path}_price")), price: SeriesTree_Cohorts_Utxo_Lth_Realized_Price::new(client.clone(), format!("{base_path}_price")),
mvrv: SeriesPattern1::new(client.clone(), "lth_mvrv".to_string()), mvrv: SeriesPattern1::new(client.clone(), "lth_mvrv".to_string()),
net_pnl: BlockChangeCumulativeDeltaSumPattern::new(client.clone(), "lth_net".to_string()), net_pnl: BlockChangeCumulativeDeltaSumPattern::new(client.clone(), "lth_net".to_string()),
sopr: SeriesTree_Cohorts_Utxo_Lth_Realized_Sopr::new(client.clone(), format!("{base_path}_sopr")), sopr: RatioValuePattern2::new(client.clone(), "lth".to_string()),
gross_pnl: BlockCumulativeSumPattern::new(client.clone(), "lth_realized_gross_pnl".to_string()), gross_pnl: BlockCumulativeSumPattern::new(client.clone(), "lth_realized_gross_pnl".to_string()),
sell_side_risk_ratio: _1m1w1y24hPattern8::new(client.clone(), "lth_sell_side_risk_ratio".to_string()), sell_side_risk_ratio: _1m1w1y24hPattern8::new(client.clone(), "lth_sell_side_risk_ratio".to_string()),
peak_regret: BlockCumulativeSumPattern::new(client.clone(), "lth_realized_peak_regret".to_string()), peak_regret: BlockCumulativeSumPattern::new(client.clone(), "lth_realized_peak_regret".to_string()),
@@ -8236,21 +8281,6 @@ impl SeriesTree_Cohorts_Utxo_Lth_Realized_Price_StdDev_1y {
} }
} }
/// Series tree node.
pub struct SeriesTree_Cohorts_Utxo_Lth_Realized_Sopr {
pub value_destroyed: AverageBlockCumulativeSumPattern<Cents>,
pub ratio: _1m1w1y24hPattern<StoredF64>,
}
impl SeriesTree_Cohorts_Utxo_Lth_Realized_Sopr {
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
Self {
value_destroyed: AverageBlockCumulativeSumPattern::new(client.clone(), "lth_value_destroyed".to_string()),
ratio: _1m1w1y24hPattern::new(client.clone(), "lth_sopr".to_string()),
}
}
}
/// Series tree node. /// Series tree node.
pub struct SeriesTree_Cohorts_Utxo_AgeRange { pub struct SeriesTree_Cohorts_Utxo_AgeRange {
pub under_1h: ActivityOutputsRealizedSupplyUnrealizedPattern, pub under_1h: ActivityOutputsRealizedSupplyUnrealizedPattern,
@@ -8466,6 +8496,561 @@ impl SeriesTree_Cohorts_Utxo_Class {
} }
} }
/// Series tree node.
pub struct SeriesTree_Cohorts_Utxo_Entry {
pub discount: SeriesTree_Cohorts_Utxo_Entry_Discount,
pub premium: SeriesTree_Cohorts_Utxo_Entry_Premium,
}
impl SeriesTree_Cohorts_Utxo_Entry {
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
Self {
discount: SeriesTree_Cohorts_Utxo_Entry_Discount::new(client.clone(), format!("{base_path}_discount")),
premium: SeriesTree_Cohorts_Utxo_Entry_Premium::new(client.clone(), format!("{base_path}_premium")),
}
}
}
/// Series tree node.
pub struct SeriesTree_Cohorts_Utxo_Entry_Discount {
pub supply: DeltaDominanceHalfInTotalPattern2,
pub outputs: SpendingSpentUnspentPattern,
pub activity: CoindaysCoinyearsDormancyTransferPattern,
pub realized: SeriesTree_Cohorts_Utxo_Entry_Discount_Realized,
pub cost_basis: InMaxMinPerSupplyPattern,
pub unrealized: CapitalizedGrossInvestedLossNetNuplProfitSentimentPattern2,
pub invested_capital: InPattern,
}
impl SeriesTree_Cohorts_Utxo_Entry_Discount {
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
Self {
supply: DeltaDominanceHalfInTotalPattern2::new(client.clone(), "veteran_supply".to_string()),
outputs: SpendingSpentUnspentPattern::new(client.clone(), "veteran".to_string()),
activity: CoindaysCoinyearsDormancyTransferPattern::new(client.clone(), "veteran".to_string()),
realized: SeriesTree_Cohorts_Utxo_Entry_Discount_Realized::new(client.clone(), format!("{base_path}_realized")),
cost_basis: InMaxMinPerSupplyPattern::new(client.clone(), "veteran".to_string()),
unrealized: CapitalizedGrossInvestedLossNetNuplProfitSentimentPattern2::new(client.clone(), "veteran".to_string()),
invested_capital: InPattern::new(client.clone(), "veteran_invested_capital_in".to_string()),
}
}
}
/// Series tree node.
pub struct SeriesTree_Cohorts_Utxo_Entry_Discount_Realized {
pub cap: CentsDeltaToUsdPattern,
pub profit: BlockCumulativeSumPattern,
pub loss: BlockCumulativeNegativeSumPattern,
pub price: SeriesTree_Cohorts_Utxo_Entry_Discount_Realized_Price,
pub mvrv: SeriesPattern1<StoredF32>,
pub net_pnl: BlockChangeCumulativeDeltaSumPattern,
pub sopr: RatioValuePattern2,
pub gross_pnl: BlockCumulativeSumPattern,
pub sell_side_risk_ratio: _1m1w1y24hPattern8,
pub peak_regret: BlockCumulativeSumPattern,
pub capitalized: PricePattern,
pub profit_to_loss_ratio: _1m1w1y24hPattern<StoredF64>,
}
impl SeriesTree_Cohorts_Utxo_Entry_Discount_Realized {
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
Self {
cap: CentsDeltaToUsdPattern::new(client.clone(), "veteran_realized_cap".to_string()),
profit: BlockCumulativeSumPattern::new(client.clone(), "veteran_realized_profit".to_string()),
loss: BlockCumulativeNegativeSumPattern::new(client.clone(), "veteran_realized_loss".to_string()),
price: SeriesTree_Cohorts_Utxo_Entry_Discount_Realized_Price::new(client.clone(), format!("{base_path}_price")),
mvrv: SeriesPattern1::new(client.clone(), "veteran_mvrv".to_string()),
net_pnl: BlockChangeCumulativeDeltaSumPattern::new(client.clone(), "veteran_net".to_string()),
sopr: RatioValuePattern2::new(client.clone(), "veteran".to_string()),
gross_pnl: BlockCumulativeSumPattern::new(client.clone(), "veteran_realized_gross_pnl".to_string()),
sell_side_risk_ratio: _1m1w1y24hPattern8::new(client.clone(), "veteran_sell_side_risk_ratio".to_string()),
peak_regret: BlockCumulativeSumPattern::new(client.clone(), "veteran_realized_peak_regret".to_string()),
capitalized: PricePattern::new(client.clone(), "veteran_capitalized_price".to_string()),
profit_to_loss_ratio: _1m1w1y24hPattern::new(client.clone(), "veteran_realized_profit_to_loss_ratio".to_string()),
}
}
}
/// Series tree node.
pub struct SeriesTree_Cohorts_Utxo_Entry_Discount_Realized_Price {
pub usd: SeriesPattern1<Dollars>,
pub cents: SeriesPattern1<Cents>,
pub sats: SeriesPattern1<SatsFract>,
pub bps: SeriesPattern1<BasisPoints32>,
pub ratio: SeriesPattern1<StoredF32>,
pub percentiles: Pct0Pct1Pct2Pct5Pct95Pct98Pct99Pattern,
pub sma: _1m1w1y2y4yAllPattern,
pub std_dev: SeriesTree_Cohorts_Utxo_Entry_Discount_Realized_Price_StdDev,
}
impl SeriesTree_Cohorts_Utxo_Entry_Discount_Realized_Price {
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
Self {
usd: SeriesPattern1::new(client.clone(), "veteran_realized_price".to_string()),
cents: SeriesPattern1::new(client.clone(), "veteran_realized_price_cents".to_string()),
sats: SeriesPattern1::new(client.clone(), "veteran_realized_price_sats".to_string()),
bps: SeriesPattern1::new(client.clone(), "veteran_realized_price_ratio_bps".to_string()),
ratio: SeriesPattern1::new(client.clone(), "veteran_realized_price_ratio".to_string()),
percentiles: Pct0Pct1Pct2Pct5Pct95Pct98Pct99Pattern::new(client.clone(), "veteran_realized_price".to_string()),
sma: _1m1w1y2y4yAllPattern::new(client.clone(), "veteran_realized_price_ratio_sma".to_string()),
std_dev: SeriesTree_Cohorts_Utxo_Entry_Discount_Realized_Price_StdDev::new(client.clone(), format!("{base_path}_std_dev")),
}
}
}
/// Series tree node.
pub struct SeriesTree_Cohorts_Utxo_Entry_Discount_Realized_Price_StdDev {
pub all: SeriesTree_Cohorts_Utxo_Entry_Discount_Realized_Price_StdDev_All,
pub _4y: SeriesTree_Cohorts_Utxo_Entry_Discount_Realized_Price_StdDev_4y,
pub _2y: SeriesTree_Cohorts_Utxo_Entry_Discount_Realized_Price_StdDev_2y,
pub _1y: SeriesTree_Cohorts_Utxo_Entry_Discount_Realized_Price_StdDev_1y,
}
impl SeriesTree_Cohorts_Utxo_Entry_Discount_Realized_Price_StdDev {
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
Self {
all: SeriesTree_Cohorts_Utxo_Entry_Discount_Realized_Price_StdDev_All::new(client.clone(), format!("{base_path}_all")),
_4y: SeriesTree_Cohorts_Utxo_Entry_Discount_Realized_Price_StdDev_4y::new(client.clone(), format!("{base_path}_4y")),
_2y: SeriesTree_Cohorts_Utxo_Entry_Discount_Realized_Price_StdDev_2y::new(client.clone(), format!("{base_path}_2y")),
_1y: SeriesTree_Cohorts_Utxo_Entry_Discount_Realized_Price_StdDev_1y::new(client.clone(), format!("{base_path}_1y")),
}
}
}
/// Series tree node.
pub struct SeriesTree_Cohorts_Utxo_Entry_Discount_Realized_Price_StdDev_All {
pub sd: SeriesPattern1<StoredF32>,
pub zscore: SeriesPattern1<StoredF32>,
pub _0sd: CentsSatsUsdPattern,
pub p0_5sd: PriceRatioPattern,
pub p1sd: PriceRatioPattern,
pub p1_5sd: PriceRatioPattern,
pub p2sd: PriceRatioPattern,
pub p2_5sd: PriceRatioPattern,
pub p3sd: PriceRatioPattern,
pub m0_5sd: PriceRatioPattern,
pub m1sd: PriceRatioPattern,
pub m1_5sd: PriceRatioPattern,
pub m2sd: PriceRatioPattern,
pub m2_5sd: PriceRatioPattern,
pub m3sd: PriceRatioPattern,
}
impl SeriesTree_Cohorts_Utxo_Entry_Discount_Realized_Price_StdDev_All {
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
Self {
sd: SeriesPattern1::new(client.clone(), "veteran_realized_price_ratio_sd".to_string()),
zscore: SeriesPattern1::new(client.clone(), "veteran_realized_price_ratio_zscore".to_string()),
_0sd: CentsSatsUsdPattern::new(client.clone(), "veteran_realized_price_0sd".to_string()),
p0_5sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "p0_5sd".to_string()),
p1sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "p1sd".to_string()),
p1_5sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "p1_5sd".to_string()),
p2sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "p2sd".to_string()),
p2_5sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "p2_5sd".to_string()),
p3sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "p3sd".to_string()),
m0_5sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "m0_5sd".to_string()),
m1sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "m1sd".to_string()),
m1_5sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "m1_5sd".to_string()),
m2sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "m2sd".to_string()),
m2_5sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "m2_5sd".to_string()),
m3sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "m3sd".to_string()),
}
}
}
/// Series tree node.
pub struct SeriesTree_Cohorts_Utxo_Entry_Discount_Realized_Price_StdDev_4y {
pub sd: SeriesPattern1<StoredF32>,
pub zscore: SeriesPattern1<StoredF32>,
pub _0sd: CentsSatsUsdPattern,
pub p0_5sd: PriceRatioPattern,
pub p1sd: PriceRatioPattern,
pub p1_5sd: PriceRatioPattern,
pub p2sd: PriceRatioPattern,
pub p2_5sd: PriceRatioPattern,
pub p3sd: PriceRatioPattern,
pub m0_5sd: PriceRatioPattern,
pub m1sd: PriceRatioPattern,
pub m1_5sd: PriceRatioPattern,
pub m2sd: PriceRatioPattern,
pub m2_5sd: PriceRatioPattern,
pub m3sd: PriceRatioPattern,
}
impl SeriesTree_Cohorts_Utxo_Entry_Discount_Realized_Price_StdDev_4y {
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
Self {
sd: SeriesPattern1::new(client.clone(), "veteran_realized_price_ratio_sd_4y".to_string()),
zscore: SeriesPattern1::new(client.clone(), "veteran_realized_price_ratio_zscore_4y".to_string()),
_0sd: CentsSatsUsdPattern::new(client.clone(), "veteran_realized_price_0sd_4y".to_string()),
p0_5sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "p0_5sd_4y".to_string()),
p1sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "p1sd_4y".to_string()),
p1_5sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "p1_5sd_4y".to_string()),
p2sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "p2sd_4y".to_string()),
p2_5sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "p2_5sd_4y".to_string()),
p3sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "p3sd_4y".to_string()),
m0_5sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "m0_5sd_4y".to_string()),
m1sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "m1sd_4y".to_string()),
m1_5sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "m1_5sd_4y".to_string()),
m2sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "m2sd_4y".to_string()),
m2_5sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "m2_5sd_4y".to_string()),
m3sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "m3sd_4y".to_string()),
}
}
}
/// Series tree node.
pub struct SeriesTree_Cohorts_Utxo_Entry_Discount_Realized_Price_StdDev_2y {
pub sd: SeriesPattern1<StoredF32>,
pub zscore: SeriesPattern1<StoredF32>,
pub _0sd: CentsSatsUsdPattern,
pub p0_5sd: PriceRatioPattern,
pub p1sd: PriceRatioPattern,
pub p1_5sd: PriceRatioPattern,
pub p2sd: PriceRatioPattern,
pub p2_5sd: PriceRatioPattern,
pub p3sd: PriceRatioPattern,
pub m0_5sd: PriceRatioPattern,
pub m1sd: PriceRatioPattern,
pub m1_5sd: PriceRatioPattern,
pub m2sd: PriceRatioPattern,
pub m2_5sd: PriceRatioPattern,
pub m3sd: PriceRatioPattern,
}
impl SeriesTree_Cohorts_Utxo_Entry_Discount_Realized_Price_StdDev_2y {
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
Self {
sd: SeriesPattern1::new(client.clone(), "veteran_realized_price_ratio_sd_2y".to_string()),
zscore: SeriesPattern1::new(client.clone(), "veteran_realized_price_ratio_zscore_2y".to_string()),
_0sd: CentsSatsUsdPattern::new(client.clone(), "veteran_realized_price_0sd_2y".to_string()),
p0_5sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "p0_5sd_2y".to_string()),
p1sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "p1sd_2y".to_string()),
p1_5sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "p1_5sd_2y".to_string()),
p2sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "p2sd_2y".to_string()),
p2_5sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "p2_5sd_2y".to_string()),
p3sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "p3sd_2y".to_string()),
m0_5sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "m0_5sd_2y".to_string()),
m1sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "m1sd_2y".to_string()),
m1_5sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "m1_5sd_2y".to_string()),
m2sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "m2sd_2y".to_string()),
m2_5sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "m2_5sd_2y".to_string()),
m3sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "m3sd_2y".to_string()),
}
}
}
/// Series tree node.
pub struct SeriesTree_Cohorts_Utxo_Entry_Discount_Realized_Price_StdDev_1y {
pub sd: SeriesPattern1<StoredF32>,
pub zscore: SeriesPattern1<StoredF32>,
pub _0sd: CentsSatsUsdPattern,
pub p0_5sd: PriceRatioPattern,
pub p1sd: PriceRatioPattern,
pub p1_5sd: PriceRatioPattern,
pub p2sd: PriceRatioPattern,
pub p2_5sd: PriceRatioPattern,
pub p3sd: PriceRatioPattern,
pub m0_5sd: PriceRatioPattern,
pub m1sd: PriceRatioPattern,
pub m1_5sd: PriceRatioPattern,
pub m2sd: PriceRatioPattern,
pub m2_5sd: PriceRatioPattern,
pub m3sd: PriceRatioPattern,
}
impl SeriesTree_Cohorts_Utxo_Entry_Discount_Realized_Price_StdDev_1y {
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
Self {
sd: SeriesPattern1::new(client.clone(), "veteran_realized_price_ratio_sd_1y".to_string()),
zscore: SeriesPattern1::new(client.clone(), "veteran_realized_price_ratio_zscore_1y".to_string()),
_0sd: CentsSatsUsdPattern::new(client.clone(), "veteran_realized_price_0sd_1y".to_string()),
p0_5sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "p0_5sd_1y".to_string()),
p1sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "p1sd_1y".to_string()),
p1_5sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "p1_5sd_1y".to_string()),
p2sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "p2sd_1y".to_string()),
p2_5sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "p2_5sd_1y".to_string()),
p3sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "p3sd_1y".to_string()),
m0_5sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "m0_5sd_1y".to_string()),
m1sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "m1sd_1y".to_string()),
m1_5sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "m1_5sd_1y".to_string()),
m2sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "m2sd_1y".to_string()),
m2_5sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "m2_5sd_1y".to_string()),
m3sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "m3sd_1y".to_string()),
}
}
}
/// Series tree node.
pub struct SeriesTree_Cohorts_Utxo_Entry_Premium {
pub supply: DeltaDominanceHalfInTotalPattern2,
pub outputs: SpendingSpentUnspentPattern,
pub activity: CoindaysCoinyearsDormancyTransferPattern,
pub realized: SeriesTree_Cohorts_Utxo_Entry_Premium_Realized,
pub cost_basis: InMaxMinPerSupplyPattern,
pub unrealized: CapitalizedGrossInvestedLossNetNuplProfitSentimentPattern2,
pub invested_capital: InPattern,
}
impl SeriesTree_Cohorts_Utxo_Entry_Premium {
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
Self {
supply: DeltaDominanceHalfInTotalPattern2::new(client.clone(), "rookie_supply".to_string()),
outputs: SpendingSpentUnspentPattern::new(client.clone(), "rookie".to_string()),
activity: CoindaysCoinyearsDormancyTransferPattern::new(client.clone(), "rookie".to_string()),
realized: SeriesTree_Cohorts_Utxo_Entry_Premium_Realized::new(client.clone(), format!("{base_path}_realized")),
cost_basis: InMaxMinPerSupplyPattern::new(client.clone(), "rookie".to_string()),
unrealized: CapitalizedGrossInvestedLossNetNuplProfitSentimentPattern2::new(client.clone(), "rookie".to_string()),
invested_capital: InPattern::new(client.clone(), "rookie_invested_capital_in".to_string()),
}
}
}
/// Series tree node.
pub struct SeriesTree_Cohorts_Utxo_Entry_Premium_Realized {
pub cap: CentsDeltaToUsdPattern,
pub profit: BlockCumulativeSumPattern,
pub loss: BlockCumulativeNegativeSumPattern,
pub price: SeriesTree_Cohorts_Utxo_Entry_Premium_Realized_Price,
pub mvrv: SeriesPattern1<StoredF32>,
pub net_pnl: BlockChangeCumulativeDeltaSumPattern,
pub sopr: RatioValuePattern2,
pub gross_pnl: BlockCumulativeSumPattern,
pub sell_side_risk_ratio: _1m1w1y24hPattern8,
pub peak_regret: BlockCumulativeSumPattern,
pub capitalized: PricePattern,
pub profit_to_loss_ratio: _1m1w1y24hPattern<StoredF64>,
}
impl SeriesTree_Cohorts_Utxo_Entry_Premium_Realized {
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
Self {
cap: CentsDeltaToUsdPattern::new(client.clone(), "rookie_realized_cap".to_string()),
profit: BlockCumulativeSumPattern::new(client.clone(), "rookie_realized_profit".to_string()),
loss: BlockCumulativeNegativeSumPattern::new(client.clone(), "rookie_realized_loss".to_string()),
price: SeriesTree_Cohorts_Utxo_Entry_Premium_Realized_Price::new(client.clone(), format!("{base_path}_price")),
mvrv: SeriesPattern1::new(client.clone(), "rookie_mvrv".to_string()),
net_pnl: BlockChangeCumulativeDeltaSumPattern::new(client.clone(), "rookie_net".to_string()),
sopr: RatioValuePattern2::new(client.clone(), "rookie".to_string()),
gross_pnl: BlockCumulativeSumPattern::new(client.clone(), "rookie_realized_gross_pnl".to_string()),
sell_side_risk_ratio: _1m1w1y24hPattern8::new(client.clone(), "rookie_sell_side_risk_ratio".to_string()),
peak_regret: BlockCumulativeSumPattern::new(client.clone(), "rookie_realized_peak_regret".to_string()),
capitalized: PricePattern::new(client.clone(), "rookie_capitalized_price".to_string()),
profit_to_loss_ratio: _1m1w1y24hPattern::new(client.clone(), "rookie_realized_profit_to_loss_ratio".to_string()),
}
}
}
/// Series tree node.
pub struct SeriesTree_Cohorts_Utxo_Entry_Premium_Realized_Price {
pub usd: SeriesPattern1<Dollars>,
pub cents: SeriesPattern1<Cents>,
pub sats: SeriesPattern1<SatsFract>,
pub bps: SeriesPattern1<BasisPoints32>,
pub ratio: SeriesPattern1<StoredF32>,
pub percentiles: Pct0Pct1Pct2Pct5Pct95Pct98Pct99Pattern,
pub sma: _1m1w1y2y4yAllPattern,
pub std_dev: SeriesTree_Cohorts_Utxo_Entry_Premium_Realized_Price_StdDev,
}
impl SeriesTree_Cohorts_Utxo_Entry_Premium_Realized_Price {
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
Self {
usd: SeriesPattern1::new(client.clone(), "rookie_realized_price".to_string()),
cents: SeriesPattern1::new(client.clone(), "rookie_realized_price_cents".to_string()),
sats: SeriesPattern1::new(client.clone(), "rookie_realized_price_sats".to_string()),
bps: SeriesPattern1::new(client.clone(), "rookie_realized_price_ratio_bps".to_string()),
ratio: SeriesPattern1::new(client.clone(), "rookie_realized_price_ratio".to_string()),
percentiles: Pct0Pct1Pct2Pct5Pct95Pct98Pct99Pattern::new(client.clone(), "rookie_realized_price".to_string()),
sma: _1m1w1y2y4yAllPattern::new(client.clone(), "rookie_realized_price_ratio_sma".to_string()),
std_dev: SeriesTree_Cohorts_Utxo_Entry_Premium_Realized_Price_StdDev::new(client.clone(), format!("{base_path}_std_dev")),
}
}
}
/// Series tree node.
pub struct SeriesTree_Cohorts_Utxo_Entry_Premium_Realized_Price_StdDev {
pub all: SeriesTree_Cohorts_Utxo_Entry_Premium_Realized_Price_StdDev_All,
pub _4y: SeriesTree_Cohorts_Utxo_Entry_Premium_Realized_Price_StdDev_4y,
pub _2y: SeriesTree_Cohorts_Utxo_Entry_Premium_Realized_Price_StdDev_2y,
pub _1y: SeriesTree_Cohorts_Utxo_Entry_Premium_Realized_Price_StdDev_1y,
}
impl SeriesTree_Cohorts_Utxo_Entry_Premium_Realized_Price_StdDev {
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
Self {
all: SeriesTree_Cohorts_Utxo_Entry_Premium_Realized_Price_StdDev_All::new(client.clone(), format!("{base_path}_all")),
_4y: SeriesTree_Cohorts_Utxo_Entry_Premium_Realized_Price_StdDev_4y::new(client.clone(), format!("{base_path}_4y")),
_2y: SeriesTree_Cohorts_Utxo_Entry_Premium_Realized_Price_StdDev_2y::new(client.clone(), format!("{base_path}_2y")),
_1y: SeriesTree_Cohorts_Utxo_Entry_Premium_Realized_Price_StdDev_1y::new(client.clone(), format!("{base_path}_1y")),
}
}
}
/// Series tree node.
pub struct SeriesTree_Cohorts_Utxo_Entry_Premium_Realized_Price_StdDev_All {
pub sd: SeriesPattern1<StoredF32>,
pub zscore: SeriesPattern1<StoredF32>,
pub _0sd: CentsSatsUsdPattern,
pub p0_5sd: PriceRatioPattern,
pub p1sd: PriceRatioPattern,
pub p1_5sd: PriceRatioPattern,
pub p2sd: PriceRatioPattern,
pub p2_5sd: PriceRatioPattern,
pub p3sd: PriceRatioPattern,
pub m0_5sd: PriceRatioPattern,
pub m1sd: PriceRatioPattern,
pub m1_5sd: PriceRatioPattern,
pub m2sd: PriceRatioPattern,
pub m2_5sd: PriceRatioPattern,
pub m3sd: PriceRatioPattern,
}
impl SeriesTree_Cohorts_Utxo_Entry_Premium_Realized_Price_StdDev_All {
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
Self {
sd: SeriesPattern1::new(client.clone(), "rookie_realized_price_ratio_sd".to_string()),
zscore: SeriesPattern1::new(client.clone(), "rookie_realized_price_ratio_zscore".to_string()),
_0sd: CentsSatsUsdPattern::new(client.clone(), "rookie_realized_price_0sd".to_string()),
p0_5sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "p0_5sd".to_string()),
p1sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "p1sd".to_string()),
p1_5sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "p1_5sd".to_string()),
p2sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "p2sd".to_string()),
p2_5sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "p2_5sd".to_string()),
p3sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "p3sd".to_string()),
m0_5sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "m0_5sd".to_string()),
m1sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "m1sd".to_string()),
m1_5sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "m1_5sd".to_string()),
m2sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "m2sd".to_string()),
m2_5sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "m2_5sd".to_string()),
m3sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "m3sd".to_string()),
}
}
}
/// Series tree node.
pub struct SeriesTree_Cohorts_Utxo_Entry_Premium_Realized_Price_StdDev_4y {
pub sd: SeriesPattern1<StoredF32>,
pub zscore: SeriesPattern1<StoredF32>,
pub _0sd: CentsSatsUsdPattern,
pub p0_5sd: PriceRatioPattern,
pub p1sd: PriceRatioPattern,
pub p1_5sd: PriceRatioPattern,
pub p2sd: PriceRatioPattern,
pub p2_5sd: PriceRatioPattern,
pub p3sd: PriceRatioPattern,
pub m0_5sd: PriceRatioPattern,
pub m1sd: PriceRatioPattern,
pub m1_5sd: PriceRatioPattern,
pub m2sd: PriceRatioPattern,
pub m2_5sd: PriceRatioPattern,
pub m3sd: PriceRatioPattern,
}
impl SeriesTree_Cohorts_Utxo_Entry_Premium_Realized_Price_StdDev_4y {
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
Self {
sd: SeriesPattern1::new(client.clone(), "rookie_realized_price_ratio_sd_4y".to_string()),
zscore: SeriesPattern1::new(client.clone(), "rookie_realized_price_ratio_zscore_4y".to_string()),
_0sd: CentsSatsUsdPattern::new(client.clone(), "rookie_realized_price_0sd_4y".to_string()),
p0_5sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "p0_5sd_4y".to_string()),
p1sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "p1sd_4y".to_string()),
p1_5sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "p1_5sd_4y".to_string()),
p2sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "p2sd_4y".to_string()),
p2_5sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "p2_5sd_4y".to_string()),
p3sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "p3sd_4y".to_string()),
m0_5sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "m0_5sd_4y".to_string()),
m1sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "m1sd_4y".to_string()),
m1_5sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "m1_5sd_4y".to_string()),
m2sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "m2sd_4y".to_string()),
m2_5sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "m2_5sd_4y".to_string()),
m3sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "m3sd_4y".to_string()),
}
}
}
/// Series tree node.
pub struct SeriesTree_Cohorts_Utxo_Entry_Premium_Realized_Price_StdDev_2y {
pub sd: SeriesPattern1<StoredF32>,
pub zscore: SeriesPattern1<StoredF32>,
pub _0sd: CentsSatsUsdPattern,
pub p0_5sd: PriceRatioPattern,
pub p1sd: PriceRatioPattern,
pub p1_5sd: PriceRatioPattern,
pub p2sd: PriceRatioPattern,
pub p2_5sd: PriceRatioPattern,
pub p3sd: PriceRatioPattern,
pub m0_5sd: PriceRatioPattern,
pub m1sd: PriceRatioPattern,
pub m1_5sd: PriceRatioPattern,
pub m2sd: PriceRatioPattern,
pub m2_5sd: PriceRatioPattern,
pub m3sd: PriceRatioPattern,
}
impl SeriesTree_Cohorts_Utxo_Entry_Premium_Realized_Price_StdDev_2y {
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
Self {
sd: SeriesPattern1::new(client.clone(), "rookie_realized_price_ratio_sd_2y".to_string()),
zscore: SeriesPattern1::new(client.clone(), "rookie_realized_price_ratio_zscore_2y".to_string()),
_0sd: CentsSatsUsdPattern::new(client.clone(), "rookie_realized_price_0sd_2y".to_string()),
p0_5sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "p0_5sd_2y".to_string()),
p1sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "p1sd_2y".to_string()),
p1_5sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "p1_5sd_2y".to_string()),
p2sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "p2sd_2y".to_string()),
p2_5sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "p2_5sd_2y".to_string()),
p3sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "p3sd_2y".to_string()),
m0_5sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "m0_5sd_2y".to_string()),
m1sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "m1sd_2y".to_string()),
m1_5sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "m1_5sd_2y".to_string()),
m2sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "m2sd_2y".to_string()),
m2_5sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "m2_5sd_2y".to_string()),
m3sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "m3sd_2y".to_string()),
}
}
}
/// Series tree node.
pub struct SeriesTree_Cohorts_Utxo_Entry_Premium_Realized_Price_StdDev_1y {
pub sd: SeriesPattern1<StoredF32>,
pub zscore: SeriesPattern1<StoredF32>,
pub _0sd: CentsSatsUsdPattern,
pub p0_5sd: PriceRatioPattern,
pub p1sd: PriceRatioPattern,
pub p1_5sd: PriceRatioPattern,
pub p2sd: PriceRatioPattern,
pub p2_5sd: PriceRatioPattern,
pub p3sd: PriceRatioPattern,
pub m0_5sd: PriceRatioPattern,
pub m1sd: PriceRatioPattern,
pub m1_5sd: PriceRatioPattern,
pub m2sd: PriceRatioPattern,
pub m2_5sd: PriceRatioPattern,
pub m3sd: PriceRatioPattern,
}
impl SeriesTree_Cohorts_Utxo_Entry_Premium_Realized_Price_StdDev_1y {
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
Self {
sd: SeriesPattern1::new(client.clone(), "rookie_realized_price_ratio_sd_1y".to_string()),
zscore: SeriesPattern1::new(client.clone(), "rookie_realized_price_ratio_zscore_1y".to_string()),
_0sd: CentsSatsUsdPattern::new(client.clone(), "rookie_realized_price_0sd_1y".to_string()),
p0_5sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "p0_5sd_1y".to_string()),
p1sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "p1sd_1y".to_string()),
p1_5sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "p1_5sd_1y".to_string()),
p2sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "p2sd_1y".to_string()),
p2_5sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "p2_5sd_1y".to_string()),
p3sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "p3sd_1y".to_string()),
m0_5sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "m0_5sd_1y".to_string()),
m1sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "m1sd_1y".to_string()),
m1_5sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "m1_5sd_1y".to_string()),
m2sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "m2sd_1y".to_string()),
m2_5sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "m2_5sd_1y".to_string()),
m3sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "m3sd_1y".to_string()),
}
}
}
/// Series tree node. /// Series tree node.
pub struct SeriesTree_Cohorts_Utxo_OverAmount { pub struct SeriesTree_Cohorts_Utxo_OverAmount {
pub _1sat: ActivityOutputsRealizedSupplyUnrealizedPattern2, pub _1sat: ActivityOutputsRealizedSupplyUnrealizedPattern2,
@@ -8953,7 +9538,7 @@ pub struct BrkClient {
impl BrkClient { impl BrkClient {
/// Client version. /// Client version.
pub const VERSION: &'static str = "v0.3.0-beta.11"; pub const VERSION: &'static str = "v0.3.4";
/// Create a new client with the given base URL. /// Create a new client with the given base URL.
pub fn new(base_url: impl Into<String>) -> Self { pub fn new(base_url: impl Into<String>) -> Self {
@@ -9281,6 +9866,15 @@ impl BrkClient {
self.base.get_json(&path) self.base.get_json(&path)
} }
/// Address hash-prefix matches
///
/// Find addresses by address type and address-payload hash prefix. Intended for privacy-preserving client-side wallet discovery without sending raw addresses or xpubs. Fetch metadata for the returned addresses through `/api/address/{address}`.
///
/// Endpoint: `GET /api/address/hash-prefix/{addr_type}/{prefix}`
pub fn get_address_hash_prefix_matches(&self, addr_type: OutputType, prefix: &str) -> Result<AddrHashPrefixMatches> {
self.base.get_json(&format!("/api/address/hash-prefix/{addr_type}/{prefix}"))
}
/// Address information /// Address information
/// ///
/// Retrieve address information including balance and transaction counts. Supports all standard Bitcoin address types (P2PKH, P2SH, P2WPKH, P2WSH, P2TR). /// Retrieve address information including balance and transaction counts. Supports all standard Bitcoin address types (P2PKH, P2SH, P2WPKH, P2WSH, P2TR).
@@ -9856,6 +10450,51 @@ impl BrkClient {
self.base.get_json(&format!("/api/mempool/price")) self.base.get_json(&format!("/api/mempool/price"))
} }
/// Live BTC/USD price
///
/// Current BTC/USD price in dollars. Same value as `/api/mempool/price`. Confirmed per-height history is available at `/api/vecs/height-to-price`.
///
/// Endpoint: `GET /api/oracle/price`
pub fn get_oracle_price(&self) -> Result<Dollars> {
self.base.get_json(&format!("/api/oracle/price"))
}
/// Live payment output histogram
///
/// Live smoothed histogram of oracle-eligible payment outputs, binned by output value on the oracle log scale. It combines the committed oracle window with the forming mempool block. A flat array of log-scale bins.
///
/// Endpoint: `GET /api/oracle/histogram/payments/live`
pub fn get_oracle_histogram_payments_live(&self) -> Result<Vec<i64>> {
self.base.get_json(&format!("/api/oracle/histogram/payments/live"))
}
/// Payment output histogram at height or day
///
/// Smoothed histogram of oracle-eligible payment outputs for a confirmed point. A block height (`840000`) gives that block's oracle payment histogram; a calendar date (`YYYY-MM-DD`) gives the average of that day's per-block payment histograms. A flat array of log-scale bins.
///
/// Endpoint: `GET /api/oracle/histogram/payments/{point}`
pub fn get_oracle_histogram_payments(&self, point: &str) -> Result<Vec<i64>> {
self.base.get_json(&format!("/api/oracle/histogram/payments/{point}"))
}
/// Live output value histogram
///
/// Live unfiltered output value histogram for the forming mempool block. Every live output is binned by value on the oracle log scale; no oracle payment filters are applied. A flat array of log-scale bins, all zero when no mempool is configured.
///
/// Endpoint: `GET /api/oracle/histogram/outputs/live`
pub fn get_oracle_histogram_outputs_live(&self) -> Result<Vec<i64>> {
self.base.get_json(&format!("/api/oracle/histogram/outputs/live"))
}
/// Output value histogram at height or day
///
/// Unfiltered output value histogram for a confirmed point. A block height (`840000`) gives every output in that block, coinbase included, binned by value on the oracle log scale; a calendar date (`YYYY-MM-DD`) sums every block that day. A flat array of log-scale bins.
///
/// Endpoint: `GET /api/oracle/histogram/outputs/{point}`
pub fn get_oracle_histogram_outputs(&self, point: &str) -> Result<Vec<i64>> {
self.base.get_json(&format!("/api/oracle/histogram/outputs/{point}"))
}
/// Txid by index /// Txid by index
/// ///
/// Retrieve the transaction ID (txid) at a given global transaction index. Returns the txid as plain text. /// Retrieve the transaction ID (txid) at a given global transaction index. Returns the txid as plain text.
+104
View File
@@ -0,0 +1,104 @@
use brk_traversable::Traversable;
use rayon::iter::{IntoParallelIterator, ParallelIterator};
use serde::Serialize;
use super::{CohortName, Filter};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EntryPrice {
Discount,
Premium,
}
impl EntryPrice {
#[inline]
pub const fn from_is_discount(is_discount: bool) -> Self {
if is_discount {
Self::Discount
} else {
Self::Premium
}
}
#[inline]
pub const fn is_discount(self) -> bool {
matches!(self, Self::Discount)
}
}
pub const ENTRY_FILTERS: ByEntry<Filter> = ByEntry {
discount: Filter::Entry(EntryPrice::Discount),
premium: Filter::Entry(EntryPrice::Premium),
};
pub const ENTRY_NAMES: ByEntry<CohortName> = ByEntry {
discount: CohortName::new("veteran", "Veteran", "Veteran Coins"),
premium: CohortName::new("rookie", "Rookie", "Rookie Coins"),
};
#[derive(Default, Clone, Traversable, Serialize)]
pub struct ByEntry<T> {
pub discount: T,
pub premium: T,
}
impl ByEntry<CohortName> {
pub const fn names() -> &'static Self {
&ENTRY_NAMES
}
}
impl<T> ByEntry<T> {
pub fn new<F>(mut create: F) -> Self
where
F: FnMut(Filter, &'static str) -> T,
{
let f = ENTRY_FILTERS;
let n = ENTRY_NAMES;
Self {
discount: create(f.discount, n.discount.id),
premium: create(f.premium, n.premium.id),
}
}
pub fn try_new<F, E>(mut create: F) -> Result<Self, E>
where
F: FnMut(Filter, &'static str) -> Result<T, E>,
{
let f = ENTRY_FILTERS;
let n = ENTRY_NAMES;
Ok(Self {
discount: create(f.discount, n.discount.id)?,
premium: create(f.premium, n.premium.id)?,
})
}
pub fn get(&self, entry: EntryPrice) -> &T {
match entry {
EntryPrice::Discount => &self.discount,
EntryPrice::Premium => &self.premium,
}
}
pub fn get_mut(&mut self, entry: EntryPrice) -> &mut T {
match entry {
EntryPrice::Discount => &mut self.discount,
EntryPrice::Premium => &mut self.premium,
}
}
pub fn iter(&self) -> impl Iterator<Item = &T> {
[&self.discount, &self.premium].into_iter()
}
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut T> {
[&mut self.discount, &mut self.premium].into_iter()
}
pub fn par_iter_mut(&mut self) -> impl ParallelIterator<Item = &mut T>
where
T: Send + Sync,
{
[&mut self.discount, &mut self.premium].into_par_iter()
}
}
+2 -1
View File
@@ -24,7 +24,7 @@ impl CohortContext {
/// Build full name for a filter, adding prefix only for Time/Amount filters. /// Build full name for a filter, adding prefix only for Time/Amount filters.
/// ///
/// Prefix rules: /// Prefix rules:
/// - No prefix: `All`, `Term`, `Epoch`, `Class`, `Type` /// - No prefix: `All`, `Term`, `Epoch`, `Class`, `Entry`, `Type`
/// - Context prefix: `Time`, `Amount` /// - Context prefix: `Time`, `Amount`
pub fn full_name(&self, filter: &Filter, name: &str) -> String { pub fn full_name(&self, filter: &Filter, name: &str) -> String {
match filter { match filter {
@@ -32,6 +32,7 @@ impl CohortContext {
| Filter::Term(_) | Filter::Term(_)
| Filter::Epoch(_) | Filter::Epoch(_)
| Filter::Class(_) | Filter::Class(_)
| Filter::Entry(_)
| Filter::Type(_) => name.to_string(), | Filter::Type(_) => name.to_string(),
Filter::Time(_) | Filter::Amount(_) => self.prefixed(name), Filter::Time(_) | Filter::Amount(_) => self.prefixed(name),
} }
+8 -3
View File
@@ -1,6 +1,6 @@
use brk_types::{Halving, OutputType, Sats, Year}; use brk_types::{Halving, OutputType, Sats, Year};
use super::{AmountFilter, CohortContext, Term, TimeFilter}; use super::{AmountFilter, CohortContext, EntryPrice, Term, TimeFilter};
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub enum Filter { pub enum Filter {
@@ -10,6 +10,7 @@ pub enum Filter {
Amount(AmountFilter), Amount(AmountFilter),
Epoch(Halving), Epoch(Halving),
Class(Year), Class(Year),
Entry(EntryPrice),
Type(OutputType), Type(OutputType),
} }
@@ -68,7 +69,8 @@ impl Filter {
} }
/// Whether to compute extended metrics (realized cap ratios, profit/loss ratios, percentiles) /// Whether to compute extended metrics (realized cap ratios, profit/loss ratios, percentiles)
/// For UTXO context: true only for age range cohorts (Range) and aggregate cohorts (All, Term) /// For UTXO context: true for age range cohorts (Range), aggregate cohorts (All, Term),
/// and immutable entry valuation cohorts.
/// For address context: always false /// For address context: always false
pub fn is_extended(&self, context: CohortContext) -> bool { pub fn is_extended(&self, context: CohortContext) -> bool {
match context { match context {
@@ -76,7 +78,10 @@ impl Filter {
CohortContext::Utxo => { CohortContext::Utxo => {
matches!( matches!(
self, self,
Filter::All | Filter::Term(_) | Filter::Time(TimeFilter::Range(_)) Filter::All
| Filter::Term(_)
| Filter::Time(TimeFilter::Range(_))
| Filter::Entry(_)
) )
} }
} }
+2
View File
@@ -7,6 +7,7 @@ mod amount_range;
mod by_addr_type; mod by_addr_type;
mod by_any_addr; mod by_any_addr;
mod by_epoch; mod by_epoch;
mod by_entry;
mod by_term; mod by_term;
mod by_type; mod by_type;
mod class; mod class;
@@ -36,6 +37,7 @@ pub use amount_range::*;
pub use by_addr_type::*; pub use by_addr_type::*;
pub use by_any_addr::*; pub use by_any_addr::*;
pub use by_epoch::*; pub use by_epoch::*;
pub use by_entry::*;
pub use by_term::*; pub use by_term::*;
pub use by_type::*; pub use by_type::*;
pub use class::*; pub use class::*;
+10 -2
View File
@@ -2,8 +2,8 @@ use brk_traversable::Traversable;
use rayon::prelude::*; use rayon::prelude::*;
use crate::{ use crate::{
AgeRange, AmountRange, ByEpoch, ByTerm, Class, Filter, OverAge, OverAmount, SpendableType, AgeRange, AmountRange, ByEntry, ByEpoch, ByTerm, Class, Filter, OverAge, OverAmount,
UnderAge, UnderAmount, SpendableType, UnderAge, UnderAmount,
}; };
#[derive(Default, Clone, Traversable)] #[derive(Default, Clone, Traversable)]
@@ -12,6 +12,7 @@ pub struct UTXOGroups<T> {
pub age_range: AgeRange<T>, pub age_range: AgeRange<T>,
pub epoch: ByEpoch<T>, pub epoch: ByEpoch<T>,
pub class: Class<T>, pub class: Class<T>,
pub entry: ByEntry<T>,
pub over_age: OverAge<T>, pub over_age: OverAge<T>,
pub over_amount: OverAmount<T>, pub over_amount: OverAmount<T>,
pub amount_range: AmountRange<T>, pub amount_range: AmountRange<T>,
@@ -31,6 +32,7 @@ impl<T> UTXOGroups<T> {
age_range: AgeRange::new(&mut create), age_range: AgeRange::new(&mut create),
epoch: ByEpoch::new(&mut create), epoch: ByEpoch::new(&mut create),
class: Class::new(&mut create), class: Class::new(&mut create),
entry: ByEntry::new(&mut create),
over_age: OverAge::new(&mut create), over_age: OverAge::new(&mut create),
over_amount: OverAmount::new(&mut create), over_amount: OverAmount::new(&mut create),
amount_range: AmountRange::new(&mut create), amount_range: AmountRange::new(&mut create),
@@ -51,6 +53,7 @@ impl<T> UTXOGroups<T> {
.chain(self.age_range.iter()) .chain(self.age_range.iter())
.chain(self.epoch.iter()) .chain(self.epoch.iter())
.chain(self.class.iter()) .chain(self.class.iter())
.chain(self.entry.iter())
.chain(self.amount_range.iter()) .chain(self.amount_range.iter())
.chain(self.under_amount.iter()) .chain(self.under_amount.iter())
.chain(self.type_.iter()) .chain(self.type_.iter())
@@ -66,6 +69,7 @@ impl<T> UTXOGroups<T> {
.chain(self.age_range.iter_mut()) .chain(self.age_range.iter_mut())
.chain(self.epoch.iter_mut()) .chain(self.epoch.iter_mut())
.chain(self.class.iter_mut()) .chain(self.class.iter_mut())
.chain(self.entry.iter_mut())
.chain(self.amount_range.iter_mut()) .chain(self.amount_range.iter_mut())
.chain(self.under_amount.iter_mut()) .chain(self.under_amount.iter_mut())
.chain(self.type_.iter_mut()) .chain(self.type_.iter_mut())
@@ -84,6 +88,7 @@ impl<T> UTXOGroups<T> {
.chain(self.age_range.par_iter_mut()) .chain(self.age_range.par_iter_mut())
.chain(self.epoch.par_iter_mut()) .chain(self.epoch.par_iter_mut())
.chain(self.class.par_iter_mut()) .chain(self.class.par_iter_mut())
.chain(self.entry.par_iter_mut())
.chain(self.amount_range.par_iter_mut()) .chain(self.amount_range.par_iter_mut())
.chain(self.under_amount.par_iter_mut()) .chain(self.under_amount.par_iter_mut())
.chain(self.type_.par_iter_mut()) .chain(self.type_.par_iter_mut())
@@ -94,6 +99,7 @@ impl<T> UTXOGroups<T> {
.iter() .iter()
.chain(self.epoch.iter()) .chain(self.epoch.iter())
.chain(self.class.iter()) .chain(self.class.iter())
.chain(self.entry.iter())
.chain(self.amount_range.iter()) .chain(self.amount_range.iter())
.chain(self.type_.iter()) .chain(self.type_.iter())
} }
@@ -103,6 +109,7 @@ impl<T> UTXOGroups<T> {
.iter_mut() .iter_mut()
.chain(self.epoch.iter_mut()) .chain(self.epoch.iter_mut())
.chain(self.class.iter_mut()) .chain(self.class.iter_mut())
.chain(self.entry.iter_mut())
.chain(self.amount_range.iter_mut()) .chain(self.amount_range.iter_mut())
.chain(self.type_.iter_mut()) .chain(self.type_.iter_mut())
} }
@@ -115,6 +122,7 @@ impl<T> UTXOGroups<T> {
.par_iter_mut() .par_iter_mut()
.chain(self.epoch.par_iter_mut()) .chain(self.epoch.par_iter_mut())
.chain(self.class.par_iter_mut()) .chain(self.class.par_iter_mut())
.chain(self.entry.par_iter_mut())
.chain(self.amount_range.par_iter_mut()) .chain(self.amount_range.par_iter_mut())
.chain(self.type_.par_iter_mut()) .chain(self.type_.par_iter_mut())
} }
+2 -2
View File
@@ -3,14 +3,14 @@ use brk_indexer::Indexer;
use vecdb::Exit; use vecdb::Exit;
use super::Vecs; use super::Vecs;
use crate::{blocks, distribution, mining, prices, supply}; use crate::{blocks, distribution, mining, price, supply};
impl Vecs { impl Vecs {
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub(crate) fn compute( pub(crate) fn compute(
&mut self, &mut self,
indexer: &Indexer, indexer: &Indexer,
prices: &prices::Vecs, prices: &price::Vecs,
blocks: &blocks::Vecs, blocks: &blocks::Vecs,
mining: &mining::Vecs, mining: &mining::Vecs,
supply_vecs: &supply::Vecs, supply_vecs: &supply::Vecs,
@@ -5,14 +5,14 @@ use vecdb::Exit;
use super::super::{activity, cap, supply}; use super::super::{activity, cap, supply};
use super::Vecs; use super::Vecs;
use crate::{distribution, prices}; use crate::{distribution, price};
impl Vecs { impl Vecs {
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub(crate) fn compute( pub(crate) fn compute(
&mut self, &mut self,
indexer: &Indexer, indexer: &Indexer,
prices: &prices::Vecs, prices: &price::Vecs,
distribution: &distribution::Vecs, distribution: &distribution::Vecs,
activity: &activity::Vecs, activity: &activity::Vecs,
supply: &supply::Vecs, supply: &supply::Vecs,
@@ -4,14 +4,14 @@ use brk_types::StoredF64;
use vecdb::Exit; use vecdb::Exit;
use super::{super::value, Vecs}; use super::{super::value, Vecs};
use crate::{blocks, internal::algo::ComputeRollingMedianFromStarts, prices}; use crate::{blocks, internal::algo::ComputeRollingMedianFromStarts, price};
impl Vecs { impl Vecs {
pub(crate) fn compute( pub(crate) fn compute(
&mut self, &mut self,
indexer: &Indexer, indexer: &Indexer,
blocks: &blocks::Vecs, blocks: &blocks::Vecs,
prices: &prices::Vecs, prices: &price::Vecs,
value: &value::Vecs, value: &value::Vecs,
exit: &Exit, exit: &Exit,
) -> Result<()> { ) -> Result<()> {
@@ -4,13 +4,13 @@ use vecdb::Exit;
use super::super::activity; use super::super::activity;
use super::Vecs; use super::Vecs;
use crate::{distribution, prices}; use crate::{distribution, price};
impl Vecs { impl Vecs {
pub(crate) fn compute( pub(crate) fn compute(
&mut self, &mut self,
indexer: &Indexer, indexer: &Indexer,
prices: &prices::Vecs, prices: &price::Vecs,
distribution: &distribution::Vecs, distribution: &distribution::Vecs,
activity: &activity::Vecs, activity: &activity::Vecs,
exit: &Exit, exit: &Exit,
@@ -5,13 +5,13 @@ use vecdb::Exit;
use super::super::activity; use super::super::activity;
use super::Vecs; use super::Vecs;
use crate::{distribution, prices}; use crate::{distribution, price};
impl Vecs { impl Vecs {
pub(crate) fn compute( pub(crate) fn compute(
&mut self, &mut self,
indexer: &Indexer, indexer: &Indexer,
prices: &prices::Vecs, prices: &price::Vecs,
distribution: &distribution::Vecs, distribution: &distribution::Vecs,
activity: &activity::Vecs, activity: &activity::Vecs,
exit: &Exit, exit: &Exit,
@@ -45,7 +45,7 @@ use super::{
count::AddrCountFundedTotalVecs, count::AddrCountFundedTotalVecs,
supply::{AddrSupplyShareVecs, AddrSupplyVecs}, supply::{AddrSupplyShareVecs, AddrSupplyVecs},
}; };
use crate::{indexes, prices}; use crate::{indexes, price};
mod state; mod state;
@@ -104,7 +104,7 @@ impl ExposedAddrVecs {
pub(crate) fn compute_rest( pub(crate) fn compute_rest(
&mut self, &mut self,
starting_lengths: &Lengths, starting_lengths: &Lengths,
prices: &prices::Vecs, prices: &price::Vecs,
all_supply_sats: &impl ReadableVec<Height, Sats>, all_supply_sats: &impl ReadableVec<Height, Sats>,
type_supply_sats: &ByAddrType<&impl ReadableVec<Height, Sats>>, type_supply_sats: &ByAddrType<&impl ReadableVec<Height, Sats>>,
exit: &Exit, exit: &Exit,
@@ -35,7 +35,7 @@ use super::{
use crate::{ use crate::{
indexes, inputs, indexes, inputs,
internal::{WindowStartVec, Windows}, internal::{WindowStartVec, Windows},
outputs, prices, outputs, price,
}; };
mod state; mod state;
@@ -112,7 +112,7 @@ impl ReusedAddrVecs {
starting_lengths: &Lengths, starting_lengths: &Lengths,
outputs_by_type: &outputs::ByTypeVecs, outputs_by_type: &outputs::ByTypeVecs,
inputs_by_type: &inputs::ByTypeVecs, inputs_by_type: &inputs::ByTypeVecs,
prices: &prices::Vecs, prices: &price::Vecs,
all_supply_sats: &impl ReadableVec<Height, Sats>, all_supply_sats: &impl ReadableVec<Height, Sats>,
type_supply_sats: &ByAddrType<&impl ReadableVec<Height, Sats>>, type_supply_sats: &ByAddrType<&impl ReadableVec<Height, Sats>>,
exit: &Exit, exit: &Exit,
@@ -13,7 +13,7 @@ use crate::{
distribution::DynCohortVecs, distribution::DynCohortVecs,
indexes, indexes,
internal::{WindowStartVec, Windows}, internal::{WindowStartVec, Windows},
prices, price,
}; };
use super::{super::traits::CohortVecs, vecs::AddrCohortVecs}; use super::{super::traits::CohortVecs, vecs::AddrCohortVecs};
@@ -95,7 +95,7 @@ impl AddrCohorts {
/// First phase of post-processing: compute index transforms. /// First phase of post-processing: compute index transforms.
pub(crate) fn compute_rest_part1( pub(crate) fn compute_rest_part1(
&mut self, &mut self,
prices: &prices::Vecs, prices: &price::Vecs,
starting_lengths: &Lengths, starting_lengths: &Lengths,
exit: &Exit, exit: &Exit,
) -> Result<()> { ) -> Result<()> {
@@ -108,7 +108,7 @@ impl AddrCohorts {
/// Second phase of post-processing: compute relative metrics. /// Second phase of post-processing: compute relative metrics.
pub(crate) fn compute_rest_part2( pub(crate) fn compute_rest_part2(
&mut self, &mut self,
prices: &prices::Vecs, prices: &price::Vecs,
starting_lengths: &Lengths, starting_lengths: &Lengths,
all_supply_sats: &impl ReadableVec<Height, Sats>, all_supply_sats: &impl ReadableVec<Height, Sats>,
all_utxo_count: &impl ReadableVec<Height, StoredU64>, all_utxo_count: &impl ReadableVec<Height, StoredU64>,
@@ -12,7 +12,7 @@ use crate::{
distribution::state::{AddrCohortState, MinimalRealizedState}, distribution::state::{AddrCohortState, MinimalRealizedState},
indexes, indexes,
internal::{PerBlockWithDeltas, WindowStartVec, Windows}, internal::{PerBlockWithDeltas, WindowStartVec, Windows},
prices, price,
}; };
use crate::distribution::metrics::{ImportConfig, MinimalCohortMetrics}; use crate::distribution::metrics::{ImportConfig, MinimalCohortMetrics};
@@ -174,7 +174,7 @@ impl DynCohortVecs for AddrCohortVecs {
fn compute_rest_part1( fn compute_rest_part1(
&mut self, &mut self,
prices: &prices::Vecs, prices: &price::Vecs,
starting_lengths: &Lengths, starting_lengths: &Lengths,
exit: &Exit, exit: &Exit,
) -> Result<()> { ) -> Result<()> {
@@ -229,7 +229,7 @@ impl CohortVecs for AddrCohortVecs {
fn compute_rest_part2( fn compute_rest_part2(
&mut self, &mut self,
prices: &prices::Vecs, prices: &price::Vecs,
starting_lengths: &Lengths, starting_lengths: &Lengths,
all_supply_sats: &impl ReadableVec<Height, Sats>, all_supply_sats: &impl ReadableVec<Height, Sats>,
all_utxo_count: &impl ReadableVec<Height, StoredU64>, all_utxo_count: &impl ReadableVec<Height, StoredU64>,
@@ -3,7 +3,7 @@ use brk_indexer::Lengths;
use brk_types::{Cents, Height, Sats, StoredU64, Version}; use brk_types::{Cents, Height, Sats, StoredU64, Version};
use vecdb::{Exit, ReadableVec}; use vecdb::{Exit, ReadableVec};
use crate::prices; use crate::price;
/// Dynamic dispatch trait for cohort vectors. /// Dynamic dispatch trait for cohort vectors.
/// ///
@@ -31,7 +31,7 @@ pub trait DynCohortVecs: Send + Sync {
/// First phase of post-processing computations. /// First phase of post-processing computations.
fn compute_rest_part1( fn compute_rest_part1(
&mut self, &mut self,
prices: &prices::Vecs, prices: &price::Vecs,
starting_lengths: &Lengths, starting_lengths: &Lengths,
exit: &Exit, exit: &Exit,
) -> Result<()>; ) -> Result<()>;
@@ -61,7 +61,7 @@ pub trait CohortVecs: DynCohortVecs {
/// Second phase of post-processing computations. /// Second phase of post-processing computations.
fn compute_rest_part2( fn compute_rest_part2(
&mut self, &mut self,
prices: &prices::Vecs, prices: &price::Vecs,
starting_lengths: &Lengths, starting_lengths: &Lengths,
all_supply_sats: &impl ReadableVec<Height, Sats>, all_supply_sats: &impl ReadableVec<Height, Sats>,
all_utxo_count: &impl ReadableVec<Height, StoredU64>, all_utxo_count: &impl ReadableVec<Height, StoredU64>,
@@ -30,18 +30,34 @@ const TREE_SIZE: usize = TIER0_COUNT + TIER1_COUNT + OVERFLOW; // 190,001
pub(super) struct CostBasisNode { pub(super) struct CostBasisNode {
all_sats: i64, all_sats: i64,
sth_sats: i64, sth_sats: i64,
discount_sats: i64,
all_usd: i128, all_usd: i128,
sth_usd: i128, sth_usd: i128,
discount_usd: i128,
} }
impl CostBasisNode { impl CostBasisNode {
#[inline(always)] #[inline(always)]
fn new(sats: i64, usd: i128, is_sth: bool) -> Self { fn new_supply(sats: i64, usd: i128, is_sth: bool) -> Self {
Self { Self {
all_sats: sats, all_sats: sats,
sth_sats: if is_sth { sats } else { 0 }, sth_sats: if is_sth { sats } else { 0 },
discount_sats: 0,
all_usd: usd, all_usd: usd,
sth_usd: if is_sth { usd } else { 0 }, sth_usd: if is_sth { usd } else { 0 },
discount_usd: 0,
}
}
#[inline(always)]
fn new_discount(sats: i64, usd: i128) -> Self {
Self {
all_sats: 0,
sth_sats: 0,
discount_sats: sats,
all_usd: 0,
sth_usd: 0,
discount_usd: usd,
} }
} }
} }
@@ -51,8 +67,10 @@ impl FenwickNode for CostBasisNode {
fn add_assign(&mut self, other: &Self) { fn add_assign(&mut self, other: &Self) {
self.all_sats += other.all_sats; self.all_sats += other.all_sats;
self.sth_sats += other.sth_sats; self.sth_sats += other.sth_sats;
self.discount_sats += other.discount_sats;
self.all_usd += other.all_usd; self.all_usd += other.all_usd;
self.sth_usd += other.sth_usd; self.sth_usd += other.sth_usd;
self.discount_usd += other.discount_usd;
} }
} }
@@ -151,16 +169,34 @@ impl CostBasisFenwick {
} }
let bucket = price_to_bucket(price); let bucket = price_to_bucket(price);
let delta = let delta =
CostBasisNode::new(net_sats, price.as_u128() as i128 * net_sats as i128, is_sth); CostBasisNode::new_supply(net_sats, price.as_u128() as i128 * net_sats as i128, is_sth);
self.tree.add(bucket, &delta); self.tree.add(bucket, &delta);
self.totals.add_assign(&delta); self.totals.add_assign(&delta);
} }
/// Bulk-initialize from BTreeMaps (one per age-range cohort). /// Apply a net delta from the discount-entry cohort.
/// Call after state import when all pending maps have been drained. ///
pub(super) fn bulk_init<'a>( /// Supply totals are maintained from the age-range cohorts; this updates
/// only the discount-entry partition so premium can be derived as all - discount.
pub(super) fn apply_discount_delta(&mut self, price: CentsCompact, pending: &PendingDelta) {
let net_sats = u64::from(pending.inc) as i64 - u64::from(pending.dec) as i64;
if net_sats == 0 {
return;
}
let bucket = price_to_bucket(price);
let delta =
CostBasisNode::new_discount(net_sats, price.as_u128() as i128 * net_sats as i128);
self.tree.add(bucket, &delta);
self.totals.add_assign(&delta);
}
/// Bulk-initialize from age-range maps plus the discount-entry map.
/// Age-range maps maintain all/STH/LTH totals; the discount-entry map
/// maintains only the discount partition used to derive premium.
pub(super) fn bulk_init_with_discount<'a>(
&mut self, &mut self,
maps: impl Iterator<Item = (&'a std::collections::BTreeMap<CentsCompact, Sats>, bool)>, maps: impl Iterator<Item = (&'a std::collections::BTreeMap<CentsCompact, Sats>, bool)>,
discount_maps: impl Iterator<Item = &'a std::collections::BTreeMap<CentsCompact, Sats>>,
) { ) {
self.tree.reset(); self.tree.reset();
self.totals = CostBasisNode::default(); self.totals = CostBasisNode::default();
@@ -169,7 +205,18 @@ impl CostBasisFenwick {
for (&price, &sats) in map.iter() { for (&price, &sats) in map.iter() {
let bucket = price_to_bucket(price); let bucket = price_to_bucket(price);
let s = u64::from(sats) as i64; let s = u64::from(sats) as i64;
let node = CostBasisNode::new(s, price.as_u128() as i128 * s as i128, is_sth); let node =
CostBasisNode::new_supply(s, price.as_u128() as i128 * s as i128, is_sth);
self.tree.add_raw(bucket, &node);
self.totals.add_assign(&node);
}
}
for map in discount_maps {
for (&price, &sats) in map.iter() {
let bucket = price_to_bucket(price);
let s = u64::from(sats) as i64;
let node = CostBasisNode::new_discount(s, price.as_u128() as i128 * s as i128);
self.tree.add_raw(bucket, &node); self.tree.add_raw(bucket, &node);
self.totals.add_assign(&node); self.totals.add_assign(&node);
} }
@@ -212,6 +259,26 @@ impl CostBasisFenwick {
) )
} }
/// Compute percentile prices for discount-entry cohort.
pub(super) fn percentiles_discount_entry(&self) -> PercentileResult {
self.compute_percentiles(
self.totals.discount_sats,
self.totals.discount_usd,
|n| n.discount_sats,
|n| n.discount_usd,
)
}
/// Compute percentile prices for premium-entry cohort (all - discount).
pub(super) fn percentiles_premium_entry(&self) -> PercentileResult {
self.compute_percentiles(
self.totals.all_sats - self.totals.discount_sats,
self.totals.all_usd - self.totals.discount_usd,
|n| n.all_sats - n.discount_sats,
|n| n.all_usd - n.discount_usd,
)
}
fn compute_percentiles( fn compute_percentiles(
&self, &self,
total_sats: i64, total_sats: i64,
@@ -271,6 +338,37 @@ impl CostBasisFenwick {
return (0, 0, 0); return (0, 0, 0);
} }
let range = self.density_range(spot_price);
let all_range = range.all_sats.max(0);
let sth_range = range.sth_sats.max(0);
let lth_range = all_range - sth_range;
let lth_total = self.totals.all_sats - self.totals.sth_sats;
(
Self::to_bps(all_range, self.totals.all_sats),
Self::to_bps(sth_range, self.totals.sth_sats),
Self::to_bps(lth_range, lth_total),
)
}
/// Compute supply density for entry cohorts: (discount_bps, premium_bps).
pub(super) fn entry_density(&self, spot_price: Cents) -> (u16, u16) {
if self.totals.all_sats <= 0 {
return (0, 0);
}
let range = self.density_range(spot_price);
let discount_range = range.discount_sats.max(0);
let premium_range = range.all_sats.max(0) - discount_range;
let premium_total = self.totals.all_sats - self.totals.discount_sats;
(
Self::to_bps(discount_range, self.totals.discount_sats),
Self::to_bps(premium_range, premium_total),
)
}
fn density_range(&self, spot_price: Cents) -> CostBasisNode {
let spot_f64 = u64::from(spot_price) as f64; let spot_f64 = u64::from(spot_price) as f64;
let low = Cents::from((spot_f64 * 0.95) as u64); let low = Cents::from((spot_f64 * 0.95) as u64);
let high = Cents::from((spot_f64 * 1.05) as u64); let high = Cents::from((spot_f64 * 1.05) as u64);
@@ -285,24 +383,23 @@ impl CostBasisFenwick {
CostBasisNode::default() CostBasisNode::default()
}; };
let all_range = (cum_high.all_sats - cum_low.all_sats).max(0); CostBasisNode {
let sth_range = (cum_high.sth_sats - cum_low.sth_sats).max(0); all_sats: cum_high.all_sats - cum_low.all_sats,
let lth_range = all_range - sth_range; sth_sats: cum_high.sth_sats - cum_low.sth_sats,
discount_sats: cum_high.discount_sats - cum_low.discount_sats,
all_usd: cum_high.all_usd - cum_low.all_usd,
sth_usd: cum_high.sth_usd - cum_low.sth_usd,
discount_usd: cum_high.discount_usd - cum_low.discount_usd,
}
}
let to_bps = |range: i64, total: i64| -> u16 { #[inline(always)]
if total <= 0 { fn to_bps(range: i64, total: i64) -> u16 {
0 if total <= 0 {
} else { 0
(range as f64 / total as f64 * 10000.0).round() as u16 } else {
} (range as f64 / total as f64 * 10000.0).round() as u16
}; }
let lth_total = self.totals.all_sats - self.totals.sth_sats;
(
to_bps(all_range, self.totals.all_sats),
to_bps(sth_range, self.totals.sth_sats),
to_bps(lth_range, lth_total),
)
} }
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
@@ -1,8 +1,8 @@
use std::path::Path; use std::path::Path;
use brk_cohort::{ use brk_cohort::{
AgeRange, AmountRange, ByEpoch, Class, CohortContext, Filter, Filtered, OverAge, OverAmount, AgeRange, AmountRange, ByEntry, ByEpoch, Class, CohortContext, Filter, Filtered, OverAge,
SpendableType, Term, UnderAge, UnderAmount, OverAmount, SpendableType, Term, UnderAge, UnderAmount,
}; };
use brk_error::Result; use brk_error::Result;
use brk_indexer::Lengths; use brk_indexer::Lengths;
@@ -16,7 +16,6 @@ use vecdb::{
use crate::{ use crate::{
blocks, blocks,
distribution::{ distribution::{
DynCohortVecs,
metrics::{ metrics::{
AllCohortMetrics, BasicCohortMetrics, CohortMetricsBase, CoreCohortMetrics, AllCohortMetrics, BasicCohortMetrics, CohortMetricsBase, CoreCohortMetrics,
ExtendedAdjustedCohortMetrics, ExtendedCohortMetrics, ImportConfig, ExtendedAdjustedCohortMetrics, ExtendedCohortMetrics, ImportConfig,
@@ -24,10 +23,11 @@ use crate::{
TypeCohortMetrics, TypeCohortMetrics,
}, },
state::UTXOCohortState, state::UTXOCohortState,
DynCohortVecs,
}, },
indexes, indexes,
internal::{ValuePerBlockCumulativeRolling, WindowStartVec, Windows}, internal::{ValuePerBlockCumulativeRolling, WindowStartVec, Windows},
prices, price,
}; };
use super::{fenwick::CostBasisFenwick, vecs::UTXOCohortVecs}; use super::{fenwick::CostBasisFenwick, vecs::UTXOCohortVecs};
@@ -45,6 +45,7 @@ pub struct UTXOCohorts<M: StorageMode = Rw> {
pub over_age: OverAge<UTXOCohortVecs<CoreCohortMetrics<M>>>, pub over_age: OverAge<UTXOCohortVecs<CoreCohortMetrics<M>>>,
pub epoch: ByEpoch<UTXOCohortVecs<CoreCohortMetrics<M>>>, pub epoch: ByEpoch<UTXOCohortVecs<CoreCohortMetrics<M>>>,
pub class: Class<UTXOCohortVecs<CoreCohortMetrics<M>>>, pub class: Class<UTXOCohortVecs<CoreCohortMetrics<M>>>,
pub entry: ByEntry<UTXOCohortVecs<ExtendedCohortMetrics<M>>>,
pub over_amount: OverAmount<UTXOCohortVecs<MinimalCohortMetrics<M>>>, pub over_amount: OverAmount<UTXOCohortVecs<MinimalCohortMetrics<M>>>,
pub amount_range: AmountRange<UTXOCohortVecs<MinimalCohortMetrics<M>>>, pub amount_range: AmountRange<UTXOCohortVecs<MinimalCohortMetrics<M>>>,
pub under_amount: UnderAmount<UTXOCohortVecs<MinimalCohortMetrics<M>>>, pub under_amount: UnderAmount<UTXOCohortVecs<MinimalCohortMetrics<M>>>,
@@ -67,8 +68,10 @@ pub(crate) struct UTXOCohortsTransientState {
} }
impl UTXOCohorts<Rw> { impl UTXOCohorts<Rw> {
/// ~71 separate cohorts (21 age + 5 epoch + 18 class + 15 amount + 12 type) /// Separate cohorts currently total 72:
const SEPARATE_COHORT_CAPACITY: usize = 80; /// 21 age + 5 epoch + 18 class + 2 entry + 15 amount + 11 spendable type.
/// Keep small headroom because this is only Vec allocation capacity.
const SEPARATE_COHORT_CAPACITY: usize = 82;
/// Import all UTXO cohorts from database. /// Import all UTXO cohorts from database.
pub(crate) fn forced_import( pub(crate) fn forced_import(
@@ -136,6 +139,26 @@ impl UTXOCohorts<Rw> {
let epoch = ByEpoch::try_new(&core_separate)?; let epoch = ByEpoch::try_new(&core_separate)?;
let class = Class::try_new(&core_separate)?; let class = Class::try_new(&core_separate)?;
let extended_separate =
|f: Filter, name: &'static str| -> Result<UTXOCohortVecs<ExtendedCohortMetrics>> {
let full_name = CohortContext::Utxo.full_name(&f, name);
let cfg = ImportConfig {
db,
filter: &f,
full_name: &full_name,
version: v,
indexes,
cached_starts,
};
let state = Some(Box::new(UTXOCohortState::new(states_path, &full_name)));
Ok(UTXOCohortVecs::new(
state,
ExtendedCohortMetrics::forced_import(&cfg)?,
))
};
let entry = ByEntry::try_new(&extended_separate)?;
// Helper for separate cohorts with MinimalCohortMetrics + MinimalRealizedState // Helper for separate cohorts with MinimalCohortMetrics + MinimalRealizedState
let minimal_separate = let minimal_separate =
|f: Filter, name: &'static str| -> Result<UTXOCohortVecs<MinimalCohortMetrics>> { |f: Filter, name: &'static str| -> Result<UTXOCohortVecs<MinimalCohortMetrics>> {
@@ -281,6 +304,7 @@ impl UTXOCohorts<Rw> {
lth, lth,
epoch, epoch,
class, class,
entry,
type_, type_,
under_age, under_age,
over_age, over_age,
@@ -309,6 +333,7 @@ impl UTXOCohorts<Rw> {
sth, sth,
caches, caches,
age_range, age_range,
entry,
.. ..
} = self; } = self;
caches caches
@@ -327,7 +352,15 @@ impl UTXOCohorts<Rw> {
Some((map, caches.fenwick.is_sth_at(i))) Some((map, caches.fenwick.is_sth_at(i)))
}) })
.collect(); .collect();
caches.fenwick.bulk_init(maps.into_iter()); let discount_maps = entry
.discount
.state
.as_ref()
.map(|state| state.cost_basis_map())
.into_iter();
caches
.fenwick
.bulk_init_with_discount(maps.into_iter(), discount_maps);
} }
/// Apply pending deltas from all age-range cohorts to the Fenwick tree. /// Apply pending deltas from all age-range cohorts to the Fenwick tree.
@@ -338,7 +371,10 @@ impl UTXOCohorts<Rw> {
} }
// Destructure to get separate borrows on caches and age_range // Destructure to get separate borrows on caches and age_range
let Self { let Self {
caches, age_range, .. caches,
age_range,
entry,
..
} = self; } = self;
for (i, sub) in age_range.iter().enumerate() { for (i, sub) in age_range.iter().enumerate() {
if let Some(state) = sub.state.as_ref() { if let Some(state) = sub.state.as_ref() {
@@ -348,6 +384,11 @@ impl UTXOCohorts<Rw> {
}); });
} }
} }
if let Some(state) = entry.discount.state.as_ref() {
state.for_each_cost_basis_pending(|&price, delta| {
caches.fenwick.apply_discount_delta(price, delta);
});
}
} }
/// Push maturation sats to the matured vecs for the given height. /// Push maturation sats to the matured vecs for the given height.
@@ -365,6 +406,7 @@ impl UTXOCohorts<Rw> {
age_range, age_range,
epoch, epoch,
class, class,
entry,
amount_range, amount_range,
type_, type_,
.. ..
@@ -374,6 +416,7 @@ impl UTXOCohorts<Rw> {
.map(|x| x as &mut dyn DynCohortVecs) .map(|x| x as &mut dyn DynCohortVecs)
.chain(epoch.par_iter_mut().map(|x| x as &mut dyn DynCohortVecs)) .chain(epoch.par_iter_mut().map(|x| x as &mut dyn DynCohortVecs))
.chain(class.par_iter_mut().map(|x| x as &mut dyn DynCohortVecs)) .chain(class.par_iter_mut().map(|x| x as &mut dyn DynCohortVecs))
.chain(entry.par_iter_mut().map(|x| x as &mut dyn DynCohortVecs))
.chain( .chain(
amount_range amount_range
.par_iter_mut() .par_iter_mut()
@@ -389,6 +432,7 @@ impl UTXOCohorts<Rw> {
age_range, age_range,
epoch, epoch,
class, class,
entry,
amount_range, amount_range,
type_, type_,
.. ..
@@ -398,6 +442,7 @@ impl UTXOCohorts<Rw> {
.map(|x| x as &mut dyn DynCohortVecs) .map(|x| x as &mut dyn DynCohortVecs)
.chain(epoch.iter_mut().map(|x| x as &mut dyn DynCohortVecs)) .chain(epoch.iter_mut().map(|x| x as &mut dyn DynCohortVecs))
.chain(class.iter_mut().map(|x| x as &mut dyn DynCohortVecs)) .chain(class.iter_mut().map(|x| x as &mut dyn DynCohortVecs))
.chain(entry.iter_mut().map(|x| x as &mut dyn DynCohortVecs))
.chain(amount_range.iter_mut().map(|x| x as &mut dyn DynCohortVecs)) .chain(amount_range.iter_mut().map(|x| x as &mut dyn DynCohortVecs))
.chain(type_.iter_mut().map(|x| x as &mut dyn DynCohortVecs)) .chain(type_.iter_mut().map(|x| x as &mut dyn DynCohortVecs))
} }
@@ -409,6 +454,7 @@ impl UTXOCohorts<Rw> {
.map(|x| x as &dyn DynCohortVecs) .map(|x| x as &dyn DynCohortVecs)
.chain(self.epoch.iter().map(|x| x as &dyn DynCohortVecs)) .chain(self.epoch.iter().map(|x| x as &dyn DynCohortVecs))
.chain(self.class.iter().map(|x| x as &dyn DynCohortVecs)) .chain(self.class.iter().map(|x| x as &dyn DynCohortVecs))
.chain(self.entry.iter().map(|x| x as &dyn DynCohortVecs))
.chain(self.amount_range.iter().map(|x| x as &dyn DynCohortVecs)) .chain(self.amount_range.iter().map(|x| x as &dyn DynCohortVecs))
.chain(self.type_.iter().map(|x| x as &dyn DynCohortVecs)) .chain(self.type_.iter().map(|x| x as &dyn DynCohortVecs))
} }
@@ -483,7 +529,7 @@ impl UTXOCohorts<Rw> {
/// First phase of post-processing: compute index transforms. /// First phase of post-processing: compute index transforms.
pub(crate) fn compute_rest_part1( pub(crate) fn compute_rest_part1(
&mut self, &mut self,
prices: &prices::Vecs, prices: &price::Vecs,
starting_lengths: &Lengths, starting_lengths: &Lengths,
exit: &Exit, exit: &Exit,
) -> Result<()> { ) -> Result<()> {
@@ -516,6 +562,7 @@ impl UTXOCohorts<Rw> {
); );
all.extend(self.epoch.iter_mut().map(|x| x as &mut dyn DynCohortVecs)); all.extend(self.epoch.iter_mut().map(|x| x as &mut dyn DynCohortVecs));
all.extend(self.class.iter_mut().map(|x| x as &mut dyn DynCohortVecs)); all.extend(self.class.iter_mut().map(|x| x as &mut dyn DynCohortVecs));
all.extend(self.entry.iter_mut().map(|x| x as &mut dyn DynCohortVecs));
all.extend( all.extend(
self.amount_range self.amount_range
.iter_mut() .iter_mut()
@@ -546,7 +593,7 @@ impl UTXOCohorts<Rw> {
pub(crate) fn compute_rest_part2( pub(crate) fn compute_rest_part2(
&mut self, &mut self,
blocks: &blocks::Vecs, blocks: &blocks::Vecs,
prices: &prices::Vecs, prices: &price::Vecs,
starting_lengths: &Lengths, starting_lengths: &Lengths,
height_to_market_cap: &impl ReadableVec<Height, Dollars>, height_to_market_cap: &impl ReadableVec<Height, Dollars>,
exit: &Exit, exit: &Exit,
@@ -604,6 +651,7 @@ impl UTXOCohorts<Rw> {
under_amount, under_amount,
epoch, epoch,
class, class,
entry,
type_, type_,
.. ..
} = self; } = self;
@@ -676,6 +724,19 @@ impl UTXOCohorts<Rw> {
.compute_rest_part2(prices, starting_lengths, ss, au, exit) .compute_rest_part2(prices, starting_lengths, ss, au, exit)
}) })
}), }),
Box::new(|| {
entry.par_iter_mut().try_for_each(|v| {
v.metrics.compute_rest_part2(
blocks,
prices,
starting_lengths,
height_to_market_cap,
ss,
au,
exit,
)
})
}),
Box::new(|| { Box::new(|| {
amount_range.par_iter_mut().try_for_each(|v| { amount_range.par_iter_mut().try_for_each(|v| {
v.metrics v.metrics
@@ -730,6 +791,9 @@ impl UTXOCohorts<Rw> {
for v in self.class.iter_mut() { for v in self.class.iter_mut() {
vecs.extend(v.metrics.collect_all_vecs_mut()); vecs.extend(v.metrics.collect_all_vecs_mut());
} }
for v in self.entry.iter_mut() {
vecs.extend(v.metrics.collect_all_vecs_mut());
}
for v in self.amount_range.iter_mut() { for v in self.amount_range.iter_mut() {
vecs.extend(v.metrics.collect_all_vecs_mut()); vecs.extend(v.metrics.collect_all_vecs_mut());
} }
@@ -813,7 +877,7 @@ impl UTXOCohorts<Rw> {
/// Aggregate RealizedFull fields from age_range states and push to all/sth/lth. /// Aggregate RealizedFull fields from age_range states and push to all/sth/lth.
/// Called during the block loop after separate cohorts' push_state but before reset. /// Called during the block loop after separate cohorts' push_state but before reset.
pub(crate) fn push_overlapping(&mut self, height_price: Cents) { pub(crate) fn push_overlapping(&mut self, height_price: Cents) -> Cents {
let Self { let Self {
all, all,
sth, sth,
@@ -852,7 +916,7 @@ impl UTXOCohorts<Rw> {
} }
} }
all.metrics.realized.push_accum(&all_acc); let all_capitalized_price = all.metrics.realized.push_accum(&all_acc);
sth.metrics.realized.push_accum(&sth_acc); sth.metrics.realized.push_accum(&sth_acc);
lth.metrics.realized.push_accum(&lth_acc); lth.metrics.realized.push_accum(&lth_acc);
@@ -880,6 +944,8 @@ impl UTXOCohorts<Rw> {
.unrealized .unrealized
.capitalized_cap_in_loss_raw .capitalized_cap_in_loss_raw
.push(CentsSquaredSats::new(lth_ccap.1)); .push(CentsSquaredSats::new(lth_ccap.1));
all_capitalized_price
} }
} }
@@ -50,6 +50,22 @@ impl UTXOCohorts {
let lth = self.caches.fenwick.percentiles_lth(); let lth = self.caches.fenwick.percentiles_lth();
push_cost_basis(&lth, lth_d, &mut self.lth.metrics.cost_basis); push_cost_basis(&lth, lth_d, &mut self.lth.metrics.cost_basis);
let (discount_d, premium_d) = self.caches.fenwick.entry_density(spot_price);
let discount = self.caches.fenwick.percentiles_discount_entry();
push_cost_basis(
&discount,
discount_d,
&mut self.entry.discount.metrics.cost_basis,
);
let premium = self.caches.fenwick.percentiles_premium_entry();
push_cost_basis(
&premium,
premium_d,
&mut self.entry.premium.metrics.cost_basis,
);
let prof = self.caches.fenwick.profitability(spot_price); let prof = self.caches.fenwick.profitability(spot_price);
push_profitability(&prof, &mut self.profitability); push_profitability(&prof, &mut self.profitability);
} }
@@ -1,3 +1,4 @@
use brk_cohort::EntryPrice;
use brk_types::{Cents, CostBasisSnapshot, Height, Timestamp}; use brk_types::{Cents, CostBasisSnapshot, Height, Timestamp};
use vecdb::Rw; use vecdb::Rw;
@@ -12,6 +13,7 @@ impl UTXOCohorts<Rw> {
/// - The "under_1h" age cohort (all new UTXOs start at 0 hours old) /// - The "under_1h" age cohort (all new UTXOs start at 0 hours old)
/// - The appropriate epoch cohort based on block height /// - The appropriate epoch cohort based on block height
/// - The appropriate class cohort based on block timestamp /// - The appropriate class cohort based on block timestamp
/// - The immutable entry valuation cohort based on creation price versus anchor
/// - The appropriate output type cohort (P2PKH, P2SH, etc.) /// - The appropriate output type cohort (P2PKH, P2SH, etc.)
/// - The appropriate amount range cohort based on value /// - The appropriate amount range cohort based on value
pub(crate) fn receive( pub(crate) fn receive(
@@ -20,13 +22,14 @@ impl UTXOCohorts<Rw> {
height: Height, height: Height,
timestamp: Timestamp, timestamp: Timestamp,
price: Cents, price: Cents,
entry: EntryPrice,
) { ) {
let supply_state = received.spendable_supply; let supply_state = received.spendable_supply;
// Pre-compute snapshot once for the 3 cohorts sharing the same supply_state // Pre-compute snapshot once for cohorts sharing the block-level supply_state
let snapshot = CostBasisSnapshot::from_utxo(price, &supply_state); let snapshot = CostBasisSnapshot::from_utxo(price, &supply_state);
// New UTXOs go into under_1h, current epoch, and current class // New UTXOs go into under_1h plus immutable creation cohorts
self.age_range self.age_range
.under_1h .under_1h
.state .state
@@ -45,6 +48,12 @@ impl UTXOCohorts<Rw> {
.unwrap() .unwrap()
.receive_utxo_snapshot(&supply_state, &snapshot); .receive_utxo_snapshot(&supply_state, &snapshot);
} }
self.entry
.get_mut(entry)
.state
.as_mut()
.unwrap()
.receive_utxo_snapshot(&supply_state, &snapshot);
// Update output type cohorts (skip types with no outputs this block) // Update output type cohorts (skip types with no outputs this block)
self.type_.iter_typed_mut().for_each(|(output_type, vecs)| { self.type_.iter_typed_mut().for_each(|(output_type, vecs)| {
@@ -49,7 +49,7 @@ impl UTXOCohorts<Rw> {
// This is the max price between receive and send heights // This is the max price between receive and send heights
let peak_price = price_range_max.max_between(receive_height, send_height); let peak_price = price_range_max.max_between(receive_height, send_height);
// Pre-compute once for age_range, epoch, year (all share sent.spendable_supply) // Pre-compute once for cohorts sharing the sent supply.
if let Some(pre) = SendPrecomputed::new( if let Some(pre) = SendPrecomputed::new(
&sent.spendable_supply, &sent.spendable_supply,
current_price, current_price,
@@ -75,6 +75,12 @@ impl UTXOCohorts<Rw> {
.unwrap() .unwrap()
.send_utxo_precomputed(&sent.spendable_supply, &pre); .send_utxo_precomputed(&sent.spendable_supply, &pre);
} }
self.entry
.get_mut(block_state.entry)
.state
.as_mut()
.unwrap()
.send_utxo_precomputed(&sent.spendable_supply, &pre);
} else if sent.spendable_supply.utxo_count > 0 { } else if sent.spendable_supply.utxo_count > 0 {
// Zero-value UTXOs: just subtract supply // Zero-value UTXOs: just subtract supply
self.age_range.get_mut(age).state.as_mut().unwrap().supply -= self.age_range.get_mut(age).state.as_mut().unwrap().supply -=
@@ -85,6 +91,12 @@ impl UTXOCohorts<Rw> {
if let Some(v) = self.class.mut_vec_from_timestamp(block_state.timestamp) { if let Some(v) = self.class.mut_vec_from_timestamp(block_state.timestamp) {
v.state.as_mut().unwrap().supply -= &sent.spendable_supply; v.state.as_mut().unwrap().supply -= &sent.spendable_supply;
} }
self.entry
.get_mut(block_state.entry)
.state
.as_mut()
.unwrap()
.supply -= &sent.spendable_supply;
} }
// Update output type cohorts (skip zero-supply entries) // Update output type cohorts (skip zero-supply entries)
@@ -6,7 +6,7 @@ use vecdb::{Exit, ReadableVec};
use crate::{ use crate::{
distribution::{cohorts::traits::DynCohortVecs, metrics::CoreCohortMetrics}, distribution::{cohorts::traits::DynCohortVecs, metrics::CoreCohortMetrics},
prices, price,
}; };
use super::UTXOCohortVecs; use super::UTXOCohortVecs;
@@ -56,7 +56,7 @@ impl DynCohortVecs for UTXOCohortVecs<CoreCohortMetrics> {
fn compute_rest_part1( fn compute_rest_part1(
&mut self, &mut self,
prices: &prices::Vecs, prices: &price::Vecs,
starting_lengths: &Lengths, starting_lengths: &Lengths,
exit: &Exit, exit: &Exit,
) -> Result<()> { ) -> Result<()> {
@@ -6,7 +6,7 @@ use vecdb::{Exit, ReadableVec};
use crate::{ use crate::{
distribution::{cohorts::traits::DynCohortVecs, metrics::MinimalCohortMetrics}, distribution::{cohorts::traits::DynCohortVecs, metrics::MinimalCohortMetrics},
prices, price,
}; };
use super::UTXOCohortVecs; use super::UTXOCohortVecs;
@@ -49,7 +49,7 @@ impl DynCohortVecs for UTXOCohortVecs<MinimalCohortMetrics> {
fn compute_rest_part1( fn compute_rest_part1(
&mut self, &mut self,
prices: &prices::Vecs, prices: &price::Vecs,
starting_lengths: &Lengths, starting_lengths: &Lengths,
exit: &Exit, exit: &Exit,
) -> Result<()> { ) -> Result<()> {
@@ -55,7 +55,7 @@ use crate::{
metrics::{CohortMetricsBase, CohortMetricsState}, metrics::{CohortMetricsBase, CohortMetricsState},
state::UTXOCohortState, state::UTXOCohortState,
}, },
prices, price,
}; };
#[derive(Traversable)] #[derive(Traversable)]
@@ -186,7 +186,7 @@ impl<M: CohortMetricsBase + Traversable> DynCohortVecs for UTXOCohortVecs<M> {
fn compute_rest_part1( fn compute_rest_part1(
&mut self, &mut self,
prices: &prices::Vecs, prices: &price::Vecs,
starting_lengths: &Lengths, starting_lengths: &Lengths,
exit: &Exit, exit: &Exit,
) -> Result<()> { ) -> Result<()> {
@@ -5,7 +5,7 @@ use brk_types::{Cents, Height, Version};
use vecdb::{Exit, ReadableVec}; use vecdb::{Exit, ReadableVec};
use crate::{ use crate::{
distribution::cohorts::traits::DynCohortVecs, distribution::metrics::TypeCohortMetrics, prices, distribution::cohorts::traits::DynCohortVecs, distribution::metrics::TypeCohortMetrics, price,
}; };
use super::UTXOCohortVecs; use super::UTXOCohortVecs;
@@ -55,7 +55,7 @@ impl DynCohortVecs for UTXOCohortVecs<TypeCohortMetrics> {
fn compute_rest_part1( fn compute_rest_part1(
&mut self, &mut self,
prices: &prices::Vecs, prices: &price::Vecs,
starting_lengths: &Lengths, starting_lengths: &Lengths,
exit: &Exit, exit: &Exit,
) -> Result<()> { ) -> Result<()> {
@@ -1,4 +1,4 @@
use brk_cohort::ByAddrType; use brk_cohort::{ByAddrType, EntryPrice};
use brk_error::Result; use brk_error::Result;
use brk_indexer::Indexer; use brk_indexer::Indexer;
use brk_types::{ use brk_types::{
@@ -46,6 +46,7 @@ pub(crate) fn process_blocks(
last_height: Height, last_height: Height,
chain_state: &mut Vec<BlockState>, chain_state: &mut Vec<BlockState>,
tx_index_to_height: &mut RangeMap<TxIndex, Height>, tx_index_to_height: &mut RangeMap<TxIndex, Height>,
mut entry_anchor: Cents,
cached_prices: &[Cents], cached_prices: &[Cents],
cached_timestamps: &[Timestamp], cached_timestamps: &[Timestamp],
cached_price_range_max: &PriceRangeMax, cached_price_range_max: &PriceRangeMax,
@@ -370,9 +371,14 @@ pub(crate) fn process_blocks(
.iterate(Sats::FIFTY_BTC, OutputType::P2PK65); .iterate(Sats::FIFTY_BTC, OutputType::P2PK65);
} }
let entry = EntryPrice::from_is_discount(
entry_anchor == Cents::ZERO || block_price <= entry_anchor,
);
// Push current block state before processing cohort updates // Push current block state before processing cohort updates
chain_state.push(BlockState { chain_state.push(BlockState {
supply: transacted.spendable_supply, supply: transacted.spendable_supply,
entry,
price: block_price, price: block_price,
timestamp, timestamp,
}); });
@@ -411,7 +417,7 @@ pub(crate) fn process_blocks(
|| { || {
// UTXO cohorts receive/send // UTXO cohorts receive/send
vecs.utxo_cohorts vecs.utxo_cohorts
.receive(transacted, height, timestamp, block_price); .receive(transacted, height, timestamp, block_price, entry);
if let Some(min_h) = if let Some(min_h) =
vecs.utxo_cohorts vecs.utxo_cohorts
.send(height_to_sent, chain_state, ctx.price_range_max) .send(height_to_sent, chain_state, ctx.price_range_max)
@@ -460,7 +466,7 @@ pub(crate) fn process_blocks(
let is_last_of_day = is_last_of_day[offset]; let is_last_of_day = is_last_of_day[offset];
let date_opt = is_last_of_day.then(|| Date::from(timestamp)); let date_opt = is_last_of_day.then(|| Date::from(timestamp));
push_cohort_states( entry_anchor = push_cohort_states(
&mut vecs.utxo_cohorts, &mut vecs.utxo_cohorts,
&mut vecs.addr_cohorts, &mut vecs.addr_cohorts,
height, height,
@@ -527,7 +533,7 @@ fn push_cohort_states(
addr_cohorts: &mut AddrCohorts, addr_cohorts: &mut AddrCohorts,
height: Height, height: Height,
height_price: Cents, height_price: Cents,
) { ) -> Cents {
// Phase 1: push + unrealized (no reset yet, states still needed for aggregation) // Phase 1: push + unrealized (no reset yet, states still needed for aggregation)
rayon::join( rayon::join(
|| { || {
@@ -545,7 +551,7 @@ fn push_cohort_states(
); );
// Phase 2: aggregate age_range states → push to overlapping cohorts // Phase 2: aggregate age_range states → push to overlapping cohorts
utxo_cohorts.push_overlapping(height_price); let all_capitalized_price = utxo_cohorts.push_overlapping(height_price);
// Phase 3: reset per-block values // Phase 3: reset per-block values
utxo_cohorts utxo_cohorts
@@ -554,4 +560,6 @@ fn push_cohort_states(
addr_cohorts addr_cohorts
.iter_separate_mut() .iter_separate_mut()
.for_each(|v| v.reset_single_iteration_values()); .for_each(|v| v.reset_single_iteration_values());
all_capitalized_price
} }
@@ -24,51 +24,60 @@ pub struct RecoveredState {
/// Returns Height::ZERO if any validation fails (triggers fresh start). /// Returns Height::ZERO if any validation fails (triggers fresh start).
pub(crate) fn recover_state( pub(crate) fn recover_state(
height: Height, height: Height,
chain_state_rollback: vecdb::Result<Stamp>, chain_state_rollback: Option<vecdb::Result<Stamp>>,
any_addr_indexes: &mut AnyAddrIndexesVecs, any_addr_indexes: &mut AnyAddrIndexesVecs,
addrs_data: &mut AddrsDataVecs, addrs_data: &mut AddrsDataVecs,
utxo_cohorts: &mut UTXOCohorts, utxo_cohorts: &mut UTXOCohorts,
addr_cohorts: &mut AddrCohorts, addr_cohorts: &mut AddrCohorts,
) -> Result<RecoveredState> { ) -> Result<RecoveredState> {
let stamp = Stamp::from(height); // `None`: clean resume, already at the checkpoint, nothing to undo.
// `Some`: reorg, undo state past the resume point.
let consistent_height = match chain_state_rollback {
None => height,
Some(chain_state_rollback) => {
let stamp = Stamp::from(height);
// Rollback address state vectors // Rollback address state vectors
let addr_indexes_rollback = any_addr_indexes.rollback_before(stamp); let addr_indexes_rollback = any_addr_indexes.rollback_before(stamp);
let addr_data_rollback = addrs_data.rollback_before(stamp); let addr_data_rollback = addrs_data.rollback_before(stamp);
// Verify rollback consistency - all must agree on the same height // Verify rollback consistency - all must agree on the same height
let consistent_height = rollback_states( let consistent_height = rollback_states(
chain_state_rollback, chain_state_rollback,
addr_indexes_rollback, addr_indexes_rollback,
addr_data_rollback, addr_data_rollback,
); );
// If rollbacks are inconsistent, start fresh // If rollbacks are inconsistent, start fresh
if consistent_height.is_zero() { if consistent_height.is_zero() {
warn!("Rollback consistency check failed: inconsistent heights"); warn!("Rollback consistency check failed: inconsistent heights");
return Ok(RecoveredState { return Ok(RecoveredState {
starting_height: Height::ZERO, starting_height: Height::ZERO,
}); });
} }
// Rollback can land at an earlier height (multi-block change file), which is fine. // Rollback can land at an earlier height (multi-block change file), which is fine.
// But if it lands AHEAD of target, that means rollback failed (missing change files). // But if it lands AHEAD of target, that means rollback failed (missing change files).
if consistent_height > height { if consistent_height > height {
warn!( warn!(
"Rollback failed: still at {} but target was {}, falling back to fresh start", "Rollback failed: still at {} but target was {}, falling back to fresh start",
consistent_height, height consistent_height, height
); );
return Ok(RecoveredState { return Ok(RecoveredState {
starting_height: Height::ZERO, starting_height: Height::ZERO,
}); });
} }
if consistent_height != height { if consistent_height != height {
debug!( debug!(
"Rollback landed at {} instead of {}, will resume from there", "Rollback landed at {} instead of {}, will resume from there",
consistent_height, height consistent_height, height
); );
} }
consistent_height
}
};
// Import UTXO cohort states - all must succeed // Import UTXO cohort states - all must succeed
debug!( debug!(
@@ -11,7 +11,7 @@ use crate::{
state::{CohortState, CostBasisOps, RealizedOps}, state::{CohortState, CostBasisOps, RealizedOps},
}, },
internal::{PerBlockCumulativeRolling, ValuePerBlockCumulativeRolling}, internal::{PerBlockCumulativeRolling, ValuePerBlockCumulativeRolling},
prices, price,
}; };
use super::ActivityMinimal; use super::ActivityMinimal;
@@ -98,7 +98,7 @@ impl ActivityCore {
pub(crate) fn compute_rest_part1( pub(crate) fn compute_rest_part1(
&mut self, &mut self,
prices: &prices::Vecs, prices: &price::Vecs,
starting_lengths: &Lengths, starting_lengths: &Lengths,
exit: &Exit, exit: &Exit,
) -> Result<()> { ) -> Result<()> {
@@ -12,7 +12,7 @@ use crate::{
metrics::ImportConfig, metrics::ImportConfig,
state::{CohortState, CostBasisOps, RealizedOps}, state::{CohortState, CostBasisOps, RealizedOps},
}, },
prices, price,
}; };
use super::ActivityCore; use super::ActivityCore;
@@ -89,7 +89,7 @@ impl ActivityFull {
pub(crate) fn compute_rest_part1( pub(crate) fn compute_rest_part1(
&mut self, &mut self,
prices: &prices::Vecs, prices: &price::Vecs,
starting_lengths: &Lengths, starting_lengths: &Lengths,
exit: &Exit, exit: &Exit,
) -> Result<()> { ) -> Result<()> {
@@ -10,7 +10,7 @@ use crate::{
state::{CohortState, CostBasisOps, RealizedOps}, state::{CohortState, CostBasisOps, RealizedOps},
}, },
internal::ValuePerBlockCumulativeRolling, internal::ValuePerBlockCumulativeRolling,
prices, price,
}; };
#[derive(Traversable)] #[derive(Traversable)]
@@ -63,7 +63,7 @@ impl ActivityMinimal {
pub(crate) fn compute_rest_part1( pub(crate) fn compute_rest_part1(
&mut self, &mut self,
prices: &prices::Vecs, prices: &price::Vecs,
starting_lengths: &Lengths, starting_lengths: &Lengths,
exit: &Exit, exit: &Exit,
) -> Result<()> { ) -> Result<()> {
@@ -13,7 +13,7 @@ use vecdb::Exit;
use crate::{ use crate::{
distribution::state::{CohortState, CostBasisOps, RealizedOps}, distribution::state::{CohortState, CostBasisOps, RealizedOps},
prices, price,
}; };
pub trait ActivityLike: Send + Sync { pub trait ActivityLike: Send + Sync {
@@ -30,7 +30,7 @@ pub trait ActivityLike: Send + Sync {
) -> Result<()>; ) -> Result<()>;
fn compute_rest_part1( fn compute_rest_part1(
&mut self, &mut self,
prices: &prices::Vecs, prices: &price::Vecs,
starting_lengths: &Lengths, starting_lengths: &Lengths,
exit: &Exit, exit: &Exit,
) -> Result<()>; ) -> Result<()>;
@@ -62,7 +62,7 @@ impl ActivityLike for ActivityCore {
} }
fn compute_rest_part1( fn compute_rest_part1(
&mut self, &mut self,
prices: &prices::Vecs, prices: &price::Vecs,
starting_lengths: &Lengths, starting_lengths: &Lengths,
exit: &Exit, exit: &Exit,
) -> Result<()> { ) -> Result<()> {
@@ -96,7 +96,7 @@ impl ActivityLike for ActivityFull {
} }
fn compute_rest_part1( fn compute_rest_part1(
&mut self, &mut self,
prices: &prices::Vecs, prices: &price::Vecs,
starting_lengths: &Lengths, starting_lengths: &Lengths,
exit: &Exit, exit: &Exit,
) -> Result<()> { ) -> Result<()> {
@@ -11,7 +11,7 @@ use crate::{
ActivityFull, AdjustedSopr, CohortMetricsBase, CostBasis, ImportConfig, OutputsBase, ActivityFull, AdjustedSopr, CohortMetricsBase, CostBasis, ImportConfig, OutputsBase,
RealizedFull, RelativeForAll, SupplyCore, UnrealizedFull, RealizedFull, RelativeForAll, SupplyCore, UnrealizedFull,
}, },
prices, price,
}; };
/// All-cohort metrics: extended realized + adjusted (as composable add-on), /// All-cohort metrics: extended realized + adjusted (as composable add-on),
@@ -100,7 +100,7 @@ impl AllCohortMetrics {
pub(crate) fn compute_rest_part2( pub(crate) fn compute_rest_part2(
&mut self, &mut self,
blocks: &blocks::Vecs, blocks: &blocks::Vecs,
prices: &prices::Vecs, prices: &price::Vecs,
starting_lengths: &Lengths, starting_lengths: &Lengths,
height_to_market_cap: &impl ReadableVec<Height, Dollars>, height_to_market_cap: &impl ReadableVec<Height, Dollars>,
under_1h_value_created: &impl ReadableVec<Height, Cents>, under_1h_value_created: &impl ReadableVec<Height, Cents>,
@@ -10,7 +10,7 @@ use crate::{
ActivityCore, CohortMetricsBase, ImportConfig, OutputsBase, RealizedCore, SupplyCore, ActivityCore, CohortMetricsBase, ImportConfig, OutputsBase, RealizedCore, SupplyCore,
UnrealizedCore, UnrealizedCore,
}, },
prices, price,
}; };
/// Basic cohort metrics: no extensions, used by age_range cohorts. /// Basic cohort metrics: no extensions, used by age_range cohorts.
@@ -61,7 +61,7 @@ impl BasicCohortMetrics {
pub(crate) fn compute_rest_part2( pub(crate) fn compute_rest_part2(
&mut self, &mut self,
prices: &prices::Vecs, prices: &price::Vecs,
starting_lengths: &Lengths, starting_lengths: &Lengths,
all_supply_sats: &impl ReadableVec<Height, Sats>, all_supply_sats: &impl ReadableVec<Height, Sats>,
all_utxo_count: &impl ReadableVec<Height, StoredU64>, all_utxo_count: &impl ReadableVec<Height, StoredU64>,
@@ -10,7 +10,7 @@ use crate::{
ActivityCore, CohortMetricsBase, ImportConfig, OutputsBase, RealizedCore, SupplyCore, ActivityCore, CohortMetricsBase, ImportConfig, OutputsBase, RealizedCore, SupplyCore,
UnrealizedCore, UnrealizedCore,
}, },
prices, price,
}; };
#[derive(Traversable)] #[derive(Traversable)]
@@ -102,7 +102,7 @@ impl CoreCohortMetrics {
pub(crate) fn compute_rest_part1( pub(crate) fn compute_rest_part1(
&mut self, &mut self,
prices: &prices::Vecs, prices: &price::Vecs,
starting_lengths: &Lengths, starting_lengths: &Lengths,
exit: &Exit, exit: &Exit,
) -> Result<()> { ) -> Result<()> {
@@ -122,7 +122,7 @@ impl CoreCohortMetrics {
pub(crate) fn compute_rest_part2( pub(crate) fn compute_rest_part2(
&mut self, &mut self,
prices: &prices::Vecs, prices: &price::Vecs,
starting_lengths: &Lengths, starting_lengths: &Lengths,
all_supply_sats: &impl ReadableVec<Height, Sats>, all_supply_sats: &impl ReadableVec<Height, Sats>,
all_utxo_count: &impl ReadableVec<Height, StoredU64>, all_utxo_count: &impl ReadableVec<Height, StoredU64>,
@@ -12,7 +12,7 @@ use crate::{
ActivityFull, CohortMetricsBase, CostBasis, ImportConfig, OutputsBase, RealizedFull, ActivityFull, CohortMetricsBase, CostBasis, ImportConfig, OutputsBase, RealizedFull,
RelativeWithExtended, SupplyCore, UnrealizedFull, RelativeWithExtended, SupplyCore, UnrealizedFull,
}, },
prices, price,
}; };
/// Cohort metrics with extended realized + extended cost basis (no adjusted). /// Cohort metrics with extended realized + extended cost basis (no adjusted).
@@ -90,7 +90,7 @@ impl ExtendedCohortMetrics {
pub(crate) fn compute_rest_part2( pub(crate) fn compute_rest_part2(
&mut self, &mut self,
blocks: &blocks::Vecs, blocks: &blocks::Vecs,
prices: &prices::Vecs, prices: &price::Vecs,
starting_lengths: &Lengths, starting_lengths: &Lengths,
height_to_market_cap: &impl ReadableVec<Height, Dollars>, height_to_market_cap: &impl ReadableVec<Height, Dollars>,
all_supply_sats: &impl ReadableVec<Height, Sats>, all_supply_sats: &impl ReadableVec<Height, Sats>,
@@ -10,7 +10,7 @@ use crate::{
distribution::metrics::{ distribution::metrics::{
ActivityFull, AdjustedSopr, CohortMetricsBase, ImportConfig, RealizedFull, UnrealizedFull, ActivityFull, AdjustedSopr, CohortMetricsBase, ImportConfig, RealizedFull, UnrealizedFull,
}, },
prices, price,
}; };
use super::ExtendedCohortMetrics; use super::ExtendedCohortMetrics;
@@ -62,7 +62,7 @@ impl ExtendedAdjustedCohortMetrics {
pub(crate) fn compute_rest_part2( pub(crate) fn compute_rest_part2(
&mut self, &mut self,
blocks: &blocks::Vecs, blocks: &blocks::Vecs,
prices: &prices::Vecs, prices: &price::Vecs,
starting_lengths: &Lengths, starting_lengths: &Lengths,
height_to_market_cap: &impl ReadableVec<Height, Dollars>, height_to_market_cap: &impl ReadableVec<Height, Dollars>,
under_1h_value_created: &impl ReadableVec<Height, Cents>, under_1h_value_created: &impl ReadableVec<Height, Cents>,
@@ -9,7 +9,7 @@ use crate::{
distribution::metrics::{ distribution::metrics::{
ActivityMinimal, ImportConfig, OutputsBase, RealizedMinimal, SupplyBase, UnrealizedMinimal, ActivityMinimal, ImportConfig, OutputsBase, RealizedMinimal, SupplyBase, UnrealizedMinimal,
}, },
prices, price,
}; };
/// MinimalCohortMetrics: supply, outputs, realized cap/price/mvrv/profit/loss + value_created/destroyed. /// MinimalCohortMetrics: supply, outputs, realized cap/price/mvrv/profit/loss + value_created/destroyed.
@@ -97,7 +97,7 @@ impl MinimalCohortMetrics {
pub(crate) fn compute_rest_part1( pub(crate) fn compute_rest_part1(
&mut self, &mut self,
prices: &prices::Vecs, prices: &price::Vecs,
starting_lengths: &Lengths, starting_lengths: &Lengths,
exit: &Exit, exit: &Exit,
) -> Result<()> { ) -> Result<()> {
@@ -111,7 +111,7 @@ impl MinimalCohortMetrics {
pub(crate) fn compute_rest_part2( pub(crate) fn compute_rest_part2(
&mut self, &mut self,
prices: &prices::Vecs, prices: &price::Vecs,
starting_lengths: &Lengths, starting_lengths: &Lengths,
all_supply_sats: &impl ReadableVec<Height, Sats>, all_supply_sats: &impl ReadableVec<Height, Sats>,
all_utxo_count: &impl ReadableVec<Height, StoredU64>, all_utxo_count: &impl ReadableVec<Height, StoredU64>,
@@ -9,7 +9,7 @@ use crate::{
distribution::metrics::{ distribution::metrics::{
ActivityMinimal, ImportConfig, OutputsBase, RealizedMinimal, SupplyCore, UnrealizedBasic, ActivityMinimal, ImportConfig, OutputsBase, RealizedMinimal, SupplyCore, UnrealizedBasic,
}, },
prices, price,
}; };
/// TypeCohortMetrics: supply(core), outputs(base), realized(minimal), unrealized(basic). /// TypeCohortMetrics: supply(core), outputs(base), realized(minimal), unrealized(basic).
@@ -59,7 +59,7 @@ impl TypeCohortMetrics {
pub(crate) fn compute_rest_part1( pub(crate) fn compute_rest_part1(
&mut self, &mut self,
prices: &prices::Vecs, prices: &price::Vecs,
starting_lengths: &Lengths, starting_lengths: &Lengths,
exit: &Exit, exit: &Exit,
) -> Result<()> { ) -> Result<()> {
@@ -73,7 +73,7 @@ impl TypeCohortMetrics {
pub(crate) fn compute_rest_part2( pub(crate) fn compute_rest_part2(
&mut self, &mut self,
prices: &prices::Vecs, prices: &price::Vecs,
starting_lengths: &Lengths, starting_lengths: &Lengths,
all_supply_sats: &impl ReadableVec<Height, Sats>, all_supply_sats: &impl ReadableVec<Height, Sats>,
all_utxo_count: &impl ReadableVec<Height, StoredU64>, all_utxo_count: &impl ReadableVec<Height, StoredU64>,
@@ -149,7 +149,7 @@ use crate::{
CohortState, CoreRealizedState, CostBasisData, CostBasisOps, CostBasisRaw, CohortState, CoreRealizedState, CostBasisData, CostBasisOps, CostBasisRaw,
MinimalRealizedState, RealizedOps, RealizedState, WithCapital, WithoutCapital, MinimalRealizedState, RealizedOps, RealizedState, WithCapital, WithoutCapital,
}, },
prices, price,
}; };
pub trait CohortMetricsState { pub trait CohortMetricsState {
@@ -270,7 +270,7 @@ pub trait CohortMetricsBase:
/// First phase of computed metrics (indexes from height). /// First phase of computed metrics (indexes from height).
fn compute_rest_part1( fn compute_rest_part1(
&mut self, &mut self,
prices: &prices::Vecs, prices: &price::Vecs,
starting_lengths: &Lengths, starting_lengths: &Lengths,
exit: &Exit, exit: &Exit,
) -> Result<()> { ) -> Result<()> {
@@ -10,7 +10,7 @@ use crate::{
internal::{ internal::{
PerBlock, RatioPerBlock, ValuePerBlock, ValuePerBlockWithDeltas, WindowStartVec, Windows, PerBlock, RatioPerBlock, ValuePerBlock, ValuePerBlockWithDeltas, WindowStartVec, Windows,
}, },
prices, price,
}; };
#[derive(Traversable)] #[derive(Traversable)]
@@ -115,7 +115,7 @@ impl ProfitabilityBucket {
pub(crate) fn compute( pub(crate) fn compute(
&mut self, &mut self,
prices: &prices::Vecs, prices: &price::Vecs,
starting_lengths: &Lengths, starting_lengths: &Lengths,
is_profit: bool, is_profit: bool,
exit: &Exit, exit: &Exit,
@@ -176,7 +176,7 @@ impl ProfitabilityBucket {
pub(crate) fn compute_from_ranges( pub(crate) fn compute_from_ranges(
&mut self, &mut self,
prices: &prices::Vecs, prices: &price::Vecs,
starting_lengths: &Lengths, starting_lengths: &Lengths,
is_profit: bool, is_profit: bool,
sources: &[&ProfitabilityBucket], sources: &[&ProfitabilityBucket],
@@ -293,7 +293,7 @@ impl ProfitabilityMetrics {
pub(crate) fn compute( pub(crate) fn compute(
&mut self, &mut self,
prices: &prices::Vecs, prices: &price::Vecs,
starting_lengths: &Lengths, starting_lengths: &Lengths,
exit: &Exit, exit: &Exit,
) -> Result<()> { ) -> Result<()> {
@@ -16,7 +16,7 @@ use crate::{
FiatPerBlockCumulativeWithSumsAndDeltas, LazyPerBlock, NegCentsUnsignedToDollars, FiatPerBlockCumulativeWithSumsAndDeltas, LazyPerBlock, NegCentsUnsignedToDollars,
PerBlockCumulativeRolling, RatioCents64, RollingWindow24hPerBlock, Windows, PerBlockCumulativeRolling, RatioCents64, RollingWindow24hPerBlock, Windows,
}, },
prices, price,
}; };
use crate::distribution::metrics::ImportConfig; use crate::distribution::metrics::ImportConfig;
@@ -166,7 +166,7 @@ impl RealizedCore {
pub(crate) fn compute_rest_part2( pub(crate) fn compute_rest_part2(
&mut self, &mut self,
prices: &prices::Vecs, prices: &price::Vecs,
starting_lengths: &Lengths, starting_lengths: &Lengths,
height_to_supply: &impl ReadableVec<Height, Bitcoin>, height_to_supply: &impl ReadableVec<Height, Bitcoin>,
transfer_volume_sum_24h_cents: &impl ReadableVec<Height, Cents>, transfer_volume_sum_24h_cents: &impl ReadableVec<Height, Cents>,
@@ -18,7 +18,7 @@ use crate::{
RatioPerBlockStdDevBands, RatioSma, RollingWindows, RollingWindowsFrom1w, RatioPerBlockStdDevBands, RatioSma, RollingWindows, RollingWindowsFrom1w,
ValuePerBlockCumulativeRolling, ValuePerBlockCumulativeRolling,
}, },
prices, price,
}; };
use crate::distribution::metrics::ImportConfig; use crate::distribution::metrics::ImportConfig;
@@ -206,7 +206,7 @@ impl RealizedFull {
} }
#[inline(always)] #[inline(always)]
pub(crate) fn push_accum(&mut self, accum: &RealizedFullAccum) { pub(crate) fn push_accum(&mut self, accum: &RealizedFullAccum) -> Cents {
self.cap_raw.push(accum.cap_raw); self.cap_raw.push(accum.cap_raw);
self.capitalized.cap_raw.push(accum.capitalized_cap_raw); self.capitalized.cap_raw.push(accum.capitalized_cap_raw);
@@ -221,6 +221,8 @@ impl RealizedFull {
self.capitalized.price.cents.height.push(capitalized_price); self.capitalized.price.cents.height.push(capitalized_price);
self.peak_regret.value.block.cents.push(accum.peak_regret()); self.peak_regret.value.block.cents.push(accum.peak_regret());
capitalized_price
} }
pub(crate) fn compute_rest_part1( pub(crate) fn compute_rest_part1(
@@ -240,7 +242,7 @@ impl RealizedFull {
pub(crate) fn compute_rest_part2( pub(crate) fn compute_rest_part2(
&mut self, &mut self,
blocks: &blocks::Vecs, blocks: &blocks::Vecs,
prices: &prices::Vecs, prices: &price::Vecs,
starting_lengths: &Lengths, starting_lengths: &Lengths,
height_to_supply: &impl ReadableVec<Height, Bitcoin>, height_to_supply: &impl ReadableVec<Height, Bitcoin>,
height_to_market_cap: &impl ReadableVec<Height, Dollars>, height_to_market_cap: &impl ReadableVec<Height, Dollars>,
@@ -13,7 +13,7 @@ use crate::{
FiatPerBlockCumulativeWithSums, FiatPerBlockWithDeltas, Identity, LazyPerBlock, FiatPerBlockCumulativeWithSums, FiatPerBlockWithDeltas, Identity, LazyPerBlock,
PriceWithRatioPerBlock, PriceWithRatioPerBlock,
}, },
prices, price,
}; };
use crate::distribution::metrics::ImportConfig; use crate::distribution::metrics::ImportConfig;
@@ -104,7 +104,7 @@ impl RealizedMinimal {
pub(crate) fn compute_rest_part2( pub(crate) fn compute_rest_part2(
&mut self, &mut self,
prices: &prices::Vecs, prices: &price::Vecs,
starting_lengths: &Lengths, starting_lengths: &Lengths,
height_to_supply: &impl ReadableVec<Height, Bitcoin>, height_to_supply: &impl ReadableVec<Height, Bitcoin>,
exit: &Exit, exit: &Exit,
@@ -3,7 +3,7 @@ use brk_traversable::Traversable;
use brk_types::{Height, Sats, StoredU64, Version}; use brk_types::{Height, Sats, StoredU64, Version};
use vecdb::{AnyStoredVec, Database, Exit, ReadableVec, Rw, StorageMode, WritableVec}; use vecdb::{AnyStoredVec, Database, Exit, ReadableVec, Rw, StorageMode, WritableVec};
use crate::{indexes, internal::ValuePerBlock, prices}; use crate::{indexes, internal::ValuePerBlock, price};
/// Average amount held per UTXO and per funded address. /// Average amount held per UTXO and per funded address.
/// ///
@@ -53,7 +53,7 @@ impl AvgAmountMetrics {
pub(crate) fn compute( pub(crate) fn compute(
&mut self, &mut self,
prices: &prices::Vecs, prices: &price::Vecs,
supply_sats: &impl ReadableVec<Height, Sats>, supply_sats: &impl ReadableVec<Height, Sats>,
utxo_count: &impl ReadableVec<Height, StoredU64>, utxo_count: &impl ReadableVec<Height, StoredU64>,
funded_addr_count: &impl ReadableVec<Height, StoredU64>, funded_addr_count: &impl ReadableVec<Height, StoredU64>,
@@ -6,7 +6,7 @@ use vecdb::{AnyStoredVec, AnyVec, Exit, ReadableVec, Rw, StorageMode, WritableVe
use crate::{ use crate::{
distribution::state::{CohortState, CostBasisOps, RealizedOps}, distribution::state::{CohortState, CostBasisOps, RealizedOps},
prices, price,
}; };
use crate::internal::{ use crate::internal::{
@@ -64,7 +64,7 @@ impl SupplyBase {
pub(crate) fn compute( pub(crate) fn compute(
&mut self, &mut self,
prices: &prices::Vecs, prices: &price::Vecs,
max_from: Height, max_from: Height,
exit: &Exit, exit: &Exit,
) -> Result<()> { ) -> Result<()> {
@@ -5,7 +5,7 @@ use brk_types::{Height, Version};
use derive_more::{Deref, DerefMut}; use derive_more::{Deref, DerefMut};
use vecdb::{AnyStoredVec, AnyVec, Exit, Rw, StorageMode, WritableVec}; use vecdb::{AnyStoredVec, AnyVec, Exit, Rw, StorageMode, WritableVec};
use crate::{distribution::state::UnrealizedState, prices}; use crate::{distribution::state::UnrealizedState, price};
use crate::internal::{ use crate::internal::{
HalveCents, HalveDollars, HalveSats, HalveSatsToBitcoin, LazyValuePerBlock, ValuePerBlock, HalveCents, HalveDollars, HalveSats, HalveSatsToBitcoin, LazyValuePerBlock, ValuePerBlock,
@@ -72,7 +72,7 @@ impl SupplyCore {
pub(crate) fn compute( pub(crate) fn compute(
&mut self, &mut self,
prices: &prices::Vecs, prices: &price::Vecs,
max_from: Height, max_from: Height,
exit: &Exit, exit: &Exit,
) -> Result<()> { ) -> Result<()> {
@@ -7,7 +7,7 @@ use vecdb::{AnyStoredVec, AnyVec, BytesVec, Exit, ReadableVec, Rw, StorageMode,
use crate::distribution::state::UnrealizedState; use crate::distribution::state::UnrealizedState;
use crate::internal::{CentsSubtractToCentsSigned, FiatPerBlock}; use crate::internal::{CentsSubtractToCentsSigned, FiatPerBlock};
use crate::{distribution::metrics::ImportConfig, prices}; use crate::{distribution::metrics::ImportConfig, price};
use super::UnrealizedCore; use super::UnrealizedCore;
@@ -99,7 +99,7 @@ impl UnrealizedFull {
pub(crate) fn compute_rest_all( pub(crate) fn compute_rest_all(
&mut self, &mut self,
prices: &prices::Vecs, prices: &price::Vecs,
starting_lengths: &Lengths, starting_lengths: &Lengths,
supply_in_profit_sats: &(impl ReadableVec<Height, Sats> + Sync), supply_in_profit_sats: &(impl ReadableVec<Height, Sats> + Sync),
supply_in_loss_sats: &(impl ReadableVec<Height, Sats> + Sync), supply_in_loss_sats: &(impl ReadableVec<Height, Sats> + Sync),
@@ -13,7 +13,7 @@ use brk_indexer::Lengths;
use brk_types::{Height, Sats}; use brk_types::{Height, Sats};
use vecdb::{Exit, ReadableVec}; use vecdb::{Exit, ReadableVec};
use crate::{distribution::state::UnrealizedState, prices}; use crate::{distribution::state::UnrealizedState, price};
pub trait UnrealizedLike: Send + Sync { pub trait UnrealizedLike: Send + Sync {
fn as_core(&self) -> &UnrealizedCore; fn as_core(&self) -> &UnrealizedCore;
@@ -22,7 +22,7 @@ pub trait UnrealizedLike: Send + Sync {
fn push_state(&mut self, state: &UnrealizedState); fn push_state(&mut self, state: &UnrealizedState);
fn compute_rest( fn compute_rest(
&mut self, &mut self,
prices: &prices::Vecs, prices: &price::Vecs,
starting_lengths: &Lengths, starting_lengths: &Lengths,
supply_in_profit_sats: &(impl ReadableVec<Height, Sats> + Sync), supply_in_profit_sats: &(impl ReadableVec<Height, Sats> + Sync),
supply_in_loss_sats: &(impl ReadableVec<Height, Sats> + Sync), supply_in_loss_sats: &(impl ReadableVec<Height, Sats> + Sync),
@@ -46,7 +46,7 @@ impl UnrealizedLike for UnrealizedCore {
} }
fn compute_rest( fn compute_rest(
&mut self, &mut self,
_prices: &prices::Vecs, _prices: &price::Vecs,
starting_lengths: &Lengths, starting_lengths: &Lengths,
_supply_in_profit_sats: &(impl ReadableVec<Height, Sats> + Sync), _supply_in_profit_sats: &(impl ReadableVec<Height, Sats> + Sync),
_supply_in_loss_sats: &(impl ReadableVec<Height, Sats> + Sync), _supply_in_loss_sats: &(impl ReadableVec<Height, Sats> + Sync),
@@ -72,7 +72,7 @@ impl UnrealizedLike for UnrealizedFull {
} }
fn compute_rest( fn compute_rest(
&mut self, &mut self,
prices: &prices::Vecs, prices: &price::Vecs,
starting_lengths: &Lengths, starting_lengths: &Lengths,
supply_in_profit_sats: &(impl ReadableVec<Height, Sats> + Sync), supply_in_profit_sats: &(impl ReadableVec<Height, Sats> + Sync),
supply_in_loss_sats: &(impl ReadableVec<Height, Sats> + Sync), supply_in_loss_sats: &(impl ReadableVec<Height, Sats> + Sync),
@@ -1,5 +1,6 @@
use std::ops::{Add, AddAssign, SubAssign}; use std::ops::{Add, AddAssign, SubAssign};
use brk_cohort::EntryPrice;
use brk_types::{Cents, SupplyState, Timestamp}; use brk_types::{Cents, SupplyState, Timestamp};
use serde::Serialize; use serde::Serialize;
@@ -8,6 +9,8 @@ pub struct BlockState {
#[serde(flatten)] #[serde(flatten)]
pub supply: SupplyState, pub supply: SupplyState,
#[serde(skip)] #[serde(skip)]
pub entry: EntryPrice,
#[serde(skip)]
pub price: Cents, pub price: Cents,
#[serde(skip)] #[serde(skip)]
pub timestamp: Timestamp, pub timestamp: Timestamp,
+49 -12
View File
@@ -1,6 +1,6 @@
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use brk_cohort::{ByAddrType, Filter}; use brk_cohort::{ByAddrType, EntryPrice, Filter};
use brk_error::Result; use brk_error::Result;
use brk_indexer::Indexer; use brk_indexer::Indexer;
use brk_traversable::Traversable; use brk_traversable::Traversable;
@@ -29,7 +29,7 @@ use crate::{
PerBlockCumulativeRolling, WindowStartVec, Windows, WithAddrTypes, PerBlockCumulativeRolling, WindowStartVec, Windows, WithAddrTypes,
db_utils::{finalize_db, open_db}, db_utils::{finalize_db, open_db},
}, },
outputs, prices, transactions, outputs, price, transactions,
}; };
use super::{ use super::{
@@ -316,7 +316,7 @@ impl Vecs {
outputs: &outputs::Vecs, outputs: &outputs::Vecs,
transactions: &transactions::Vecs, transactions: &transactions::Vecs,
blocks: &blocks::Vecs, blocks: &blocks::Vecs,
prices: &prices::Vecs, prices: &price::Vecs,
exit: &Exit, exit: &Exit,
) -> Result<()> { ) -> Result<()> {
self.db.sync_bg_tasks()?; self.db.sync_bg_tasks()?;
@@ -341,12 +341,13 @@ impl Vecs {
// Try to resume from checkpoint, fall back to fresh start if needed // Try to resume from checkpoint, fall back to fresh start if needed
let recovered_height = match start_mode { let recovered_height = match start_mode {
StartMode::Resume(height) => { StartMode::Resume(height) => {
let stamp = Stamp::from(height); // Roll back only on a reorg. A clean resume has nothing to undo, and an
// interrupted run wrote no rollback metadata (periodic flushes use
// with_changes=false; only the final write creates the `changes/` dir),
// so `rollback_before` would fail with `NotFound`.
let chain_state_rollback = (height < current_height)
.then(|| self.supply_state.rollback_before(Stamp::from(height)));
// Rollback BytesVec state and capture results for validation
let chain_state_rollback = self.supply_state.rollback_before(stamp);
// Validate all rollbacks and imports are consistent
let recovered = recover_state( let recovered = recover_state(
height, height,
chain_state_rollback, chain_state_rollback,
@@ -435,13 +436,34 @@ impl Vecs {
let end = usize::from(recovered_height); let end = usize::from(recovered_height);
debug!("building supply_state vec for {} heights", recovered_height); debug!("building supply_state vec for {} heights", recovered_height);
let supply_state_data: Vec<_> = self.supply_state.collect_range_at(0, end); let supply_state_data: Vec<_> = self.supply_state.collect_range_at(0, end);
let capitalized_price_data: Vec<_> = self
.utxo_cohorts
.all
.metrics
.realized
.capitalized
.price
.cents
.height
.collect_range_at(0, end);
let mut entry_anchor = Cents::ZERO;
chain_state = supply_state_data chain_state = supply_state_data
.into_iter() .into_iter()
.enumerate() .enumerate()
.map(|(h, supply)| BlockState { .map(|(h, supply)| {
supply, let price = self.caches.prices[h];
price: self.caches.prices[h], let entry = EntryPrice::from_is_discount(
timestamp: self.caches.timestamps[h], entry_anchor == Cents::ZERO || price <= entry_anchor,
);
entry_anchor = capitalized_price_data[h];
BlockState {
supply,
entry,
price,
timestamp: self.caches.timestamps[h],
}
}) })
.collect(); .collect();
debug!("chain_state rebuilt"); debug!("chain_state rebuilt");
@@ -473,6 +495,20 @@ impl Vecs {
let prices = std::mem::take(&mut self.caches.prices); let prices = std::mem::take(&mut self.caches.prices);
let timestamps = std::mem::take(&mut self.caches.timestamps); let timestamps = std::mem::take(&mut self.caches.timestamps);
let price_range_max = std::mem::take(&mut self.caches.price_range_max); let price_range_max = std::mem::take(&mut self.caches.price_range_max);
let entry_anchor = starting_height
.decremented()
.and_then(|height| {
self.utxo_cohorts
.all
.metrics
.realized
.capitalized
.price
.cents
.height
.collect_one(height)
})
.unwrap_or(Cents::ZERO);
process_blocks( process_blocks(
self, self,
@@ -485,6 +521,7 @@ impl Vecs {
last_height, last_height,
&mut chain_state, &mut chain_state,
&mut tx_index_to_height, &mut tx_index_to_height,
entry_anchor,
&prices, &prices,
&timestamps, &timestamps,
&price_range_max, &price_range_max,
@@ -6,7 +6,7 @@ use brk_traversable::Traversable;
use brk_types::Version; use brk_types::Version;
use vecdb::{Database, Exit, Rw, StorageMode}; use vecdb::{Database, Exit, Rw, StorageMode};
use crate::{distribution, indexes, prices}; use crate::{distribution, indexes, price};
pub use inner::RarityMeterInner; pub use inner::RarityMeterInner;
@@ -37,7 +37,7 @@ impl RarityMeter {
&mut self, &mut self,
indexer: &Indexer, indexer: &Indexer,
distribution: &distribution::Vecs, distribution: &distribution::Vecs,
prices: &prices::Vecs, prices: &price::Vecs,
exit: &Exit, exit: &Exit,
) -> Result<()> { ) -> Result<()> {
let realized = &distribution.utxo_cohorts.all.metrics.realized; let realized = &distribution.utxo_cohorts.all.metrics.realized;
@@ -6,7 +6,7 @@ use derive_more::{Deref, DerefMut};
use vecdb::{Database, EagerVec, Exit, PcoVec, ReadableVec, Rw, StorageMode}; use vecdb::{Database, EagerVec, Exit, PcoVec, ReadableVec, Rw, StorageMode};
use crate::internal::{LazyPerBlock, PerBlock, Price}; use crate::internal::{LazyPerBlock, PerBlock, Price};
use crate::{indexes, prices}; use crate::{indexes, price};
use super::{RatioPerBlock, RatioPerBlockPercentiles}; use super::{RatioPerBlock, RatioPerBlockPercentiles};
@@ -63,7 +63,7 @@ impl PriceWithRatioPerBlock {
/// Compute price via closure (in cents), then compute ratio. /// Compute price via closure (in cents), then compute ratio.
pub(crate) fn compute_all<F>( pub(crate) fn compute_all<F>(
&mut self, &mut self,
prices: &prices::Vecs, prices: &price::Vecs,
starting_lengths: &Lengths, starting_lengths: &Lengths,
exit: &Exit, exit: &Exit,
mut compute_price: F, mut compute_price: F,
@@ -101,7 +101,7 @@ impl PriceWithRatioExtendedPerBlock {
/// Compute ratio and percentiles from already-computed price cents. /// Compute ratio and percentiles from already-computed price cents.
pub(crate) fn compute_rest( pub(crate) fn compute_rest(
&mut self, &mut self,
prices: &prices::Vecs, prices: &price::Vecs,
starting_lengths: &Lengths, starting_lengths: &Lengths,
exit: &Exit, exit: &Exit,
) -> Result<()> { ) -> Result<()> {
@@ -120,7 +120,7 @@ impl PriceWithRatioExtendedPerBlock {
/// Compute price via closure (in cents), then compute ratio and percentiles. /// Compute price via closure (in cents), then compute ratio and percentiles.
pub(crate) fn compute_all<F>( pub(crate) fn compute_all<F>(
&mut self, &mut self,
prices: &prices::Vecs, prices: &price::Vecs,
starting_lengths: &Lengths, starting_lengths: &Lengths,
exit: &Exit, exit: &Exit,
mut compute_price: F, mut compute_price: F,
@@ -10,7 +10,7 @@ use crate::{
CentsUnsignedToDollars, LazyPerBlock, NumericValue, PerBlock, SatsSignedToBitcoin, CentsUnsignedToDollars, LazyPerBlock, NumericValue, PerBlock, SatsSignedToBitcoin,
SatsToBitcoin, SatsToCents, SatsToBitcoin, SatsToCents,
}, },
prices, price,
}; };
/// Trait that associates a sats type with its transform to Bitcoin. /// Trait that associates a sats type with its transform to Bitcoin.
@@ -69,7 +69,7 @@ impl ValuePerBlock {
pub(crate) fn compute( pub(crate) fn compute(
&mut self, &mut self,
prices: &prices::Vecs, prices: &price::Vecs,
max_from: Height, max_from: Height,
exit: &Exit, exit: &Exit,
) -> Result<()> { ) -> Result<()> {
@@ -8,7 +8,7 @@ use vecdb::{
use crate::{ use crate::{
internal::{CentsUnsignedToDollars, SatsToBitcoin, SatsToCents}, internal::{CentsUnsignedToDollars, SatsToBitcoin, SatsToCents},
prices, price,
}; };
/// Raw per-block amount data: sats + cents (stored), btc + usd (lazy), no resolutions. /// Raw per-block amount data: sats + cents (stored), btc + usd (lazy), no resolutions.
@@ -44,7 +44,7 @@ impl ValueBlock {
pub(crate) fn compute_cents( pub(crate) fn compute_cents(
&mut self, &mut self,
max_from: Height, max_from: Height,
prices: &prices::Vecs, prices: &price::Vecs,
exit: &Exit, exit: &Exit,
) -> Result<()> { ) -> Result<()> {
self.cents.compute_binary::<Sats, Cents, SatsToCents>( self.cents.compute_binary::<Sats, Cents, SatsToCents>(
@@ -6,7 +6,7 @@ use vecdb::{Database, EagerVec, Exit, PcoVec, Rw, StorageMode};
use crate::{ use crate::{
indexes, indexes,
internal::{ValueBlock, ValuePerBlock}, internal::{ValueBlock, ValuePerBlock},
prices, price,
}; };
#[derive(Traversable)] #[derive(Traversable)]
@@ -39,7 +39,7 @@ impl ValuePerBlockCumulative {
pub(crate) fn compute( pub(crate) fn compute(
&mut self, &mut self,
prices: &prices::Vecs, prices: &price::Vecs,
max_from: Height, max_from: Height,
exit: &Exit, exit: &Exit,
) -> Result<()> { ) -> Result<()> {
@@ -61,7 +61,7 @@ impl ValuePerBlockCumulative {
pub(crate) fn compute_with( pub(crate) fn compute_with(
&mut self, &mut self,
max_from: Height, max_from: Height,
prices: &prices::Vecs, prices: &price::Vecs,
exit: &Exit, exit: &Exit,
compute_sats: impl FnOnce(&mut EagerVec<PcoVec<Height, Sats>>) -> Result<()>, compute_sats: impl FnOnce(&mut EagerVec<PcoVec<Height, Sats>>) -> Result<()>,
) -> Result<()> { ) -> Result<()> {
@@ -10,7 +10,7 @@ use crate::{
LazyRollingAvgsAmountFromHeight, LazyRollingSumsAmountFromHeight, ValuePerBlockCumulative, LazyRollingAvgsAmountFromHeight, LazyRollingSumsAmountFromHeight, ValuePerBlockCumulative,
WindowStartVec, Windows, WindowStartVec, Windows,
}, },
prices, price,
}; };
#[derive(Deref, DerefMut, Traversable)] #[derive(Deref, DerefMut, Traversable)]
@@ -63,7 +63,7 @@ impl ValuePerBlockCumulativeRolling {
pub(crate) fn compute( pub(crate) fn compute(
&mut self, &mut self,
max_from: Height, max_from: Height,
prices: &prices::Vecs, prices: &price::Vecs,
exit: &Exit, exit: &Exit,
compute_sats: impl FnOnce(&mut EagerVec<PcoVec<Height, Sats>>) -> Result<()>, compute_sats: impl FnOnce(&mut EagerVec<PcoVec<Height, Sats>>) -> Result<()>,
) -> Result<()> { ) -> Result<()> {
@@ -74,7 +74,7 @@ impl ValuePerBlockCumulativeRolling {
pub(crate) fn compute_rest( pub(crate) fn compute_rest(
&mut self, &mut self,
max_from: Height, max_from: Height,
prices: &prices::Vecs, prices: &price::Vecs,
exit: &Exit, exit: &Exit,
) -> Result<()> { ) -> Result<()> {
self.inner.compute(prices, max_from, exit) self.inner.compute(prices, max_from, exit)
@@ -10,7 +10,7 @@ use crate::{
RollingDistributionValuePerBlock, ValuePerBlockCumulativeRolling, WindowStartVec, RollingDistributionValuePerBlock, ValuePerBlockCumulativeRolling, WindowStartVec,
WindowStarts, Windows, WindowStarts, Windows,
}, },
prices, price,
}; };
#[derive(Deref, DerefMut, Traversable)] #[derive(Deref, DerefMut, Traversable)]
@@ -49,7 +49,7 @@ impl ValuePerBlockFull {
&mut self, &mut self,
max_from: Height, max_from: Height,
windows: &WindowStarts<'_>, windows: &WindowStarts<'_>,
prices: &prices::Vecs, prices: &price::Vecs,
exit: &Exit, exit: &Exit,
compute_sats: impl FnOnce(&mut EagerVec<PcoVec<Height, Sats>>) -> Result<()>, compute_sats: impl FnOnce(&mut EagerVec<PcoVec<Height, Sats>>) -> Result<()>,
) -> Result<()> { ) -> Result<()> {
@@ -11,7 +11,7 @@ use rayon::prelude::*;
use schemars::JsonSchema; use schemars::JsonSchema;
use vecdb::{AnyStoredVec, AnyVec, Database, EagerVec, Exit, PcoVec, WritableVec}; use vecdb::{AnyStoredVec, AnyVec, Database, EagerVec, Exit, PcoVec, WritableVec};
use crate::{indexes, prices}; use crate::{indexes, price};
use super::{ use super::{
BpsType, NumericValue, PerBlock, PerBlockCumulativeRolling, PercentPerBlock, ValuePerBlock, BpsType, NumericValue, PerBlock, PerBlockCumulativeRolling, PercentPerBlock, ValuePerBlock,
@@ -229,7 +229,7 @@ impl WithAddrTypes<ValuePerBlock> {
pub(crate) fn compute_rest( pub(crate) fn compute_rest(
&mut self, &mut self,
max_from: Height, max_from: Height,
prices: &prices::Vecs, prices: &price::Vecs,
exit: &Exit, exit: &Exit,
) -> Result<()> { ) -> Result<()> {
self.all.compute(prices, max_from, exit)?; self.all.compute(prices, max_from, exit)?;
+2 -2
View File
@@ -4,7 +4,7 @@ use brk_types::{BasisPointsSigned32, Bitcoin, Cents, Date, Day1, Dollars, Sats};
use vecdb::{AnyVec, Exit, ReadableOptionVec, ReadableVec, VecIndex}; use vecdb::{AnyVec, Exit, ReadableOptionVec, ReadableVec, VecIndex};
use super::{ByDcaPeriod, Vecs}; use super::{ByDcaPeriod, Vecs};
use crate::{blocks, indexes, internal::RatioDiffCentsBps32, market, prices}; use crate::{blocks, indexes, internal::RatioDiffCentsBps32, market, price};
const DCA_AMOUNT: Dollars = Dollars::mint(100.0); const DCA_AMOUNT: Dollars = Dollars::mint(100.0);
@@ -13,7 +13,7 @@ impl Vecs {
&mut self, &mut self,
indexer: &Indexer, indexer: &Indexer,
indexes: &indexes::Vecs, indexes: &indexes::Vecs,
prices: &prices::Vecs, prices: &price::Vecs,
blocks: &blocks::Vecs, blocks: &blocks::Vecs,
lookback: &market::lookback::Vecs, lookback: &market::lookback::Vecs,
exit: &Exit, exit: &Exit,
+20 -20
View File
@@ -22,7 +22,7 @@ mod market;
mod mining; mod mining;
mod outputs; mod outputs;
mod pools; mod pools;
pub mod prices; pub mod price;
mod supply; mod supply;
mod transactions; mod transactions;
@@ -38,7 +38,7 @@ pub struct Computer<M: StorageMode = Rw> {
pub investing: Box<investing::Vecs<M>>, pub investing: Box<investing::Vecs<M>>,
pub market: Box<market::Vecs<M>>, pub market: Box<market::Vecs<M>>,
pub pools: Box<pools::Vecs<M>>, pub pools: Box<pools::Vecs<M>>,
pub prices: Box<prices::Vecs<M>>, pub price: Box<price::Vecs<M>>,
#[traversable(flatten)] #[traversable(flatten)]
pub distribution: Box<distribution::Vecs<M>>, pub distribution: Box<distribution::Vecs<M>>,
pub supply: Box<supply::Vecs<M>>, pub supply: Box<supply::Vecs<M>>,
@@ -66,14 +66,14 @@ impl Computer {
)?)) )?))
})?; })?;
let (constants, prices) = timed("Imported prices/constants", || -> Result<_> { let (constants, price) = timed("Imported price/constants", || -> Result<_> {
let constants = Box::new(constants::Vecs::new(VERSION, &indexes)); let constants = Box::new(constants::Vecs::new(VERSION, &indexes));
let prices = Box::new(prices::Vecs::forced_import( let price = Box::new(price::Vecs::forced_import(
&computed_path, &computed_path,
VERSION, VERSION,
&indexes, &indexes,
)?); )?);
Ok((constants, prices)) Ok((constants, price))
})?; })?;
let blocks = timed("Imported blocks", || -> Result<_> { let blocks = timed("Imported blocks", || -> Result<_> {
@@ -223,7 +223,7 @@ impl Computer {
cointime, cointime,
indexes, indexes,
inputs, inputs,
prices, price,
outputs, outputs,
}; };
@@ -244,7 +244,7 @@ impl Computer {
investing::DB_NAME, investing::DB_NAME,
market::DB_NAME, market::DB_NAME,
pools::DB_NAME, pools::DB_NAME,
prices::DB_NAME, price::DB_NAME,
distribution::DB_NAME, distribution::DB_NAME,
supply::DB_NAME, supply::DB_NAME,
inputs::DB_NAME, inputs::DB_NAME,
@@ -297,8 +297,8 @@ impl Computer {
}) })
}, },
|| { || {
timed("Computed prices", || { timed("Computed price", || {
self.prices.compute(indexer, &self.indexes, exit) self.price.compute(indexer, &self.indexes, exit)
}) })
}, },
); );
@@ -310,7 +310,7 @@ impl Computer {
let market = scope.spawn(|| { let market = scope.spawn(|| {
timed("Computed market", || { timed("Computed market", || {
self.market self.market
.compute(indexer, &self.prices, &self.indexes, &self.blocks, exit) .compute(indexer, &self.price, &self.indexes, &self.blocks, exit)
}) })
}); });
@@ -321,7 +321,7 @@ impl Computer {
&self.indexes, &self.indexes,
&self.blocks, &self.blocks,
&self.inputs, &self.inputs,
&self.prices, &self.price,
exit, exit,
) )
})?; })?;
@@ -331,7 +331,7 @@ impl Computer {
&self.indexes, &self.indexes,
&self.blocks, &self.blocks,
&self.transactions, &self.transactions,
&self.prices, &self.price,
exit, exit,
) )
}) })
@@ -343,7 +343,7 @@ impl Computer {
&self.indexes, &self.indexes,
&self.inputs, &self.inputs,
&self.blocks, &self.blocks,
&self.prices, &self.price,
exit, exit,
) )
})?; })?;
@@ -360,7 +360,7 @@ impl Computer {
indexer, indexer,
&self.indexes, &self.indexes,
&self.blocks, &self.blocks,
&self.prices, &self.price,
&self.mining, &self.mining,
exit, exit,
) )
@@ -372,7 +372,7 @@ impl Computer {
self.investing.compute( self.investing.compute(
indexer, indexer,
&self.indexes, &self.indexes,
&self.prices, &self.price,
&self.blocks, &self.blocks,
&self.market.lookback, &self.market.lookback,
exit, exit,
@@ -388,7 +388,7 @@ impl Computer {
&self.outputs, &self.outputs,
&self.transactions, &self.transactions,
&self.blocks, &self.blocks,
&self.prices, &self.price,
exit, exit,
) )
})?; })?;
@@ -421,7 +421,7 @@ impl Computer {
&self.blocks, &self.blocks,
&self.mining, &self.mining,
&self.transactions, &self.transactions,
&self.prices, &self.price,
&self.distribution, &self.distribution,
exit, exit,
) )
@@ -430,7 +430,7 @@ impl Computer {
timed("Computed cointime", || { timed("Computed cointime", || {
self.cointime.compute( self.cointime.compute(
indexer, indexer,
&self.prices, &self.price,
&self.blocks, &self.blocks,
&self.mining, &self.mining,
&self.supply, &self.supply,
@@ -445,7 +445,7 @@ impl Computer {
self.indicators self.indicators
.rarity_meter .rarity_meter
.compute(indexer, &self.distribution, &self.prices, exit)?; .compute(indexer, &self.distribution, &self.price, exit)?;
info!("Total compute time: {:?}", compute_start.elapsed()); info!("Total compute time: {:?}", compute_start.elapsed());
Ok(()) Ok(())
@@ -498,7 +498,7 @@ impl_iter_named!(
investing, investing,
market, market,
pools, pools,
prices, price,
distribution, distribution,
supply, supply,
inputs, inputs,
@@ -4,13 +4,13 @@ use brk_types::{StoredF32, Timestamp};
use vecdb::{Exit, ReadableVec, VecIndex}; use vecdb::{Exit, ReadableVec, VecIndex};
use super::Vecs; use super::Vecs;
use crate::{indexes, prices}; use crate::{indexes, price};
impl Vecs { impl Vecs {
pub(crate) fn compute( pub(crate) fn compute(
&mut self, &mut self,
indexer: &Indexer, indexer: &Indexer,
prices: &prices::Vecs, prices: &price::Vecs,
indexes: &indexes::Vecs, indexes: &indexes::Vecs,
exit: &Exit, exit: &Exit,
) -> Result<()> { ) -> Result<()> {
+2 -2
View File
@@ -2,7 +2,7 @@ use brk_error::Result;
use brk_indexer::Indexer; use brk_indexer::Indexer;
use vecdb::Exit; use vecdb::Exit;
use crate::{blocks, indexes, prices}; use crate::{blocks, indexes, price};
use super::Vecs; use super::Vecs;
@@ -10,7 +10,7 @@ impl Vecs {
pub(crate) fn compute( pub(crate) fn compute(
&mut self, &mut self,
indexer: &Indexer, indexer: &Indexer,
prices: &prices::Vecs, prices: &price::Vecs,
indexes: &indexes::Vecs, indexes: &indexes::Vecs,
blocks: &blocks::Vecs, blocks: &blocks::Vecs,
exit: &Exit, exit: &Exit,
@@ -3,14 +3,14 @@ use brk_indexer::Indexer;
use vecdb::Exit; use vecdb::Exit;
use super::Vecs; use super::Vecs;
use crate::{blocks, prices}; use crate::{blocks, price};
impl Vecs { impl Vecs {
pub(crate) fn compute( pub(crate) fn compute(
&mut self, &mut self,
indexer: &Indexer, indexer: &Indexer,
blocks: &blocks::Vecs, blocks: &blocks::Vecs,
prices: &prices::Vecs, prices: &price::Vecs,
exit: &Exit, exit: &Exit,
) -> Result<()> { ) -> Result<()> {
let starting_height = indexer.safe_lengths().height; let starting_height = indexer.safe_lengths().height;
@@ -3,14 +3,14 @@ use brk_indexer::Indexer;
use vecdb::Exit; use vecdb::Exit;
use super::Vecs; use super::Vecs;
use crate::{blocks, prices}; use crate::{blocks, price};
impl Vecs { impl Vecs {
pub(crate) fn compute( pub(crate) fn compute(
&mut self, &mut self,
indexer: &Indexer, indexer: &Indexer,
blocks: &blocks::Vecs, blocks: &blocks::Vecs,
prices: &prices::Vecs, prices: &price::Vecs,
exit: &Exit, exit: &Exit,
) -> Result<()> { ) -> Result<()> {
let starting_lengths = indexer.safe_lengths(); let starting_lengths = indexer.safe_lengths();
@@ -4,13 +4,13 @@ use brk_types::{BasisPoints16, StoredF32};
use vecdb::{Exit, ReadableVec, VecIndex}; use vecdb::{Exit, ReadableVec, VecIndex};
use super::Vecs; use super::Vecs;
use crate::{blocks, prices}; use crate::{blocks, price};
impl Vecs { impl Vecs {
pub(crate) fn compute( pub(crate) fn compute(
&mut self, &mut self,
indexer: &Indexer, indexer: &Indexer,
prices: &prices::Vecs, prices: &price::Vecs,
blocks: &blocks::Vecs, blocks: &blocks::Vecs,
exit: &Exit, exit: &Exit,
) -> Result<()> { ) -> Result<()> {
@@ -5,14 +5,14 @@ use vecdb::Exit;
use super::Vecs; use super::Vecs;
use crate::{ use crate::{
blocks, internal::RatioDiffDollarsBps32, investing::ByDcaPeriod, market::lookback, prices, blocks, internal::RatioDiffDollarsBps32, investing::ByDcaPeriod, market::lookback, price,
}; };
impl Vecs { impl Vecs {
pub(crate) fn compute( pub(crate) fn compute(
&mut self, &mut self,
indexer: &Indexer, indexer: &Indexer,
prices: &prices::Vecs, prices: &price::Vecs,
blocks: &blocks::Vecs, blocks: &blocks::Vecs,
lookback: &lookback::Vecs, lookback: &lookback::Vecs,
exit: &Exit, exit: &Exit,
@@ -10,7 +10,7 @@ use super::{
use crate::{ use crate::{
blocks, blocks,
internal::{RatioDollarsBp32, WindowsTo1m}, internal::{RatioDollarsBp32, WindowsTo1m},
prices, price,
}; };
impl Vecs { impl Vecs {
@@ -19,7 +19,7 @@ impl Vecs {
&mut self, &mut self,
indexer: &Indexer, indexer: &Indexer,
returns: &returns::Vecs, returns: &returns::Vecs,
prices: &prices::Vecs, prices: &price::Vecs,
blocks: &blocks::Vecs, blocks: &blocks::Vecs,
moving_average: &moving_average::Vecs, moving_average: &moving_average::Vecs,
exit: &Exit, exit: &Exit,
@@ -3,14 +3,14 @@ use brk_indexer::Indexer;
use vecdb::Exit; use vecdb::Exit;
use super::MacdChain; use super::MacdChain;
use crate::{blocks, prices}; use crate::{blocks, price};
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub(super) fn compute( pub(super) fn compute(
chain: &mut MacdChain, chain: &mut MacdChain,
indexer: &Indexer, indexer: &Indexer,
blocks: &blocks::Vecs, blocks: &blocks::Vecs,
prices: &prices::Vecs, prices: &price::Vecs,
fast_days: usize, fast_days: usize,
slow_days: usize, slow_days: usize,
signal_days: usize, signal_days: usize,
+2 -2
View File
@@ -3,7 +3,7 @@ use brk_indexer::Indexer;
use vecdb::Exit; use vecdb::Exit;
use super::Vecs; use super::Vecs;
use crate::{blocks, indexes, prices, transactions}; use crate::{blocks, indexes, price, transactions};
impl Vecs { impl Vecs {
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
@@ -13,7 +13,7 @@ impl Vecs {
indexes: &indexes::Vecs, indexes: &indexes::Vecs,
blocks: &blocks::Vecs, blocks: &blocks::Vecs,
transactions: &transactions::Vecs, transactions: &transactions::Vecs,
prices: &prices::Vecs, prices: &price::Vecs,
exit: &Exit, exit: &Exit,
) -> Result<()> { ) -> Result<()> {
self.db.sync_bg_tasks()?; self.db.sync_bg_tasks()?;
@@ -7,7 +7,7 @@ use super::Vecs;
use crate::{ use crate::{
blocks, indexes, blocks, indexes,
internal::{RatioDollarsBp32, RatioSatsBp16}, internal::{RatioDollarsBp32, RatioSatsBp16},
prices, transactions, price, transactions,
}; };
impl Vecs { impl Vecs {
@@ -18,7 +18,7 @@ impl Vecs {
indexes: &indexes::Vecs, indexes: &indexes::Vecs,
lookback: &blocks::LookbackVecs, lookback: &blocks::LookbackVecs,
transactions: &transactions::Vecs, transactions: &transactions::Vecs,
prices: &prices::Vecs, prices: &price::Vecs,
exit: &Exit, exit: &Exit,
) -> Result<()> { ) -> Result<()> {
let starting_height = indexer.safe_lengths().height; let starting_height = indexer.safe_lengths().height;
+2 -2
View File
@@ -3,7 +3,7 @@ use brk_indexer::Indexer;
use vecdb::Exit; use vecdb::Exit;
use super::Vecs; use super::Vecs;
use crate::{blocks, indexes, inputs, prices}; use crate::{blocks, indexes, inputs, price};
impl Vecs { impl Vecs {
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
@@ -13,7 +13,7 @@ impl Vecs {
indexes: &indexes::Vecs, indexes: &indexes::Vecs,
inputs: &inputs::Vecs, inputs: &inputs::Vecs,
blocks: &blocks::Vecs, blocks: &blocks::Vecs,
prices: &prices::Vecs, prices: &price::Vecs,
exit: &Exit, exit: &Exit,
) -> Result<()> { ) -> Result<()> {
self.db.sync_bg_tasks()?; self.db.sync_bg_tasks()?;
@@ -4,13 +4,13 @@ use brk_types::{Height, OutputType, Sats, TxOutIndex};
use vecdb::{AnyStoredVec, AnyVec, Exit, ReadableVec, VecIndex, WritableVec}; use vecdb::{AnyStoredVec, AnyVec, Exit, ReadableVec, VecIndex, WritableVec};
use super::Vecs; use super::Vecs;
use crate::prices; use crate::price;
impl Vecs { impl Vecs {
pub(crate) fn compute( pub(crate) fn compute(
&mut self, &mut self,
indexer: &Indexer, indexer: &Indexer,
prices: &prices::Vecs, prices: &price::Vecs,
exit: &Exit, exit: &Exit,
) -> Result<()> { ) -> Result<()> {
let starting_lengths = indexer.safe_lengths(); let starting_lengths = indexer.safe_lengths();
+2 -2
View File
@@ -11,7 +11,7 @@ use crate::{
MaskSats, PercentRollingWindows, RatioU64Bp16, ValuePerBlockCumulativeRolling, MaskSats, PercentRollingWindows, RatioU64Bp16, ValuePerBlockCumulativeRolling,
WindowStartVec, Windows, WindowStartVec, Windows,
}, },
mining, prices, mining, price,
}; };
use super::minor; use super::minor;
@@ -63,7 +63,7 @@ impl Vecs {
indexer: &Indexer, indexer: &Indexer,
pool: &impl ReadableVec<Height, PoolSlug>, pool: &impl ReadableVec<Height, PoolSlug>,
blocks: &blocks::Vecs, blocks: &blocks::Vecs,
prices: &prices::Vecs, prices: &price::Vecs,
mining: &mining::Vecs, mining: &mining::Vecs,
exit: &Exit, exit: &Exit,
) -> Result<()> { ) -> Result<()> {
+2 -2
View File
@@ -22,7 +22,7 @@ use crate::{
WindowStartVec, Windows, WindowStartVec, Windows,
db_utils::{finalize_db, open_db}, db_utils::{finalize_db, open_db},
}, },
mining, prices, mining, price,
}; };
pub const DB_NAME: &str = "pools"; pub const DB_NAME: &str = "pools";
@@ -90,7 +90,7 @@ impl Vecs {
indexer: &Indexer, indexer: &Indexer,
indexes: &indexes::Vecs, indexes: &indexes::Vecs,
blocks: &blocks::Vecs, blocks: &blocks::Vecs,
prices: &prices::Vecs, prices: &price::Vecs,
mining: &mining::Vecs, mining: &mining::Vecs,
exit: &Exit, exit: &Exit,
) -> Result<()> { ) -> Result<()> {
@@ -2,7 +2,9 @@ use std::ops::Range;
use brk_error::Result; use brk_error::Result;
use brk_indexer::{Indexer, Lengths}; use brk_indexer::{Indexer, Lengths};
use brk_oracle::{Config, Histogram, Oracle, START_HEIGHT, bin_to_cents, cents_to_bin}; use brk_oracle::{
bin_to_cents, cents_to_bin, Config, Oracle, PaymentFilter, START_HEIGHT_FAST, START_HEIGHT_SLOW,
};
use brk_types::{Cents, OutputType, Sats, TxIndex, TxOutIndex}; use brk_types::{Cents, OutputType, Sats, TxIndex, TxOutIndex};
use tracing::info; use tracing::info;
use vecdb::{AnyStoredVec, AnyVec, Exit, ReadableVec, StorageMode, VecIndex, WritableVec}; use vecdb::{AnyStoredVec, AnyVec, Exit, ReadableVec, StorageMode, VecIndex, WritableVec};
@@ -61,8 +63,8 @@ impl Vecs {
fn compute_prices(&mut self, indexer: &Indexer, exit: &Exit) -> Result<()> { fn compute_prices(&mut self, indexer: &Indexer, exit: &Exit) -> Result<()> {
let starting_height = indexer.safe_lengths().height; let starting_height = indexer.safe_lengths().height;
let source_version = indexer.vecs.outputs.value.version() let source_version =
+ indexer.vecs.outputs.output_type.version(); indexer.vecs.outputs.value.version() + indexer.vecs.outputs.output_type.version();
self.spot self.spot
.cents .cents
.height .height
@@ -71,7 +73,7 @@ impl Vecs {
let total_heights = indexer.vecs.blocks.timestamp.len(); let total_heights = indexer.vecs.blocks.timestamp.len();
if total_heights <= START_HEIGHT { if total_heights <= START_HEIGHT_SLOW {
return Ok(()); return Ok(());
} }
@@ -83,17 +85,12 @@ impl Vecs {
.inner .inner
.truncate_if_needed_at(truncate_to)?; .truncate_if_needed_at(truncate_to)?;
if self.spot.cents.height.len() < START_HEIGHT { if self.spot.cents.height.len() < START_HEIGHT_SLOW {
for line in brk_oracle::PRICES for cents in brk_oracle::pre_oracle_prices_from(self.spot.cents.height.len()) {
.lines() if self.spot.cents.height.len() >= START_HEIGHT_SLOW {
.skip(self.spot.cents.height.len())
{
if self.spot.cents.height.len() >= START_HEIGHT {
break; break;
} }
let dollars: f64 = line.parse().unwrap_or(0.0); self.spot.cents.height.inner.push(cents);
let cents = (dollars * 100.0).round() as u64;
self.spot.cents.height.inner.push(Cents::new(cents));
} }
} }
@@ -101,8 +98,8 @@ impl Vecs {
return Ok(()); return Ok(());
} }
let config = Config::default();
let committed = self.spot.cents.height.len(); let committed = self.spot.cents.height.len();
let config = Config::for_height(committed);
let prev_cents = self let prev_cents = self
.spot .spot
.cents .cents
@@ -110,9 +107,9 @@ impl Vecs {
.collect_one_at(committed - 1) .collect_one_at(committed - 1)
.unwrap(); .unwrap();
let seed_bin = cents_to_bin(prev_cents.inner() as f64); let seed_bin = cents_to_bin(prev_cents.inner() as f64);
let warmup = config.window_size.min(committed - START_HEIGHT); let warmup = config.window_size.min(committed - START_HEIGHT_SLOW);
let mut oracle = Oracle::from_checkpoint(seed_bin, config, |o| { let mut oracle = Oracle::from_checkpoint(seed_bin, config, |o| {
Self::feed_blocks(o, indexer, (committed - warmup)..committed, None); Self::feed_blocks_for_warmup(o, indexer, (committed - warmup)..committed, None);
}); });
let num_new = total_heights - committed; let num_new = total_heights - committed;
@@ -121,19 +118,48 @@ impl Vecs {
committed, total_heights committed, total_heights
); );
let ref_bins = // Slow cold-start EMA up to START_HEIGHT_FAST, then switch to the fast
Self::feed_blocks(&mut oracle, indexer, committed..total_heights, None); // mature-market EMA. Steady-state runs start past START_HEIGHT_FAST and skip
// the slow segment entirely.
{
let mut processed = 0usize;
let mut push_ref_bin = |ref_bin| {
self.spot
.cents
.height
.inner
.push(Cents::new(bin_to_cents(ref_bin)));
for (i, ref_bin) in ref_bins.into_iter().enumerate() { processed += 1;
self.spot let progress = (processed * 100 / num_new) as u8;
.cents if processed > 1 && progress > (((processed - 1) * 100 / num_new) as u8) {
.height info!("Oracle price computation: {}%", progress);
.inner }
.push(Cents::new(bin_to_cents(ref_bin))); };
let progress = ((i + 1) * 100 / num_new) as u8; if committed < START_HEIGHT_FAST {
if i > 0 && progress > ((i * 100 / num_new) as u8) { let slow_end = START_HEIGHT_FAST.min(total_heights);
info!("Oracle price computation: {}%", progress); Self::feed_blocks_with(
&mut oracle,
indexer,
committed..slow_end,
None,
|_, _, ref_bin| push_ref_bin(ref_bin),
);
if slow_end == START_HEIGHT_FAST {
oracle.reconfigure(Config::default());
}
}
let fast_start = committed.max(START_HEIGHT_FAST);
if fast_start < total_heights {
Self::feed_blocks_with(
&mut oracle,
indexer,
fast_start..total_heights,
None,
|_, _, ref_bin| push_ref_bin(ref_bin),
);
} }
} }
@@ -153,10 +179,6 @@ impl Vecs {
/// Feed a range of blocks from the indexer into an Oracle (skipping coinbase), /// Feed a range of blocks from the indexer into an Oracle (skipping coinbase),
/// returning per-block ref_bin values. /// returning per-block ref_bin values.
/// ///
/// A transaction carrying an `OP_RETURN` output is protocol machinery, not a
/// dollar-denominated payment, so all of its outputs are dropped from the
/// histogram. This needs per-transaction grouping of a block's outputs.
///
/// Pass `cap = None` from compute paths, when the indexer is quiescent and /// Pass `cap = None` from compute paths, when the indexer is quiescent and
/// raw vec lengths are authoritative. Pass `cap = Some(&safe_lengths)` from /// raw vec lengths are authoritative. Pass `cap = Some(&safe_lengths)` from
/// reader paths so concurrent writer pushes past the cap are invisible. /// reader paths so concurrent writer pushes past the cap are invisible.
@@ -166,6 +188,33 @@ impl Vecs {
range: Range<usize>, range: Range<usize>,
cap: Option<&Lengths>, cap: Option<&Lengths>,
) -> Vec<f64> { ) -> Vec<f64> {
let mut ref_bins = Vec::with_capacity(range.len());
Self::feed_blocks_with(oracle, indexer, range, cap, |_, _, ref_bin| {
ref_bins.push(ref_bin);
});
ref_bins
}
/// Feed blocks into an Oracle when callers only need the warmed EMA/window state.
pub fn feed_blocks_for_warmup<IM: StorageMode>(
oracle: &mut Oracle,
indexer: &Indexer<IM>,
range: Range<usize>,
cap: Option<&Lengths>,
) {
Self::feed_blocks_with(oracle, indexer, range, cap, |_, _, _| {});
}
/// Feed a range of blocks into an Oracle and call `on_block` after each
/// processed block. This lets callers observe derived state such as EMA
/// without duplicating the histogram extraction path.
pub fn feed_blocks_with<IM: StorageMode>(
oracle: &mut Oracle,
indexer: &Indexer<IM>,
range: Range<usize>,
cap: Option<&Lengths>,
mut on_block: impl FnMut(usize, &Oracle, f64),
) {
let (total_txs, total_outputs, height_len) = match cap { let (total_txs, total_outputs, height_len) = match cap {
Some(c) => ( Some(c) => (
c.tx_index.to_usize(), c.tx_index.to_usize(),
@@ -193,8 +242,6 @@ impl Vecs {
.first_txout_index .first_txout_index
.collect_range_at(range.start, collect_end); .collect_range_at(range.start, collect_end);
let mut ref_bins = Vec::with_capacity(range.len());
// Cursor avoids per-block PcoVec page decompression for the // Cursor avoids per-block PcoVec page decompression for the
// tx-indexed first_txout_index lookup. Accessed tx_index values // tx-indexed first_txout_index lookup. Accessed tx_index values
// are strictly increasing across blocks, so it only advances forward. // are strictly increasing across blocks, so it only advances forward.
@@ -239,26 +286,21 @@ impl Vecs {
&mut output_types, &mut output_types,
); );
let mut hist = Histogram::zeros(); let tx_outputs = (0..tx_count).map(|tx| {
for tx in 0..tx_count {
let lo = tx_starts[tx] - out_start; let lo = tx_starts[tx] - out_start;
let hi = tx_starts let hi = tx_starts
.get(tx + 1) .get(tx + 1)
.map(|s| s - out_start) .map(|s| s - out_start)
.unwrap_or(out_end - out_start); .unwrap_or(out_end - out_start);
if output_types[lo..hi].contains(&OutputType::OpReturn) { values[lo..hi]
continue; .iter()
} .copied()
for i in lo..hi { .zip(output_types[lo..hi].iter().copied())
if let Some(bin) = oracle.output_to_bin(values[i], output_types[i]) { });
hist.increment(bin); let hist = PaymentFilter::for_height(range.start + idx).histogram(tx_outputs);
}
}
}
ref_bins.push(oracle.process_histogram(&hist)); let ref_bin = oracle.process_histogram(&hist);
on_block(range.start + idx, oracle, ref_bin);
} }
ref_bins
} }
} }
@@ -21,7 +21,7 @@ use crate::{
use by_unit::{OhlcByUnit, PriceByUnit, SplitByUnit, SplitCloseByUnit, SplitIndexesByUnit}; use by_unit::{OhlcByUnit, PriceByUnit, SplitByUnit, SplitCloseByUnit, SplitIndexesByUnit};
use ohlcs::{LazyOhlcVecs, OhlcVecs}; use ohlcs::{LazyOhlcVecs, OhlcVecs};
pub const DB_NAME: &str = "prices"; pub const DB_NAME: &str = "price";
#[derive(Traversable)] #[derive(Traversable)]
pub struct Vecs<M: StorageMode = Rw> { pub struct Vecs<M: StorageMode = Rw> {
@@ -4,7 +4,7 @@ use brk_types::Sats;
use vecdb::{Exit, VecIndex}; use vecdb::{Exit, VecIndex};
use super::Vecs; use super::Vecs;
use crate::{mining, outputs, prices}; use crate::{mining, outputs, price};
impl Vecs { impl Vecs {
pub(crate) fn compute( pub(crate) fn compute(
@@ -12,7 +12,7 @@ impl Vecs {
indexer: &Indexer, indexer: &Indexer,
outputs: &outputs::Vecs, outputs: &outputs::Vecs,
mining: &mining::Vecs, mining: &mining::Vecs,
prices: &prices::Vecs, prices: &price::Vecs,
exit: &Exit, exit: &Exit,
) -> Result<()> { ) -> Result<()> {
let starting_height = indexer.safe_lengths().height; let starting_height = indexer.safe_lengths().height;
+2 -2
View File
@@ -7,7 +7,7 @@ use vecdb::Exit;
const INITIAL_SUBSIDY: f64 = Sats::ONE_BTC_U64 as f64 * 50.0; const INITIAL_SUBSIDY: f64 = Sats::ONE_BTC_U64 as f64 * 50.0;
use super::Vecs; use super::Vecs;
use crate::{blocks, distribution, mining, outputs, prices, transactions}; use crate::{blocks, distribution, mining, outputs, price, transactions};
impl Vecs { impl Vecs {
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
@@ -18,7 +18,7 @@ impl Vecs {
blocks: &blocks::Vecs, blocks: &blocks::Vecs,
mining: &mining::Vecs, mining: &mining::Vecs,
transactions: &transactions::Vecs, transactions: &transactions::Vecs,
prices: &prices::Vecs, prices: &price::Vecs,
distribution: &distribution::Vecs, distribution: &distribution::Vecs,
exit: &Exit, exit: &Exit,
) -> Result<()> { ) -> Result<()> {
@@ -3,7 +3,7 @@ use brk_indexer::Indexer;
use vecdb::Exit; use vecdb::Exit;
use super::Vecs; use super::Vecs;
use crate::{blocks, indexes, inputs, prices}; use crate::{blocks, indexes, inputs, price};
impl Vecs { impl Vecs {
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
@@ -13,7 +13,7 @@ impl Vecs {
indexes: &indexes::Vecs, indexes: &indexes::Vecs,
blocks: &blocks::Vecs, blocks: &blocks::Vecs,
inputs: &inputs::Vecs, inputs: &inputs::Vecs,
prices: &prices::Vecs, prices: &price::Vecs,
exit: &Exit, exit: &Exit,
) -> Result<()> { ) -> Result<()> {
self.db.sync_bg_tasks()?; self.db.sync_bg_tasks()?;
@@ -5,7 +5,7 @@ use vecdb::Exit;
use super::Vecs; use super::Vecs;
use crate::transactions::{count, fees}; use crate::transactions::{count, fees};
use crate::{indexes, internal::Windows, prices}; use crate::{indexes, internal::Windows, price};
impl Vecs { impl Vecs {
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
@@ -13,7 +13,7 @@ impl Vecs {
&mut self, &mut self,
indexer: &Indexer, indexer: &Indexer,
indexes: &indexes::Vecs, indexes: &indexes::Vecs,
prices: &prices::Vecs, prices: &price::Vecs,
count_vecs: &count::Vecs, count_vecs: &count::Vecs,
fees_vecs: &fees::Vecs, fees_vecs: &fees::Vecs,
exit: &Exit, exit: &Exit,
+16 -8
View File
@@ -1,6 +1,6 @@
//! Mempool info + price-blending output histogram. //! Mempool info + price-blending output histogram.
use brk_oracle::Histogram; use brk_oracle::HistogramRaw;
use brk_types::MempoolInfo; use brk_types::MempoolInfo;
use crate::Mempool; use crate::Mempool;
@@ -11,13 +11,21 @@ impl Mempool {
self.read().info.clone() self.read().info.clone()
} }
/// Snapshot of pre-bucketed oracle bins across all live mempool tx /// Snapshot of pre-bucketed round-dollar-eligible bins across all live
/// outputs. The total is maintained incrementally by `TxStore` on /// mempool tx outputs. Maintained incrementally by `TxStore` on every
/// every insert/remove, so this hot path is `O(NUM_BINS)` regardless /// insert/remove, so this hot path is `O(NUM_BINS)` regardless of pool
/// of pool size. Used by `live_price` to blend the mempool into the /// size. Used by `live_price` to blend the mempool into the committed
/// committed oracle without re-parsing scripts per request. /// oracle without re-parsing scripts per request.
#[must_use] #[must_use]
pub fn live_histogram(&self) -> Histogram { pub fn live_eligible_histogram(&self) -> HistogramRaw {
self.read().txs.live_histogram() self.read().txs.live_eligible_histogram()
}
/// Snapshot of the raw histogram: every live mempool output binned by
/// value with no payment filtering. Backs the `histogram/raw/live`
/// endpoint.
#[must_use]
pub fn live_raw_histogram(&self) -> HistogramRaw {
self.read().txs.live_raw_histogram()
} }
} }
@@ -0,0 +1,56 @@
use brk_oracle::{sats_to_bin, HistogramRaw, PaymentFilter};
use brk_types::Transaction;
use crate::stores::tx_store::TxRecord;
/// The two live per-bin histograms the pool maintains incrementally as txs
/// enter and leave: `eligible` applies the round-dollar payment filter (it
/// feeds the oracle blend), `raw` bins every output by value with no filtering.
/// Add and remove run through the same code so the two stay symmetric.
#[derive(Default)]
pub struct LiveHistograms {
eligible: HistogramRaw,
raw: HistogramRaw,
}
impl LiveHistograms {
/// Fold a record's outputs into both histograms.
pub fn add(&mut self, record: &TxRecord) {
Self::eligible_bins(&record.tx, |bin| self.eligible[bin as usize] += 1);
for bin in Self::raw_bins(&record.tx) {
self.raw[bin] += 1;
}
}
/// Reverse a previous `add` for the same record.
pub fn remove(&mut self, record: &TxRecord) {
Self::eligible_bins(&record.tx, |bin| self.eligible[bin as usize] -= 1);
for bin in Self::raw_bins(&record.tx) {
self.raw[bin] -= 1;
}
}
/// Round-dollar-eligible bins, blended into the oracle by `live_price`.
pub fn eligible(&self) -> HistogramRaw {
self.eligible.clone()
}
/// Every live output binned by value, no payment filtering.
pub fn raw(&self) -> HistogramRaw {
self.raw.clone()
}
/// Round-dollar-eligible bins, applying the oracle payment filter. Calls
/// `emit(bin)` per eligible output. Deterministic over a tx's outputs,
/// which are never mutated after insert, so add and remove recompute it
/// identically rather than caching.
fn eligible_bins(tx: &Transaction, emit: impl FnMut(u16)) {
PaymentFilter::MODERN.for_each_bin(tx.output.iter().map(|o| (o.value, o.type_())), emit);
}
/// Raw bin index per output, dropping only values outside the bin domain
/// (zero / out-of-range).
fn raw_bins(tx: &Transaction) -> impl Iterator<Item = usize> + '_ {
tx.output.iter().filter_map(|o| sats_to_bin(o.value))
}
}
+2 -2
View File
@@ -4,13 +4,13 @@
//! one lock-order discipline. //! one lock-order discipline.
mod addr_tracker; mod addr_tracker;
mod live_histograms;
mod outpoint_spends; mod outpoint_spends;
mod output_bins;
mod tx_graveyard; mod tx_graveyard;
mod tx_store; mod tx_store;
pub use addr_tracker::AddrTracker; pub use addr_tracker::AddrTracker;
pub use live_histograms::LiveHistograms;
pub use outpoint_spends::OutpointSpends; pub use outpoint_spends::OutpointSpends;
pub use output_bins::OutputBins;
pub use tx_graveyard::{TxGraveyard, TxTombstone}; pub use tx_graveyard::{TxGraveyard, TxTombstone};
pub use tx_store::TxStore; pub use tx_store::TxStore;
@@ -1,23 +0,0 @@
use brk_oracle::default_eligible_bin;
use brk_types::Transaction;
use smallvec::SmallVec;
/// Pre-bucketed oracle bins for a tx's eligible outputs. Computed once on
/// insert so `Mempool::live_histogram` can bin all live outputs without
/// re-parsing scripts or recomputing eligibility per request.
pub struct OutputBins(SmallVec<[u16; 4]>);
impl OutputBins {
pub fn from_tx(tx: &Transaction) -> Self {
Self(
tx.output
.iter()
.filter_map(|o| default_eligible_bin(o.value, o.type_()))
.collect(),
)
}
pub fn iter(&self) -> impl Iterator<Item = u16> + '_ {
self.0.iter().copied()
}
}
+63 -34
View File
@@ -1,28 +1,21 @@
use brk_oracle::Histogram; use brk_oracle::HistogramRaw;
use brk_types::{MempoolRecentTx, Transaction, TxOut, Txid, TxidPrefix, Vin}; use brk_types::{MempoolRecentTx, Transaction, TxOut, Txid, TxidPrefix, Vin};
use rustc_hash::{FxHashMap, FxHashSet}; use rustc_hash::{FxHashMap, FxHashSet};
use crate::{state::TxEntry, stores::OutputBins}; use crate::{state::TxEntry, stores::LiveHistograms};
const RECENT_CAP: usize = 10; const RECENT_CAP: usize = 10;
/// Per-tx record: live tx body, its mempool entry, and the pre-bucketed /// Per-tx record: live tx body and its mempool entry, kept under one key
/// oracle bins for its outputs. Kept under one key so a single map probe /// so a single map probe returns everything readers need.
/// returns everything readers need.
pub struct TxRecord { pub struct TxRecord {
pub tx: Transaction, pub tx: Transaction,
pub entry: TxEntry, pub entry: TxEntry,
pub output_bins: OutputBins,
} }
impl TxRecord { impl TxRecord {
pub fn new(tx: Transaction, entry: TxEntry) -> Self { pub fn new(tx: Transaction, entry: TxEntry) -> Self {
let output_bins = OutputBins::from_tx(&tx); Self { tx, entry }
Self {
tx,
entry,
output_bins,
}
} }
} }
@@ -32,15 +25,15 @@ impl TxRecord {
/// set of prefixes whose tx still has at least one `prevout: None`, /// set of prefixes whose tx still has at least one `prevout: None`,
/// maintained on every `insert` / `remove_by_prefix` / `apply_fills` /// maintained on every `insert` / `remove_by_prefix` / `apply_fills`
/// so the post-update prevout filler can early-exit when empty. /// so the post-update prevout filler can early-exit when empty.
/// `live_histogram` mirrors the union of each record's `OutputBins`, /// `histograms` holds the eligible (oracle-blend) and raw per-bin output
/// kept in sync on `insert` / `remove_by_prefix` so the oracle-blend /// histograms, kept in sync on `insert` / `remove_by_prefix` so each read
/// read path is a single array clone, not a full pool walk. /// path is a single array clone, not a full pool walk.
#[derive(Default)] #[derive(Default)]
pub struct TxStore { pub struct TxStore {
records: FxHashMap<TxidPrefix, TxRecord>, records: FxHashMap<TxidPrefix, TxRecord>,
recent: Vec<MempoolRecentTx>, recent: Vec<MempoolRecentTx>,
unresolved: FxHashSet<TxidPrefix>, unresolved: FxHashSet<TxidPrefix>,
live_histogram: Histogram, histograms: LiveHistograms,
} }
impl TxStore { impl TxStore {
@@ -92,9 +85,7 @@ impl TxStore {
self.unresolved.insert(prefix); self.unresolved.insert(prefix);
} }
let record = TxRecord::new(tx, entry); let record = TxRecord::new(tx, entry);
for bin in record.output_bins.iter() { self.histograms.add(&record);
self.live_histogram[bin as usize] += 1;
}
self.records.insert(prefix, record); self.records.insert(prefix, record);
} }
@@ -112,16 +103,21 @@ impl TxStore {
pub fn remove_by_prefix(&mut self, prefix: &TxidPrefix) -> Option<TxRecord> { pub fn remove_by_prefix(&mut self, prefix: &TxidPrefix) -> Option<TxRecord> {
let record = self.records.remove(prefix)?; let record = self.records.remove(prefix)?;
self.unresolved.remove(prefix); self.unresolved.remove(prefix);
for bin in record.output_bins.iter() { self.histograms.remove(&record);
self.live_histogram[bin as usize] -= 1;
}
Some(record) Some(record)
} }
/// Snapshot the live oracle-bin histogram. Maintained incrementally /// Snapshot the round-dollar-eligible histogram that feeds the oracle
/// on insert/remove, so this is `O(NUM_BINS)`, not `O(live_outputs)`. /// blend. Maintained incrementally, so this is `O(NUM_BINS)`, not
pub fn live_histogram(&self) -> Histogram { /// `O(live_outputs)`.
self.live_histogram.clone() pub fn live_eligible_histogram(&self) -> HistogramRaw {
self.histograms.eligible()
}
/// Snapshot the raw histogram: every live output binned by value with no
/// payment filtering. Maintained incrementally alongside the eligible one.
pub fn live_raw_histogram(&self) -> HistogramRaw {
self.histograms.raw()
} }
/// Set of prefixes with at least one unfilled prevout. Used by the /// Set of prefixes with at least one unfilled prevout. Used by the
@@ -263,7 +259,10 @@ mod tests {
assert_eq!(applied[0].value, new_prevout.value); assert_eq!(applied[0].value, new_prevout.value);
let record = store.record_by_prefix(&prefix).expect("record present"); let record = store.record_by_prefix(&prefix).expect("record present");
assert_eq!(record.tx.input[0].prevout.as_ref().unwrap().value, new_prevout.value); assert_eq!(
record.tx.input[0].prevout.as_ref().unwrap().value,
new_prevout.value
);
assert_eq!( assert_eq!(
record.tx.input[1].prevout.as_ref().unwrap().value, record.tx.input[1].prevout.as_ref().unwrap().value,
prev_present.value prev_present.value
@@ -277,7 +276,10 @@ mod tests {
let stray_prefix = TxidPrefix::from(&fake_txid(0xFF)); let stray_prefix = TxidPrefix::from(&fake_txid(0xFF));
let applied = store.apply_fills( let applied = store.apply_fills(
&stray_prefix, &stray_prefix,
vec![(Vin::from(0u32), TxOut::from((ScriptBuf::new(), Sats::from(1u64))))], vec![(
Vin::from(0u32),
TxOut::from((ScriptBuf::new(), Sats::from(1u64))),
)],
); );
assert!(applied.is_empty()); assert!(applied.is_empty());
} }
@@ -319,10 +321,7 @@ mod tests {
let tx_a = fake_tx( let tx_a = fake_tx(
20, 20,
&[Some(TxOut::from((p2wpkh_script(8), Sats::from(1_234u64))))], &[Some(TxOut::from((p2wpkh_script(8), Sats::from(1_234u64))))],
&[ &[(p2wpkh_script(9), 2_345), (p2wpkh_script(10), 3_456)],
(p2wpkh_script(9), 2_345),
(p2wpkh_script(10), 3_456),
],
); );
let tx_b = fake_tx( let tx_b = fake_tx(
21, 21,
@@ -335,11 +334,41 @@ mod tests {
store.insert(tx_a, entry_a); store.insert(tx_a, entry_a);
store.insert(tx_b, entry_b); store.insert(tx_b, entry_b);
let total_after_both: u32 = store.live_histogram().iter().sum(); let total_after_both: u32 = store.live_eligible_histogram().iter().sum();
assert_eq!(total_after_both, 3, "two outputs + one output"); assert_eq!(total_after_both, 3, "two outputs + one output");
store.remove_by_prefix(&prefix_a); store.remove_by_prefix(&prefix_a);
let total_after_remove: u32 = store.live_histogram().iter().sum(); let total_after_remove: u32 = store.live_eligible_histogram().iter().sum();
assert_eq!(total_after_remove, 1); assert_eq!(total_after_remove, 1);
} }
#[test]
fn raw_histogram_bins_outputs_the_eligible_filter_drops() {
let mut store = TxStore::default();
// 2_345 sats is a round-dollar-eligible payment; 100_000_000 sats (1 BTC)
// is a round-BTC value the eligible filter drops but raw still bins.
let tx = fake_tx(
30,
&[Some(TxOut::from((p2wpkh_script(1), Sats::from(50_000u64))))],
&[(p2wpkh_script(2), 2_345), (p2wpkh_script(3), 100_000_000)],
);
let entry = entry_for(&tx, 100, 100);
let prefix = entry.txid_prefix();
store.insert(tx, entry);
assert_eq!(
store.live_eligible_histogram().iter().sum::<u32>(),
1,
"round-BTC output filtered out of the eligible histogram"
);
assert_eq!(
store.live_raw_histogram().iter().sum::<u32>(),
2,
"raw histogram bins every output"
);
store.remove_by_prefix(&prefix);
assert_eq!(store.live_eligible_histogram().iter().sum::<u32>(), 0);
assert_eq!(store.live_raw_histogram().iter().sum::<u32>(), 0);
}
} }
+61 -37
View File
@@ -1,8 +1,8 @@
# brk_oracle # brk_oracle
**Version 2** **Version 3**
Pure on-chain BTC/USD price oracle. No exchange feeds, no external APIs. Derives the bitcoin price from transaction data alone. Tracks block by block from height 525,000 (May 2018) onward. Pure on-chain BTC/USD price oracle. No exchange feeds, no external APIs. Derives the bitcoin price from transaction data alone. Tracks block by block from height 340,000 (January 2015) onward.
Inspired by [UTXOracle](https://utxo.live/oracle/) by [@SteveSimple](https://x.com/SteveSimple), which proved the concept. brk_oracle takes the same core insight and redesigns the algorithm for per-block resolution and rolling operation. See [comparison](#comparison-with-utxoracle) below. Inspired by [UTXOracle](https://utxo.live/oracle/) by [@SteveSimple](https://x.com/SteveSimple), which proved the concept. brk_oracle takes the same core insight and redesigns the algorithm for per-block resolution and rolling operation. See [comparison](#comparison-with-utxoracle) below.
@@ -42,13 +42,13 @@ The spacing between spikes is constant (set by the ratios between dollar amounts
## How it works ## How it works
The oracle tracks the price incrementally, block by block, starting from a known seed price. Each new block nudges the estimate. The search window is narrow (about ±10 bins, or ±12%), so the oracle can only follow gradual movement — it cannot jump to an arbitrary price from scratch. This is by design: it makes the algorithm resistant to noise. The oracle tracks the price incrementally, block by block, starting from a known seed price. Each new block nudges the estimate. The search window is narrow (about 12 bins, or +15% / -12% in price), so the oracle can only follow gradual movement, not jump to an arbitrary price from scratch. This is by design: it makes the algorithm resistant to noise.
For each new block: For each new block:
### 1. Filter outputs ### 1. Filter outputs
Skip the coinbase transaction, and skip every output of a transaction carrying an `OP_RETURN`: that transaction is protocol machinery, not a dollar-denominated payment, so its payout amounts are not price signal. Then exclude noisy outputs: script types dominated by protocol activity (P2TR by default), dust below 1,000 sats, and round BTC amounts (0.01, 0.1, 1.0 BTC, etc.) that create false spikes unrelated to dollar purchases. Skip the coinbase transaction, and skip every output of a transaction carrying an `OP_RETURN`: that transaction is protocol machinery, not a dollar-denominated payment, so its payout amounts are not price signal. Below height 630,000, also skip every output of a transaction with more than 100 outputs: a large fan-out is a batch payout (exchange sweep, mixer), not a round-dollar payment, and the thin early signal needs it removed. At and above height 630,000, the transaction fan-out cap relaxes to 250 outputs so dense-chain payment activity remains visible while very large fan-outs cannot dominate one EMA slot. Then exclude noisy outputs: script types dominated by protocol activity (P2TR by default), dust below 1,000 sats, and round BTC amounts (0.01, 0.1, 1.0 BTC, etc.) that create false spikes unrelated to dollar purchases.
### 2. Build a log-scale histogram ### 2. Build a log-scale histogram
@@ -87,7 +87,7 @@ The fixed ratios between round-dollar amounts ($1, $2, $3, $5, ... $10,000) crea
The oracle slides this stencil across the EMA histogram within the search window. At each candidate position: The oracle slides this stencil across the EMA histogram within the search window. At each candidate position:
1. **Read** the EMA value at all 19 expected spike locations 1. **Read** the EMA value at all 19 expected spike locations
2. **Normalize** each value by dividing by that offset's peak within the search window this gives rare amounts like $3 equal voting weight to common amounts like $100 2. **Normalize** each value by dividing by that offset's peak within the search window: this gives rare amounts like $3 equal voting weight to common amounts like $100
3. **Sum** the 19 normalized values into a single score 3. **Sum** the 19 normalized values into a single score
The position with the highest score is where the fingerprint best matches the histogram. The position with the highest score is where the fingerprint best matches the histogram.
@@ -102,7 +102,7 @@ A $100 purchase at price P produces `$100 / P × 10⁸` sats, which lands in bin
= (10 log₁₀(P)) × 200 = (10 log₁₀(P)) × 200
``` ```
So the stencil's winning position the bin where $100 purchases land directly encodes the price: So the stencil's winning position, the bin where $100 purchases land, directly encodes the price:
``` ```
price = 10^(10 bin / 200) dollars price = 10^(10 bin / 200) dollars
@@ -122,9 +122,9 @@ Parabolic interpolation between the best bin and its two neighbors refines the e
The oracle consumes one pre-built histogram per block via `process_histogram(&hist)`, a `[u32; 2400]` bin-count array, and returns the updated reference bin. The oracle consumes one pre-built histogram per block via `process_histogram(&hist)`, a `[u32; 2400]` bin-count array, and returns the updated reference bin.
The caller does the filtering when it builds the histogram. For each block it skips the coinbase, drops every output of a transaction carrying an `OP_RETURN`, then bins the rest. `default_eligible_bin(sats, output_type)` (or `Oracle::output_to_bin` for a non-default `Config`) applies the per-output rules: excluded script types, dust, and round-BTC values. It returns the bin index, or `None` for a filtered output. The caller filters as it builds the histogram, applying the [step 1](#1-filter-outputs) rules. `PaymentFilter::for_height(height).histogram(txs)` builds a fresh block histogram from non-coinbase transaction outputs. Incremental live callers use `PaymentFilter::MODERN.for_each_bin(outputs, emit)`, which applies the modern fan-out cap without requiring a height. `PaymentFilter::eligible_bin(sats, output_type)` returns an individual output's bin index, or `None` if filtered. The transaction-level rules include the OP_RETURN drop, the >100 transaction-output fan-out cap below height 630,000, and the >250 cap from height 630,000 onward.
The initial seed must be close to the real price at the starting height. The crate includes a `PRICES` constant with exchange prices for every height up to 630,000 to derive a seed from. The initial seed must be close to the real price at the starting height. The crate includes typed pre-oracle helpers for exchange prices at heights 0..340,000. `Oracle::from_seed()` uses the last baked price, height 339,999 (one below `START_HEIGHT_SLOW`), and the slow cold-start config to seed the oracle's first on-chain computation at height 340,000.
## Configuration ## Configuration
@@ -134,10 +134,12 @@ All parameters via `Config` with sensible defaults:
|-----------|---------|---------| |-----------|---------|---------|
| `alpha` | 2/7 | EMA decay rate (~6-block span) | | `alpha` | 2/7 | EMA decay rate (~6-block span) |
| `window_size` | 12 | Ring buffer depth in blocks | | `window_size` | 12 | Ring buffer depth in blocks |
| `search_below` / `search_above` | 9 / 11 | Search window around previous estimate (bins) | | `search_below` / `search_above` | 12 / 11 | Search window around previous estimate (bins) |
| `min_sats` | 1,000 | Dust threshold | | `shape_weight` | 0 | Shape-anchoring restoring-force weight. 0 disables it. `Config::slow()` sets 8 for the cold-start |
| `exclude_common_round_values` | true | Filter d × 10ⁿ (d ∈ {1,2,3,5,6}) to prevent false stencil matches |
| `excluded_output_types` | P2TR | Script types dominated by protocol activity | The output-filtering rules (1,000-sat dust floor, excluded P2TR, round-BTC exclusion) are not `Config` parameters: they are constants in the `filter` module so the indexer, per-request reconstruction, and mempool all bin identically. See [Input](#input).
Between heights 340,000 and 508,000 the oracle runs a slower cold-start configuration (`Config::slow()`: `alpha` = 0.10, ~19-block span, `window_size` = 40, `shape_weight` = 8). In the thin pre-2018 output mix the fast default octave-locks onto the round-dollar half-price pattern, so the slow EMA and the shape-anchoring restoring force resist that drift. At 508,000 `Oracle::reconfigure` switches to the defaults above (`shape_weight` back to 0), and `Config::for_height` returns the right one for any height.
## Comparison with UTXOracle ## Comparison with UTXOracle
@@ -145,35 +147,36 @@ All parameters via `Config` with sensible defaults:
| | brk_oracle | UTXOracle | | | brk_oracle | UTXOracle |
|---|---|---| |---|---|---|
| Resolution | Per-block (~10 min) + daily candles | Per-run consensus price + per-output intraday scatter | | Resolution | Per-block (~10 min); daily OHLC built downstream | Per-run consensus price + per-output intraday scatter |
| Operation | Rolling: EMA over ring buffer, updates each block | Batch: processes a full day from scratch, stateless | | Operation | Rolling: EMA over ring buffer, updates each block | Batch: processes a full day from scratch, stateless |
| Algorithm | Single-pass stencil scoring with per-offset normalization | Multi-step: dual stencil → rough estimate → output-to-USD mapping → iterative convergence | | Algorithm | Single-pass stencil scoring with per-offset normalization | Multi-step: dual stencil → rough estimate → output-to-USD mapping → iterative convergence |
| Steps to compute price | 7 (filter+bin → ring insert → EMA → per-offset peaks → score → argmax+parabolic → bin→price) | 10 (filter+bin → clip → smooth round BTC → sum → normalize → cap extremes → dual-stencil slide → neighbor weight-avg → output-to-USD map → iterative central price) |
| Stencil | 19 round-USD offsets ($1 to $10k), each normalized to its own peak | 803-point Gaussian + weighted spike template targeting 17 round-USD amounts | | Stencil | 19 round-USD offsets ($1 to $10k), each normalized to its own peak | 803-point Gaussian + weighted spike template targeting 17 round-USD amounts |
| Round BTC handling | Excluded from histogram entirely | Histogram bins smoothed by averaging neighbors | | Round BTC handling | Excluded from histogram entirely | Histogram bins smoothed by averaging neighbors |
| Output filtering | Per-tx OP_RETURN drop, then per-output: script type, dust threshold, round BTC | Per-tx: exactly 2 outputs, ≤5 inputs, no same-day inputs, ≤500-byte witness | | Output filtering | Per-tx OP_RETURN drop, then per-output: script type, dust threshold, round BTC | Per-tx: not coinbase, no OP_RETURN, exactly 2 outputs, ≤5 inputs, no same-day inputs, ≤500-byte witness |
| Validated from | Height 525,000 (May 2018) | December 2023 | | Validated from | Height 340,000 (January 2015) | Dec 15, 2023 |
| Language | Rust | Python | | Language | Rust | Python |
| Dependencies | None (pure computation, caller provides block data) | Bitcoin Core RPC | | Dependencies | None (pure computation, caller provides block data) | bitcoin-cli + direct blk file reads |
| Bins per decade | 200 | 200 | | Bins per decade | 200 | 200 |
## Accuracy ## Accuracy
Tested over 411,251 blocks (heights 525,000 to 949,800, as of May 2026) against exchange OHLC data. Error is measured per block as distance from the oracle estimate to the exchange high/low range at that height. If the oracle falls within the range, the error is zero. Tested over 596,251 exchange-covered blocks after running the oracle from height 340,000 through height 952,314. Error is measured per block as distance from the oracle estimate to the exchange high/low range at that height. If the oracle falls within the range, the error is zero.
### Per-block ### Per-block
| Metric | Value | | Metric | Value |
|--------|-------| |--------|-------|
| Median error | 0.11% | | Median error | 0.15% |
| 95th percentile | 0.67% | | 95th percentile | 1.2% |
| 99th percentile | 1.7% | | 99th percentile | 3.4% |
| 99.9th percentile | 5.4% | | 99.9th percentile | 15.6% |
| RMSE | 0.50% | | RMSE | 0.97% |
| Max error | 33.4% | | Max error | 33.8% |
| Bias | +0.00 bins (essentially zero) | | Bias | +0.06 bins (essentially zero) |
| Blocks > 5% error | 472 (0.11%) | | Blocks > 5% error | 3,233 (0.542%) |
| Blocks > 10% error | 177 | | Blocks > 10% error | 1,323 |
| Blocks > 20% error | 3 | | Blocks > 20% error | 154 |
### Daily candles ### Daily candles
@@ -181,36 +184,57 @@ Oracle daily OHLC built from per-block prices vs exchange daily OHLC:
| | Median | RMSE | Max | | | Median | RMSE | Max |
|-------|--------|------|-----| |-------|--------|------|-----|
| Open | 0.21% | 0.65% | 15.3% | | Open | 0.24% | 1.07% | 29.1% |
| High | 0.53% | 1.12% | 28.0% | | High | 0.58% | 1.47% | 27.3% |
| Low | 0.51% | 1.38% | 19.7% | | Low | 0.53% | 1.95% | 55.1% |
| Close | 0.24% | 0.73% | 15.4% | | Close | 0.27% | 1.18% | 29.2% |
### By year ### By year
| Year | Blocks | Median | RMSE | Max | >5% | >10% | >20% | Price range | | Year | Blocks | Median | RMSE | Max | >5% | >10% | >20% | Price range |
|------|--------|--------|------|-----|-----|------|------|-------------| |------|--------|--------|------|-----|-----|------|------|-------------|
| 2018 | 31,492 | 0.21% | 1.11% | 33.4% | 169 | 109 | 3 | $3,129$8,488 | | 2015 | 51,249 | 0.26% | 1.67% | 33.8% | 916 | 449 | 25 | $198$500 |
| 2019 | 54,272 | 0.16% | 0.69% | 17.4% | 165 | 53 | 0 | $3,338$13,868 | | 2016 | 54,753 | 0.33% | 0.80% | 16.9% | 150 | 33 | 0 | $351$989 |
| 2020 | 53,102 | 0.10% | 0.44% | 12.6% | 70 | 6 | 0 | $3,858$29,322 | | 2017 | 55,959 | 0.45% | 2.05% | 28.6% | 1,527 | 606 | 67 | $0$19,892 |
| 2018 | 54,531 | 0.18% | 1.31% | 31.6% | 411 | 207 | 62 | $3,129$17,178 |
| 2019 | 54,272 | 0.16% | 0.59% | 17.4% | 100 | 16 | 0 | $3,338$13,868 |
| 2020 | 53,102 | 0.10% | 0.42% | 11.6% | 61 | 3 | 0 | $3,858$29,322 |
| 2021 | 52,733 | 0.07% | 0.47% | 14.4% | 42 | 9 | 0 | $27,678$69,000 | | 2021 | 52,733 | 0.07% | 0.47% | 14.4% | 42 | 9 | 0 | $27,678$69,000 |
| 2022 | 53,230 | 0.07% | 0.32% | 6.8% | 10 | 0 | 0 | $15,460$48,240 | | 2022 | 53,230 | 0.07% | 0.32% | 6.8% | 10 | 0 | 0 | $15,460$48,240 |
| 2023 | 54,032 | 0.10% | 0.25% | 6.6% | 5 | 0 | 0 | $16,490$44,700 | | 2023 | 54,032 | 0.10% | 0.25% | 6.6% | 5 | 0 | 0 | $16,490$44,700 |
| 2024 | 53,367 | 0.10% | 0.28% | 6.7% | 7 | 0 | 0 | $38,555$108,298 | | 2024 | 53,367 | 0.10% | 0.28% | 6.7% | 7 | 0 | 0 | $38,555$108,298 |
| 2025 | 53,113 | 0.11% | 0.25% | 5.8% | 4 | 0 | 0 | $74,409$126,198 | | 2025 | 53,113 | 0.11% | 0.25% | 5.8% | 4 | 0 | 0 | $74,409$126,198 |
| 2026 | 5,910 | 0.11% | 0.27% | 3.2% | 0 | 0 | 0 | $60,000$97,900 | | 2026 | 5,910 | 0.10% | 0.27% | 3.2% | 0 | 0 | 0 | $60,000$97,900 |
The oracle is only as good as the signal it reads. The largest errors cluster in late 2018: the November price crash fell faster than the narrow search window could follow (33% max error), and on-chain volume was lower then, so the round-dollar pattern was weaker (1.1% RMSE for the year). By 2020 the signal is strong enough for 0.1% median accuracy, and since 2022 no block exceeds 10% error. The oracle is only as good as the signal it reads. The largest errors cluster in the early cold-start, where thin 2015 on-chain volume gives a weaker round-dollar pattern: the 33.8% max error sits at height 341,498 (oracle ~$287 vs exchange ~$213) during the first weeks of warm-up. A second cluster sits just below the 508,000 regime switch, where the slow EMA lagged the fast early-2018 rally (~31.6% at height 507,278, oracle ~$6,685 vs exchange ~$8,800) before handing off to the fast default. The thin pre-2018 mix means 2015, 2017, and 2018 carry the bulk of the error (1.67%, 2.05%, and 1.31% RMSE). From 2019 the signal strengthens: by 2020 the oracle reaches 0.1% median accuracy, and since 2022 no block exceeds 10% error.
### Why no outlier smoothing? ### Why no outlier smoothing?
Post-hoc smoothing for example, correcting any block whose price deviates more than 5% from both its neighbors would improve the aggregate numbers. This is deliberately not done, for two reasons: Post-hoc smoothing, for example correcting any block whose price deviates more than 5% from both its neighbors, would improve the aggregate numbers. This is deliberately not done, for two reasons:
1. **Simplicity**: The oracle is a single forward pass with no lookback corrections. Adding smoothing means defining thresholds, neighbor windows, and replacement strategies, all of which add complexity for marginal gain. 1. **Simplicity**: The oracle is a single forward pass with no lookback corrections. Adding smoothing means defining thresholds, neighbor windows, and replacement strategies, all of which add complexity for marginal gain.
2. **Finality**: Each block's price is produced once and never revised (unless the block itself is reorged). Downstream consumers can treat the oracle output as append-only. Smoothing would require retroactively changing already-published prices, breaking that property. 2. **Finality**: Each block's price is produced once and never revised (unless the block itself is reorged). Downstream consumers can treat the oracle output as append-only. Smoothing would require retroactively changing already-published prices, breaking that property.
## Changelog ## Changelog
### v4
Changes from v3:
- **Modern fan-out cap**: below height 630,000 the oracle keeps the strict >100-output transaction drop introduced in v3. At and above 630,000 the cap now relaxes to 250 outputs instead of being fully lifted. This preserves dense-chain payment signal while preventing very large modern fan-outs from dominating a single EMA slot and creating a transient false round-dollar ladder.
`VERSION` is bumped to 4 so downstream consumers invalidate prices computed by an earlier algorithm.
### v3
Changes from v2:
- **Earlier start with a cold-start regime**: on-chain tracking begins at height 340,000 (January 2015) instead of 525,000, adding about 185,000 blocks of history. Below height 508,000 the oracle runs a slower EMA (`Config::slow()`, ~19-block span, window 40) paired with a shape-anchoring restoring force (`shape_weight` 8) that pulls candidate scores toward a slowly-adapted profile of the round-dollar arm shape, resisting the half-price octave drift the fast default locks onto in the thinner pre-2018 output mix. At height 508,000 it switches to the fast default via `Oracle::reconfigure`, which restores `shape_weight` to 0 and turns the force off.
- **Max-outputs filter**: a transaction with more than 100 outputs is dropped from the histogram below height 630,000. Large fan-outs (exchange sweeps, mixer payouts) are batch machinery, not round-dollar payments, and the thin 2018-2020 signal needs them removed to stay locked onto the pattern. Above 630,000 on-chain volume is dense enough that the cap removes more genuine signal than noise, so it is lifted.
- **Wider up-reach**: `search_below` raised from 9 to 12 bins. The sharp 2018 reversal candles need extra room to follow a fast move upward in price.
`VERSION` is bumped to 3 so downstream consumers invalidate prices computed by an earlier algorithm.
### v2 ### v2
Changes from v1: Changes from v1:
@@ -1,295 +0,0 @@
//! Compare specific digit filter configurations across multiple start heights.
//!
//! Run with: cargo run -p brk_oracle --example compare_digits --release
use std::path::PathBuf;
use std::time::Instant;
use brk_indexer::Indexer;
use brk_oracle::{Config, Histogram, NUM_BINS, Oracle, PRICES, cents_to_bin, sats_to_bin};
use brk_types::{OutputType, Sats, TxIndex, TxOutIndex};
use vecdb::{AnyVec, ReadableVec, VecIndex};
const BINS_5PCT: f64 = 4.24;
const BINS_10PCT: f64 = 8.28;
const BINS_20PCT: f64 = 15.84;
fn bins_to_pct(bins: f64) -> f64 {
(10.0_f64.powf(bins / 200.0) - 1.0) * 100.0
}
fn seed_bin(start_height: usize) -> f64 {
let price: f64 = PRICES
.lines()
.nth(start_height - 1)
.expect("prices.txt too short")
.parse()
.expect("Failed to parse seed price");
cents_to_bin(price * 100.0)
}
fn leading_digit(sats: u64) -> u8 {
let log = (sats as f64).log10();
let magnitude = 10.0_f64.powf(log.floor());
let d = (sats as f64 / magnitude).round() as u8;
if d >= 10 { 1 } else { d }
}
fn is_round(sats: u64) -> bool {
let log = (sats as f64).log10();
let magnitude = 10.0_f64.powf(log.floor());
let leading = (sats as f64 / magnitude).round();
let round_val = leading * magnitude;
(sats as f64 - round_val).abs() <= round_val * 0.001
}
struct Stats {
total_sq_err: f64,
total_bias: f64,
max_err: f64,
total_blocks: u64,
gt_5pct: u64,
gt_10pct: u64,
gt_20pct: u64,
}
impl Stats {
fn new() -> Self {
Self {
total_sq_err: 0.0,
total_bias: 0.0,
max_err: 0.0,
total_blocks: 0,
gt_5pct: 0,
gt_10pct: 0,
gt_20pct: 0,
}
}
fn update(&mut self, err: f64) {
self.total_sq_err += err * err;
self.total_bias += err;
self.total_blocks += 1;
let abs_err = err.abs();
if abs_err > self.max_err {
self.max_err = abs_err;
}
if abs_err > BINS_5PCT {
self.gt_5pct += 1;
}
if abs_err > BINS_10PCT {
self.gt_10pct += 1;
}
if abs_err > BINS_20PCT {
self.gt_20pct += 1;
}
}
fn rmse_pct(&self) -> f64 {
bins_to_pct((self.total_sq_err / self.total_blocks as f64).sqrt())
}
fn max_pct(&self) -> f64 {
bins_to_pct(self.max_err)
}
fn bias(&self) -> f64 {
self.total_bias / self.total_blocks as f64
}
}
fn main() {
let t0 = Instant::now();
let data_dir = std::env::var("BRK_DIR")
.map(PathBuf::from)
.unwrap_or_else(|_| {
let home = std::env::var("HOME").unwrap();
PathBuf::from(home).join(".brk")
});
let indexer = Indexer::forced_import(&data_dir).expect("Failed to load indexer");
let total_heights = indexer.vecs.blocks.timestamp.len();
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let height_ohlc: Vec<[f64; 4]> = serde_json::from_str(
&std::fs::read_to_string(format!("{manifest_dir}/examples/height_price_ohlc.json"))
.expect("Failed to read height_price_ohlc.json"),
)
.expect("Failed to parse height OHLC");
let height_bands: Vec<(f64, f64)> = height_ohlc
.iter()
.map(|ohlc| {
let high = ohlc[1];
let low = ohlc[2];
if high > 0.0 && low > 0.0 {
(cents_to_bin(high * 100.0), cents_to_bin(low * 100.0))
} else {
(0.0, 0.0)
}
})
.collect();
// Configs to compare.
// 987654321
let masks: &[(u16, &str)] = &[
(0b0_0111_0111, "{1,2,3,5,6,7}"),
(0b0_0011_0111, "{1,2,3,5,6}"),
(0b0_0001_1111, "{1,2,3,4,5}"),
(0b0_0001_0111, "{1,2,3,5}"),
];
let start_heights: &[usize] = &[575_000, 600_000, 630_000];
// (mask_idx, start_idx) -> (Oracle, Stats)
let n = masks.len() * start_heights.len();
let mut oracles: Vec<Option<Oracle>> = (0..n).map(|_| None).collect();
let mut stats: Vec<Stats> = (0..n).map(|_| Stats::new()).collect();
let idx = |m: usize, s: usize| -> usize { m * start_heights.len() + s };
let total_txs = indexer.vecs.transactions.txid.len();
let total_outputs = indexer.vecs.outputs.value.len();
let first_tx_index: Vec<TxIndex> = indexer.vecs.transactions.first_tx_index.collect();
let out_first: Vec<TxOutIndex> = indexer.vecs.outputs.first_txout_index.collect();
let ref_config = Config::default();
let earliest_start = *start_heights.iter().min().unwrap();
for h in earliest_start..total_heights {
let ft = first_tx_index[h];
let next_ft = first_tx_index
.get(h + 1)
.copied()
.unwrap_or(TxIndex::from(total_txs));
let out_start = if ft.to_usize() + 1 < next_ft.to_usize() {
indexer
.vecs
.transactions
.first_txout_index
.collect_one(ft + 1)
.unwrap()
.to_usize()
} else {
out_first
.get(h + 1)
.copied()
.unwrap_or(TxOutIndex::from(total_outputs))
.to_usize()
};
let out_end = out_first
.get(h + 1)
.copied()
.unwrap_or(TxOutIndex::from(total_outputs))
.to_usize();
let values: Vec<Sats> = indexer
.vecs
.outputs
.value
.collect_range_at(out_start, out_end);
let output_types: Vec<OutputType> = indexer
.vecs
.outputs
.output_type
.collect_range_at(out_start, out_end);
// Build full histogram and per-digit histograms.
let mut full_hist = Histogram::zeros();
let mut digit_hist: [Histogram; 9] = std::array::from_fn(|_| Histogram::zeros());
for (sats, output_type) in values.into_iter().zip(output_types) {
if ref_config.excluded_output_types.contains(&output_type) {
continue;
}
if *sats < ref_config.min_sats {
continue;
}
if let Some(bin) = sats_to_bin(sats) {
full_hist.increment(bin);
if is_round(*sats) {
let d = leading_digit(*sats);
if (1..=9).contains(&d) {
digit_hist[(d - 1) as usize].increment(bin);
}
}
}
}
// Feed each (mask, start_height) combo.
for (mi, &(mask, _)) in masks.iter().enumerate() {
// Build filtered histogram for this mask.
let mut hist = full_hist.clone();
(0..9usize).for_each(|d| {
if mask & (1 << d) != 0 {
for bin in 0..NUM_BINS {
hist[bin] -= digit_hist[d][bin];
}
}
});
for (si, &sh) in start_heights.iter().enumerate() {
if h < sh {
continue;
}
let i = idx(mi, si);
if oracles[i].is_none() {
oracles[i] = Some(Oracle::new(
seed_bin(sh),
Config {
exclude_common_round_values: false,
..Default::default()
},
));
}
let ref_bin = oracles[i].as_mut().unwrap().process_histogram(&hist);
if h < height_bands.len() {
let (high_bin, low_bin) = height_bands[h];
if high_bin > 0.0 && low_bin > 0.0 {
let err = if ref_bin < high_bin {
ref_bin - high_bin
} else if ref_bin > low_bin {
ref_bin - low_bin
} else {
0.0
};
stats[i].update(err);
}
}
}
}
}
// Print results grouped by start height.
for (si, &sh) in start_heights.iter().enumerate() {
println!();
println!("@ {}k:", sh / 1000);
println!(
" {:<16} {:>8} {:>10} {:>10} {:>6} {:>6} {:>6} {:>8}",
"Digits", "Blocks", "RMSE%", "Max%", ">5%", ">10%", ">20%", "Bias"
);
println!(" {}", "-".repeat(72));
for (mi, &(_, label)) in masks.iter().enumerate() {
let s = &stats[idx(mi, si)];
println!(
" {:<16} {:>8} {:>7.3}% {:>7.1}% {:>6} {:>6} {:>6} {:>+8.2}",
label,
s.total_blocks,
s.rmse_pct(),
s.max_pct(),
s.gt_5pct,
s.gt_10pct,
s.gt_20pct,
s.bias()
);
}
}
println!("\nDone in {:.1}s", t0.elapsed().as_secs_f64());
}
+131 -139
View File
@@ -1,10 +1,11 @@
//! Verify oracle determinism: oracles started from different heights converge //! Verify the production restart property: an oracle restored via
//! to identical ref_bin values after the ring buffer fills. //! `from_checkpoint` (seeded from the previous block's stored cents price,
//! replayed over the last `window_size` blocks) produces bit-exact `ref_bin`
//! values matching a continuously-running oracle from the restart height
//! onward.
//! //!
//! Creates a reference oracle at height 575k and test oracles every 1000 blocks //! Mirrors the production transaction filter exactly, so it exercises the same code path
//! up to 630k. After window_size blocks, each test oracle should produce the //! `brk_computer::price::compute::feed_blocks` uses at runtime.
//! same ref_bin as the reference, proving the truncated EMA provides
//! start-point independence.
//! //!
//! Run with: cargo run -p brk_oracle --example determinism --release //! Run with: cargo run -p brk_oracle --example determinism --release
@@ -12,26 +13,35 @@ use std::path::PathBuf;
use brk_indexer::Indexer; use brk_indexer::Indexer;
use brk_oracle::{ use brk_oracle::{
Config, Histogram, Oracle, PRICES, START_HEIGHT, cents_to_bin, default_eligible_bin, bin_to_cents, cents_to_bin, Config, HistogramRaw, Oracle, PaymentFilter, START_HEIGHT_FAST,
START_HEIGHT_SLOW,
}; };
use brk_types::{OutputType, Sats, TxIndex, TxOutIndex}; use brk_types::{OutputType, Sats, TxIndex, TxOutIndex};
use vecdb::{AnyVec, ReadableVec, VecIndex}; use vecdb::{AnyVec, ReadableVec, VecIndex};
fn seed_bin(height: usize) -> f64 { struct Block {
let price: f64 = PRICES height: usize,
.lines() values: Vec<Sats>,
.nth(height - 1) output_types: Vec<OutputType>,
.expect("prices.txt too short") tx_starts: Vec<usize>,
.parse() out_start: usize,
.expect("Failed to parse seed price"); out_end: usize,
cents_to_bin(price * 100.0)
} }
struct TestRun { fn build_histogram(block: &Block) -> HistogramRaw {
start_height: usize, let tx_outputs = (0..block.tx_starts.len()).map(|tx| {
oracle: Option<Oracle>, let lo = block.tx_starts[tx] - block.out_start;
converged_at: Option<usize>, let hi = block
diverged_after: bool, .tx_starts
.get(tx + 1)
.map(|s| s - block.out_start)
.unwrap_or(block.out_end - block.out_start);
block.values[lo..hi]
.iter()
.copied()
.zip(block.output_types[lo..hi].iter().copied())
});
PaymentFilter::for_height(block.height).histogram(tx_outputs)
} }
fn main() { fn main() {
@@ -45,62 +55,54 @@ fn main() {
let indexer = Indexer::forced_import(&data_dir).expect("Failed to load indexer"); let indexer = Indexer::forced_import(&data_dir).expect("Failed to load indexer");
let total_heights = indexer.vecs.blocks.timestamp.len(); let total_heights = indexer.vecs.blocks.timestamp.len();
let config = Config::default(); let fast_config = Config::default();
let window_size = config.window_size; let window_size = fast_config.window_size;
let restart_offset = 1000;
let end_offset = restart_offset + window_size * 4;
let end_height = (START_HEIGHT_FAST + end_offset).min(total_heights);
let restart_at = START_HEIGHT_FAST + restart_offset;
let warmup_start = restart_at - window_size;
let load_start = START_HEIGHT_SLOW;
assert!(
end_height > restart_at,
"indexer has {total_heights} blocks; need at least {} to test restart at {restart_at}",
restart_at + 1
);
println!(
"Loading {} blocks ({load_start}..{end_height})...",
end_height - load_start
);
let total_txs = indexer.vecs.transactions.txid.len(); let total_txs = indexer.vecs.transactions.txid.len();
let total_outputs = indexer.vecs.outputs.value.len(); let total_outputs = indexer.vecs.outputs.value.len();
let first_tx_index: Vec<TxIndex> = indexer.vecs.transactions.first_tx_index.collect(); let first_tx_index: Vec<TxIndex> = indexer.vecs.transactions.first_tx_index.collect();
let out_first: Vec<TxOutIndex> = indexer.vecs.outputs.first_txout_index.collect(); let out_first: Vec<TxOutIndex> = indexer.vecs.outputs.first_txout_index.collect();
let mut txout_cursor = indexer.vecs.transactions.first_txout_index.cursor();
// Reference oracle at 575k. let mut blocks: Vec<Block> = Vec::with_capacity(end_height - load_start);
let ref_start = START_HEIGHT; for h in load_start..end_height {
let mut ref_oracle = Oracle::new(seed_bin(ref_start), Config::default());
// Test oracles every 1000 blocks from 576k to 630k.
let mut runs: Vec<TestRun> = (576_000..=630_000)
.step_by(1000)
.map(|h| TestRun {
start_height: h,
oracle: None,
converged_at: None,
diverged_after: false,
})
.collect();
let last_start = runs.last().map(|r| r.start_height).unwrap_or(ref_start);
// Process enough blocks for all oracles to converge + verification margin.
let end_height = (last_start + window_size + 100).min(total_heights);
for h in START_HEIGHT..end_height {
let ft = first_tx_index[h]; let ft = first_tx_index[h];
let next_ft = first_tx_index let next_ft = first_tx_index
.get(h + 1) .get(h + 1)
.copied() .copied()
.unwrap_or(TxIndex::from(total_txs)); .unwrap_or(TxIndex::from(total_txs));
let block_first_tx = ft.to_usize() + 1;
let out_start = if ft.to_usize() + 1 < next_ft.to_usize() { let tx_count = next_ft.to_usize() - block_first_tx;
indexer
.vecs
.transactions
.first_txout_index
.collect_one(ft + 1)
.unwrap()
.to_usize()
} else {
out_first
.get(h + 1)
.copied()
.unwrap_or(TxOutIndex::from(total_outputs))
.to_usize()
};
let out_end = out_first let out_end = out_first
.get(h + 1) .get(h + 1)
.copied() .copied()
.unwrap_or(TxOutIndex::from(total_outputs)) .unwrap_or(TxOutIndex::from(total_outputs))
.to_usize(); .to_usize();
txout_cursor.advance(block_first_tx - txout_cursor.position());
let mut tx_starts: Vec<usize> = Vec::with_capacity(tx_count);
for _ in 0..tx_count {
tx_starts.push(txout_cursor.next().unwrap().to_usize());
}
let out_start = tx_starts.first().copied().unwrap_or(out_end);
let values: Vec<Sats> = indexer let values: Vec<Sats> = indexer
.vecs .vecs
.outputs .outputs
@@ -112,95 +114,85 @@ fn main() {
.output_type .output_type
.collect_range_at(out_start, out_end); .collect_range_at(out_start, out_end);
let mut hist = Histogram::zeros(); blocks.push(Block {
for (sats, output_type) in values.into_iter().zip(output_types) { height: h,
if let Some(bin) = default_eligible_bin(sats, output_type) { values,
hist.increment(bin as usize); output_types,
} tx_starts,
} out_start,
out_end,
let ref_bin = ref_oracle.process_histogram(&hist); });
for run in &mut runs {
if h < run.start_height {
continue;
}
if run.oracle.is_none() {
run.oracle = Some(Oracle::new(seed_bin(run.start_height), Config::default()));
}
let test_bin = run.oracle.as_mut().unwrap().process_histogram(&hist);
if run.converged_at.is_some() {
if test_bin != ref_bin {
run.diverged_after = true;
}
} else if test_bin == ref_bin {
run.converged_at = Some(h);
}
}
} }
// Print results. let mut continuous = Oracle::from_seed();
println!(); let continuous_bins: Vec<f64> = blocks
println!("{:<12} {:>16} {:>8}", "Start", "Converged at", "Blocks"); .iter()
println!("{}", "-".repeat(40)); .map(|b| {
if b.height == START_HEIGHT_FAST {
let mut max_blocks = 0usize; continuous.reconfigure(fast_config);
let mut failed = Vec::new();
let mut diverged = Vec::new();
for run in &runs {
if let Some(converged) = run.converged_at {
let blocks = converged - run.start_height;
if blocks > max_blocks {
max_blocks = blocks;
} }
println!("{:<12} {:>16} {:>8}", run.start_height, converged, blocks); continuous.process_histogram(&build_histogram(b))
if run.diverged_after { })
diverged.push(run.start_height); .collect();
}
} else {
println!("{:<12} {:>16} {:>8}", run.start_height, "NEVER", "-");
failed.push(run.start_height);
}
}
println!();
println!( println!(
"{}/{} converged, max {} blocks to converge (window_size={})", "Continuous oracle: {} blocks processed",
runs.len() - failed.len(), continuous_bins.len()
runs.len(),
max_blocks,
window_size,
); );
if !diverged.is_empty() { let prev_bin = continuous_bins[restart_at - load_start - 1];
println!("DIVERGED after convergence: {:?}", diverged); let seed_bin = cents_to_bin(bin_to_cents(prev_bin) as f64);
} println!(
if !failed.is_empty() { "Restart at {restart_at}: prev_bin={prev_bin:.4} -> cents -> seed_bin={seed_bin:.4} (delta {:.6})",
println!("NEVER converged: {:?}", failed); seed_bin - prev_bin
);
let warmup_slice = &blocks[warmup_start - load_start..restart_at - load_start];
let mut restored = Oracle::from_checkpoint(seed_bin, fast_config, |o| {
for b in warmup_slice {
o.process_histogram(&build_histogram(b));
}
});
let restored_bins: Vec<f64> = blocks[restart_at - load_start..]
.iter()
.map(|b| restored.process_histogram(&build_histogram(b)))
.collect();
println!("Restored oracle: {} blocks processed", restored_bins.len());
let mut mismatches: Vec<(usize, f64, f64)> = Vec::new();
for (i, &r) in restored_bins.iter().enumerate() {
let c = continuous_bins[restart_at - load_start + i];
if r != c {
mismatches.push((restart_at + i, c, r));
}
} }
// Assertions. println!();
assert!( if mismatches.is_empty() {
failed.is_empty(), println!(
"{} oracles never converged: {:?}", "All {} blocks from {restart_at} onward match exactly.",
failed.len(), restored_bins.len()
failed );
); } else {
assert!( println!(
diverged.is_empty(), "{} of {} blocks differ (showing up to 5):",
"{} oracles diverged after convergence: {:?}", mismatches.len(),
diverged.len(), restored_bins.len()
diverged );
); for (h, c, r) in mismatches.iter().take(5) {
assert!( println!(
max_blocks <= window_size * 2, " h={h}: continuous={c:.6}, restored={r:.6}, delta={:.6}",
"Convergence took {} blocks, expected <= {} (2 * window_size)", r - c
max_blocks, );
window_size * 2 }
}
assert_eq!(
mismatches.len(),
0,
"restored oracle diverged from continuous oracle"
); );
println!(); println!();
println!("All assertions passed!"); println!("Assertion passed: from_checkpoint restart is bit-exact.");
} }
+132
View File
@@ -0,0 +1,132 @@
//! Dump the RAW per-output data over a height range for fully offline analysis.
//! Nothing is filtered or binned, so any downstream filter (round-BTC tolerance,
//! dust floor, type exclusion, OP_RETURN / batch-payout tx drops, log-bin
//! resolution) can be reconstructed in analysis WITHOUT re-dumping.
//!
//! For every non-coinbase output in [ORACLE_START, ORACLE_END) (default
//! 500000..510000) one row is written:
//! oracle_outputs_{start}_{end}.csv height,tx,sats,otype
//! where `tx` is the 0-based index of the (non-coinbase) transaction within the
//! block (so OP_RETURN-tx and >N-output-tx drops can be reapplied by grouping),
//! `sats` is the exact output value, and `otype` is `OutputType as u8`.
//!
//! Plus per-block metadata:
//! oracle_meta_{start}_{end}.csv height,timestamp,ex_low,ex_high,ex_close
//!
//! Run: cargo run -p brk_oracle --example dump_hist --release
use std::{
fs::File,
io::{BufWriter, Write},
path::PathBuf,
};
use brk_indexer::Indexer;
use brk_types::{OutputType, Sats, TxIndex, TxOutIndex};
use vecdb::{AnyVec, ReadableVec, VecIndex};
fn main() {
let data_dir = std::env::var("BRK_DIR")
.map(PathBuf::from)
.unwrap_or_else(|_| PathBuf::from(std::env::var("HOME").unwrap()).join(".brk"));
let out_dir = std::env::var("DUMP_DIR").unwrap_or_else(|_| "/tmp".to_string());
let start: usize = std::env::var("ORACLE_START")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(500_000);
let end: usize = std::env::var("ORACLE_END")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(510_000);
let indexer = Indexer::forced_import(&data_dir).expect("Failed to load indexer");
let total_heights = indexer.vecs.blocks.timestamp.len();
let end = end.min(total_heights);
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let height_ohlc: Vec<[f64; 4]> = serde_json::from_str(
&std::fs::read_to_string(format!("{manifest_dir}/examples/height_price_ohlc.json"))
.expect("read height_price_ohlc.json"),
)
.expect("parse height OHLC");
let timestamps: Vec<brk_types::Timestamp> = indexer.vecs.blocks.timestamp.collect();
let total_txs = indexer.vecs.transactions.txid.len();
let total_outputs = indexer.vecs.outputs.value.len();
let first_tx_index: Vec<TxIndex> = indexer.vecs.transactions.first_tx_index.collect();
let out_first: Vec<TxOutIndex> = indexer.vecs.outputs.first_txout_index.collect();
let mut txout_cursor = indexer.vecs.transactions.first_txout_index.cursor();
let mut tx_starts: Vec<usize> = Vec::new();
let out_path = format!("{out_dir}/oracle_outputs_{start}_{end}.csv");
let meta_path = format!("{out_dir}/oracle_meta_{start}_{end}.csv");
let mut out_w = BufWriter::new(File::create(&out_path).expect("create outputs csv"));
let mut meta_w = BufWriter::new(File::create(&meta_path).expect("create meta csv"));
writeln!(out_w, "height,tx,sats,otype").unwrap();
writeln!(meta_w, "height,timestamp,ex_low,ex_high,ex_close").unwrap();
eprintln!(
"otype legend: OpReturn={} P2TR={}",
OutputType::OpReturn as u8,
OutputType::P2TR as u8
);
let mut rows: u64 = 0;
for h in start..end {
let ft = first_tx_index[h];
let next_ft = first_tx_index
.get(h + 1)
.copied()
.unwrap_or(TxIndex::from(total_txs));
let block_first_tx = ft.to_usize() + 1; // skip coinbase
let tx_count = next_ft.to_usize() - block_first_tx;
let out_end = out_first
.get(h + 1)
.copied()
.unwrap_or(TxOutIndex::from(total_outputs))
.to_usize();
txout_cursor.advance(block_first_tx - txout_cursor.position());
tx_starts.clear();
for _ in 0..tx_count {
tx_starts.push(txout_cursor.next().unwrap().to_usize());
}
let out_start = tx_starts.first().copied().unwrap_or(out_end);
let values: Vec<Sats> = indexer
.vecs
.outputs
.value
.collect_range_at(out_start, out_end);
let output_types: Vec<OutputType> = indexer
.vecs
.outputs
.output_type
.collect_range_at(out_start, out_end);
for tx in 0..tx_count {
let lo = tx_starts[tx] - out_start;
let hi = tx_starts
.get(tx + 1)
.map(|s| s - out_start)
.unwrap_or(out_end - out_start);
for i in lo..hi {
writeln!(out_w, "{h},{tx},{},{}", *values[i], output_types[i] as u8).unwrap();
rows += 1;
}
}
let o = height_ohlc.get(h).copied().unwrap_or([0.0; 4]);
writeln!(
meta_w,
"{h},{},{:.2},{:.2},{:.2}",
*timestamps[h], o[2], o[1], o[3]
)
.unwrap();
}
out_w.flush().unwrap();
meta_w.flush().unwrap();
eprintln!("wrote {out_path} ({rows} output rows)");
eprintln!("wrote {meta_path}");
}

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