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
/*.json
/*.html
!/btc-cycle-sim.html
/research
/filter_*
/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.license = "MIT"
package.edition = "2024"
package.version = "0.3.0"
package.version = "0.3.4"
package.homepage = "https://bitcoinresearchkit.org"
package.repository = "https://github.com/bitcoinresearchkit/brk"
package.readme = "README.md"
@@ -35,38 +35,38 @@ debug = true
[workspace.dependencies]
aide = { version = "0.16.0-alpha.4", features = ["axum-json", "axum-query"] }
axum = { version = "0.8.9", default-features = false, features = ["http1", "json", "query", "tokio", "tracing"] }
bitcoin = { version = "0.32.9", features = ["serde"] }
brk_alloc = { version = "0.3.0", path = "crates/brk_alloc" }
brk_bencher = { version = "0.3.0", path = "crates/brk_bencher" }
brk_bindgen = { version = "0.3.0", path = "crates/brk_bindgen" }
brk_cli = { version = "0.3.0", path = "crates/brk_cli" }
brk_client = { version = "0.3.0", path = "crates/brk_client" }
brk_cohort = { version = "0.3.0", path = "crates/brk_cohort" }
brk_computer = { version = "0.3.0", path = "crates/brk_computer" }
brk_error = { version = "0.3.0", path = "crates/brk_error" }
brk_fetcher = { version = "0.3.0", path = "crates/brk_fetcher" }
brk_indexer = { version = "0.3.0", path = "crates/brk_indexer" }
brk_iterator = { version = "0.3.0", path = "crates/brk_iterator" }
brk_logger = { version = "0.3.0", path = "crates/brk_logger" }
brk_mempool = { version = "0.3.0", path = "crates/brk_mempool" }
brk_oracle = { version = "0.3.0", path = "crates/brk_oracle" }
brk_query = { version = "0.3.0", path = "crates/brk_query", features = ["tokio"] }
brk_reader = { version = "0.3.0", path = "crates/brk_reader" }
brk_rpc = { version = "0.3.0", path = "crates/brk_rpc" }
brk_server = { version = "0.3.0", path = "crates/brk_server" }
brk_store = { version = "0.3.0", path = "crates/brk_store" }
brk_traversable = { version = "0.3.0", path = "crates/brk_traversable", features = ["pco", "derive"] }
brk_traversable_derive = { version = "0.3.0", path = "crates/brk_traversable_derive" }
brk_types = { version = "0.3.0", path = "crates/brk_types" }
brk_website = { version = "0.3.0", path = "crates/brk_website" }
bitcoin = { version = "0.32.10", features = ["serde"] }
brk_alloc = { version = "0.3.4", path = "crates/brk_alloc" }
brk_bencher = { version = "0.3.4", path = "crates/brk_bencher" }
brk_bindgen = { version = "0.3.4", path = "crates/brk_bindgen" }
brk_cli = { version = "0.3.4", path = "crates/brk_cli" }
brk_client = { version = "0.3.4", path = "crates/brk_client" }
brk_cohort = { version = "0.3.4", path = "crates/brk_cohort" }
brk_computer = { version = "0.3.4", path = "crates/brk_computer" }
brk_error = { version = "0.3.4", path = "crates/brk_error" }
brk_fetcher = { version = "0.3.4", path = "crates/brk_fetcher" }
brk_indexer = { version = "0.3.4", path = "crates/brk_indexer" }
brk_iterator = { version = "0.3.4", path = "crates/brk_iterator" }
brk_logger = { version = "0.3.4", path = "crates/brk_logger" }
brk_mempool = { version = "0.3.4", path = "crates/brk_mempool" }
brk_oracle = { version = "0.3.4", path = "crates/brk_oracle" }
brk_query = { version = "0.3.4", path = "crates/brk_query", features = ["tokio"] }
brk_reader = { version = "0.3.4", path = "crates/brk_reader" }
brk_rpc = { version = "0.3.4", path = "crates/brk_rpc" }
brk_server = { version = "0.3.4", path = "crates/brk_server" }
brk_store = { version = "0.3.4", path = "crates/brk_store" }
brk_traversable = { version = "0.3.4", path = "crates/brk_traversable", features = ["pco", "derive"] }
brk_traversable_derive = { version = "0.3.4", path = "crates/brk_traversable_derive" }
brk_types = { version = "0.3.4", path = "crates/brk_types" }
brk_website = { version = "0.3.4", path = "crates/brk_website" }
byteview = "0.10.1"
color-eyre = "0.6.5"
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"] }
fjall = "3.1.4"
fjall = "3.1.5"
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"
parking_lot = "0.12.5"
pco = "1.0.2"
@@ -76,10 +76,10 @@ schemars = { version = "1.2.1", features = ["indexmap2"] }
serde = "1.0.228"
serde_bytes = "0.11.19"
serde_derive = "1.0.228"
serde_json = { version = "1.0.149", features = ["float_roundtrip", "preserve_order"] }
smallvec = "1.15.1"
serde_json = { version = "1.0.150", features = ["float_roundtrip", "preserve_order"] }
smallvec = "1.15.2"
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"
tracing = { version = "0.1", default-features = false, features = ["std"] }
ureq = { version = "3.3.0", features = ["json"] }
+2 -2
View File
@@ -8,5 +8,5 @@ homepage.workspace = true
repository.workspace = true
[dependencies]
libmimalloc-sys = { version = "0.1.47", features = ["extended"] }
mimalloc = { version = "0.1.50" }
libmimalloc-sys = { version = "0.1.49", features = ["extended"] }
mimalloc = { version = "0.1.52" }
+4 -3
View File
@@ -6,9 +6,9 @@
use std::collections::BTreeMap;
use brk_cohort::{
AGE_RANGE_NAMES, AMOUNT_RANGE_NAMES, CLASS_NAMES, EPOCH_NAMES, LOSS_NAMES, OVER_AGE_NAMES,
OVER_AMOUNT_NAMES, PROFIT_NAMES, PROFITABILITY_RANGE_NAMES, SPENDABLE_TYPE_NAMES, TERM_NAMES,
UNDER_AGE_NAMES, UNDER_AMOUNT_NAMES,
AGE_RANGE_NAMES, AMOUNT_RANGE_NAMES, CLASS_NAMES, ENTRY_NAMES, EPOCH_NAMES, LOSS_NAMES,
OVER_AGE_NAMES, OVER_AMOUNT_NAMES, PROFIT_NAMES, PROFITABILITY_RANGE_NAMES,
SPENDABLE_TYPE_NAMES, TERM_NAMES, UNDER_AGE_NAMES, UNDER_AMOUNT_NAMES,
};
use brk_types::{Index, pools};
use serde::Serialize;
@@ -59,6 +59,7 @@ impl CohortConstants {
("TERM_NAMES", to_value(&TERM_NAMES)),
("EPOCH_NAMES", to_value(&EPOCH_NAMES)),
("CLASS_NAMES", to_value(&CLASS_NAMES)),
("ENTRY_NAMES", to_value(&ENTRY_NAMES)),
("SPENDABLE_TYPE_NAMES", to_value(&SPENDABLE_TYPE_NAMES)),
("AGE_RANGE_NAMES", to_value(&AGE_RANGE_NAMES)),
("UNDER_AGE_NAMES", to_value(&UNDER_AGE_NAMES)),
@@ -51,7 +51,7 @@ fn generate_get_method(output: &mut String, endpoint: &Endpoint) {
}
writeln!(
output,
" * @param {{{{ signal?: AbortSignal, onValue?: (value: {}) => void }}}} [options]",
" * @param {{{{ signal?: AbortSignal, onValue?: (value: {}) => void, cache?: boolean }}}} [options]",
return_type
)
.unwrap();
@@ -60,22 +60,22 @@ fn generate_get_method(output: &mut String, endpoint: &Endpoint) {
let params = build_method_params(endpoint);
let params_with_opts = if params.is_empty() {
"{ signal, onValue } = {}".to_string()
"{ signal, onValue, cache } = {}".to_string()
} else {
format!("{}, {{ signal, onValue }} = {{}}", params)
format!("{}, {{ signal, onValue, cache }} = {{}}", params)
};
writeln!(output, " async {}({}) {{", method_name, params_with_opts).unwrap();
let path = build_path_template(&endpoint.path, &endpoint.path_params);
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() {
"this.getJson(path, { signal, onValue })".to_string()
"this.getJson(path, { signal, onValue, cache })".to_string()
} 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 {
"this.getText(path, { signal, onValue })".to_string()
"this.getText(path, { signal, onValue, cache })".to_string()
};
write_path_assignment(output, endpoint, &path);
@@ -83,7 +83,7 @@ fn generate_get_method(output: &mut String, endpoint: &Endpoint) {
if endpoint.supports_csv {
writeln!(
output,
" if (format === 'csv') return this.getText(path, {{ signal, onValue }});"
" if (format === 'csv') return this.getText(path, {{ signal, onValue, cache }});"
)
.unwrap();
}
@@ -448,14 +448,17 @@ class BrkClientBase {{
/**
* @param {{string}} path
* @param {{{{ signal?: AbortSignal }}}} [options]
* @param {{{{ signal?: AbortSignal, cache?: boolean }}}} [options]
* @returns {{Promise<Response>}}
*/
async get(path, {{ signal }} = {{}}) {{
async get(path, {{ signal, cache = true }} = {{}}) {{
const url = `${{this.baseUrl}}${{path}}`;
const signals = [AbortSignal.timeout(this.timeout)];
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);
return res;
}}
@@ -475,14 +478,21 @@ class BrkClientBase {{
* @template T
* @param {{string}} path
* @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>}}
*/
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}}`;
/** @type {{_MemEntry<T> | undefined}} */
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.
// ETag match → zero parse, zero clone, zero cache write, no second onValue fire.
@@ -497,8 +507,8 @@ class BrkClientBase {{
this._memSet(url, netEtag, value);
if (onValue) onValue(value);
if (cloned && browserCache) {{
const cache = browserCache;
_runIdle(() => cache.put(url, cloned));
const cacheStore = browserCache;
_runIdle(() => cacheStore.put(url, cloned));
}}
return value;
}} catch {{
@@ -531,8 +541,8 @@ class BrkClientBase {{
this._memSet(url, netEtag, value);
if (onValue) onValue(value);
if (cloned && browserCache) {{
const cache = browserCache;
_runIdle(() => cache.put(url, cloned));
const cacheStore = browserCache;
_runIdle(() => cacheStore.put(url, cloned));
}}
return value;
}} catch (e) {{
@@ -546,7 +556,7 @@ class BrkClientBase {{
* Make a GET request expecting a JSON response. Cached and supports `onValue`.
* @template T
* @param {{string}} path
* @param {{{{ onValue?: (value: T) => void, signal?: AbortSignal }}}} [options]
* @param {{{{ onValue?: (value: T) => void, signal?: AbortSignal, cache?: boolean }}}} [options]
* @returns {{Promise<T>}}
*/
getJson(path, options) {{
@@ -557,7 +567,7 @@ class BrkClientBase {{
* Make a GET request expecting a text response (text/plain, text/csv, ...).
* Cached and supports `onValue`, same as `getJson`.
* @param {{string}} path
* @param {{{{ onValue?: (value: string) => void, signal?: AbortSignal }}}} [options]
* @param {{{{ onValue?: (value: string) => void, signal?: AbortSignal, cache?: boolean }}}} [options]
* @returns {{Promise<string>}}
*/
getText(path, options) {{
@@ -568,7 +578,7 @@ class BrkClientBase {{
* Make a GET request expecting binary data (application/octet-stream).
* Cached and supports `onValue`, same as `getJson`.
* @param {{string}} path
* @param {{{{ onValue?: (value: Uint8Array) => void, signal?: AbortSignal }}}} [options]
* @param {{{{ onValue?: (value: Uint8Array) => void, signal?: AbortSignal, cache?: boolean }}}} [options]
* @returns {{Promise<Uint8Array>}}
*/
getBytes(path, options) {{
+1 -1
View File
@@ -17,7 +17,7 @@ fn main() -> brk_client::Result<()> {
// day1() returns DateMetricEndpointBuilder, so fetch() returns DateMetricData
let price_close = client
.series()
.prices
.price
.split
.close
.usd
+673 -34
View File
@@ -1191,6 +1191,22 @@ pub struct CapCapitalizedGrossLossMvrvNetPeakPriceProfitSellSoprPattern {
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.
pub struct EmptyOpP2aP2msP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshUnknownPattern2 {
pub empty: _1m1w1y24hBpsPercentRatioPattern,
@@ -1658,6 +1674,17 @@ pub struct ActiveInputOutputSpendablePattern {
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.
pub struct CapLossMvrvNetPriceProfitSoprPattern {
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.
pub struct RatioValuePattern {
pub ratio: _24hPattern,
@@ -3534,7 +3577,7 @@ pub struct SeriesTree {
pub investing: SeriesTree_Investing,
pub market: SeriesTree_Market,
pub pools: SeriesTree_Pools,
pub prices: SeriesTree_Prices,
pub price: SeriesTree_Price,
pub supply: SeriesTree_Supply,
pub cohorts: SeriesTree_Cohorts,
}
@@ -3556,7 +3599,7 @@ impl SeriesTree {
investing: SeriesTree_Investing::new(client.clone(), format!("{base_path}_investing")),
market: SeriesTree_Market::new(client.clone(), format!("{base_path}_market")),
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")),
cohorts: SeriesTree_Cohorts::new(client.clone(), format!("{base_path}_cohorts")),
}
@@ -7063,31 +7106,31 @@ impl SeriesTree_Pools_Minor {
}
/// Series tree node.
pub struct SeriesTree_Prices {
pub split: SeriesTree_Prices_Split,
pub ohlc: SeriesTree_Prices_Ohlc,
pub spot: SeriesTree_Prices_Spot,
pub struct SeriesTree_Price {
pub split: SeriesTree_Price_Split,
pub ohlc: SeriesTree_Price_Ohlc,
pub spot: SeriesTree_Price_Spot,
}
impl SeriesTree_Prices {
impl SeriesTree_Price {
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
Self {
split: SeriesTree_Prices_Split::new(client.clone(), format!("{base_path}_split")),
ohlc: SeriesTree_Prices_Ohlc::new(client.clone(), format!("{base_path}_ohlc")),
spot: SeriesTree_Prices_Spot::new(client.clone(), format!("{base_path}_spot")),
split: SeriesTree_Price_Split::new(client.clone(), format!("{base_path}_split")),
ohlc: SeriesTree_Price_Ohlc::new(client.clone(), format!("{base_path}_ohlc")),
spot: SeriesTree_Price_Spot::new(client.clone(), format!("{base_path}_spot")),
}
}
}
/// Series tree node.
pub struct SeriesTree_Prices_Split {
pub struct SeriesTree_Price_Split {
pub open: CentsSatsUsdPattern3,
pub high: CentsSatsUsdPattern3,
pub low: CentsSatsUsdPattern3,
pub close: CentsSatsUsdPattern3,
}
impl SeriesTree_Prices_Split {
impl SeriesTree_Price_Split {
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
Self {
open: CentsSatsUsdPattern3::new(client.clone(), "price_open".to_string()),
@@ -7099,13 +7142,13 @@ impl SeriesTree_Prices_Split {
}
/// Series tree node.
pub struct SeriesTree_Prices_Ohlc {
pub struct SeriesTree_Price_Ohlc {
pub usd: SeriesPattern2<OHLCDollars>,
pub cents: SeriesPattern2<OHLCCents>,
pub sats: SeriesPattern2<OHLCSats>,
}
impl SeriesTree_Prices_Ohlc {
impl SeriesTree_Price_Ohlc {
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
Self {
usd: SeriesPattern2::new(client.clone(), "price_ohlc".to_string()),
@@ -7116,13 +7159,13 @@ impl SeriesTree_Prices_Ohlc {
}
/// Series tree node.
pub struct SeriesTree_Prices_Spot {
pub struct SeriesTree_Price_Spot {
pub usd: SeriesPattern1<Dollars>,
pub cents: SeriesPattern1<Cents>,
pub sats: SeriesPattern1<Sats>,
}
impl SeriesTree_Prices_Spot {
impl SeriesTree_Price_Spot {
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
Self {
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 epoch: SeriesTree_Cohorts_Utxo_Epoch,
pub class: SeriesTree_Cohorts_Utxo_Class,
pub entry: SeriesTree_Cohorts_Utxo_Entry,
pub over_amount: SeriesTree_Cohorts_Utxo_OverAmount,
pub amount_range: SeriesTree_Cohorts_Utxo_AmountRange,
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")),
epoch: SeriesTree_Cohorts_Utxo_Epoch::new(client.clone(), format!("{base_path}_epoch")),
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")),
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")),
@@ -7999,7 +8044,7 @@ pub struct SeriesTree_Cohorts_Utxo_Lth_Realized {
pub price: SeriesTree_Cohorts_Utxo_Lth_Realized_Price,
pub mvrv: SeriesPattern1<StoredF32>,
pub net_pnl: BlockChangeCumulativeDeltaSumPattern,
pub sopr: SeriesTree_Cohorts_Utxo_Lth_Realized_Sopr,
pub sopr: RatioValuePattern2,
pub gross_pnl: BlockCumulativeSumPattern,
pub sell_side_risk_ratio: _1m1w1y24hPattern8,
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")),
mvrv: SeriesPattern1::new(client.clone(), "lth_mvrv".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()),
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()),
@@ -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.
pub struct SeriesTree_Cohorts_Utxo_AgeRange {
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.
pub struct SeriesTree_Cohorts_Utxo_OverAmount {
pub _1sat: ActivityOutputsRealizedSupplyUnrealizedPattern2,
@@ -8953,7 +9538,7 @@ pub struct BrkClient {
impl BrkClient {
/// 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.
pub fn new(base_url: impl Into<String>) -> Self {
@@ -9281,6 +9866,15 @@ impl BrkClient {
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
///
/// 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"))
}
/// 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
///
/// 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.
///
/// Prefix rules:
/// - No prefix: `All`, `Term`, `Epoch`, `Class`, `Type`
/// - No prefix: `All`, `Term`, `Epoch`, `Class`, `Entry`, `Type`
/// - Context prefix: `Time`, `Amount`
pub fn full_name(&self, filter: &Filter, name: &str) -> String {
match filter {
@@ -32,6 +32,7 @@ impl CohortContext {
| Filter::Term(_)
| Filter::Epoch(_)
| Filter::Class(_)
| Filter::Entry(_)
| Filter::Type(_) => name.to_string(),
Filter::Time(_) | Filter::Amount(_) => self.prefixed(name),
}
+8 -3
View File
@@ -1,6 +1,6 @@
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)]
pub enum Filter {
@@ -10,6 +10,7 @@ pub enum Filter {
Amount(AmountFilter),
Epoch(Halving),
Class(Year),
Entry(EntryPrice),
Type(OutputType),
}
@@ -68,7 +69,8 @@ impl Filter {
}
/// 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
pub fn is_extended(&self, context: CohortContext) -> bool {
match context {
@@ -76,7 +78,10 @@ impl Filter {
CohortContext::Utxo => {
matches!(
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_any_addr;
mod by_epoch;
mod by_entry;
mod by_term;
mod by_type;
mod class;
@@ -36,6 +37,7 @@ pub use amount_range::*;
pub use by_addr_type::*;
pub use by_any_addr::*;
pub use by_epoch::*;
pub use by_entry::*;
pub use by_term::*;
pub use by_type::*;
pub use class::*;
+10 -2
View File
@@ -2,8 +2,8 @@ use brk_traversable::Traversable;
use rayon::prelude::*;
use crate::{
AgeRange, AmountRange, ByEpoch, ByTerm, Class, Filter, OverAge, OverAmount, SpendableType,
UnderAge, UnderAmount,
AgeRange, AmountRange, ByEntry, ByEpoch, ByTerm, Class, Filter, OverAge, OverAmount,
SpendableType, UnderAge, UnderAmount,
};
#[derive(Default, Clone, Traversable)]
@@ -12,6 +12,7 @@ pub struct UTXOGroups<T> {
pub age_range: AgeRange<T>,
pub epoch: ByEpoch<T>,
pub class: Class<T>,
pub entry: ByEntry<T>,
pub over_age: OverAge<T>,
pub over_amount: OverAmount<T>,
pub amount_range: AmountRange<T>,
@@ -31,6 +32,7 @@ impl<T> UTXOGroups<T> {
age_range: AgeRange::new(&mut create),
epoch: ByEpoch::new(&mut create),
class: Class::new(&mut create),
entry: ByEntry::new(&mut create),
over_age: OverAge::new(&mut create),
over_amount: OverAmount::new(&mut create),
amount_range: AmountRange::new(&mut create),
@@ -51,6 +53,7 @@ impl<T> UTXOGroups<T> {
.chain(self.age_range.iter())
.chain(self.epoch.iter())
.chain(self.class.iter())
.chain(self.entry.iter())
.chain(self.amount_range.iter())
.chain(self.under_amount.iter())
.chain(self.type_.iter())
@@ -66,6 +69,7 @@ impl<T> UTXOGroups<T> {
.chain(self.age_range.iter_mut())
.chain(self.epoch.iter_mut())
.chain(self.class.iter_mut())
.chain(self.entry.iter_mut())
.chain(self.amount_range.iter_mut())
.chain(self.under_amount.iter_mut())
.chain(self.type_.iter_mut())
@@ -84,6 +88,7 @@ impl<T> UTXOGroups<T> {
.chain(self.age_range.par_iter_mut())
.chain(self.epoch.par_iter_mut())
.chain(self.class.par_iter_mut())
.chain(self.entry.par_iter_mut())
.chain(self.amount_range.par_iter_mut())
.chain(self.under_amount.par_iter_mut())
.chain(self.type_.par_iter_mut())
@@ -94,6 +99,7 @@ impl<T> UTXOGroups<T> {
.iter()
.chain(self.epoch.iter())
.chain(self.class.iter())
.chain(self.entry.iter())
.chain(self.amount_range.iter())
.chain(self.type_.iter())
}
@@ -103,6 +109,7 @@ impl<T> UTXOGroups<T> {
.iter_mut()
.chain(self.epoch.iter_mut())
.chain(self.class.iter_mut())
.chain(self.entry.iter_mut())
.chain(self.amount_range.iter_mut())
.chain(self.type_.iter_mut())
}
@@ -115,6 +122,7 @@ impl<T> UTXOGroups<T> {
.par_iter_mut()
.chain(self.epoch.par_iter_mut())
.chain(self.class.par_iter_mut())
.chain(self.entry.par_iter_mut())
.chain(self.amount_range.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 super::Vecs;
use crate::{blocks, distribution, mining, prices, supply};
use crate::{blocks, distribution, mining, price, supply};
impl Vecs {
#[allow(clippy::too_many_arguments)]
pub(crate) fn compute(
&mut self,
indexer: &Indexer,
prices: &prices::Vecs,
prices: &price::Vecs,
blocks: &blocks::Vecs,
mining: &mining::Vecs,
supply_vecs: &supply::Vecs,
@@ -5,14 +5,14 @@ use vecdb::Exit;
use super::super::{activity, cap, supply};
use super::Vecs;
use crate::{distribution, prices};
use crate::{distribution, price};
impl Vecs {
#[allow(clippy::too_many_arguments)]
pub(crate) fn compute(
&mut self,
indexer: &Indexer,
prices: &prices::Vecs,
prices: &price::Vecs,
distribution: &distribution::Vecs,
activity: &activity::Vecs,
supply: &supply::Vecs,
@@ -4,14 +4,14 @@ use brk_types::StoredF64;
use vecdb::Exit;
use super::{super::value, Vecs};
use crate::{blocks, internal::algo::ComputeRollingMedianFromStarts, prices};
use crate::{blocks, internal::algo::ComputeRollingMedianFromStarts, price};
impl Vecs {
pub(crate) fn compute(
&mut self,
indexer: &Indexer,
blocks: &blocks::Vecs,
prices: &prices::Vecs,
prices: &price::Vecs,
value: &value::Vecs,
exit: &Exit,
) -> Result<()> {
@@ -4,13 +4,13 @@ use vecdb::Exit;
use super::super::activity;
use super::Vecs;
use crate::{distribution, prices};
use crate::{distribution, price};
impl Vecs {
pub(crate) fn compute(
&mut self,
indexer: &Indexer,
prices: &prices::Vecs,
prices: &price::Vecs,
distribution: &distribution::Vecs,
activity: &activity::Vecs,
exit: &Exit,
@@ -5,13 +5,13 @@ use vecdb::Exit;
use super::super::activity;
use super::Vecs;
use crate::{distribution, prices};
use crate::{distribution, price};
impl Vecs {
pub(crate) fn compute(
&mut self,
indexer: &Indexer,
prices: &prices::Vecs,
prices: &price::Vecs,
distribution: &distribution::Vecs,
activity: &activity::Vecs,
exit: &Exit,
@@ -45,7 +45,7 @@ use super::{
count::AddrCountFundedTotalVecs,
supply::{AddrSupplyShareVecs, AddrSupplyVecs},
};
use crate::{indexes, prices};
use crate::{indexes, price};
mod state;
@@ -104,7 +104,7 @@ impl ExposedAddrVecs {
pub(crate) fn compute_rest(
&mut self,
starting_lengths: &Lengths,
prices: &prices::Vecs,
prices: &price::Vecs,
all_supply_sats: &impl ReadableVec<Height, Sats>,
type_supply_sats: &ByAddrType<&impl ReadableVec<Height, Sats>>,
exit: &Exit,
@@ -35,7 +35,7 @@ use super::{
use crate::{
indexes, inputs,
internal::{WindowStartVec, Windows},
outputs, prices,
outputs, price,
};
mod state;
@@ -112,7 +112,7 @@ impl ReusedAddrVecs {
starting_lengths: &Lengths,
outputs_by_type: &outputs::ByTypeVecs,
inputs_by_type: &inputs::ByTypeVecs,
prices: &prices::Vecs,
prices: &price::Vecs,
all_supply_sats: &impl ReadableVec<Height, Sats>,
type_supply_sats: &ByAddrType<&impl ReadableVec<Height, Sats>>,
exit: &Exit,
@@ -13,7 +13,7 @@ use crate::{
distribution::DynCohortVecs,
indexes,
internal::{WindowStartVec, Windows},
prices,
price,
};
use super::{super::traits::CohortVecs, vecs::AddrCohortVecs};
@@ -95,7 +95,7 @@ impl AddrCohorts {
/// First phase of post-processing: compute index transforms.
pub(crate) fn compute_rest_part1(
&mut self,
prices: &prices::Vecs,
prices: &price::Vecs,
starting_lengths: &Lengths,
exit: &Exit,
) -> Result<()> {
@@ -108,7 +108,7 @@ impl AddrCohorts {
/// Second phase of post-processing: compute relative metrics.
pub(crate) fn compute_rest_part2(
&mut self,
prices: &prices::Vecs,
prices: &price::Vecs,
starting_lengths: &Lengths,
all_supply_sats: &impl ReadableVec<Height, Sats>,
all_utxo_count: &impl ReadableVec<Height, StoredU64>,
@@ -12,7 +12,7 @@ use crate::{
distribution::state::{AddrCohortState, MinimalRealizedState},
indexes,
internal::{PerBlockWithDeltas, WindowStartVec, Windows},
prices,
price,
};
use crate::distribution::metrics::{ImportConfig, MinimalCohortMetrics};
@@ -174,7 +174,7 @@ impl DynCohortVecs for AddrCohortVecs {
fn compute_rest_part1(
&mut self,
prices: &prices::Vecs,
prices: &price::Vecs,
starting_lengths: &Lengths,
exit: &Exit,
) -> Result<()> {
@@ -229,7 +229,7 @@ impl CohortVecs for AddrCohortVecs {
fn compute_rest_part2(
&mut self,
prices: &prices::Vecs,
prices: &price::Vecs,
starting_lengths: &Lengths,
all_supply_sats: &impl ReadableVec<Height, Sats>,
all_utxo_count: &impl ReadableVec<Height, StoredU64>,
@@ -3,7 +3,7 @@ use brk_indexer::Lengths;
use brk_types::{Cents, Height, Sats, StoredU64, Version};
use vecdb::{Exit, ReadableVec};
use crate::prices;
use crate::price;
/// Dynamic dispatch trait for cohort vectors.
///
@@ -31,7 +31,7 @@ pub trait DynCohortVecs: Send + Sync {
/// First phase of post-processing computations.
fn compute_rest_part1(
&mut self,
prices: &prices::Vecs,
prices: &price::Vecs,
starting_lengths: &Lengths,
exit: &Exit,
) -> Result<()>;
@@ -61,7 +61,7 @@ pub trait CohortVecs: DynCohortVecs {
/// Second phase of post-processing computations.
fn compute_rest_part2(
&mut self,
prices: &prices::Vecs,
prices: &price::Vecs,
starting_lengths: &Lengths,
all_supply_sats: &impl ReadableVec<Height, Sats>,
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 {
all_sats: i64,
sth_sats: i64,
discount_sats: i64,
all_usd: i128,
sth_usd: i128,
discount_usd: i128,
}
impl CostBasisNode {
#[inline(always)]
fn new(sats: i64, usd: i128, is_sth: bool) -> Self {
fn new_supply(sats: i64, usd: i128, is_sth: bool) -> Self {
Self {
all_sats: sats,
sth_sats: if is_sth { sats } else { 0 },
discount_sats: 0,
all_usd: usd,
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) {
self.all_sats += other.all_sats;
self.sth_sats += other.sth_sats;
self.discount_sats += other.discount_sats;
self.all_usd += other.all_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 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.totals.add_assign(&delta);
}
/// Bulk-initialize from BTreeMaps (one per age-range cohort).
/// Call after state import when all pending maps have been drained.
pub(super) fn bulk_init<'a>(
/// Apply a net delta from the discount-entry cohort.
///
/// 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,
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.totals = CostBasisNode::default();
@@ -169,7 +205,18 @@ impl CostBasisFenwick {
for (&price, &sats) in map.iter() {
let bucket = price_to_bucket(price);
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.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(
&self,
total_sats: i64,
@@ -271,6 +338,37 @@ impl CostBasisFenwick {
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 low = Cents::from((spot_f64 * 0.95) as u64);
let high = Cents::from((spot_f64 * 1.05) as u64);
@@ -285,24 +383,23 @@ impl CostBasisFenwick {
CostBasisNode::default()
};
let all_range = (cum_high.all_sats - cum_low.all_sats).max(0);
let sth_range = (cum_high.sth_sats - cum_low.sth_sats).max(0);
let lth_range = all_range - sth_range;
CostBasisNode {
all_sats: cum_high.all_sats - cum_low.all_sats,
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 {
if total <= 0 {
0
} 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),
)
#[inline(always)]
fn to_bps(range: i64, total: i64) -> u16 {
if total <= 0 {
0
} else {
(range as f64 / total as f64 * 10000.0).round() as u16
}
}
// -----------------------------------------------------------------------
@@ -1,8 +1,8 @@
use std::path::Path;
use brk_cohort::{
AgeRange, AmountRange, ByEpoch, Class, CohortContext, Filter, Filtered, OverAge, OverAmount,
SpendableType, Term, UnderAge, UnderAmount,
AgeRange, AmountRange, ByEntry, ByEpoch, Class, CohortContext, Filter, Filtered, OverAge,
OverAmount, SpendableType, Term, UnderAge, UnderAmount,
};
use brk_error::Result;
use brk_indexer::Lengths;
@@ -16,7 +16,6 @@ use vecdb::{
use crate::{
blocks,
distribution::{
DynCohortVecs,
metrics::{
AllCohortMetrics, BasicCohortMetrics, CohortMetricsBase, CoreCohortMetrics,
ExtendedAdjustedCohortMetrics, ExtendedCohortMetrics, ImportConfig,
@@ -24,10 +23,11 @@ use crate::{
TypeCohortMetrics,
},
state::UTXOCohortState,
DynCohortVecs,
},
indexes,
internal::{ValuePerBlockCumulativeRolling, WindowStartVec, Windows},
prices,
price,
};
use super::{fenwick::CostBasisFenwick, vecs::UTXOCohortVecs};
@@ -45,6 +45,7 @@ pub struct UTXOCohorts<M: StorageMode = Rw> {
pub over_age: OverAge<UTXOCohortVecs<CoreCohortMetrics<M>>>,
pub epoch: ByEpoch<UTXOCohortVecs<CoreCohortMetrics<M>>>,
pub class: Class<UTXOCohortVecs<CoreCohortMetrics<M>>>,
pub entry: ByEntry<UTXOCohortVecs<ExtendedCohortMetrics<M>>>,
pub over_amount: OverAmount<UTXOCohortVecs<MinimalCohortMetrics<M>>>,
pub amount_range: AmountRange<UTXOCohortVecs<MinimalCohortMetrics<M>>>,
pub under_amount: UnderAmount<UTXOCohortVecs<MinimalCohortMetrics<M>>>,
@@ -67,8 +68,10 @@ pub(crate) struct UTXOCohortsTransientState {
}
impl UTXOCohorts<Rw> {
/// ~71 separate cohorts (21 age + 5 epoch + 18 class + 15 amount + 12 type)
const SEPARATE_COHORT_CAPACITY: usize = 80;
/// Separate cohorts currently total 72:
/// 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.
pub(crate) fn forced_import(
@@ -136,6 +139,26 @@ impl UTXOCohorts<Rw> {
let epoch = ByEpoch::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
let minimal_separate =
|f: Filter, name: &'static str| -> Result<UTXOCohortVecs<MinimalCohortMetrics>> {
@@ -281,6 +304,7 @@ impl UTXOCohorts<Rw> {
lth,
epoch,
class,
entry,
type_,
under_age,
over_age,
@@ -309,6 +333,7 @@ impl UTXOCohorts<Rw> {
sth,
caches,
age_range,
entry,
..
} = self;
caches
@@ -327,7 +352,15 @@ impl UTXOCohorts<Rw> {
Some((map, caches.fenwick.is_sth_at(i)))
})
.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.
@@ -338,7 +371,10 @@ impl UTXOCohorts<Rw> {
}
// Destructure to get separate borrows on caches and age_range
let Self {
caches, age_range, ..
caches,
age_range,
entry,
..
} = self;
for (i, sub) in age_range.iter().enumerate() {
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.
@@ -365,6 +406,7 @@ impl UTXOCohorts<Rw> {
age_range,
epoch,
class,
entry,
amount_range,
type_,
..
@@ -374,6 +416,7 @@ impl UTXOCohorts<Rw> {
.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(entry.par_iter_mut().map(|x| x as &mut dyn DynCohortVecs))
.chain(
amount_range
.par_iter_mut()
@@ -389,6 +432,7 @@ impl UTXOCohorts<Rw> {
age_range,
epoch,
class,
entry,
amount_range,
type_,
..
@@ -398,6 +442,7 @@ impl UTXOCohorts<Rw> {
.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(entry.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))
}
@@ -409,6 +454,7 @@ impl UTXOCohorts<Rw> {
.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.entry.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))
}
@@ -483,7 +529,7 @@ impl UTXOCohorts<Rw> {
/// First phase of post-processing: compute index transforms.
pub(crate) fn compute_rest_part1(
&mut self,
prices: &prices::Vecs,
prices: &price::Vecs,
starting_lengths: &Lengths,
exit: &Exit,
) -> Result<()> {
@@ -516,6 +562,7 @@ impl UTXOCohorts<Rw> {
);
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.entry.iter_mut().map(|x| x as &mut dyn DynCohortVecs));
all.extend(
self.amount_range
.iter_mut()
@@ -546,7 +593,7 @@ impl UTXOCohorts<Rw> {
pub(crate) fn compute_rest_part2(
&mut self,
blocks: &blocks::Vecs,
prices: &prices::Vecs,
prices: &price::Vecs,
starting_lengths: &Lengths,
height_to_market_cap: &impl ReadableVec<Height, Dollars>,
exit: &Exit,
@@ -604,6 +651,7 @@ impl UTXOCohorts<Rw> {
under_amount,
epoch,
class,
entry,
type_,
..
} = self;
@@ -676,6 +724,19 @@ impl UTXOCohorts<Rw> {
.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(|| {
amount_range.par_iter_mut().try_for_each(|v| {
v.metrics
@@ -730,6 +791,9 @@ impl UTXOCohorts<Rw> {
for v in self.class.iter_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() {
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.
/// 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 {
all,
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);
lth.metrics.realized.push_accum(&lth_acc);
@@ -880,6 +944,8 @@ impl UTXOCohorts<Rw> {
.unrealized
.capitalized_cap_in_loss_raw
.push(CentsSquaredSats::new(lth_ccap.1));
all_capitalized_price
}
}
@@ -50,6 +50,22 @@ impl UTXOCohorts {
let lth = self.caches.fenwick.percentiles_lth();
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);
push_profitability(&prof, &mut self.profitability);
}
@@ -1,3 +1,4 @@
use brk_cohort::EntryPrice;
use brk_types::{Cents, CostBasisSnapshot, Height, Timestamp};
use vecdb::Rw;
@@ -12,6 +13,7 @@ impl UTXOCohorts<Rw> {
/// - The "under_1h" age cohort (all new UTXOs start at 0 hours old)
/// - The appropriate epoch cohort based on block height
/// - 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 amount range cohort based on value
pub(crate) fn receive(
@@ -20,13 +22,14 @@ impl UTXOCohorts<Rw> {
height: Height,
timestamp: Timestamp,
price: Cents,
entry: EntryPrice,
) {
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);
// New UTXOs go into under_1h, current epoch, and current class
// New UTXOs go into under_1h plus immutable creation cohorts
self.age_range
.under_1h
.state
@@ -45,6 +48,12 @@ impl UTXOCohorts<Rw> {
.unwrap()
.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)
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
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(
&sent.spendable_supply,
current_price,
@@ -75,6 +75,12 @@ impl UTXOCohorts<Rw> {
.unwrap()
.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 {
// Zero-value UTXOs: just subtract 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) {
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)
@@ -6,7 +6,7 @@ use vecdb::{Exit, ReadableVec};
use crate::{
distribution::{cohorts::traits::DynCohortVecs, metrics::CoreCohortMetrics},
prices,
price,
};
use super::UTXOCohortVecs;
@@ -56,7 +56,7 @@ impl DynCohortVecs for UTXOCohortVecs<CoreCohortMetrics> {
fn compute_rest_part1(
&mut self,
prices: &prices::Vecs,
prices: &price::Vecs,
starting_lengths: &Lengths,
exit: &Exit,
) -> Result<()> {
@@ -6,7 +6,7 @@ use vecdb::{Exit, ReadableVec};
use crate::{
distribution::{cohorts::traits::DynCohortVecs, metrics::MinimalCohortMetrics},
prices,
price,
};
use super::UTXOCohortVecs;
@@ -49,7 +49,7 @@ impl DynCohortVecs for UTXOCohortVecs<MinimalCohortMetrics> {
fn compute_rest_part1(
&mut self,
prices: &prices::Vecs,
prices: &price::Vecs,
starting_lengths: &Lengths,
exit: &Exit,
) -> Result<()> {
@@ -55,7 +55,7 @@ use crate::{
metrics::{CohortMetricsBase, CohortMetricsState},
state::UTXOCohortState,
},
prices,
price,
};
#[derive(Traversable)]
@@ -186,7 +186,7 @@ impl<M: CohortMetricsBase + Traversable> DynCohortVecs for UTXOCohortVecs<M> {
fn compute_rest_part1(
&mut self,
prices: &prices::Vecs,
prices: &price::Vecs,
starting_lengths: &Lengths,
exit: &Exit,
) -> Result<()> {
@@ -5,7 +5,7 @@ use brk_types::{Cents, Height, Version};
use vecdb::{Exit, ReadableVec};
use crate::{
distribution::cohorts::traits::DynCohortVecs, distribution::metrics::TypeCohortMetrics, prices,
distribution::cohorts::traits::DynCohortVecs, distribution::metrics::TypeCohortMetrics, price,
};
use super::UTXOCohortVecs;
@@ -55,7 +55,7 @@ impl DynCohortVecs for UTXOCohortVecs<TypeCohortMetrics> {
fn compute_rest_part1(
&mut self,
prices: &prices::Vecs,
prices: &price::Vecs,
starting_lengths: &Lengths,
exit: &Exit,
) -> Result<()> {
@@ -1,4 +1,4 @@
use brk_cohort::ByAddrType;
use brk_cohort::{ByAddrType, EntryPrice};
use brk_error::Result;
use brk_indexer::Indexer;
use brk_types::{
@@ -46,6 +46,7 @@ pub(crate) fn process_blocks(
last_height: Height,
chain_state: &mut Vec<BlockState>,
tx_index_to_height: &mut RangeMap<TxIndex, Height>,
mut entry_anchor: Cents,
cached_prices: &[Cents],
cached_timestamps: &[Timestamp],
cached_price_range_max: &PriceRangeMax,
@@ -370,9 +371,14 @@ pub(crate) fn process_blocks(
.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
chain_state.push(BlockState {
supply: transacted.spendable_supply,
entry,
price: block_price,
timestamp,
});
@@ -411,7 +417,7 @@ pub(crate) fn process_blocks(
|| {
// UTXO cohorts receive/send
vecs.utxo_cohorts
.receive(transacted, height, timestamp, block_price);
.receive(transacted, height, timestamp, block_price, entry);
if let Some(min_h) =
vecs.utxo_cohorts
.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 date_opt = is_last_of_day.then(|| Date::from(timestamp));
push_cohort_states(
entry_anchor = push_cohort_states(
&mut vecs.utxo_cohorts,
&mut vecs.addr_cohorts,
height,
@@ -527,7 +533,7 @@ fn push_cohort_states(
addr_cohorts: &mut AddrCohorts,
height: Height,
height_price: Cents,
) {
) -> Cents {
// Phase 1: push + unrealized (no reset yet, states still needed for aggregation)
rayon::join(
|| {
@@ -545,7 +551,7 @@ fn push_cohort_states(
);
// 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
utxo_cohorts
@@ -554,4 +560,6 @@ fn push_cohort_states(
addr_cohorts
.iter_separate_mut()
.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).
pub(crate) fn recover_state(
height: Height,
chain_state_rollback: vecdb::Result<Stamp>,
chain_state_rollback: Option<vecdb::Result<Stamp>>,
any_addr_indexes: &mut AnyAddrIndexesVecs,
addrs_data: &mut AddrsDataVecs,
utxo_cohorts: &mut UTXOCohorts,
addr_cohorts: &mut AddrCohorts,
) -> 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
let addr_indexes_rollback = any_addr_indexes.rollback_before(stamp);
let addr_data_rollback = addrs_data.rollback_before(stamp);
// Rollback address state vectors
let addr_indexes_rollback = any_addr_indexes.rollback_before(stamp);
let addr_data_rollback = addrs_data.rollback_before(stamp);
// Verify rollback consistency - all must agree on the same height
let consistent_height = rollback_states(
chain_state_rollback,
addr_indexes_rollback,
addr_data_rollback,
);
// Verify rollback consistency - all must agree on the same height
let consistent_height = rollback_states(
chain_state_rollback,
addr_indexes_rollback,
addr_data_rollback,
);
// If rollbacks are inconsistent, start fresh
if consistent_height.is_zero() {
warn!("Rollback consistency check failed: inconsistent heights");
return Ok(RecoveredState {
starting_height: Height::ZERO,
});
}
// If rollbacks are inconsistent, start fresh
if consistent_height.is_zero() {
warn!("Rollback consistency check failed: inconsistent heights");
return Ok(RecoveredState {
starting_height: Height::ZERO,
});
}
// 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).
if consistent_height > height {
warn!(
"Rollback failed: still at {} but target was {}, falling back to fresh start",
consistent_height, height
);
return Ok(RecoveredState {
starting_height: Height::ZERO,
});
}
// 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).
if consistent_height > height {
warn!(
"Rollback failed: still at {} but target was {}, falling back to fresh start",
consistent_height, height
);
return Ok(RecoveredState {
starting_height: Height::ZERO,
});
}
if consistent_height != height {
debug!(
"Rollback landed at {} instead of {}, will resume from there",
consistent_height, height
);
}
if consistent_height != height {
debug!(
"Rollback landed at {} instead of {}, will resume from there",
consistent_height, height
);
}
consistent_height
}
};
// Import UTXO cohort states - all must succeed
debug!(
@@ -11,7 +11,7 @@ use crate::{
state::{CohortState, CostBasisOps, RealizedOps},
},
internal::{PerBlockCumulativeRolling, ValuePerBlockCumulativeRolling},
prices,
price,
};
use super::ActivityMinimal;
@@ -98,7 +98,7 @@ impl ActivityCore {
pub(crate) fn compute_rest_part1(
&mut self,
prices: &prices::Vecs,
prices: &price::Vecs,
starting_lengths: &Lengths,
exit: &Exit,
) -> Result<()> {
@@ -12,7 +12,7 @@ use crate::{
metrics::ImportConfig,
state::{CohortState, CostBasisOps, RealizedOps},
},
prices,
price,
};
use super::ActivityCore;
@@ -89,7 +89,7 @@ impl ActivityFull {
pub(crate) fn compute_rest_part1(
&mut self,
prices: &prices::Vecs,
prices: &price::Vecs,
starting_lengths: &Lengths,
exit: &Exit,
) -> Result<()> {
@@ -10,7 +10,7 @@ use crate::{
state::{CohortState, CostBasisOps, RealizedOps},
},
internal::ValuePerBlockCumulativeRolling,
prices,
price,
};
#[derive(Traversable)]
@@ -63,7 +63,7 @@ impl ActivityMinimal {
pub(crate) fn compute_rest_part1(
&mut self,
prices: &prices::Vecs,
prices: &price::Vecs,
starting_lengths: &Lengths,
exit: &Exit,
) -> Result<()> {
@@ -13,7 +13,7 @@ use vecdb::Exit;
use crate::{
distribution::state::{CohortState, CostBasisOps, RealizedOps},
prices,
price,
};
pub trait ActivityLike: Send + Sync {
@@ -30,7 +30,7 @@ pub trait ActivityLike: Send + Sync {
) -> Result<()>;
fn compute_rest_part1(
&mut self,
prices: &prices::Vecs,
prices: &price::Vecs,
starting_lengths: &Lengths,
exit: &Exit,
) -> Result<()>;
@@ -62,7 +62,7 @@ impl ActivityLike for ActivityCore {
}
fn compute_rest_part1(
&mut self,
prices: &prices::Vecs,
prices: &price::Vecs,
starting_lengths: &Lengths,
exit: &Exit,
) -> Result<()> {
@@ -96,7 +96,7 @@ impl ActivityLike for ActivityFull {
}
fn compute_rest_part1(
&mut self,
prices: &prices::Vecs,
prices: &price::Vecs,
starting_lengths: &Lengths,
exit: &Exit,
) -> Result<()> {
@@ -11,7 +11,7 @@ use crate::{
ActivityFull, AdjustedSopr, CohortMetricsBase, CostBasis, ImportConfig, OutputsBase,
RealizedFull, RelativeForAll, SupplyCore, UnrealizedFull,
},
prices,
price,
};
/// All-cohort metrics: extended realized + adjusted (as composable add-on),
@@ -100,7 +100,7 @@ impl AllCohortMetrics {
pub(crate) fn compute_rest_part2(
&mut self,
blocks: &blocks::Vecs,
prices: &prices::Vecs,
prices: &price::Vecs,
starting_lengths: &Lengths,
height_to_market_cap: &impl ReadableVec<Height, Dollars>,
under_1h_value_created: &impl ReadableVec<Height, Cents>,
@@ -10,7 +10,7 @@ use crate::{
ActivityCore, CohortMetricsBase, ImportConfig, OutputsBase, RealizedCore, SupplyCore,
UnrealizedCore,
},
prices,
price,
};
/// Basic cohort metrics: no extensions, used by age_range cohorts.
@@ -61,7 +61,7 @@ impl BasicCohortMetrics {
pub(crate) fn compute_rest_part2(
&mut self,
prices: &prices::Vecs,
prices: &price::Vecs,
starting_lengths: &Lengths,
all_supply_sats: &impl ReadableVec<Height, Sats>,
all_utxo_count: &impl ReadableVec<Height, StoredU64>,
@@ -10,7 +10,7 @@ use crate::{
ActivityCore, CohortMetricsBase, ImportConfig, OutputsBase, RealizedCore, SupplyCore,
UnrealizedCore,
},
prices,
price,
};
#[derive(Traversable)]
@@ -102,7 +102,7 @@ impl CoreCohortMetrics {
pub(crate) fn compute_rest_part1(
&mut self,
prices: &prices::Vecs,
prices: &price::Vecs,
starting_lengths: &Lengths,
exit: &Exit,
) -> Result<()> {
@@ -122,7 +122,7 @@ impl CoreCohortMetrics {
pub(crate) fn compute_rest_part2(
&mut self,
prices: &prices::Vecs,
prices: &price::Vecs,
starting_lengths: &Lengths,
all_supply_sats: &impl ReadableVec<Height, Sats>,
all_utxo_count: &impl ReadableVec<Height, StoredU64>,
@@ -12,7 +12,7 @@ use crate::{
ActivityFull, CohortMetricsBase, CostBasis, ImportConfig, OutputsBase, RealizedFull,
RelativeWithExtended, SupplyCore, UnrealizedFull,
},
prices,
price,
};
/// Cohort metrics with extended realized + extended cost basis (no adjusted).
@@ -90,7 +90,7 @@ impl ExtendedCohortMetrics {
pub(crate) fn compute_rest_part2(
&mut self,
blocks: &blocks::Vecs,
prices: &prices::Vecs,
prices: &price::Vecs,
starting_lengths: &Lengths,
height_to_market_cap: &impl ReadableVec<Height, Dollars>,
all_supply_sats: &impl ReadableVec<Height, Sats>,
@@ -10,7 +10,7 @@ use crate::{
distribution::metrics::{
ActivityFull, AdjustedSopr, CohortMetricsBase, ImportConfig, RealizedFull, UnrealizedFull,
},
prices,
price,
};
use super::ExtendedCohortMetrics;
@@ -62,7 +62,7 @@ impl ExtendedAdjustedCohortMetrics {
pub(crate) fn compute_rest_part2(
&mut self,
blocks: &blocks::Vecs,
prices: &prices::Vecs,
prices: &price::Vecs,
starting_lengths: &Lengths,
height_to_market_cap: &impl ReadableVec<Height, Dollars>,
under_1h_value_created: &impl ReadableVec<Height, Cents>,
@@ -9,7 +9,7 @@ use crate::{
distribution::metrics::{
ActivityMinimal, ImportConfig, OutputsBase, RealizedMinimal, SupplyBase, UnrealizedMinimal,
},
prices,
price,
};
/// MinimalCohortMetrics: supply, outputs, realized cap/price/mvrv/profit/loss + value_created/destroyed.
@@ -97,7 +97,7 @@ impl MinimalCohortMetrics {
pub(crate) fn compute_rest_part1(
&mut self,
prices: &prices::Vecs,
prices: &price::Vecs,
starting_lengths: &Lengths,
exit: &Exit,
) -> Result<()> {
@@ -111,7 +111,7 @@ impl MinimalCohortMetrics {
pub(crate) fn compute_rest_part2(
&mut self,
prices: &prices::Vecs,
prices: &price::Vecs,
starting_lengths: &Lengths,
all_supply_sats: &impl ReadableVec<Height, Sats>,
all_utxo_count: &impl ReadableVec<Height, StoredU64>,
@@ -9,7 +9,7 @@ use crate::{
distribution::metrics::{
ActivityMinimal, ImportConfig, OutputsBase, RealizedMinimal, SupplyCore, UnrealizedBasic,
},
prices,
price,
};
/// TypeCohortMetrics: supply(core), outputs(base), realized(minimal), unrealized(basic).
@@ -59,7 +59,7 @@ impl TypeCohortMetrics {
pub(crate) fn compute_rest_part1(
&mut self,
prices: &prices::Vecs,
prices: &price::Vecs,
starting_lengths: &Lengths,
exit: &Exit,
) -> Result<()> {
@@ -73,7 +73,7 @@ impl TypeCohortMetrics {
pub(crate) fn compute_rest_part2(
&mut self,
prices: &prices::Vecs,
prices: &price::Vecs,
starting_lengths: &Lengths,
all_supply_sats: &impl ReadableVec<Height, Sats>,
all_utxo_count: &impl ReadableVec<Height, StoredU64>,
@@ -149,7 +149,7 @@ use crate::{
CohortState, CoreRealizedState, CostBasisData, CostBasisOps, CostBasisRaw,
MinimalRealizedState, RealizedOps, RealizedState, WithCapital, WithoutCapital,
},
prices,
price,
};
pub trait CohortMetricsState {
@@ -270,7 +270,7 @@ pub trait CohortMetricsBase:
/// First phase of computed metrics (indexes from height).
fn compute_rest_part1(
&mut self,
prices: &prices::Vecs,
prices: &price::Vecs,
starting_lengths: &Lengths,
exit: &Exit,
) -> Result<()> {
@@ -10,7 +10,7 @@ use crate::{
internal::{
PerBlock, RatioPerBlock, ValuePerBlock, ValuePerBlockWithDeltas, WindowStartVec, Windows,
},
prices,
price,
};
#[derive(Traversable)]
@@ -115,7 +115,7 @@ impl ProfitabilityBucket {
pub(crate) fn compute(
&mut self,
prices: &prices::Vecs,
prices: &price::Vecs,
starting_lengths: &Lengths,
is_profit: bool,
exit: &Exit,
@@ -176,7 +176,7 @@ impl ProfitabilityBucket {
pub(crate) fn compute_from_ranges(
&mut self,
prices: &prices::Vecs,
prices: &price::Vecs,
starting_lengths: &Lengths,
is_profit: bool,
sources: &[&ProfitabilityBucket],
@@ -293,7 +293,7 @@ impl ProfitabilityMetrics {
pub(crate) fn compute(
&mut self,
prices: &prices::Vecs,
prices: &price::Vecs,
starting_lengths: &Lengths,
exit: &Exit,
) -> Result<()> {
@@ -16,7 +16,7 @@ use crate::{
FiatPerBlockCumulativeWithSumsAndDeltas, LazyPerBlock, NegCentsUnsignedToDollars,
PerBlockCumulativeRolling, RatioCents64, RollingWindow24hPerBlock, Windows,
},
prices,
price,
};
use crate::distribution::metrics::ImportConfig;
@@ -166,7 +166,7 @@ impl RealizedCore {
pub(crate) fn compute_rest_part2(
&mut self,
prices: &prices::Vecs,
prices: &price::Vecs,
starting_lengths: &Lengths,
height_to_supply: &impl ReadableVec<Height, Bitcoin>,
transfer_volume_sum_24h_cents: &impl ReadableVec<Height, Cents>,
@@ -18,7 +18,7 @@ use crate::{
RatioPerBlockStdDevBands, RatioSma, RollingWindows, RollingWindowsFrom1w,
ValuePerBlockCumulativeRolling,
},
prices,
price,
};
use crate::distribution::metrics::ImportConfig;
@@ -206,7 +206,7 @@ impl RealizedFull {
}
#[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.capitalized.cap_raw.push(accum.capitalized_cap_raw);
@@ -221,6 +221,8 @@ impl RealizedFull {
self.capitalized.price.cents.height.push(capitalized_price);
self.peak_regret.value.block.cents.push(accum.peak_regret());
capitalized_price
}
pub(crate) fn compute_rest_part1(
@@ -240,7 +242,7 @@ impl RealizedFull {
pub(crate) fn compute_rest_part2(
&mut self,
blocks: &blocks::Vecs,
prices: &prices::Vecs,
prices: &price::Vecs,
starting_lengths: &Lengths,
height_to_supply: &impl ReadableVec<Height, Bitcoin>,
height_to_market_cap: &impl ReadableVec<Height, Dollars>,
@@ -13,7 +13,7 @@ use crate::{
FiatPerBlockCumulativeWithSums, FiatPerBlockWithDeltas, Identity, LazyPerBlock,
PriceWithRatioPerBlock,
},
prices,
price,
};
use crate::distribution::metrics::ImportConfig;
@@ -104,7 +104,7 @@ impl RealizedMinimal {
pub(crate) fn compute_rest_part2(
&mut self,
prices: &prices::Vecs,
prices: &price::Vecs,
starting_lengths: &Lengths,
height_to_supply: &impl ReadableVec<Height, Bitcoin>,
exit: &Exit,
@@ -3,7 +3,7 @@ use brk_traversable::Traversable;
use brk_types::{Height, Sats, StoredU64, Version};
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.
///
@@ -53,7 +53,7 @@ impl AvgAmountMetrics {
pub(crate) fn compute(
&mut self,
prices: &prices::Vecs,
prices: &price::Vecs,
supply_sats: &impl ReadableVec<Height, Sats>,
utxo_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::{
distribution::state::{CohortState, CostBasisOps, RealizedOps},
prices,
price,
};
use crate::internal::{
@@ -64,7 +64,7 @@ impl SupplyBase {
pub(crate) fn compute(
&mut self,
prices: &prices::Vecs,
prices: &price::Vecs,
max_from: Height,
exit: &Exit,
) -> Result<()> {
@@ -5,7 +5,7 @@ use brk_types::{Height, Version};
use derive_more::{Deref, DerefMut};
use vecdb::{AnyStoredVec, AnyVec, Exit, Rw, StorageMode, WritableVec};
use crate::{distribution::state::UnrealizedState, prices};
use crate::{distribution::state::UnrealizedState, price};
use crate::internal::{
HalveCents, HalveDollars, HalveSats, HalveSatsToBitcoin, LazyValuePerBlock, ValuePerBlock,
@@ -72,7 +72,7 @@ impl SupplyCore {
pub(crate) fn compute(
&mut self,
prices: &prices::Vecs,
prices: &price::Vecs,
max_from: Height,
exit: &Exit,
) -> Result<()> {
@@ -7,7 +7,7 @@ use vecdb::{AnyStoredVec, AnyVec, BytesVec, Exit, ReadableVec, Rw, StorageMode,
use crate::distribution::state::UnrealizedState;
use crate::internal::{CentsSubtractToCentsSigned, FiatPerBlock};
use crate::{distribution::metrics::ImportConfig, prices};
use crate::{distribution::metrics::ImportConfig, price};
use super::UnrealizedCore;
@@ -99,7 +99,7 @@ impl UnrealizedFull {
pub(crate) fn compute_rest_all(
&mut self,
prices: &prices::Vecs,
prices: &price::Vecs,
starting_lengths: &Lengths,
supply_in_profit_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 vecdb::{Exit, ReadableVec};
use crate::{distribution::state::UnrealizedState, prices};
use crate::{distribution::state::UnrealizedState, price};
pub trait UnrealizedLike: Send + Sync {
fn as_core(&self) -> &UnrealizedCore;
@@ -22,7 +22,7 @@ pub trait UnrealizedLike: Send + Sync {
fn push_state(&mut self, state: &UnrealizedState);
fn compute_rest(
&mut self,
prices: &prices::Vecs,
prices: &price::Vecs,
starting_lengths: &Lengths,
supply_in_profit_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(
&mut self,
_prices: &prices::Vecs,
_prices: &price::Vecs,
starting_lengths: &Lengths,
_supply_in_profit_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(
&mut self,
prices: &prices::Vecs,
prices: &price::Vecs,
starting_lengths: &Lengths,
supply_in_profit_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 brk_cohort::EntryPrice;
use brk_types::{Cents, SupplyState, Timestamp};
use serde::Serialize;
@@ -8,6 +9,8 @@ pub struct BlockState {
#[serde(flatten)]
pub supply: SupplyState,
#[serde(skip)]
pub entry: EntryPrice,
#[serde(skip)]
pub price: Cents,
#[serde(skip)]
pub timestamp: Timestamp,
+49 -12
View File
@@ -1,6 +1,6 @@
use std::path::{Path, PathBuf};
use brk_cohort::{ByAddrType, Filter};
use brk_cohort::{ByAddrType, EntryPrice, Filter};
use brk_error::Result;
use brk_indexer::Indexer;
use brk_traversable::Traversable;
@@ -29,7 +29,7 @@ use crate::{
PerBlockCumulativeRolling, WindowStartVec, Windows, WithAddrTypes,
db_utils::{finalize_db, open_db},
},
outputs, prices, transactions,
outputs, price, transactions,
};
use super::{
@@ -316,7 +316,7 @@ impl Vecs {
outputs: &outputs::Vecs,
transactions: &transactions::Vecs,
blocks: &blocks::Vecs,
prices: &prices::Vecs,
prices: &price::Vecs,
exit: &Exit,
) -> Result<()> {
self.db.sync_bg_tasks()?;
@@ -341,12 +341,13 @@ impl Vecs {
// Try to resume from checkpoint, fall back to fresh start if needed
let recovered_height = match start_mode {
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(
height,
chain_state_rollback,
@@ -435,13 +436,34 @@ impl Vecs {
let end = usize::from(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 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
.into_iter()
.enumerate()
.map(|(h, supply)| BlockState {
supply,
price: self.caches.prices[h],
timestamp: self.caches.timestamps[h],
.map(|(h, supply)| {
let price = self.caches.prices[h];
let entry = EntryPrice::from_is_discount(
entry_anchor == Cents::ZERO || price <= entry_anchor,
);
entry_anchor = capitalized_price_data[h];
BlockState {
supply,
entry,
price,
timestamp: self.caches.timestamps[h],
}
})
.collect();
debug!("chain_state rebuilt");
@@ -473,6 +495,20 @@ impl Vecs {
let prices = std::mem::take(&mut self.caches.prices);
let timestamps = std::mem::take(&mut self.caches.timestamps);
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(
self,
@@ -485,6 +521,7 @@ impl Vecs {
last_height,
&mut chain_state,
&mut tx_index_to_height,
entry_anchor,
&prices,
&timestamps,
&price_range_max,
@@ -6,7 +6,7 @@ use brk_traversable::Traversable;
use brk_types::Version;
use vecdb::{Database, Exit, Rw, StorageMode};
use crate::{distribution, indexes, prices};
use crate::{distribution, indexes, price};
pub use inner::RarityMeterInner;
@@ -37,7 +37,7 @@ impl RarityMeter {
&mut self,
indexer: &Indexer,
distribution: &distribution::Vecs,
prices: &prices::Vecs,
prices: &price::Vecs,
exit: &Exit,
) -> Result<()> {
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 crate::internal::{LazyPerBlock, PerBlock, Price};
use crate::{indexes, prices};
use crate::{indexes, price};
use super::{RatioPerBlock, RatioPerBlockPercentiles};
@@ -63,7 +63,7 @@ impl PriceWithRatioPerBlock {
/// Compute price via closure (in cents), then compute ratio.
pub(crate) fn compute_all<F>(
&mut self,
prices: &prices::Vecs,
prices: &price::Vecs,
starting_lengths: &Lengths,
exit: &Exit,
mut compute_price: F,
@@ -101,7 +101,7 @@ impl PriceWithRatioExtendedPerBlock {
/// Compute ratio and percentiles from already-computed price cents.
pub(crate) fn compute_rest(
&mut self,
prices: &prices::Vecs,
prices: &price::Vecs,
starting_lengths: &Lengths,
exit: &Exit,
) -> Result<()> {
@@ -120,7 +120,7 @@ impl PriceWithRatioExtendedPerBlock {
/// Compute price via closure (in cents), then compute ratio and percentiles.
pub(crate) fn compute_all<F>(
&mut self,
prices: &prices::Vecs,
prices: &price::Vecs,
starting_lengths: &Lengths,
exit: &Exit,
mut compute_price: F,
@@ -10,7 +10,7 @@ use crate::{
CentsUnsignedToDollars, LazyPerBlock, NumericValue, PerBlock, SatsSignedToBitcoin,
SatsToBitcoin, SatsToCents,
},
prices,
price,
};
/// Trait that associates a sats type with its transform to Bitcoin.
@@ -69,7 +69,7 @@ impl ValuePerBlock {
pub(crate) fn compute(
&mut self,
prices: &prices::Vecs,
prices: &price::Vecs,
max_from: Height,
exit: &Exit,
) -> Result<()> {
@@ -8,7 +8,7 @@ use vecdb::{
use crate::{
internal::{CentsUnsignedToDollars, SatsToBitcoin, SatsToCents},
prices,
price,
};
/// Raw per-block amount data: sats + cents (stored), btc + usd (lazy), no resolutions.
@@ -44,7 +44,7 @@ impl ValueBlock {
pub(crate) fn compute_cents(
&mut self,
max_from: Height,
prices: &prices::Vecs,
prices: &price::Vecs,
exit: &Exit,
) -> Result<()> {
self.cents.compute_binary::<Sats, Cents, SatsToCents>(
@@ -6,7 +6,7 @@ use vecdb::{Database, EagerVec, Exit, PcoVec, Rw, StorageMode};
use crate::{
indexes,
internal::{ValueBlock, ValuePerBlock},
prices,
price,
};
#[derive(Traversable)]
@@ -39,7 +39,7 @@ impl ValuePerBlockCumulative {
pub(crate) fn compute(
&mut self,
prices: &prices::Vecs,
prices: &price::Vecs,
max_from: Height,
exit: &Exit,
) -> Result<()> {
@@ -61,7 +61,7 @@ impl ValuePerBlockCumulative {
pub(crate) fn compute_with(
&mut self,
max_from: Height,
prices: &prices::Vecs,
prices: &price::Vecs,
exit: &Exit,
compute_sats: impl FnOnce(&mut EagerVec<PcoVec<Height, Sats>>) -> Result<()>,
) -> Result<()> {
@@ -10,7 +10,7 @@ use crate::{
LazyRollingAvgsAmountFromHeight, LazyRollingSumsAmountFromHeight, ValuePerBlockCumulative,
WindowStartVec, Windows,
},
prices,
price,
};
#[derive(Deref, DerefMut, Traversable)]
@@ -63,7 +63,7 @@ impl ValuePerBlockCumulativeRolling {
pub(crate) fn compute(
&mut self,
max_from: Height,
prices: &prices::Vecs,
prices: &price::Vecs,
exit: &Exit,
compute_sats: impl FnOnce(&mut EagerVec<PcoVec<Height, Sats>>) -> Result<()>,
) -> Result<()> {
@@ -74,7 +74,7 @@ impl ValuePerBlockCumulativeRolling {
pub(crate) fn compute_rest(
&mut self,
max_from: Height,
prices: &prices::Vecs,
prices: &price::Vecs,
exit: &Exit,
) -> Result<()> {
self.inner.compute(prices, max_from, exit)
@@ -10,7 +10,7 @@ use crate::{
RollingDistributionValuePerBlock, ValuePerBlockCumulativeRolling, WindowStartVec,
WindowStarts, Windows,
},
prices,
price,
};
#[derive(Deref, DerefMut, Traversable)]
@@ -49,7 +49,7 @@ impl ValuePerBlockFull {
&mut self,
max_from: Height,
windows: &WindowStarts<'_>,
prices: &prices::Vecs,
prices: &price::Vecs,
exit: &Exit,
compute_sats: impl FnOnce(&mut EagerVec<PcoVec<Height, Sats>>) -> Result<()>,
) -> Result<()> {
@@ -11,7 +11,7 @@ use rayon::prelude::*;
use schemars::JsonSchema;
use vecdb::{AnyStoredVec, AnyVec, Database, EagerVec, Exit, PcoVec, WritableVec};
use crate::{indexes, prices};
use crate::{indexes, price};
use super::{
BpsType, NumericValue, PerBlock, PerBlockCumulativeRolling, PercentPerBlock, ValuePerBlock,
@@ -229,7 +229,7 @@ impl WithAddrTypes<ValuePerBlock> {
pub(crate) fn compute_rest(
&mut self,
max_from: Height,
prices: &prices::Vecs,
prices: &price::Vecs,
exit: &Exit,
) -> Result<()> {
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 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);
@@ -13,7 +13,7 @@ impl Vecs {
&mut self,
indexer: &Indexer,
indexes: &indexes::Vecs,
prices: &prices::Vecs,
prices: &price::Vecs,
blocks: &blocks::Vecs,
lookback: &market::lookback::Vecs,
exit: &Exit,
+20 -20
View File
@@ -22,7 +22,7 @@ mod market;
mod mining;
mod outputs;
mod pools;
pub mod prices;
pub mod price;
mod supply;
mod transactions;
@@ -38,7 +38,7 @@ pub struct Computer<M: StorageMode = Rw> {
pub investing: Box<investing::Vecs<M>>,
pub market: Box<market::Vecs<M>>,
pub pools: Box<pools::Vecs<M>>,
pub prices: Box<prices::Vecs<M>>,
pub price: Box<price::Vecs<M>>,
#[traversable(flatten)]
pub distribution: Box<distribution::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 prices = Box::new(prices::Vecs::forced_import(
let price = Box::new(price::Vecs::forced_import(
&computed_path,
VERSION,
&indexes,
)?);
Ok((constants, prices))
Ok((constants, price))
})?;
let blocks = timed("Imported blocks", || -> Result<_> {
@@ -223,7 +223,7 @@ impl Computer {
cointime,
indexes,
inputs,
prices,
price,
outputs,
};
@@ -244,7 +244,7 @@ impl Computer {
investing::DB_NAME,
market::DB_NAME,
pools::DB_NAME,
prices::DB_NAME,
price::DB_NAME,
distribution::DB_NAME,
supply::DB_NAME,
inputs::DB_NAME,
@@ -297,8 +297,8 @@ impl Computer {
})
},
|| {
timed("Computed prices", || {
self.prices.compute(indexer, &self.indexes, exit)
timed("Computed price", || {
self.price.compute(indexer, &self.indexes, exit)
})
},
);
@@ -310,7 +310,7 @@ impl Computer {
let market = scope.spawn(|| {
timed("Computed 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.blocks,
&self.inputs,
&self.prices,
&self.price,
exit,
)
})?;
@@ -331,7 +331,7 @@ impl Computer {
&self.indexes,
&self.blocks,
&self.transactions,
&self.prices,
&self.price,
exit,
)
})
@@ -343,7 +343,7 @@ impl Computer {
&self.indexes,
&self.inputs,
&self.blocks,
&self.prices,
&self.price,
exit,
)
})?;
@@ -360,7 +360,7 @@ impl Computer {
indexer,
&self.indexes,
&self.blocks,
&self.prices,
&self.price,
&self.mining,
exit,
)
@@ -372,7 +372,7 @@ impl Computer {
self.investing.compute(
indexer,
&self.indexes,
&self.prices,
&self.price,
&self.blocks,
&self.market.lookback,
exit,
@@ -388,7 +388,7 @@ impl Computer {
&self.outputs,
&self.transactions,
&self.blocks,
&self.prices,
&self.price,
exit,
)
})?;
@@ -421,7 +421,7 @@ impl Computer {
&self.blocks,
&self.mining,
&self.transactions,
&self.prices,
&self.price,
&self.distribution,
exit,
)
@@ -430,7 +430,7 @@ impl Computer {
timed("Computed cointime", || {
self.cointime.compute(
indexer,
&self.prices,
&self.price,
&self.blocks,
&self.mining,
&self.supply,
@@ -445,7 +445,7 @@ impl Computer {
self.indicators
.rarity_meter
.compute(indexer, &self.distribution, &self.prices, exit)?;
.compute(indexer, &self.distribution, &self.price, exit)?;
info!("Total compute time: {:?}", compute_start.elapsed());
Ok(())
@@ -498,7 +498,7 @@ impl_iter_named!(
investing,
market,
pools,
prices,
price,
distribution,
supply,
inputs,
@@ -4,13 +4,13 @@ use brk_types::{StoredF32, Timestamp};
use vecdb::{Exit, ReadableVec, VecIndex};
use super::Vecs;
use crate::{indexes, prices};
use crate::{indexes, price};
impl Vecs {
pub(crate) fn compute(
&mut self,
indexer: &Indexer,
prices: &prices::Vecs,
prices: &price::Vecs,
indexes: &indexes::Vecs,
exit: &Exit,
) -> Result<()> {
+2 -2
View File
@@ -2,7 +2,7 @@ use brk_error::Result;
use brk_indexer::Indexer;
use vecdb::Exit;
use crate::{blocks, indexes, prices};
use crate::{blocks, indexes, price};
use super::Vecs;
@@ -10,7 +10,7 @@ impl Vecs {
pub(crate) fn compute(
&mut self,
indexer: &Indexer,
prices: &prices::Vecs,
prices: &price::Vecs,
indexes: &indexes::Vecs,
blocks: &blocks::Vecs,
exit: &Exit,
@@ -3,14 +3,14 @@ use brk_indexer::Indexer;
use vecdb::Exit;
use super::Vecs;
use crate::{blocks, prices};
use crate::{blocks, price};
impl Vecs {
pub(crate) fn compute(
&mut self,
indexer: &Indexer,
blocks: &blocks::Vecs,
prices: &prices::Vecs,
prices: &price::Vecs,
exit: &Exit,
) -> Result<()> {
let starting_height = indexer.safe_lengths().height;
@@ -3,14 +3,14 @@ use brk_indexer::Indexer;
use vecdb::Exit;
use super::Vecs;
use crate::{blocks, prices};
use crate::{blocks, price};
impl Vecs {
pub(crate) fn compute(
&mut self,
indexer: &Indexer,
blocks: &blocks::Vecs,
prices: &prices::Vecs,
prices: &price::Vecs,
exit: &Exit,
) -> Result<()> {
let starting_lengths = indexer.safe_lengths();
@@ -4,13 +4,13 @@ use brk_types::{BasisPoints16, StoredF32};
use vecdb::{Exit, ReadableVec, VecIndex};
use super::Vecs;
use crate::{blocks, prices};
use crate::{blocks, price};
impl Vecs {
pub(crate) fn compute(
&mut self,
indexer: &Indexer,
prices: &prices::Vecs,
prices: &price::Vecs,
blocks: &blocks::Vecs,
exit: &Exit,
) -> Result<()> {
@@ -5,14 +5,14 @@ use vecdb::Exit;
use super::Vecs;
use crate::{
blocks, internal::RatioDiffDollarsBps32, investing::ByDcaPeriod, market::lookback, prices,
blocks, internal::RatioDiffDollarsBps32, investing::ByDcaPeriod, market::lookback, price,
};
impl Vecs {
pub(crate) fn compute(
&mut self,
indexer: &Indexer,
prices: &prices::Vecs,
prices: &price::Vecs,
blocks: &blocks::Vecs,
lookback: &lookback::Vecs,
exit: &Exit,
@@ -10,7 +10,7 @@ use super::{
use crate::{
blocks,
internal::{RatioDollarsBp32, WindowsTo1m},
prices,
price,
};
impl Vecs {
@@ -19,7 +19,7 @@ impl Vecs {
&mut self,
indexer: &Indexer,
returns: &returns::Vecs,
prices: &prices::Vecs,
prices: &price::Vecs,
blocks: &blocks::Vecs,
moving_average: &moving_average::Vecs,
exit: &Exit,
@@ -3,14 +3,14 @@ use brk_indexer::Indexer;
use vecdb::Exit;
use super::MacdChain;
use crate::{blocks, prices};
use crate::{blocks, price};
#[allow(clippy::too_many_arguments)]
pub(super) fn compute(
chain: &mut MacdChain,
indexer: &Indexer,
blocks: &blocks::Vecs,
prices: &prices::Vecs,
prices: &price::Vecs,
fast_days: usize,
slow_days: usize,
signal_days: usize,
+2 -2
View File
@@ -3,7 +3,7 @@ use brk_indexer::Indexer;
use vecdb::Exit;
use super::Vecs;
use crate::{blocks, indexes, prices, transactions};
use crate::{blocks, indexes, price, transactions};
impl Vecs {
#[allow(clippy::too_many_arguments)]
@@ -13,7 +13,7 @@ impl Vecs {
indexes: &indexes::Vecs,
blocks: &blocks::Vecs,
transactions: &transactions::Vecs,
prices: &prices::Vecs,
prices: &price::Vecs,
exit: &Exit,
) -> Result<()> {
self.db.sync_bg_tasks()?;
@@ -7,7 +7,7 @@ use super::Vecs;
use crate::{
blocks, indexes,
internal::{RatioDollarsBp32, RatioSatsBp16},
prices, transactions,
price, transactions,
};
impl Vecs {
@@ -18,7 +18,7 @@ impl Vecs {
indexes: &indexes::Vecs,
lookback: &blocks::LookbackVecs,
transactions: &transactions::Vecs,
prices: &prices::Vecs,
prices: &price::Vecs,
exit: &Exit,
) -> Result<()> {
let starting_height = indexer.safe_lengths().height;
+2 -2
View File
@@ -3,7 +3,7 @@ use brk_indexer::Indexer;
use vecdb::Exit;
use super::Vecs;
use crate::{blocks, indexes, inputs, prices};
use crate::{blocks, indexes, inputs, price};
impl Vecs {
#[allow(clippy::too_many_arguments)]
@@ -13,7 +13,7 @@ impl Vecs {
indexes: &indexes::Vecs,
inputs: &inputs::Vecs,
blocks: &blocks::Vecs,
prices: &prices::Vecs,
prices: &price::Vecs,
exit: &Exit,
) -> Result<()> {
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 super::Vecs;
use crate::prices;
use crate::price;
impl Vecs {
pub(crate) fn compute(
&mut self,
indexer: &Indexer,
prices: &prices::Vecs,
prices: &price::Vecs,
exit: &Exit,
) -> Result<()> {
let starting_lengths = indexer.safe_lengths();
+2 -2
View File
@@ -11,7 +11,7 @@ use crate::{
MaskSats, PercentRollingWindows, RatioU64Bp16, ValuePerBlockCumulativeRolling,
WindowStartVec, Windows,
},
mining, prices,
mining, price,
};
use super::minor;
@@ -63,7 +63,7 @@ impl Vecs {
indexer: &Indexer,
pool: &impl ReadableVec<Height, PoolSlug>,
blocks: &blocks::Vecs,
prices: &prices::Vecs,
prices: &price::Vecs,
mining: &mining::Vecs,
exit: &Exit,
) -> Result<()> {
+2 -2
View File
@@ -22,7 +22,7 @@ use crate::{
WindowStartVec, Windows,
db_utils::{finalize_db, open_db},
},
mining, prices,
mining, price,
};
pub const DB_NAME: &str = "pools";
@@ -90,7 +90,7 @@ impl Vecs {
indexer: &Indexer,
indexes: &indexes::Vecs,
blocks: &blocks::Vecs,
prices: &prices::Vecs,
prices: &price::Vecs,
mining: &mining::Vecs,
exit: &Exit,
) -> Result<()> {
@@ -2,7 +2,9 @@ use std::ops::Range;
use brk_error::Result;
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 tracing::info;
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<()> {
let starting_height = indexer.safe_lengths().height;
let source_version = indexer.vecs.outputs.value.version()
+ indexer.vecs.outputs.output_type.version();
let source_version =
indexer.vecs.outputs.value.version() + indexer.vecs.outputs.output_type.version();
self.spot
.cents
.height
@@ -71,7 +73,7 @@ impl Vecs {
let total_heights = indexer.vecs.blocks.timestamp.len();
if total_heights <= START_HEIGHT {
if total_heights <= START_HEIGHT_SLOW {
return Ok(());
}
@@ -83,17 +85,12 @@ impl Vecs {
.inner
.truncate_if_needed_at(truncate_to)?;
if self.spot.cents.height.len() < START_HEIGHT {
for line in brk_oracle::PRICES
.lines()
.skip(self.spot.cents.height.len())
{
if self.spot.cents.height.len() >= START_HEIGHT {
if self.spot.cents.height.len() < START_HEIGHT_SLOW {
for cents in brk_oracle::pre_oracle_prices_from(self.spot.cents.height.len()) {
if self.spot.cents.height.len() >= START_HEIGHT_SLOW {
break;
}
let dollars: f64 = line.parse().unwrap_or(0.0);
let cents = (dollars * 100.0).round() as u64;
self.spot.cents.height.inner.push(Cents::new(cents));
self.spot.cents.height.inner.push(cents);
}
}
@@ -101,8 +98,8 @@ impl Vecs {
return Ok(());
}
let config = Config::default();
let committed = self.spot.cents.height.len();
let config = Config::for_height(committed);
let prev_cents = self
.spot
.cents
@@ -110,9 +107,9 @@ impl Vecs {
.collect_one_at(committed - 1)
.unwrap();
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| {
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;
@@ -121,19 +118,48 @@ impl Vecs {
committed, total_heights
);
let ref_bins =
Self::feed_blocks(&mut oracle, indexer, committed..total_heights, None);
// Slow cold-start EMA up to START_HEIGHT_FAST, then switch to the fast
// 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() {
self.spot
.cents
.height
.inner
.push(Cents::new(bin_to_cents(ref_bin)));
processed += 1;
let progress = (processed * 100 / num_new) as u8;
if processed > 1 && progress > (((processed - 1) * 100 / num_new) as u8) {
info!("Oracle price computation: {}%", progress);
}
};
let progress = ((i + 1) * 100 / num_new) as u8;
if i > 0 && progress > ((i * 100 / num_new) as u8) {
info!("Oracle price computation: {}%", progress);
if committed < START_HEIGHT_FAST {
let slow_end = START_HEIGHT_FAST.min(total_heights);
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),
/// 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
/// raw vec lengths are authoritative. Pass `cap = Some(&safe_lengths)` from
/// reader paths so concurrent writer pushes past the cap are invisible.
@@ -166,6 +188,33 @@ impl Vecs {
range: Range<usize>,
cap: Option<&Lengths>,
) -> 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 {
Some(c) => (
c.tx_index.to_usize(),
@@ -193,8 +242,6 @@ impl Vecs {
.first_txout_index
.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
// tx-indexed first_txout_index lookup. Accessed tx_index values
// are strictly increasing across blocks, so it only advances forward.
@@ -239,26 +286,21 @@ impl Vecs {
&mut output_types,
);
let mut hist = Histogram::zeros();
for tx in 0..tx_count {
let tx_outputs = (0..tx_count).map(|tx| {
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);
if output_types[lo..hi].contains(&OutputType::OpReturn) {
continue;
}
for i in lo..hi {
if let Some(bin) = oracle.output_to_bin(values[i], output_types[i]) {
hist.increment(bin);
}
}
}
values[lo..hi]
.iter()
.copied()
.zip(output_types[lo..hi].iter().copied())
});
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 ohlcs::{LazyOhlcVecs, OhlcVecs};
pub const DB_NAME: &str = "prices";
pub const DB_NAME: &str = "price";
#[derive(Traversable)]
pub struct Vecs<M: StorageMode = Rw> {
@@ -4,7 +4,7 @@ use brk_types::Sats;
use vecdb::{Exit, VecIndex};
use super::Vecs;
use crate::{mining, outputs, prices};
use crate::{mining, outputs, price};
impl Vecs {
pub(crate) fn compute(
@@ -12,7 +12,7 @@ impl Vecs {
indexer: &Indexer,
outputs: &outputs::Vecs,
mining: &mining::Vecs,
prices: &prices::Vecs,
prices: &price::Vecs,
exit: &Exit,
) -> Result<()> {
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;
use super::Vecs;
use crate::{blocks, distribution, mining, outputs, prices, transactions};
use crate::{blocks, distribution, mining, outputs, price, transactions};
impl Vecs {
#[allow(clippy::too_many_arguments)]
@@ -18,7 +18,7 @@ impl Vecs {
blocks: &blocks::Vecs,
mining: &mining::Vecs,
transactions: &transactions::Vecs,
prices: &prices::Vecs,
prices: &price::Vecs,
distribution: &distribution::Vecs,
exit: &Exit,
) -> Result<()> {
@@ -3,7 +3,7 @@ use brk_indexer::Indexer;
use vecdb::Exit;
use super::Vecs;
use crate::{blocks, indexes, inputs, prices};
use crate::{blocks, indexes, inputs, price};
impl Vecs {
#[allow(clippy::too_many_arguments)]
@@ -13,7 +13,7 @@ impl Vecs {
indexes: &indexes::Vecs,
blocks: &blocks::Vecs,
inputs: &inputs::Vecs,
prices: &prices::Vecs,
prices: &price::Vecs,
exit: &Exit,
) -> Result<()> {
self.db.sync_bg_tasks()?;
@@ -5,7 +5,7 @@ use vecdb::Exit;
use super::Vecs;
use crate::transactions::{count, fees};
use crate::{indexes, internal::Windows, prices};
use crate::{indexes, internal::Windows, price};
impl Vecs {
#[allow(clippy::too_many_arguments)]
@@ -13,7 +13,7 @@ impl Vecs {
&mut self,
indexer: &Indexer,
indexes: &indexes::Vecs,
prices: &prices::Vecs,
prices: &price::Vecs,
count_vecs: &count::Vecs,
fees_vecs: &fees::Vecs,
exit: &Exit,
+16 -8
View File
@@ -1,6 +1,6 @@
//! Mempool info + price-blending output histogram.
use brk_oracle::Histogram;
use brk_oracle::HistogramRaw;
use brk_types::MempoolInfo;
use crate::Mempool;
@@ -11,13 +11,21 @@ impl Mempool {
self.read().info.clone()
}
/// Snapshot of pre-bucketed oracle bins across all live mempool tx
/// outputs. The total is maintained incrementally by `TxStore` on
/// every insert/remove, so this hot path is `O(NUM_BINS)` regardless
/// of pool size. Used by `live_price` to blend the mempool into the
/// committed oracle without re-parsing scripts per request.
/// Snapshot of pre-bucketed round-dollar-eligible bins across all live
/// mempool tx outputs. Maintained incrementally by `TxStore` on every
/// insert/remove, so this hot path is `O(NUM_BINS)` regardless of pool
/// size. Used by `live_price` to blend the mempool into the committed
/// oracle without re-parsing scripts per request.
#[must_use]
pub fn live_histogram(&self) -> Histogram {
self.read().txs.live_histogram()
pub fn live_eligible_histogram(&self) -> HistogramRaw {
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.
mod addr_tracker;
mod live_histograms;
mod outpoint_spends;
mod output_bins;
mod tx_graveyard;
mod tx_store;
pub use addr_tracker::AddrTracker;
pub use live_histograms::LiveHistograms;
pub use outpoint_spends::OutpointSpends;
pub use output_bins::OutputBins;
pub use tx_graveyard::{TxGraveyard, TxTombstone};
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 rustc_hash::{FxHashMap, FxHashSet};
use crate::{state::TxEntry, stores::OutputBins};
use crate::{state::TxEntry, stores::LiveHistograms};
const RECENT_CAP: usize = 10;
/// Per-tx record: live tx body, its mempool entry, and the pre-bucketed
/// oracle bins for its outputs. Kept under one key so a single map probe
/// returns everything readers need.
/// Per-tx record: live tx body and its mempool entry, kept under one key
/// so a single map probe returns everything readers need.
pub struct TxRecord {
pub tx: Transaction,
pub entry: TxEntry,
pub output_bins: OutputBins,
}
impl TxRecord {
pub fn new(tx: Transaction, entry: TxEntry) -> Self {
let output_bins = OutputBins::from_tx(&tx);
Self {
tx,
entry,
output_bins,
}
Self { tx, entry }
}
}
@@ -32,15 +25,15 @@ impl TxRecord {
/// set of prefixes whose tx still has at least one `prevout: None`,
/// maintained on every `insert` / `remove_by_prefix` / `apply_fills`
/// so the post-update prevout filler can early-exit when empty.
/// `live_histogram` mirrors the union of each record's `OutputBins`,
/// kept in sync on `insert` / `remove_by_prefix` so the oracle-blend
/// read path is a single array clone, not a full pool walk.
/// `histograms` holds the eligible (oracle-blend) and raw per-bin output
/// histograms, kept in sync on `insert` / `remove_by_prefix` so each read
/// path is a single array clone, not a full pool walk.
#[derive(Default)]
pub struct TxStore {
records: FxHashMap<TxidPrefix, TxRecord>,
recent: Vec<MempoolRecentTx>,
unresolved: FxHashSet<TxidPrefix>,
live_histogram: Histogram,
histograms: LiveHistograms,
}
impl TxStore {
@@ -92,9 +85,7 @@ impl TxStore {
self.unresolved.insert(prefix);
}
let record = TxRecord::new(tx, entry);
for bin in record.output_bins.iter() {
self.live_histogram[bin as usize] += 1;
}
self.histograms.add(&record);
self.records.insert(prefix, record);
}
@@ -112,16 +103,21 @@ impl TxStore {
pub fn remove_by_prefix(&mut self, prefix: &TxidPrefix) -> Option<TxRecord> {
let record = self.records.remove(prefix)?;
self.unresolved.remove(prefix);
for bin in record.output_bins.iter() {
self.live_histogram[bin as usize] -= 1;
}
self.histograms.remove(&record);
Some(record)
}
/// Snapshot the live oracle-bin histogram. Maintained incrementally
/// on insert/remove, so this is `O(NUM_BINS)`, not `O(live_outputs)`.
pub fn live_histogram(&self) -> Histogram {
self.live_histogram.clone()
/// Snapshot the round-dollar-eligible histogram that feeds the oracle
/// blend. Maintained incrementally, so this is `O(NUM_BINS)`, not
/// `O(live_outputs)`.
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
@@ -263,7 +259,10 @@ mod tests {
assert_eq!(applied[0].value, new_prevout.value);
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!(
record.tx.input[1].prevout.as_ref().unwrap().value,
prev_present.value
@@ -277,7 +276,10 @@ mod tests {
let stray_prefix = TxidPrefix::from(&fake_txid(0xFF));
let applied = store.apply_fills(
&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());
}
@@ -319,10 +321,7 @@ mod tests {
let tx_a = fake_tx(
20,
&[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(
21,
@@ -335,11 +334,41 @@ mod tests {
store.insert(tx_a, entry_a);
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");
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);
}
#[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
**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.
@@ -42,13 +42,13 @@ The spacing between spikes is constant (set by the ratios between dollar amounts
## 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:
### 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
@@ -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:
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
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
```
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
@@ -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 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
@@ -134,10 +134,12 @@ All parameters via `Config` with sensible defaults:
|-----------|---------|---------|
| `alpha` | 2/7 | EMA decay rate (~6-block span) |
| `window_size` | 12 | Ring buffer depth in blocks |
| `search_below` / `search_above` | 9 / 11 | Search window around previous estimate (bins) |
| `min_sats` | 1,000 | Dust threshold |
| `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 |
| `search_below` / `search_above` | 12 / 11 | Search window around previous estimate (bins) |
| `shape_weight` | 0 | Shape-anchoring restoring-force weight. 0 disables it. `Config::slow()` sets 8 for the cold-start |
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
@@ -145,35 +147,36 @@ All parameters via `Config` with sensible defaults:
| | 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 |
| 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 |
| 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 |
| Validated from | Height 525,000 (May 2018) | December 2023 |
| 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 340,000 (January 2015) | Dec 15, 2023 |
| 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 |
## 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
| Metric | Value |
|--------|-------|
| Median error | 0.11% |
| 95th percentile | 0.67% |
| 99th percentile | 1.7% |
| 99.9th percentile | 5.4% |
| RMSE | 0.50% |
| Max error | 33.4% |
| Bias | +0.00 bins (essentially zero) |
| Blocks > 5% error | 472 (0.11%) |
| Blocks > 10% error | 177 |
| Blocks > 20% error | 3 |
| Median error | 0.15% |
| 95th percentile | 1.2% |
| 99th percentile | 3.4% |
| 99.9th percentile | 15.6% |
| RMSE | 0.97% |
| Max error | 33.8% |
| Bias | +0.06 bins (essentially zero) |
| Blocks > 5% error | 3,233 (0.542%) |
| Blocks > 10% error | 1,323 |
| Blocks > 20% error | 154 |
### Daily candles
@@ -181,36 +184,57 @@ Oracle daily OHLC built from per-block prices vs exchange daily OHLC:
| | Median | RMSE | Max |
|-------|--------|------|-----|
| Open | 0.21% | 0.65% | 15.3% |
| High | 0.53% | 1.12% | 28.0% |
| Low | 0.51% | 1.38% | 19.7% |
| Close | 0.24% | 0.73% | 15.4% |
| Open | 0.24% | 1.07% | 29.1% |
| High | 0.58% | 1.47% | 27.3% |
| Low | 0.53% | 1.95% | 55.1% |
| Close | 0.27% | 1.18% | 29.2% |
### By year
| 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 |
| 2019 | 54,272 | 0.16% | 0.69% | 17.4% | 165 | 53 | 0 | $3,338$13,868 |
| 2020 | 53,102 | 0.10% | 0.44% | 12.6% | 70 | 6 | 0 | $3,858$29,322 |
| 2015 | 51,249 | 0.26% | 1.67% | 33.8% | 916 | 449 | 25 | $198$500 |
| 2016 | 54,753 | 0.33% | 0.80% | 16.9% | 150 | 33 | 0 | $351$989 |
| 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 |
| 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 |
| 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 |
| 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?
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.
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
### 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
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
//! to identical ref_bin values after the ring buffer fills.
//! Verify the production restart property: an oracle restored via
//! `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
//! up to 630k. After window_size blocks, each test oracle should produce the
//! same ref_bin as the reference, proving the truncated EMA provides
//! start-point independence.
//! Mirrors the production transaction filter exactly, so it exercises the same code path
//! `brk_computer::price::compute::feed_blocks` uses at runtime.
//!
//! Run with: cargo run -p brk_oracle --example determinism --release
@@ -12,26 +13,35 @@ use std::path::PathBuf;
use brk_indexer::Indexer;
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 vecdb::{AnyVec, ReadableVec, VecIndex};
fn seed_bin(height: usize) -> f64 {
let price: f64 = PRICES
.lines()
.nth(height - 1)
.expect("prices.txt too short")
.parse()
.expect("Failed to parse seed price");
cents_to_bin(price * 100.0)
struct Block {
height: usize,
values: Vec<Sats>,
output_types: Vec<OutputType>,
tx_starts: Vec<usize>,
out_start: usize,
out_end: usize,
}
struct TestRun {
start_height: usize,
oracle: Option<Oracle>,
converged_at: Option<usize>,
diverged_after: bool,
fn build_histogram(block: &Block) -> HistogramRaw {
let tx_outputs = (0..block.tx_starts.len()).map(|tx| {
let lo = block.tx_starts[tx] - block.out_start;
let hi = block
.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() {
@@ -45,62 +55,54 @@ fn main() {
let indexer = Indexer::forced_import(&data_dir).expect("Failed to load indexer");
let total_heights = indexer.vecs.blocks.timestamp.len();
let config = Config::default();
let window_size = config.window_size;
let fast_config = Config::default();
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_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();
// Reference oracle at 575k.
let ref_start = START_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 mut blocks: Vec<Block> = Vec::with_capacity(end_height - load_start);
for h in load_start..end_height {
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 block_first_tx = ft.to_usize() + 1;
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());
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
.vecs
.outputs
@@ -112,95 +114,85 @@ fn main() {
.output_type
.collect_range_at(out_start, out_end);
let mut hist = Histogram::zeros();
for (sats, output_type) in values.into_iter().zip(output_types) {
if let Some(bin) = default_eligible_bin(sats, output_type) {
hist.increment(bin as usize);
}
}
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);
}
}
blocks.push(Block {
height: h,
values,
output_types,
tx_starts,
out_start,
out_end,
});
}
// Print results.
println!();
println!("{:<12} {:>16} {:>8}", "Start", "Converged at", "Blocks");
println!("{}", "-".repeat(40));
let mut max_blocks = 0usize;
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;
let mut continuous = Oracle::from_seed();
let continuous_bins: Vec<f64> = blocks
.iter()
.map(|b| {
if b.height == START_HEIGHT_FAST {
continuous.reconfigure(fast_config);
}
println!("{:<12} {:>16} {:>8}", run.start_height, converged, blocks);
if run.diverged_after {
diverged.push(run.start_height);
}
} else {
println!("{:<12} {:>16} {:>8}", run.start_height, "NEVER", "-");
failed.push(run.start_height);
}
}
println!();
continuous.process_histogram(&build_histogram(b))
})
.collect();
println!(
"{}/{} converged, max {} blocks to converge (window_size={})",
runs.len() - failed.len(),
runs.len(),
max_blocks,
window_size,
"Continuous oracle: {} blocks processed",
continuous_bins.len()
);
if !diverged.is_empty() {
println!("DIVERGED after convergence: {:?}", diverged);
}
if !failed.is_empty() {
println!("NEVER converged: {:?}", failed);
let prev_bin = continuous_bins[restart_at - load_start - 1];
let seed_bin = cents_to_bin(bin_to_cents(prev_bin) as f64);
println!(
"Restart at {restart_at}: prev_bin={prev_bin:.4} -> cents -> seed_bin={seed_bin:.4} (delta {:.6})",
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.
assert!(
failed.is_empty(),
"{} oracles never converged: {:?}",
failed.len(),
failed
);
assert!(
diverged.is_empty(),
"{} oracles diverged after convergence: {:?}",
diverged.len(),
diverged
);
assert!(
max_blocks <= window_size * 2,
"Convergence took {} blocks, expected <= {} (2 * window_size)",
max_blocks,
window_size * 2
println!();
if mismatches.is_empty() {
println!(
"All {} blocks from {restart_at} onward match exactly.",
restored_bins.len()
);
} else {
println!(
"{} of {} blocks differ (showing up to 5):",
mismatches.len(),
restored_bins.len()
);
for (h, c, r) in mismatches.iter().take(5) {
println!(
" h={h}: continuous={c:.6}, restored={r:.6}, delta={:.6}",
r - c
);
}
}
assert_eq!(
mismatches.len(),
0,
"restored oracle diverged from continuous oracle"
);
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