Compare commits

...

84 Commits

Author SHA1 Message Date
nym21 8d43f340a0 website_next: part 12 2026-07-10 12:45:55 +02:00
nym21 e279116bff website_next: part 11 2026-07-09 23:08:47 +02:00
nym21 5c376acb9f website_next: part 10 2026-07-09 18:49:01 +02:00
nym21 a43b00c12c website_next: part 9 2026-07-08 11:24:52 +02:00
nym21 15fbdc168f website_next: part 8 2026-07-08 11:24:39 +02:00
nym21 046c624abf website_next: part 7 2026-07-06 18:52:51 +02:00
nym21 4d7212fb63 website_next: part 6 2026-07-06 18:05:31 +02:00
nym21 2207ec1b7e website_next: part 5 2026-07-06 15:54:25 +02:00
nym21 3f2a48f248 website_next: part 4 2026-07-06 13:17:11 +02:00
nym21 3f9edb211e website_next: part 3 2026-07-06 11:15:39 +02:00
nym21 eee1a10d2a website_next: part 2 2026-07-05 14:08:57 +02:00
nym21 0bf2cd77dc website_next: snapshot 2026-07-04 22:09:40 +02:00
nym21 a99c06013b website_next: snapshot 2026-07-04 13:27:57 +02:00
nym21 10ef95497f website_next: snapshot 2026-07-03 21:06:32 +02:00
nym21 e0a618837e website_next: snapshot 2026-07-03 11:29:13 +02:00
nym21 5bd1f0b625 deps: bumped 2026-07-02 16:46:47 +02:00
nym21 4d3e8bbcaf website: auto port selection 2026-07-01 17:23:02 +02:00
nym21 153fcdf4e0 website_next: move hash logic to clients 2026-07-01 17:04:29 +02:00
nym21 5d83ee4d70 changelog: updated 2026-06-29 17:02:34 +02:00
nym21 10d29aa2ea clients: bump versions 2026-06-27 18:03:51 +02:00
nym21 528880bd04 release: v0.3.6 2026-06-27 12:18:42 +02:00
nym21 5937fbe724 docs: update generated docs 2026-06-27 12:18:11 +02:00
nym21 7a718293c0 website: fix capture button part 2 2026-06-27 12:12:38 +02:00
nym21 75f37b6695 website: fix capture button 2026-06-27 12:05:39 +02:00
nym21 f9688629ca deps: bumped 2026-06-26 19:40:43 +02:00
nym21 c9e704bdf4 clients: bump versions 2026-06-25 12:31:07 +02:00
nym21 1e0a7d6d0f release: v0.3.5 2026-06-23 18:54:46 +02:00
nym21 565251f12f docs: update generated docs 2026-06-23 18:54:14 +02:00
nym21 9879a986aa fixes 2026-06-23 18:47:51 +02:00
nym21 8d7ef8da61 fixes 2026-06-23 18:42:28 +02:00
nym21 17478a4ac4 website: redesign part 34 2026-06-22 16:42:14 +02:00
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
1128 changed files with 35696 additions and 5271 deletions
+1
View File
@@ -18,6 +18,7 @@ _*
/*.py
/*.json
/*.html
!/btc-cycle-sim.html
/research
/filter_*
/heatmaps*
Generated
+222 -309
View File
File diff suppressed because it is too large Load Diff
+31 -30
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.1"
package.version = "0.3.6"
package.homepage = "https://bitcoinresearchkit.org"
package.repository = "https://github.com/bitcoinresearchkit/brk"
package.readme = "README.md"
@@ -35,51 +35,52 @@ 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.100", features = ["serde"] }
brk_alloc = { version = "0.3.1", path = "crates/brk_alloc" }
brk_bencher = { version = "0.3.1", path = "crates/brk_bencher" }
brk_bindgen = { version = "0.3.1", path = "crates/brk_bindgen" }
brk_cli = { version = "0.3.1", path = "crates/brk_cli" }
brk_client = { version = "0.3.1", path = "crates/brk_client" }
brk_cohort = { version = "0.3.1", path = "crates/brk_cohort" }
brk_computer = { version = "0.3.1", path = "crates/brk_computer" }
brk_error = { version = "0.3.1", path = "crates/brk_error" }
brk_fetcher = { version = "0.3.1", path = "crates/brk_fetcher" }
brk_indexer = { version = "0.3.1", path = "crates/brk_indexer" }
brk_iterator = { version = "0.3.1", path = "crates/brk_iterator" }
brk_logger = { version = "0.3.1", path = "crates/brk_logger" }
brk_mempool = { version = "0.3.1", path = "crates/brk_mempool" }
brk_oracle = { version = "0.3.1", path = "crates/brk_oracle" }
brk_query = { version = "0.3.1", path = "crates/brk_query", features = ["tokio"] }
brk_reader = { version = "0.3.1", path = "crates/brk_reader" }
brk_rpc = { version = "0.3.1", path = "crates/brk_rpc" }
brk_server = { version = "0.3.1", path = "crates/brk_server" }
brk_store = { version = "0.3.1", path = "crates/brk_store" }
brk_traversable = { version = "0.3.1", path = "crates/brk_traversable", features = ["pco", "derive"] }
brk_traversable_derive = { version = "0.3.1", path = "crates/brk_traversable_derive" }
brk_types = { version = "0.3.1", path = "crates/brk_types" }
brk_website = { version = "0.3.1", path = "crates/brk_website" }
bitcoin = { version = "0.32.101", features = ["serde"] }
brk_alloc = { version = "0.3.6", path = "crates/brk_alloc" }
brk_bencher = { version = "0.3.6", path = "crates/brk_bencher" }
brk_bindgen = { version = "0.3.6", path = "crates/brk_bindgen" }
brk_cli = { version = "0.3.6", path = "crates/brk_cli" }
brk_client = { version = "0.3.6", path = "crates/brk_client" }
brk_cohort = { version = "0.3.6", path = "crates/brk_cohort" }
brk_computer = { version = "0.3.6", path = "crates/brk_computer" }
brk_error = { version = "0.3.6", path = "crates/brk_error" }
brk_fetcher = { version = "0.3.6", path = "crates/brk_fetcher" }
brk_indexer = { version = "0.3.6", path = "crates/brk_indexer" }
brk_iterator = { version = "0.3.6", path = "crates/brk_iterator" }
brk_logger = { version = "0.3.6", path = "crates/brk_logger" }
brk_mempool = { version = "0.3.6", path = "crates/brk_mempool" }
brk_oracle = { version = "0.3.6", path = "crates/brk_oracle" }
brk_query = { version = "0.3.6", path = "crates/brk_query", features = ["tokio"] }
brk_reader = { version = "0.3.6", path = "crates/brk_reader" }
brk_rpc = { version = "0.3.6", path = "crates/brk_rpc" }
brk_server = { version = "0.3.6", path = "crates/brk_server" }
brk_store = { version = "0.3.6", path = "crates/brk_store" }
brk_traversable = { version = "0.3.6", path = "crates/brk_traversable", features = ["pco", "derive"] }
brk_traversable_derive = { version = "0.3.6", path = "crates/brk_traversable_derive" }
brk_types = { version = "0.3.6", path = "crates/brk_types" }
brk_website = { version = "0.3.6", 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.14.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.28", features = ["perf-inline", "tz-system"], default-features = false }
jiff = { version = "0.2.31", features = ["perf-inline", "tz-system"], default-features = false }
owo-colors = "4.3.0"
parking_lot = "0.12.5"
pco = "1.0.2"
rayon = "1.12.0"
rapidhash = "4.4.2"
rustc-hash = "2.1.2"
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.150", features = ["float_roundtrip", "preserve_order"] }
smallvec = "1.15.1"
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"] }
+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, cache?: boolean }}}} [options]",
" * @param {{{{ signal?: AbortSignal, onValue?: (value: {}) => void, cache?: boolean, memCache?: 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, cache } = {}".to_string()
"{ signal, onValue, cache, memCache } = {}".to_string()
} else {
format!("{}, {{ signal, onValue, cache }} = {{}}", params)
format!("{}, {{ signal, onValue, cache, memCache }} = {{}}", 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, cache })".to_string()
"this.getBytes(path, { signal, onValue, cache, memCache })".to_string()
} else if endpoint.returns_json() {
"this.getJson(path, { signal, onValue, cache })".to_string()
"this.getJson(path, { signal, onValue, cache, memCache })".to_string()
} else if endpoint.response_kind.text_is_numeric() {
"Number(await this.getText(path, { signal, cache, onValue: onValue ? (v) => onValue(Number(v)) : undefined }))".to_string()
"Number(await this.getText(path, { signal, cache, memCache, onValue: onValue ? (v) => onValue(Number(v)) : undefined }))".to_string()
} else {
"this.getText(path, { signal, onValue, cache })".to_string()
"this.getText(path, { signal, onValue, cache, memCache })".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, cache }});"
" if (format === 'csv') return this.getText(path, {{ signal, onValue, cache, memCache }});"
)
.unwrap();
}
@@ -51,6 +51,12 @@ const _openBrowserCache = (option) => {{
return caches.open(name).catch(() => null);
}};
/**
* @param {{string}} url
* @returns {{URL}}
*/
const _parseBaseUrl = (url) => new URL(url, typeof location === 'undefined' ? undefined : location.href);
/**
* Custom error class for BRK client errors
*/
@@ -237,8 +243,8 @@ function _wrapSeriesData(raw) {{
* @property {{(n: number) => RangeBuilder<T>}} first - Get first n items
* @property {{(n: number) => RangeBuilder<T>}} last - Get last n items
* @property {{(n: number) => SkippedBuilder<T>}} skip - Skip first n items, chain with take()
* @property {{(onValue?: (value: SeriesData<T>) => void) => Promise<SeriesData<T>>}} fetch - Fetch all data
* @property {{() => Promise<string>}} fetchCsv - Fetch all data as CSV
* @property {{(arg?: SeriesFetchArg<T>, options?: ClientFetchOptions<SeriesData<T>>) => Promise<SeriesData<T>>}} fetch - Fetch all data
* @property {{(options?: ClientFetchOptions<string>) => Promise<string>}} fetchCsv - Fetch all data as CSV
* @property {{() => Promise<number>}} len - Get total number of data points
* @property {{() => Promise<Version>}} version - Get the current version of the series
* @property {{Thenable<T>}} then - Thenable (await endpoint)
@@ -253,8 +259,8 @@ function _wrapSeriesData(raw) {{
* @property {{(n: number) => DateRangeBuilder<T>}} first - Get first n items
* @property {{(n: number) => DateRangeBuilder<T>}} last - Get last n items
* @property {{(n: number) => DateSkippedBuilder<T>}} skip - Skip first n items, chain with take()
* @property {{(onValue?: (value: DateSeriesData<T>) => void) => Promise<DateSeriesData<T>>}} fetch - Fetch all data
* @property {{() => Promise<string>}} fetchCsv - Fetch all data as CSV
* @property {{(arg?: DateSeriesFetchArg<T>, options?: ClientFetchOptions<DateSeriesData<T>>) => Promise<DateSeriesData<T>>}} fetch - Fetch all data
* @property {{(options?: ClientFetchOptions<string>) => Promise<string>}} fetchCsv - Fetch all data as CSV
* @property {{() => Promise<number>}} len - Get total number of data points
* @property {{() => Promise<Version>}} version - Get the current version of the series
* @property {{DateThenable<T>}} then - Thenable (await endpoint)
@@ -263,41 +269,53 @@ function _wrapSeriesData(raw) {{
/** @typedef {{SeriesEndpoint<any>}} AnySeriesEndpoint */
/**
* @template T
* @typedef {{Object}} ClientFetchOptions
* @property {{AbortSignal}} [signal] - Abort this request
* @property {{boolean}} [cache] - Use HTTP/browser/client caches. Set false for a no-store network fetch.
* @property {{boolean}} [memCache] - Use the parsed in-memory response cache. Set false for large one-shot reads.
* @property {{(value: T) => void}} [onValue] - Receive stale/fresh values as they arrive
*/
/** @template T @typedef {{ClientFetchOptions<SeriesData<T>> | ((value: SeriesData<T>) => void)}} SeriesFetchArg */
/** @template T @typedef {{ClientFetchOptions<DateSeriesData<T>> | ((value: DateSeriesData<T>) => void)}} DateSeriesFetchArg */
/** @template T @typedef {{Object}} SingleItemBuilder
* @property {{(onValue?: (value: SeriesData<T>) => void) => Promise<SeriesData<T>>}} fetch - Fetch the item
* @property {{() => Promise<string>}} fetchCsv - Fetch as CSV
* @property {{(arg?: SeriesFetchArg<T>, options?: ClientFetchOptions<SeriesData<T>>) => Promise<SeriesData<T>>}} fetch - Fetch the item
* @property {{(options?: ClientFetchOptions<string>) => Promise<string>}} fetchCsv - Fetch as CSV
* @property {{Thenable<T>}} then - Thenable
*/
/** @template T @typedef {{Object}} DateSingleItemBuilder
* @property {{(onValue?: (value: DateSeriesData<T>) => void) => Promise<DateSeriesData<T>>}} fetch - Fetch the item
* @property {{() => Promise<string>}} fetchCsv - Fetch as CSV
* @property {{(arg?: DateSeriesFetchArg<T>, options?: ClientFetchOptions<DateSeriesData<T>>) => Promise<DateSeriesData<T>>}} fetch - Fetch the item
* @property {{(options?: ClientFetchOptions<string>) => Promise<string>}} fetchCsv - Fetch as CSV
* @property {{DateThenable<T>}} then - Thenable
*/
/** @template T @typedef {{Object}} SkippedBuilder
* @property {{(n: number) => RangeBuilder<T>}} take - Take n items after skipped position
* @property {{(onValue?: (value: SeriesData<T>) => void) => Promise<SeriesData<T>>}} fetch - Fetch from skipped position to end
* @property {{() => Promise<string>}} fetchCsv - Fetch as CSV
* @property {{(arg?: SeriesFetchArg<T>, options?: ClientFetchOptions<SeriesData<T>>) => Promise<SeriesData<T>>}} fetch - Fetch from skipped position to end
* @property {{(options?: ClientFetchOptions<string>) => Promise<string>}} fetchCsv - Fetch as CSV
* @property {{Thenable<T>}} then - Thenable
*/
/** @template T @typedef {{Object}} DateSkippedBuilder
* @property {{(n: number) => DateRangeBuilder<T>}} take - Take n items after skipped position
* @property {{(onValue?: (value: DateSeriesData<T>) => void) => Promise<DateSeriesData<T>>}} fetch - Fetch from skipped position to end
* @property {{() => Promise<string>}} fetchCsv - Fetch as CSV
* @property {{(arg?: DateSeriesFetchArg<T>, options?: ClientFetchOptions<DateSeriesData<T>>) => Promise<DateSeriesData<T>>}} fetch - Fetch from skipped position to end
* @property {{(options?: ClientFetchOptions<string>) => Promise<string>}} fetchCsv - Fetch as CSV
* @property {{DateThenable<T>}} then - Thenable
*/
/** @template T @typedef {{Object}} RangeBuilder
* @property {{(onValue?: (value: SeriesData<T>) => void) => Promise<SeriesData<T>>}} fetch - Fetch the range
* @property {{() => Promise<string>}} fetchCsv - Fetch as CSV
* @property {{(arg?: SeriesFetchArg<T>, options?: ClientFetchOptions<SeriesData<T>>) => Promise<SeriesData<T>>}} fetch - Fetch the range
* @property {{(options?: ClientFetchOptions<string>) => Promise<string>}} fetchCsv - Fetch as CSV
* @property {{Thenable<T>}} then - Thenable
*/
/** @template T @typedef {{Object}} DateRangeBuilder
* @property {{(onValue?: (value: DateSeriesData<T>) => void) => Promise<DateSeriesData<T>>}} fetch - Fetch the range
* @property {{() => Promise<string>}} fetchCsv - Fetch as CSV
* @property {{(arg?: DateSeriesFetchArg<T>, options?: ClientFetchOptions<DateSeriesData<T>>) => Promise<DateSeriesData<T>>}} fetch - Fetch the range
* @property {{(options?: ClientFetchOptions<string>) => Promise<string>}} fetchCsv - Fetch as CSV
* @property {{DateThenable<T>}} then - Thenable
*/
@@ -344,8 +362,8 @@ function _endpoint(client, name, index) {{
* @returns {{DateRangeBuilder<T>}}
*/
const rangeBuilder = (start, end) => ({{
fetch(onValue) {{ return client._fetchSeriesData(buildPath(start, end), onValue); }},
fetchCsv() {{ return client.getText(buildPath(start, end, 'csv')); }},
fetch(arg, options) {{ return client._fetchSeriesData(buildPath(start, end), arg, options); }},
fetchCsv(options) {{ return client.getText(buildPath(start, end, 'csv'), options); }},
then(resolve, reject) {{ return this.fetch().then(resolve, reject); }},
}});
@@ -354,8 +372,8 @@ function _endpoint(client, name, index) {{
* @returns {{DateSingleItemBuilder<T>}}
*/
const singleItemBuilder = (idx) => ({{
fetch(onValue) {{ return client._fetchSeriesData(buildPath(idx, idx + 1), onValue); }},
fetchCsv() {{ return client.getText(buildPath(idx, idx + 1, 'csv')); }},
fetch(arg, options) {{ return client._fetchSeriesData(buildPath(idx, idx + 1), arg, options); }},
fetchCsv(options) {{ return client.getText(buildPath(idx, idx + 1, 'csv'), options); }},
then(resolve, reject) {{ return this.fetch().then(resolve, reject); }},
}});
@@ -365,8 +383,8 @@ function _endpoint(client, name, index) {{
*/
const skippedBuilder = (start) => ({{
take(n) {{ return rangeBuilder(start, start + n); }},
fetch(onValue) {{ return client._fetchSeriesData(buildPath(start, undefined), onValue); }},
fetchCsv() {{ return client.getText(buildPath(start, undefined, 'csv')); }},
fetch(arg, options) {{ return client._fetchSeriesData(buildPath(start, undefined), arg, options); }},
fetchCsv(options) {{ return client.getText(buildPath(start, undefined, 'csv'), options); }},
then(resolve, reject) {{ return this.fetch().then(resolve, reject); }},
}});
@@ -381,8 +399,8 @@ function _endpoint(client, name, index) {{
first(n) {{ return rangeBuilder(undefined, n); }},
last(n) {{ return n === 0 ? rangeBuilder(undefined, 0) : rangeBuilder(-n, undefined); }},
skip(n) {{ return skippedBuilder(n); }},
fetch(onValue) {{ return client._fetchSeriesData(buildPath(), onValue); }},
fetchCsv() {{ return client.getText(buildPath(undefined, undefined, 'csv')); }},
fetch(arg, options) {{ return client._fetchSeriesData(buildPath(), arg, options); }},
fetchCsv(options) {{ return client.getText(buildPath(undefined, undefined, 'csv'), options); }},
len() {{ return client.getSeriesLen(name, index); }},
version() {{ return client.getSeriesVersion(name, index); }},
then(resolve, reject) {{ return this.fetch().then(resolve, reject); }},
@@ -403,6 +421,9 @@ class BrkClientBase {{
const isString = typeof options === 'string';
const rawUrl = isString ? options : options.baseUrl;
this.baseUrl = rawUrl.endsWith('/') ? rawUrl.slice(0, -1) : rawUrl;
const url = _parseBaseUrl(this.baseUrl);
this.url = url.href.endsWith('/') ? url.href.slice(0, -1) : url.href;
this.domain = url.hostname;
this.timeout = isString ? 5000 : (options.timeout ?? 5000);
/** @type {{Promise<Cache | null>}} */
this._browserCachePromise = _openBrowserCache(isString ? undefined : options.browserCache);
@@ -478,10 +499,10 @@ class BrkClientBase {{
* @template T
* @param {{string}} path
* @param {{(res: Response) => Promise<T>}} parse - Response body reader
* @param {{{{ onValue?: (value: T) => void, signal?: AbortSignal, cache?: boolean }}}} [options]
* @param {{ClientFetchOptions<T>}} [options]
* @returns {{Promise<T>}}
*/
async _getCached(path, parse, {{ onValue, signal, cache = true }} = {{}}) {{
async _getCached(path, parse, {{ onValue, signal, cache = true, memCache = true }} = {{}}) {{
if (!cache) {{
const res = await this.get(path, {{ signal, cache }});
const value = await parse(res);
@@ -490,8 +511,9 @@ class BrkClientBase {{
}}
const url = `${{this.baseUrl}}${{path}}`;
const useMemCache = memCache !== false;
/** @type {{_MemEntry<T> | undefined}} */
const memHit = this._memGet(url);
const memHit = useMemCache ? this._memGet(url) : undefined;
const browserCache = this._browserCache;
// L1 fast path: deliver from memCache, revalidate via network.
@@ -504,7 +526,7 @@ class BrkClientBase {{
if (netEtag && netEtag === memHit.etag) return memHit.value;
const cloned = browserCache ? res.clone() : null;
const value = await parse(res);
this._memSet(url, netEtag, value);
if (useMemCache) this._memSet(url, netEtag, value);
if (onValue) onValue(value);
if (cloned && browserCache) {{
const cacheStore = browserCache;
@@ -523,7 +545,7 @@ class BrkClientBase {{
if (!res || networkSettled) return null;
const value = await parse(res);
if (networkSettled) return value;
this._memSet(url, res.headers.get('ETag'), value);
if (useMemCache) this._memSet(url, res.headers.get('ETag'), value);
onValue(value);
return value;
}}).catch(() => null)
@@ -534,11 +556,11 @@ class BrkClientBase {{
networkSettled = true;
const netEtag = res.headers.get('ETag');
// Stale won and populated memCache with matching ETag → reuse, skip parse + second onValue.
const populated = /** @type {{_MemEntry<T> | undefined}} */ (this._memGet(url));
const populated = useMemCache ? /** @type {{_MemEntry<T> | undefined}} */ (this._memGet(url)) : undefined;
if (populated && netEtag && netEtag === populated.etag) return populated.value;
const cloned = browserCache ? res.clone() : null;
const value = await parse(res);
this._memSet(url, netEtag, value);
if (useMemCache) this._memSet(url, netEtag, value);
if (onValue) onValue(value);
if (cloned && browserCache) {{
const cacheStore = browserCache;
@@ -556,7 +578,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, cache?: boolean }}}} [options]
* @param {{ClientFetchOptions<T>}} [options]
* @returns {{Promise<T>}}
*/
getJson(path, options) {{
@@ -567,7 +589,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, cache?: boolean }}}} [options]
* @param {{ClientFetchOptions<string>}} [options]
* @returns {{Promise<string>}}
*/
getText(path, options) {{
@@ -578,7 +600,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, cache?: boolean }}}} [options]
* @param {{ClientFetchOptions<Uint8Array>}} [options]
* @returns {{Promise<Uint8Array>}}
*/
getBytes(path, options) {{
@@ -650,12 +672,17 @@ class BrkClientBase {{
* Fetch series data and wrap with helper methods (internal)
* @template T
* @param {{string}} path
* @param {{(value: DateSeriesData<T>) => void}} [onValue]
* @param {{DateSeriesFetchArg<T>}} [arg]
* @param {{ClientFetchOptions<DateSeriesData<T>>}} [options]
* @returns {{Promise<DateSeriesData<T>>}}
*/
async _fetchSeriesData(path, onValue) {{
async _fetchSeriesData(path, arg, options) {{
const requestOptions = typeof arg === 'function'
? {{ ...(options ?? {{}}), onValue: arg }}
: {{ ...(arg ?? {{}}), ...(options ?? {{}}) }};
const onValue = requestOptions.onValue;
const wrappedOnValue = onValue ? (/** @type {{SeriesData<T>}} */ raw) => onValue(_wrapSeriesData(raw)) : undefined;
const raw = await this.getJson(path, {{ onValue: wrappedOnValue }});
const raw = await this.getJson(path, {{ ...requestOptions, onValue: wrappedOnValue }});
return _wrapSeriesData(raw);
}}
}}
@@ -679,6 +706,160 @@ const _p = (prefix, acc) => acc ? `${{prefix}}_${{acc}}` : prefix;
"#
)
.unwrap();
output.push_str(r##"
const _MASK_64 = 0xffffffffffffffffn;
const _RAPIDHASH_SECRETS = /** @type {const} */ ([
0x2d358dccaa6c78a5n,
0x8bb84b93962eacc9n,
0x4b33a62ed433d4a3n,
0x4d5a2da51de1aa47n,
0xa0761d6478bd642fn,
0xe7037ed1a0b428dbn,
0x90ed1765281c388cn,
]);
const _RAPIDHASH_SEED = _rapidHashSeed(0n);
/** @param {bigint} value */
function _u64(value) {
return value & _MASK_64;
}
/** @param {bigint} left @param {bigint} right */
function _rapidMix(left, right) {
const result = _u64(left) * _u64(right);
return _u64(result) ^ _u64(result >> 64n);
}
/** @param {bigint} left @param {bigint} right @returns {[bigint, bigint]} */
function _rapidMum(left, right) {
const result = _u64(left) * _u64(right);
return [_u64(result), _u64(result >> 64n)];
}
/** @param {bigint} seed */
function _rapidHashSeed(seed) {
return _u64(seed ^ _rapidMix(seed ^ _RAPIDHASH_SECRETS[2], _RAPIDHASH_SECRETS[1]));
}
/** @param {Uint8Array} bytes @param {number} offset */
function _readU32(bytes, offset) {
return (
BigInt(bytes[offset]) |
(BigInt(bytes[offset + 1]) << 8n) |
(BigInt(bytes[offset + 2]) << 16n) |
(BigInt(bytes[offset + 3]) << 24n)
);
}
/** @param {Uint8Array} bytes @param {number} offset */
function _readU64(bytes, offset) {
return _readU32(bytes, offset) | (_readU32(bytes, offset + 4) << 32n);
}
/** @param {Uint8Array | ArrayBuffer | ArrayBufferView | number[]} payload */
function _asUint8Array(payload) {
if (payload instanceof Uint8Array) return payload;
if (payload instanceof ArrayBuffer) return new Uint8Array(payload);
if (ArrayBuffer.isView(payload)) return new Uint8Array(payload.buffer, payload.byteOffset, payload.byteLength);
if (Array.isArray(payload)) return new Uint8Array(payload);
throw new Error("Expected address payload bytes");
}
/** @param {Uint8Array | ArrayBuffer | ArrayBufferView | number[]} payload */
function _rapidHashV3(payload) {
const bytes = _asUint8Array(payload);
const length = bytes.length;
if (length === 0) throw new Error("Expected a non-empty address payload");
if (length > 65) throw new Error("Expected at most 65 address payload bytes");
let seed = _RAPIDHASH_SEED;
let a = 0n;
let b = 0n;
let remainder;
if (length <= 16) {
if (length >= 4) {
seed ^= BigInt(length);
if (length >= 8) {
a ^= _readU64(bytes, 0);
b ^= _readU64(bytes, length - 8);
} else {
a ^= _readU32(bytes, 0);
b ^= _readU32(bytes, length - 4);
}
} else if (length > 0) {
a ^= (BigInt(bytes[0]) << 45n) | BigInt(bytes[length - 1]);
b ^= BigInt(bytes[length >> 1]);
}
remainder = BigInt(length);
} else {
seed = _rapidMix(_readU64(bytes, 0) ^ _RAPIDHASH_SECRETS[2], _readU64(bytes, 8) ^ seed);
if (length > 32) {
seed = _rapidMix(_readU64(bytes, 16) ^ _RAPIDHASH_SECRETS[2], _readU64(bytes, 24) ^ seed);
if (length > 48) {
seed = _rapidMix(_readU64(bytes, 32) ^ _RAPIDHASH_SECRETS[1], _readU64(bytes, 40) ^ seed);
if (length > 64) {
seed = _rapidMix(_readU64(bytes, 48) ^ _RAPIDHASH_SECRETS[1], _readU64(bytes, 56) ^ seed);
}
}
}
remainder = BigInt(length);
a ^= _readU64(bytes, length - 16) ^ remainder;
b ^= _readU64(bytes, length - 8);
}
a ^= _RAPIDHASH_SECRETS[1];
b ^= seed;
[a, b] = _rapidMum(a, b);
return _rapidMix(a ^ 0xaaaaaaaaaaaaaaaan, b ^ _RAPIDHASH_SECRETS[1] ^ remainder);
}
/** @param {number} nibbles */
function _validateHashPrefixNibbles(nibbles) {
if (!Number.isInteger(nibbles) || nibbles < 1 || nibbles > 16) {
throw new Error("Expected hash-prefix length from 1 to 16 hex nibbles");
}
}
/** @param {OutputType} addrType @returns {number[]} */
function _addressPayloadLengths(addrType) {
switch (addrType) {
case "p2a": return [2];
case "p2pk": return [33, 65];
case "p2pkh":
case "p2sh":
case "v0_p2wpkh": return [20];
case "v0_p2wsh":
case "v1_p2tr": return [32];
default:
throw new Error(`Unsupported address type for address payload hash-prefix: ${addrType}`);
}
}
/**
* @param {OutputType} addrType
* @param {Uint8Array | ArrayBuffer | ArrayBufferView | number[]} payload
*/
function _validateAddressPayloadForType(addrType, payload) {
const length = _asUint8Array(payload).length;
const expected = _addressPayloadLengths(addrType);
if (!expected.includes(length)) {
throw new Error(`Expected ${addrType} address payload length ${expected.join(" or ")} bytes`);
}
}
/**
* Compute the RapidHash v3 hash-prefix used by `/api/address/hash-prefix/{addr_type}/{prefix}`.
* @param {Uint8Array | ArrayBuffer | ArrayBufferView | number[]} payload - Raw address payload bytes
* @param {number} nibbles - Prefix length from 1 to 16 hex nibbles
* @returns {string}
*/
function addressPayloadHashPrefix(payload, nibbles) {
_validateHashPrefixNibbles(nibbles);
return _rapidHashV3(payload).toString(16).padStart(16, "0").slice(0, nibbles);
}
"##);
}
/// Generate static constants for the BrkClient class.
@@ -111,6 +111,32 @@ pub fn generate_main_client(
writeln!(output, " this.series = this._buildTree();").unwrap();
writeln!(output, " }}\n").unwrap();
output.push_str(r##" /**
* Compute the RapidHash v3 hash-prefix for raw address payload bytes.
* @param {Uint8Array | ArrayBuffer | ArrayBufferView | number[]} payload
* @param {number} nibbles
* @returns {string}
*/
static addressPayloadHashPrefix(payload, nibbles) {
return addressPayloadHashPrefix(payload, nibbles);
}
/**
* Fetch address hash-prefix matches from raw address payload bytes.
* @param {OutputType} addrType
* @param {Uint8Array | ArrayBuffer | ArrayBufferView | number[]} payload - Raw payload bytes matching addrType length
* @param {number} nibbles
* @param {{ signal?: AbortSignal, onValue?: (value: AddrHashPrefixMatches) => void, cache?: boolean, memCache?: boolean }} [options]
* @returns {Promise<AddrHashPrefixMatches>}
*/
getAddressPayloadHashPrefixMatches(addrType, payload, nibbles, options = {}) {
_validateAddressPayloadForType(addrType, payload);
const prefix = addressPayloadHashPrefix(payload, nibbles);
return this.getAddressHashPrefixMatches(addrType, prefix, options);
}
"##);
writeln!(output, " /**").unwrap();
writeln!(output, " * @private").unwrap();
writeln!(output, " * @returns {{SeriesTree}}").unwrap();
@@ -161,7 +187,11 @@ pub fn generate_main_client(
writeln!(output, "}}\n").unwrap();
writeln!(output, "export {{ BrkClient, BrkError }};").unwrap();
writeln!(
output,
"export {{ BrkClient, BrkError, addressPayloadHashPrefix }};"
)
.unwrap();
}
#[allow(clippy::too_many_arguments)]
@@ -84,6 +84,23 @@ pub fn generate_main_client(output: &mut String, endpoints: &[Endpoint]) {
.unwrap();
writeln!(output, " return _date_to_index(index, d)").unwrap();
writeln!(output).unwrap();
output.push_str(r#" @staticmethod
def address_payload_hash_prefix(payload: Union[bytes, bytearray, memoryview], nibbles: int) -> str:
"""Compute the RapidHash v3 hash-prefix for raw address payload bytes."""
return address_payload_hash_prefix(payload, nibbles)
def get_address_payload_hash_prefix_matches(
self,
addr_type: OutputType,
payload: Union[bytes, bytearray, memoryview],
nibbles: int,
) -> AddrHashPrefixMatches:
"""Fetch address hash-prefix matches from raw payload bytes matching addr_type length."""
_validate_address_payload_for_type(addr_type, payload)
return self.get_address_hash_prefix_matches(addr_type, address_payload_hash_prefix(payload, nibbles))
"#);
// Generate API methods
generate_api_methods(output, endpoints);
}
@@ -147,6 +147,125 @@ def _p(prefix: str, acc: str) -> str:
"#
)
.unwrap();
output.push_str(r#"
_MASK_64 = (1 << 64) - 1
_RAPIDHASH_SECRETS = (
0x2d358dccaa6c78a5,
0x8bb84b93962eacc9,
0x4b33a62ed433d4a3,
0x4d5a2da51de1aa47,
0xa0761d6478bd642f,
0xe7037ed1a0b428db,
0x90ed1765281c388c,
)
_RAPIDHASH_SEED = 0
def _u64(value: int) -> int:
return value & _MASK_64
def _rapid_mix(left: int, right: int) -> int:
result = _u64(left) * _u64(right)
return _u64(result) ^ _u64(result >> 64)
def _rapid_mum(left: int, right: int) -> Tuple[int, int]:
result = _u64(left) * _u64(right)
return _u64(result), _u64(result >> 64)
def _rapid_hash_seed(seed: int) -> int:
return _u64(seed ^ _rapid_mix(seed ^ _RAPIDHASH_SECRETS[2], _RAPIDHASH_SECRETS[1]))
_RAPIDHASH_SEED = _rapid_hash_seed(0)
def _read_u32(data: bytes, offset: int) -> int:
return int.from_bytes(data[offset:offset + 4], "little")
def _read_u64(data: bytes, offset: int) -> int:
return int.from_bytes(data[offset:offset + 8], "little")
def _rapid_hash_v3(payload: Union[bytes, bytearray, memoryview]) -> int:
data = bytes(payload)
length = len(data)
if length == 0:
raise ValueError("Expected a non-empty address payload")
if length > 65:
raise ValueError("Expected at most 65 address payload bytes")
seed = _RAPIDHASH_SEED
a = 0
b = 0
if length <= 16:
if length >= 4:
seed ^= length
if length >= 8:
a ^= _read_u64(data, 0)
b ^= _read_u64(data, length - 8)
else:
a ^= _read_u32(data, 0)
b ^= _read_u32(data, length - 4)
elif length > 0:
a ^= (data[0] << 45) | data[length - 1]
b ^= data[length >> 1]
remainder = length
else:
if length > 16:
seed = _rapid_mix(_read_u64(data, 0) ^ _RAPIDHASH_SECRETS[2], _read_u64(data, 8) ^ seed)
if length > 32:
seed = _rapid_mix(_read_u64(data, 16) ^ _RAPIDHASH_SECRETS[2], _read_u64(data, 24) ^ seed)
if length > 48:
seed = _rapid_mix(_read_u64(data, 32) ^ _RAPIDHASH_SECRETS[1], _read_u64(data, 40) ^ seed)
if length > 64:
seed = _rapid_mix(_read_u64(data, 48) ^ _RAPIDHASH_SECRETS[1], _read_u64(data, 56) ^ seed)
remainder = length
a ^= _read_u64(data, length - 16) ^ remainder
b ^= _read_u64(data, length - 8)
a ^= _RAPIDHASH_SECRETS[1]
b ^= seed
a, b = _rapid_mum(a, b)
return _rapid_mix(a ^ 0xaaaaaaaaaaaaaaaa, b ^ _RAPIDHASH_SECRETS[1] ^ remainder)
def _validate_hash_prefix_nibbles(nibbles: int) -> None:
if isinstance(nibbles, bool) or not isinstance(nibbles, int) or nibbles < 1 or nibbles > 16:
raise ValueError("Expected hash-prefix length from 1 to 16 hex nibbles")
def _address_payload_lengths(addr_type: OutputType) -> Tuple[int, ...]:
if addr_type == "p2a":
return (2,)
if addr_type == "p2pk":
return (33, 65)
if addr_type in ("p2pkh", "p2sh", "v0_p2wpkh"):
return (20,)
if addr_type in ("v0_p2wsh", "v1_p2tr"):
return (32,)
raise ValueError(f"Unsupported address type for address payload hash-prefix: {addr_type}")
def _validate_address_payload_for_type(addr_type: OutputType, payload: Union[bytes, bytearray, memoryview]) -> None:
length = len(bytes(payload))
expected = _address_payload_lengths(addr_type)
if length not in expected:
joined = " or ".join(str(value) for value in expected)
raise ValueError(f"Expected {addr_type} address payload length {joined} bytes")
def address_payload_hash_prefix(payload: Union[bytes, bytearray, memoryview], nibbles: int) -> str:
"""Compute the RapidHash v3 hash-prefix used by `/api/address/hash-prefix/{addr_type}/{prefix}`."""
_validate_hash_prefix_nibbles(nibbles)
return f"{_rapid_hash_v3(payload):016x}"[:nibbles]
"#);
}
/// Generate the SeriesData and SeriesEndpoint classes
@@ -76,6 +76,36 @@ impl BrkClient {{
)
.unwrap();
output.push_str(r#" /// Decode a mainnet Bitcoin address into the BRK address type and raw payload bytes.
pub fn decode_address_payload(address: &str) -> Result<AddressPayload> {
decode_address_payload(address)
}
/// Compute the RapidHash v3 hash-prefix for raw address payload bytes.
pub fn address_payload_hash_prefix(payload: &[u8], nibbles: usize) -> Result<String> {
address_payload_hash_prefix(payload, nibbles)
}
/// Decode a mainnet Bitcoin address and compute its hash prefix.
pub fn address_hash_prefix(address: &str, nibbles: usize) -> Result<AddressHashPrefix> {
address_hash_prefix(address, nibbles)
}
/// Fetch address hash-prefix matches from raw payload bytes matching `addr_type` length.
pub fn get_address_payload_hash_prefix_matches(&self, addr_type: OutputType, payload: &[u8], nibbles: usize) -> Result<AddrHashPrefixMatches> {
validate_address_payload_for_type(addr_type, payload)?;
let prefix = address_payload_hash_prefix(payload, nibbles)?;
self.get_address_hash_prefix_matches(addr_type, &prefix)
}
/// Fetch address hash-prefix matches for a mainnet Bitcoin address.
pub fn get_address_hash_prefix_matches_for_address(&self, address: &str, nibbles: usize) -> Result<AddrHashPrefixMatches> {
let hashed = address_hash_prefix(address, nibbles)?;
self.get_address_hash_prefix_matches(hashed.addr_type, &hashed.prefix)
}
"#);
generate_api_methods(output, endpoints);
writeln!(output, "}}").unwrap();
@@ -118,6 +148,23 @@ fn generate_get_method(output: &mut String, endpoint: &Endpoint) {
"get_text"
};
if endpoint.path == "/api/address/hash-prefix/{addr_type}/{prefix}" {
writeln!(
output,
" let addr_type = address_payload_type_path(addr_type)?;"
)
.unwrap();
writeln!(
output,
" self.base.{}(&format!(\"{}\"{}))",
fetch_method, path, index_arg
)
.unwrap();
writeln!(output, " }}").unwrap();
writeln!(output).unwrap();
return;
}
if endpoint.query_params.is_empty() {
writeln!(
output,
@@ -11,7 +11,8 @@ use crate::{
pub fn generate_imports(output: &mut String) {
writeln!(
output,
r#"use std::sync::Arc;
r#"use std::str::FromStr;
use std::sync::Arc;
use std::ops::{{Bound, RangeBounds}};
use serde::de::DeserializeOwned;
pub use brk_cohort::*;
@@ -43,6 +44,97 @@ impl std::error::Error for BrkError {{}}
/// Result type for BRK client operations.
pub type Result<T> = std::result::Result<T, BrkError>;
/// BRK address type and raw payload bytes used by the hash-prefix index.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AddressPayload {{
pub addr_type: OutputType,
pub payload: Vec<u8>,
}}
/// BRK address type and leading hex nibbles of the address-payload hash.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AddressHashPrefix {{
pub addr_type: OutputType,
pub prefix: String,
}}
/// Compute the RapidHash v3 hash-prefix used by `/api/address/hash-prefix/{{addr_type}}/{{prefix}}`.
pub fn address_payload_hash_prefix(payload: &[u8], nibbles: usize) -> Result<String> {{
if payload.is_empty() {{
return Err(BrkError {{ message: "Expected a non-empty address payload".to_string() }});
}}
if payload.len() > 65 {{
return Err(BrkError {{ message: "Expected at most 65 address payload bytes".to_string() }});
}}
if !(1..=16).contains(&nibbles) {{
return Err(BrkError {{ message: "Expected hash-prefix length from 1 to 16 hex nibbles".to_string() }});
}}
Ok(format!("{{:016x}}", rapidhash::v3::rapidhash_v3(payload))[..nibbles].to_string())
}}
fn validate_address_payload_for_type(addr_type: OutputType, payload: &[u8]) -> Result<()> {{
let expected: &[usize] = match addr_type {{
OutputType::P2A => &[2],
OutputType::P2PK33 => &[33],
OutputType::P2PK65 => &[65],
OutputType::P2PKH | OutputType::P2SH | OutputType::P2WPKH => &[20],
OutputType::P2WSH | OutputType::P2TR => &[32],
OutputType::P2MS | OutputType::OpReturn | OutputType::Empty | OutputType::Unknown => {{
return Err(BrkError {{ message: format!("Unsupported address type for address payload hash-prefix: {{addr_type:?}}") }});
}},
}};
let addr_type = address_payload_type_path(addr_type)?;
if !expected.contains(&payload.len()) {{
let joined = expected
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(" or ");
return Err(BrkError {{ message: format!("Expected {{addr_type}} address payload length {{joined}} bytes") }});
}}
Ok(())
}}
fn address_payload_type_path(addr_type: OutputType) -> Result<&'static str> {{
match addr_type {{
OutputType::P2A => Ok("p2a"),
OutputType::P2PK33 | OutputType::P2PK65 => Ok("p2pk"),
OutputType::P2PKH => Ok("p2pkh"),
OutputType::P2SH => Ok("p2sh"),
OutputType::P2WPKH => Ok("v0_p2wpkh"),
OutputType::P2WSH => Ok("v0_p2wsh"),
OutputType::P2TR => Ok("v1_p2tr"),
OutputType::P2MS | OutputType::OpReturn | OutputType::Empty | OutputType::Unknown => {{
Err(BrkError {{ message: format!("Unsupported address type for address payload hash-prefix: {{addr_type:?}}") }})
}},
}}
}}
/// Decode a mainnet Bitcoin address into the BRK address type and raw payload bytes.
pub fn decode_address_payload(address: &str) -> Result<AddressPayload> {{
if address.is_empty() {{
return Err(BrkError {{ message: "Expected an address string".to_string() }});
}}
let addr_bytes = AddrBytes::from_str(address).map_err(|e| BrkError {{ message: e.to_string() }})?;
let addr_type = OutputType::from(&addr_bytes);
Ok(AddressPayload {{
addr_type,
payload: addr_bytes.as_slice().to_vec(),
}})
}}
/// Decode a mainnet Bitcoin address and compute its hash prefix.
pub fn address_hash_prefix(address: &str, nibbles: usize) -> Result<AddressHashPrefix> {{
let decoded = decode_address_payload(address)?;
Ok(AddressHashPrefix {{
addr_type: decoded.addr_type,
prefix: address_payload_hash_prefix(&decoded.payload, nibbles)?,
}})
}}
/// Options for configuring the BRK client.
#[derive(Debug, Clone)]
pub struct BrkClientOptions {{
+1
View File
@@ -13,6 +13,7 @@ exclude = ["examples/"]
[dependencies]
brk_cohort = { workspace = true }
brk_types = { workspace = true }
rapidhash = { workspace = true }
ureq = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
+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
+749 -34
View File
@@ -8,6 +8,7 @@
#![allow(clippy::useless_format)]
#![allow(clippy::unnecessary_to_owned)]
use std::str::FromStr;
use std::sync::Arc;
use std::ops::{Bound, RangeBounds};
use serde::de::DeserializeOwned;
@@ -32,6 +33,97 @@ impl std::error::Error for BrkError {}
/// Result type for BRK client operations.
pub type Result<T> = std::result::Result<T, BrkError>;
/// BRK address type and raw payload bytes used by the hash-prefix index.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AddressPayload {
pub addr_type: OutputType,
pub payload: Vec<u8>,
}
/// BRK address type and leading hex nibbles of the address-payload hash.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AddressHashPrefix {
pub addr_type: OutputType,
pub prefix: String,
}
/// Compute the RapidHash v3 hash-prefix used by `/api/address/hash-prefix/{addr_type}/{prefix}`.
pub fn address_payload_hash_prefix(payload: &[u8], nibbles: usize) -> Result<String> {
if payload.is_empty() {
return Err(BrkError { message: "Expected a non-empty address payload".to_string() });
}
if payload.len() > 65 {
return Err(BrkError { message: "Expected at most 65 address payload bytes".to_string() });
}
if !(1..=16).contains(&nibbles) {
return Err(BrkError { message: "Expected hash-prefix length from 1 to 16 hex nibbles".to_string() });
}
Ok(format!("{:016x}", rapidhash::v3::rapidhash_v3(payload))[..nibbles].to_string())
}
fn validate_address_payload_for_type(addr_type: OutputType, payload: &[u8]) -> Result<()> {
let expected: &[usize] = match addr_type {
OutputType::P2A => &[2],
OutputType::P2PK33 => &[33],
OutputType::P2PK65 => &[65],
OutputType::P2PKH | OutputType::P2SH | OutputType::P2WPKH => &[20],
OutputType::P2WSH | OutputType::P2TR => &[32],
OutputType::P2MS | OutputType::OpReturn | OutputType::Empty | OutputType::Unknown => {
return Err(BrkError { message: format!("Unsupported address type for address payload hash-prefix: {addr_type:?}") });
},
};
let addr_type = address_payload_type_path(addr_type)?;
if !expected.contains(&payload.len()) {
let joined = expected
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(" or ");
return Err(BrkError { message: format!("Expected {addr_type} address payload length {joined} bytes") });
}
Ok(())
}
fn address_payload_type_path(addr_type: OutputType) -> Result<&'static str> {
match addr_type {
OutputType::P2A => Ok("p2a"),
OutputType::P2PK33 | OutputType::P2PK65 => Ok("p2pk"),
OutputType::P2PKH => Ok("p2pkh"),
OutputType::P2SH => Ok("p2sh"),
OutputType::P2WPKH => Ok("v0_p2wpkh"),
OutputType::P2WSH => Ok("v0_p2wsh"),
OutputType::P2TR => Ok("v1_p2tr"),
OutputType::P2MS | OutputType::OpReturn | OutputType::Empty | OutputType::Unknown => {
Err(BrkError { message: format!("Unsupported address type for address payload hash-prefix: {addr_type:?}") })
},
}
}
/// Decode a mainnet Bitcoin address into the BRK address type and raw payload bytes.
pub fn decode_address_payload(address: &str) -> Result<AddressPayload> {
if address.is_empty() {
return Err(BrkError { message: "Expected an address string".to_string() });
}
let addr_bytes = AddrBytes::from_str(address).map_err(|e| BrkError { message: e.to_string() })?;
let addr_type = OutputType::from(&addr_bytes);
Ok(AddressPayload {
addr_type,
payload: addr_bytes.as_slice().to_vec(),
})
}
/// Decode a mainnet Bitcoin address and compute its hash prefix.
pub fn address_hash_prefix(address: &str, nibbles: usize) -> Result<AddressHashPrefix> {
let decoded = decode_address_payload(address)?;
Ok(AddressHashPrefix {
addr_type: decoded.addr_type,
prefix: address_payload_hash_prefix(&decoded.payload, nibbles)?,
})
}
/// Options for configuring the BRK client.
#[derive(Debug, Clone)]
pub struct BrkClientOptions {
@@ -1191,6 +1283,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 +1766,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 +3527,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 +3669,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 +3691,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 +7198,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 +7234,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 +7251,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 +7334,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 +7354,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 +8136,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 +8153,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 +8373,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 +8588,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 +9630,7 @@ pub struct BrkClient {
impl BrkClient {
/// Client version.
pub const VERSION: &'static str = "v0.3.0";
pub const VERSION: &'static str = "v0.3.6";
/// Create a new client with the given base URL.
pub fn new(base_url: impl Into<String>) -> Self {
@@ -9007,6 +9684,34 @@ impl BrkClient {
))
}
/// Decode a mainnet Bitcoin address into the BRK address type and raw payload bytes.
pub fn decode_address_payload(address: &str) -> Result<AddressPayload> {
decode_address_payload(address)
}
/// Compute the RapidHash v3 hash-prefix for raw address payload bytes.
pub fn address_payload_hash_prefix(payload: &[u8], nibbles: usize) -> Result<String> {
address_payload_hash_prefix(payload, nibbles)
}
/// Decode a mainnet Bitcoin address and compute its hash prefix.
pub fn address_hash_prefix(address: &str, nibbles: usize) -> Result<AddressHashPrefix> {
address_hash_prefix(address, nibbles)
}
/// Fetch address hash-prefix matches from raw payload bytes matching `addr_type` length.
pub fn get_address_payload_hash_prefix_matches(&self, addr_type: OutputType, payload: &[u8], nibbles: usize) -> Result<AddrHashPrefixMatches> {
validate_address_payload_for_type(addr_type, payload)?;
let prefix = address_payload_hash_prefix(payload, nibbles)?;
self.get_address_hash_prefix_matches(addr_type, &prefix)
}
/// Fetch address hash-prefix matches for a mainnet Bitcoin address.
pub fn get_address_hash_prefix_matches_for_address(&self, address: &str, nibbles: usize) -> Result<AddrHashPrefixMatches> {
let hashed = address_hash_prefix(address, nibbles)?;
self.get_address_hash_prefix_matches(hashed.addr_type, &hashed.prefix)
}
/// Health check
///
/// Liveness probe. Returns server identity, uptime, and indexed/computed heights from local state only (no bitcoind round-trip). For real chain-tip catch-up, see `/api/server/sync`.
@@ -9281,6 +9986,16 @@ impl BrkClient {
self.base.get_json(&path)
}
/// Address hash-prefix matches
///
/// Find addresses by address type and by the first 1-16 hex nibbles of RapidHash v3 over the raw address payload bytes. 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> {
let addr_type = address_payload_type_path(addr_type)?;
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).
+44
View File
@@ -0,0 +1,44 @@
use brk_client::{
BrkClient, OutputType, address_hash_prefix, address_payload_hash_prefix, decode_address_payload,
};
#[test]
fn address_payload_hash_prefix_vectors() {
let vectors = [
(vec![0x4e, 0x73], "58101afa51a1ecfd"),
((0_u8..20).collect::<Vec<_>>(), "c3327ecb8ae1ff23"),
((0_u8..32).collect::<Vec<_>>(), "c0186990f026b180"),
((0_u8..65).collect::<Vec<_>>(), "0d4b77027ae7d700"),
];
for (payload, expected) in vectors.iter() {
assert_eq!(address_payload_hash_prefix(payload, 16).unwrap(), *expected);
assert_eq!(
BrkClient::address_payload_hash_prefix(payload, 8).unwrap(),
&expected[..8]
);
}
}
#[test]
fn address_payload_hash_prefix_validation() {
assert!(address_payload_hash_prefix(&[], 16).is_err());
assert!(address_payload_hash_prefix(&[0; 66], 16).is_err());
assert!(address_payload_hash_prefix(&[1, 2], 0).is_err());
assert!(address_payload_hash_prefix(&[1, 2], 17).is_err());
}
#[test]
fn address_hash_prefix_uses_brk_address_parser() {
let address = "1BoatSLRHtKNngkdXEeobR76b53LETtpyT";
let decoded = decode_address_payload(address).unwrap();
assert_eq!(decoded.addr_type, OutputType::P2PKH);
assert_eq!(decoded.payload.len(), 20);
let hashed = address_hash_prefix(address, 8).unwrap();
assert_eq!(hashed.addr_type, OutputType::P2PKH);
assert_eq!(
hashed.prefix,
address_payload_hash_prefix(&decoded.payload, 8).unwrap()
);
}
+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
}
@@ -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,
+43 -7
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()?;
@@ -436,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");
@@ -474,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,
@@ -486,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<()> {
@@ -3,8 +3,7 @@ use std::ops::Range;
use brk_error::Result;
use brk_indexer::{Indexer, Lengths};
use brk_oracle::{
bin_to_cents, cents_to_bin, for_each_round_dollar_bin, Config, HistogramRaw, Oracle,
START_HEIGHT_FAST, START_HEIGHT_SLOW,
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;
@@ -87,16 +86,11 @@ impl Vecs {
.truncate_if_needed_at(truncate_to)?;
if self.spot.cents.height.len() < START_HEIGHT_SLOW {
for line in brk_oracle::PRICES
.lines()
.skip(self.spot.cents.height.len())
{
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);
}
}
@@ -183,8 +177,7 @@ impl Vecs {
}
/// Feed a range of blocks from the indexer into an Oracle (skipping coinbase),
/// returning per-block ref_bin values. Outputs are grouped per transaction
/// because `for_each_round_dollar_bin` drops a whole tx on any OP_RETURN.
/// returning per-block ref_bin values.
///
/// Pass `cap = None` from compute paths, when the indexer is quiescent and
/// raw vec lengths are authoritative. Pass `cap = Some(&safe_lengths)` from
@@ -293,21 +286,18 @@ impl Vecs {
&mut output_types,
);
let mut hist = HistogramRaw::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);
let outputs = values[lo..hi]
values[lo..hi]
.iter()
.copied()
.zip(output_types[lo..hi].iter().copied());
for_each_round_dollar_bin(range.start + idx, outputs, |bin| {
hist.increment(bin as usize)
});
}
.zip(output_types[lo..hi].iter().copied())
});
let hist = PaymentFilter::for_height(range.start + idx).histogram(tx_outputs);
let ref_bin = oracle.process_histogram(&hist);
on_block(range.start + idx, oracle, ref_bin);
@@ -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,
@@ -1,4 +1,4 @@
use brk_oracle::{HistogramRaw, for_each_round_dollar_bin, sats_to_bin};
use brk_oracle::{sats_to_bin, HistogramRaw, PaymentFilter};
use brk_types::Transaction;
use crate::stores::tx_store::TxRecord;
@@ -43,14 +43,9 @@ impl LiveHistograms {
/// 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. Live mempool txs are post-tip, always
/// above the historical max-outputs cap window, so the cap never applies.
/// identically rather than caching.
fn eligible_bins(tx: &Transaction, emit: impl FnMut(u16)) {
for_each_round_dollar_bin(
usize::MAX,
tx.output.iter().map(|o| (o.value, o.type_())),
emit,
);
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
+18 -10
View File
@@ -48,7 +48,7 @@ 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. 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. 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
@@ -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 filters as it builds the histogram, applying the [step 1](#1-filter-outputs) rules. Two helpers are exported for this: `eligible_bin(sats, output_type)` returns an output's bin index, or `None` if filtered, and `for_each_round_dollar_bin` wraps it with the per-transaction drops (coinbase, OP_RETURN, the >100-output cap below height 630,000) for callers holding a whole transaction's outputs.
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 heights 0..340,000. Its last entry, height 339,999 (one below `START_HEIGHT_SLOW`), seeds the oracle's first on-chain computation at height 340,000.
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
@@ -161,7 +161,7 @@ Between heights 340,000 and 508,000 the oracle runs a slower cold-start configur
## Accuracy
Tested over 596,251 blocks (heights 340,000 to 950,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
@@ -173,9 +173,9 @@ Tested over 596,251 blocks (heights 340,000 to 950,800, as of May 2026) against
| 99.9th percentile | 15.6% |
| RMSE | 0.97% |
| Max error | 33.8% |
| Bias | +0.05 bins (essentially zero) |
| Blocks > 5% error | 3,235 (0.543%) |
| Blocks > 10% error | 1,324 |
| Bias | +0.06 bins (essentially zero) |
| Blocks > 5% error | 3,233 (0.542%) |
| Blocks > 10% error | 1,323 |
| Blocks > 20% error | 154 |
### Daily candles
@@ -185,7 +185,7 @@ Oracle daily OHLC built from per-block prices vs exchange daily OHLC:
| | Median | RMSE | Max |
|-------|--------|------|-----|
| Open | 0.24% | 1.07% | 29.1% |
| High | 0.58% | 1.48% | 27.3% |
| High | 0.58% | 1.47% | 27.3% |
| Low | 0.53% | 1.95% | 55.1% |
| Close | 0.27% | 1.18% | 29.2% |
@@ -199,10 +199,10 @@ Oracle daily OHLC built from per-block prices vs exchange daily OHLC:
| 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% | 43 | 10 | 0 | $27,678$69,000 |
| 2021 | 52,733 | 0.07% | 0.47% | 14.4% | 42 | 9 | 0 | $27,678$69,000 |
| 2022 | 53,230 | 0.07% | 0.32% | 6.8% | 10 | 0 | 0 | $15,460$48,240 |
| 2023 | 54,032 | 0.10% | 0.25% | 6.6% | 5 | 0 | 0 | $16,490$44,700 |
| 2024 | 53,367 | 0.10% | 0.28% | 7.1% | 8 | 0 | 0 | $38,555$108,298 |
| 2024 | 53,367 | 0.10% | 0.28% | 6.7% | 7 | 0 | 0 | $38,555$108,298 |
| 2025 | 53,113 | 0.11% | 0.25% | 5.8% | 4 | 0 | 0 | $74,409$126,198 |
| 2026 | 5,910 | 0.10% | 0.27% | 3.2% | 0 | 0 | 0 | $60,000$97,900 |
@@ -217,6 +217,14 @@ Post-hoc smoothing, for example correcting any block whose price deviates more t
## 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:
+28 -35
View File
@@ -4,9 +4,8 @@
//! values matching a continuously-running oracle from the restart height
//! onward.
//!
//! Mirrors the production filter exactly (per-tx OP_RETURN drop + per-output
//! `eligible_bin`), so it exercises the same code path
//! `brk_computer::prices::compute::feed_blocks` uses at runtime.
//! 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
@@ -14,22 +13,12 @@ use std::path::PathBuf;
use brk_indexer::Indexer;
use brk_oracle::{
Config, HistogramRaw, Oracle, PRICES, START_HEIGHT_FAST, bin_to_cents, cents_to_bin,
for_each_round_dollar_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_for_start_height() -> f64 {
let price: f64 = PRICES
.lines()
.nth(START_HEIGHT_FAST - 1)
.expect("prices.txt too short for START_HEIGHT_FAST")
.parse()
.expect("Failed to parse seed price");
cents_to_bin(price * 100.0)
}
struct Block {
height: usize,
values: Vec<Sats>,
@@ -40,21 +29,19 @@ struct Block {
}
fn build_histogram(block: &Block) -> HistogramRaw {
let mut hist = HistogramRaw::zeros();
for tx in 0..block.tx_starts.len() {
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);
let outputs = block.values[lo..hi]
block.values[lo..hi]
.iter()
.copied()
.zip(block.output_types[lo..hi].iter().copied());
for_each_round_dollar_bin(block.height, outputs, |bin| hist.increment(bin as usize));
}
hist
.zip(block.output_types[lo..hi].iter().copied())
});
PaymentFilter::for_height(block.height).histogram(tx_outputs)
}
fn main() {
@@ -68,14 +55,15 @@ 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,
@@ -84,8 +72,8 @@ fn main() {
);
println!(
"Loading {} blocks ({START_HEIGHT_FAST}..{end_height})...",
end_height - START_HEIGHT_FAST
"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();
@@ -93,8 +81,8 @@ fn main() {
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 blocks: Vec<Block> = Vec::with_capacity(end_height - START_HEIGHT_FAST);
for h in START_HEIGHT_FAST..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)
@@ -136,31 +124,36 @@ fn main() {
});
}
let mut continuous = Oracle::new(seed_bin_for_start_height(), config.clone());
let mut continuous = Oracle::from_seed();
let continuous_bins: Vec<f64> = blocks
.iter()
.map(|b| continuous.process_histogram(&build_histogram(b)))
.map(|b| {
if b.height == START_HEIGHT_FAST {
continuous.reconfigure(fast_config);
}
continuous.process_histogram(&build_histogram(b))
})
.collect();
println!(
"Continuous oracle: {} blocks processed",
continuous_bins.len()
);
let prev_bin = continuous_bins[restart_at - START_HEIGHT_FAST - 1];
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 - START_HEIGHT_FAST..restart_at - START_HEIGHT_FAST];
let mut restored = Oracle::from_checkpoint(seed_bin, config.clone(), |o| {
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 - START_HEIGHT_FAST..]
let restored_bins: Vec<f64> = blocks[restart_at - load_start..]
.iter()
.map(|b| restored.process_histogram(&build_histogram(b)))
.collect();
@@ -168,7 +161,7 @@ fn main() {
let mut mismatches: Vec<(usize, f64, f64)> = Vec::new();
for (i, &r) in restored_bins.iter().enumerate() {
let c = continuous_bins[restart_at - START_HEIGHT_FAST + i];
let c = continuous_bins[restart_at - load_start + i];
if r != c {
mismatches.push((restart_at + i, c, r));
}
+690
View File
@@ -0,0 +1,690 @@
//! Compare oracle filter/EMA variants against the historical OHLC set.
//!
//! This is a diagnostic harness, not production code. It mirrors the production
//! state machine closely enough to compare candidate changes in one pass over
//! the indexed chain while recording the recent bad-lock heights.
//!
//! Run:
//! cargo run -p brk_oracle --example experiment --release
use std::{cmp::Ordering, env, path::PathBuf};
use brk_indexer::Indexer;
use brk_oracle::{
bin_to_cents, cents_to_bin, seed_bin as oracle_seed_bin, Config, PaymentFilter,
BINS_PER_DECADE, NUM_BINS, START_HEIGHT_FAST, START_HEIGHT_SLOW,
};
use brk_types::{OutputType, Sats, TxIndex, TxOutIndex};
use vecdb::{AnyVec, ReadableVec, VecIndex};
const GENESIS_DAY: u32 = 14252;
const BINS_5PCT: f64 = 4.24;
const BINS_10PCT: f64 = 8.28;
const BINS_20PCT: f64 = 15.84;
const STENCIL_OFFSETS: [i32; 19] = [
-400, -340, -305, -260, -200, -165, -140, -120, -105, -60, 0, 35, 60, 95, 140, 200, 260, 340,
400,
];
const N_ARMS: usize = STENCIL_OFFSETS.len();
const TARGET_HEIGHTS: &[usize] = &[952_286, 952_287, 952_288, 952_289, 952_290];
fn bins_to_pct(bins: f64) -> f64 {
(10.0_f64.powf(bins / BINS_PER_DECADE as f64) - 1.0) * 100.0
}
fn timestamp_to_year(ts: u32) -> u16 {
let years_since_1970 = ts as f64 / 31_557_600.0;
(1970.0 + years_since_1970) as u16
}
#[derive(Clone)]
struct ShapeAnchor {
weight: f64,
profile: [f64; N_ARMS],
}
impl ShapeAnchor {
fn new(weight: f64) -> Self {
Self {
weight,
profile: [1.0 / N_ARMS as f64; N_ARMS],
}
}
fn score(&self, state: &OracleState, center: i64) -> f64 {
if self.weight == 0.0 {
return 0.0;
}
self.weight
* normalized_arms_at(state, center)
.map(|arms| {
1.0 - (0..N_ARMS)
.map(|i| (arms[i] - self.profile[i]).abs())
.sum::<f64>()
})
.unwrap_or(0.0)
}
fn update(&mut self, state: &OracleState, pick: i64) {
const BETA: f64 = 0.004;
if self.weight == 0.0 {
return;
}
if let Some(arms) = normalized_arms_at(state, pick) {
for (p, arm) in self.profile.iter_mut().zip(arms) {
*p = (1.0 - BETA) * *p + BETA * arm;
}
}
}
}
#[derive(Clone)]
struct OracleState {
config: Config,
ring: Vec<Vec<f64>>,
nonzero: Vec<Vec<usize>>,
weights: Vec<f64>,
cursor: usize,
filled: usize,
ref_bin: f64,
warmup: bool,
shape: ShapeAnchor,
}
impl OracleState {
fn new(ref_bin: f64, config: Config) -> Self {
let weights = weights(config.window_size, config.alpha);
Self {
ring: vec![vec![0.0; NUM_BINS]; config.window_size],
nonzero: vec![Vec::new(); config.window_size],
weights,
cursor: 0,
filled: 0,
ref_bin,
warmup: false,
shape: ShapeAnchor::new(config.shape_weight),
config,
}
}
fn reconfigure(&mut self, config: Config) {
let kept = self.recent(config.window_size);
let mut next = Self::new(self.ref_bin, config);
next.warmup = true;
for hist in kept {
next.push_existing(hist);
}
next.warmup = false;
*self = next;
}
fn recent(&self, n: usize) -> Vec<Vec<f64>> {
(0..self.filled.min(n))
.rev()
.map(|age| self.ring[self.index_at_age(age)].clone())
.collect()
}
fn index_at_age(&self, age: usize) -> usize {
(self.cursor + self.ring.len() - 1 - age) % self.ring.len()
}
fn start_block(&mut self) {
for bin in self.nonzero[self.cursor].drain(..) {
self.ring[self.cursor][bin] = 0.0;
}
}
fn add(&mut self, bin: usize, weight: f64) {
let slot = &mut self.ring[self.cursor];
if slot[bin] == 0.0 {
self.nonzero[self.cursor].push(bin);
}
slot[bin] += weight;
}
fn push_existing(&mut self, hist: Vec<f64>) {
self.start_block();
for (bin, value) in hist
.into_iter()
.enumerate()
.filter(|(_, value)| *value != 0.0)
{
self.add(bin, value);
}
self.finish_block();
}
fn finish_block(&mut self) {
self.cursor = (self.cursor + 1) % self.ring.len();
self.filled = (self.filled + 1).min(self.ring.len());
if self.warmup {
return;
}
self.ref_bin = find_best_bin(self);
let mut shape = self.shape.clone();
shape.update(self, self.ref_bin.round() as i64);
self.shape = shape;
}
fn value_at(&self, bin: i64) -> f64 {
if bin < 0 || bin as usize >= NUM_BINS {
return 0.0;
}
let bin = bin as usize;
(0..self.filled)
.map(|age| self.weights[age] * self.ring[self.index_at_age(age)][bin])
.sum()
}
}
fn weights(window_size: usize, alpha: f64) -> Vec<f64> {
let decay = 1.0 - alpha;
(0..window_size)
.map(|i| alpha * decay.powi(i as i32))
.collect()
}
fn normalized_arms_at(state: &OracleState, center: i64) -> Option<[f64; N_ARMS]> {
let mut arms = STENCIL_OFFSETS.map(|offset| state.value_at(center + offset as i64));
let sum: f64 = arms.iter().sum();
if sum <= 0.0 {
return None;
}
for arm in &mut arms {
*arm /= sum;
}
Some(arms)
}
fn find_best_bin(state: &OracleState) -> f64 {
let center = state.ref_bin.round() as usize;
let search_start = center.saturating_sub(state.config.search_below);
let search_end = (center + state.config.search_above + 1).min(NUM_BINS);
if search_start >= search_end {
return state.ref_bin;
}
let mut arm_peaks = [0.0f64; N_ARMS];
for (i, &offset) in STENCIL_OFFSETS.iter().enumerate() {
for bin in search_start..search_end {
arm_peaks[i] = arm_peaks[i].max(state.value_at(bin as i64 + offset as i64));
}
}
let score = |bin: usize| -> f64 {
let mut total = 0.0;
for (i, &offset) in STENCIL_OFFSETS.iter().enumerate() {
if arm_peaks[i] > 0.0 {
total += state.value_at(bin as i64 + offset as i64) / arm_peaks[i];
}
}
total + state.shape.score(state, bin as i64)
};
let mut best_bin = search_start;
let mut best_score = score(search_start);
for bin in (search_start + 1)..search_end {
let candidate = score(bin);
if candidate > best_score {
best_score = candidate;
best_bin = bin;
}
}
let score_center = best_score;
let score_left = if best_bin > search_start {
score(best_bin - 1)
} else {
score_center
};
let score_right = if best_bin + 1 < search_end {
score(best_bin + 1)
} else {
score_center
};
let denom = score_left - 2.0 * score_center + score_right;
let sub_bin = if denom.abs() > 1e-10 {
(0.5 * (score_left - score_right) / denom).clamp(-0.5, 0.5)
} else {
0.0
};
best_bin as f64 + sub_bin
}
#[derive(Clone)]
struct VariantCfg {
name: String,
fast_alpha: f64,
fast_window: usize,
max_outputs: Option<usize>,
max_outputs_until: usize,
max_outputs_after: Option<usize>,
}
struct Variant {
cfg: VariantCfg,
state: OracleState,
overall: YearStats,
years: Vec<YearStats>,
bias: f64,
target_prices: Vec<(usize, f64)>,
}
fn cap_label(cap: Option<usize>) -> String {
cap.map(|cap| cap.to_string())
.unwrap_or_else(|| "none".to_string())
}
fn target_price(target_prices: &[(usize, f64)], height: usize) -> Option<f64> {
target_prices
.iter()
.find(|(h, _)| *h == height)
.map(|(_, price)| *price)
}
fn fixes_bad_lock(target_prices: &[(usize, f64)]) -> bool {
target_price(target_prices, 952_287).is_some_and(|price| price > 62_000.0)
&& target_price(target_prices, 952_288).is_some_and(|price| price > 62_000.0)
}
impl Variant {
fn new(cfg: VariantCfg, seed_bin: f64) -> Self {
Self {
cfg,
state: OracleState::new(seed_bin, Config::slow()),
overall: YearStats::new(0),
years: Vec::new(),
bias: 0.0,
target_prices: Vec::new(),
}
}
fn fast_config(&self) -> Config {
Config {
alpha: self.cfg.fast_alpha,
window_size: self.cfg.fast_window,
..Config::default()
}
}
fn maybe_reconfigure(&mut self, height: usize) {
if height == START_HEIGHT_FAST {
self.state.reconfigure(self.fast_config());
}
}
fn should_drop_tx(&self, height: usize, output_count: usize) -> bool {
if height < self.cfg.max_outputs_until {
self.cfg.max_outputs.is_some_and(|max| output_count > max)
} else {
self.cfg
.max_outputs_after
.is_some_and(|max| output_count > max)
}
}
fn add_tx(&mut self, bins: &[u16], height: usize, output_count: usize) {
if bins.is_empty() || self.should_drop_tx(height, output_count) {
return;
}
for &bin in bins {
self.state.add(bin as usize, 1.0);
}
}
fn finish_block(&mut self, height: usize) {
self.state.finish_block();
if TARGET_HEIGHTS.contains(&height) {
self.target_prices
.push((height, bin_to_cents(self.state.ref_bin) as f64 / 100.0));
}
}
fn update_stats(
&mut self,
height: usize,
height_bands: &[(f64, f64)],
height_ohlc: &[[f64; 4]],
height_years: &[u16],
) {
if height >= height_bands.len() {
return;
}
let (high_bin, low_bin) = height_bands[height];
if high_bin <= 0.0 || low_bin <= 0.0 {
return;
}
let err = if self.state.ref_bin < high_bin {
self.state.ref_bin - high_bin
} else if self.state.ref_bin > low_bin {
self.state.ref_bin - low_bin
} else {
0.0
};
let exchange_high = height_ohlc[height][1];
let exchange_low = height_ohlc[height][2];
self.overall.update(err, exchange_high, exchange_low);
self.bias += err;
let year = height_years[height];
if self.years.last().is_none_or(|stats| stats.year != year) {
self.years.push(YearStats::new(year));
}
self.years
.last_mut()
.unwrap()
.update(err, exchange_high, exchange_low);
}
}
struct YearStats {
year: u16,
total_sq_err: f64,
max_err: f64,
total_blocks: u64,
gt_5pct: u64,
gt_10pct: u64,
gt_20pct: u64,
errors: Vec<f64>,
}
impl YearStats {
fn new(year: u16) -> Self {
Self {
year,
total_sq_err: 0.0,
max_err: 0.0,
total_blocks: 0,
gt_5pct: 0,
gt_10pct: 0,
gt_20pct: 0,
errors: Vec::new(),
}
}
fn update(&mut self, err: f64, _exchange_high: f64, _exchange_low: f64) {
let abs_err = err.abs();
self.total_sq_err += err * err;
self.total_blocks += 1;
self.errors.push(bins_to_pct(abs_err));
self.max_err = self.max_err.max(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 {
if self.total_blocks == 0 {
return 0.0;
}
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 percentile(&self, p: f64) -> f64 {
if self.errors.is_empty() {
return 0.0;
}
let mut errors = self.errors.clone();
errors.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal));
let idx = ((p / 100.0) * (errors.len() - 1) as f64).round() as usize;
errors[idx.min(errors.len() - 1)]
}
}
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 end_override = std::env::var("ORACLE_END")
.ok()
.and_then(|s| s.parse::<usize>().ok());
let stats_start = std::env::var("ORACLE_STATS_START")
.ok()
.and_then(|s| s.parse::<usize>().ok())
.unwrap_or(START_HEIGHT_SLOW)
.max(START_HEIGHT_SLOW);
let indexer = Indexer::forced_import(&data_dir).expect("Failed to load indexer");
let total_heights = indexer.vecs.blocks.timestamp.len();
let end = end_override.unwrap_or(total_heights).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 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();
let timestamps: Vec<brk_types::Timestamp> = indexer.vecs.blocks.timestamp.collect();
let height_years: Vec<u16> = timestamps
.iter()
.map(|ts| timestamp_to_year(**ts))
.collect();
let _height_day1s: Vec<usize> = timestamps
.iter()
.map(|ts| (**ts / 86_400).saturating_sub(GENESIS_DAY) as usize)
.collect();
let seed_bin = oracle_seed_bin();
let current_alpha = 2.0 / 7.0;
let current_window = 12;
let mut cfgs = Vec::<VariantCfg>::new();
let mut add_cfg = |name: String,
max_outputs: Option<usize>,
max_outputs_until: usize,
max_outputs_after: Option<usize>| {
cfgs.push(VariantCfg {
name,
fast_alpha: current_alpha,
fast_window: current_window,
max_outputs,
max_outputs_until,
max_outputs_after,
});
};
for post in [200, 250] {
add_cfg(
format!("pre100_post{post}"),
Some(100),
PaymentFilter::MODERN_TX_OUTPUT_FANOUT_CAP_START_HEIGHT,
Some(post),
);
}
cfgs.dedup_by(|a, b| a.name == b.name);
if let Ok(only) = env::var("BRK_ORACLE_EXPERIMENT_ONLY") {
let names = only
.split(',')
.map(str::trim)
.filter(|name| !name.is_empty())
.collect::<Vec<_>>();
cfgs.retain(|cfg| names.iter().any(|name| *name == cfg.name));
}
let mut variants: Vec<Variant> = cfgs
.into_iter()
.map(|cfg| Variant::new(cfg, seed_bin))
.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 mut values: Vec<Sats> = Vec::new();
let mut output_types: Vec<OutputType> = Vec::new();
let mut bins: Vec<u16> = Vec::new();
eprintln!(
"running {} variants over heights {START_HEIGHT_SLOW}..{end}; stats from {stats_start}",
variants.len()
);
for h in START_HEIGHT_SLOW..end {
if h % 25_000 == 0 {
eprintln!("height {h}");
}
for variant in &mut variants {
variant.maybe_reconfigure(h);
variant.state.start_block();
}
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;
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);
indexer
.vecs
.outputs
.value
.collect_range_into_at(out_start, out_end, &mut values);
indexer.vecs.outputs.output_type.collect_range_into_at(
out_start,
out_end,
&mut output_types,
);
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);
if output_types[lo..hi].contains(&OutputType::OpReturn) {
continue;
}
bins.clear();
for i in lo..hi {
if let Some(bin) = PaymentFilter::eligible_bin(values[i], output_types[i]) {
bins.push(bin);
}
}
for variant in &mut variants {
variant.add_tx(&bins, h, hi - lo);
}
}
for variant in &mut variants {
variant.finish_block(h);
if h >= stats_start {
variant.update_stats(h, &height_bands, &height_ohlc, &height_years);
}
}
}
variants.sort_by(|a, b| {
fixes_bad_lock(&b.target_prices)
.cmp(&fixes_bad_lock(&a.target_prices))
.then_with(|| {
a.overall
.rmse_pct()
.partial_cmp(&b.overall.rmse_pct())
.unwrap_or(Ordering::Equal)
})
.then_with(|| a.overall.gt_5pct.cmp(&b.overall.gt_5pct))
});
println!(
"variant\tpre_cap\tpost_cap\tfixed\tmedian\tp95\tp99\tp999\trmse\tmax\tbias_bins\tgt5\tgt10\tgt20\tp952287\tp952288\ttarget_prices\trmse_by_year\tgt5_by_year"
);
for variant in &variants {
let overall = &variant.overall;
let bias = if overall.total_blocks > 0 {
variant.bias / overall.total_blocks as f64
} else {
0.0
};
let rmse_by_year = (2015..=2026)
.map(|year| {
let rmse = variant
.years
.iter()
.find(|stats| stats.year == year)
.map(YearStats::rmse_pct)
.unwrap_or(0.0);
format!("{year}:{rmse:.3}")
})
.collect::<Vec<_>>()
.join(",");
let gt5_by_year = (2015..=2026)
.map(|year| {
let gt5 = variant
.years
.iter()
.find(|stats| stats.year == year)
.map(|stats| stats.gt_5pct)
.unwrap_or(0);
format!("{year}:{gt5}")
})
.collect::<Vec<_>>()
.join(",");
println!(
"{}\t{}\t{}\t{}\t{:.3}\t{:.3}\t{:.3}\t{:.3}\t{:.3}\t{:.3}\t{:.3}\t{}\t{}\t{}\t{:.2}\t{:.2}\t{}\t{}\t{}",
variant.cfg.name,
cap_label(variant.cfg.max_outputs),
cap_label(variant.cfg.max_outputs_after),
fixes_bad_lock(&variant.target_prices),
overall.percentile(50.0),
overall.percentile(95.0),
overall.percentile(99.0),
overall.percentile(99.9),
overall.rmse_pct(),
overall.max_pct(),
bias,
overall.gt_5pct,
overall.gt_10pct,
overall.gt_20pct,
target_price(&variant.target_prices, 952_287).unwrap_or(0.0),
target_price(&variant.target_prices, 952_288).unwrap_or(0.0),
variant
.target_prices
.iter()
.map(|(height, price)| format!("{height}:{price:.2}"))
.collect::<Vec<_>>()
.join(","),
rmse_by_year,
gt5_by_year
);
}
}
+24 -28
View File
@@ -6,8 +6,7 @@ use std::path::PathBuf;
use brk_indexer::Indexer;
use brk_oracle::{
Config, HistogramRaw, Oracle, PRICES, START_HEIGHT_FAST, bin_to_cents, cents_to_bin,
eligible_bin,
bin_to_cents, cents_to_bin, Config, Oracle, PaymentFilter, START_HEIGHT_FAST, START_HEIGHT_SLOW,
};
use brk_types::{OutputType, Sats, TxIndex, TxOutIndex};
use vecdb::{AnyVec, ReadableVec, VecIndex};
@@ -94,7 +93,11 @@ impl YearStats {
fn median_pct(&mut self) -> f64 {
self.errors.sort_by(|a, b| a.partial_cmp(b).unwrap());
let n = self.errors.len();
if n == 0 { 0.0 } else { self.errors[n / 2] }
if n == 0 {
0.0
} else {
self.errors[n / 2]
}
}
fn percentile(&self, p: f64) -> f64 {
@@ -172,15 +175,7 @@ fn main() {
.map(|ts| (**ts / 86400).saturating_sub(GENESIS_DAY) as usize)
.collect();
let start_price: f64 = PRICES
.lines()
.nth(START_HEIGHT_FAST - 1)
.expect("prices.txt too short")
.parse()
.expect("Failed to parse seed price");
let config = Config::default();
let mut oracle = Oracle::new(cents_to_bin(start_price * 100.0), config);
let mut oracle = Oracle::from_seed();
let total_txs = indexer.vecs.transactions.txid.len();
let total_outputs = indexer.vecs.outputs.value.len();
@@ -201,7 +196,11 @@ fn main() {
let mut oracle_candles: Vec<DayCandle> = Vec::new();
let mut current_di: Option<usize> = None;
for h in START_HEIGHT_FAST..total_heights {
for h in START_HEIGHT_SLOW..total_heights {
if h == START_HEIGHT_FAST {
oracle.reconfigure(Config::default());
}
let ft = first_tx_index[h];
let next_ft = first_tx_index
.get(h + 1)
@@ -235,23 +234,18 @@ fn main() {
.output_type
.collect_range_at(out_start, out_end);
// Drop every output of a tx carrying an OP_RETURN (protocol machinery).
let mut hist = HistogramRaw::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) = eligible_bin(values[i], output_types[i]) {
hist.increment(bin as usize);
}
}
}
values[lo..hi]
.iter()
.copied()
.zip(output_types[lo..hi].iter().copied())
});
let hist = PaymentFilter::for_height(h).histogram(tx_outputs);
let ref_bin = oracle.process_histogram(&hist);
let oracle_price = bin_to_cents(ref_bin) as f64 / 100.0;
@@ -373,10 +367,12 @@ fn main() {
println!(" brk_oracle accuracy report");
println!(" ══════════════════════════");
println!();
println!(" Config: w12, alpha=2/7, search -9/+11, noisy/dust/round-btc filtered");
println!(
" Test range: height {} .. {} ({} blocks)",
START_HEIGHT_FAST,
" Config: slow w40 alpha=0.10 until {START_HEIGHT_FAST}, then w12 alpha=2/7; shared payment filter"
);
println!(
" Test range: height {} .. {} ({} exchange-covered blocks)",
START_HEIGHT_SLOW,
total_heights - 1,
overall.total_blocks
);

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