mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-06-23 04:36:11 -07:00
Compare commits
54 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 17478a4ac4 | |||
| b3031b3375 | |||
| 2e401379a0 | |||
| 45ab6ebf71 | |||
| 00f7d69ea6 | |||
| 408d83c350 | |||
| 43df9e098c | |||
| 0c7861071d | |||
| 6f430bdb8c | |||
| 4b415b215d | |||
| 8614e9eded | |||
| c85da92cbc | |||
| 297fc3b855 | |||
| c9d5a62fcb | |||
| 90b3b51c48 | |||
| 5966ab05e4 | |||
| c3506339cd | |||
| e54843291e | |||
| b0b261fe9f | |||
| 6786be296d | |||
| e5068bbbf3 | |||
| 36cfe49b20 | |||
| 33cc13708a | |||
| 2389632812 | |||
| e0bcdb8105 | |||
| 45e83c98b9 | |||
| 753bbf3e7e | |||
| 54cc0cb446 | |||
| d64dcb75a9 | |||
| f599115f6c | |||
| 9fc45625ad | |||
| c68d1d1fda | |||
| 6cbe09af23 | |||
| 96d35d1d29 | |||
| e23554811b | |||
| 041c542046 | |||
| 66dc7cd8f5 | |||
| b00692249c | |||
| ff2c04a100 | |||
| 7cee0e2c5a | |||
| 744032f1f1 | |||
| 99b171bad6 | |||
| 37e2b6eae2 | |||
| a967fe8f35 | |||
| a3f3c54675 | |||
| f41874f438 | |||
| 98bbfec525 | |||
| 1bcf3235b6 | |||
| 07734b8bab | |||
| a2fd1e03ad | |||
| 90e8741fb7 | |||
| 5f5563fece | |||
| c7edfce481 | |||
| 7b3dd83b93 |
@@ -18,6 +18,7 @@ _*
|
||||
/*.py
|
||||
/*.json
|
||||
/*.html
|
||||
!/btc-cycle-sim.html
|
||||
/research
|
||||
/filter_*
|
||||
/heatmaps*
|
||||
|
||||
Generated
+171
-290
File diff suppressed because it is too large
Load Diff
+30
-30
@@ -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.4"
|
||||
package.homepage = "https://bitcoinresearchkit.org"
|
||||
package.repository = "https://github.com/bitcoinresearchkit/brk"
|
||||
package.readme = "README.md"
|
||||
@@ -35,38 +35,38 @@ debug = true
|
||||
[workspace.dependencies]
|
||||
aide = { version = "0.16.0-alpha.4", features = ["axum-json", "axum-query"] }
|
||||
axum = { version = "0.8.9", default-features = false, features = ["http1", "json", "query", "tokio", "tracing"] }
|
||||
bitcoin = { version = "0.32.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.10", features = ["serde"] }
|
||||
brk_alloc = { version = "0.3.4", path = "crates/brk_alloc" }
|
||||
brk_bencher = { version = "0.3.4", path = "crates/brk_bencher" }
|
||||
brk_bindgen = { version = "0.3.4", path = "crates/brk_bindgen" }
|
||||
brk_cli = { version = "0.3.4", path = "crates/brk_cli" }
|
||||
brk_client = { version = "0.3.4", path = "crates/brk_client" }
|
||||
brk_cohort = { version = "0.3.4", path = "crates/brk_cohort" }
|
||||
brk_computer = { version = "0.3.4", path = "crates/brk_computer" }
|
||||
brk_error = { version = "0.3.4", path = "crates/brk_error" }
|
||||
brk_fetcher = { version = "0.3.4", path = "crates/brk_fetcher" }
|
||||
brk_indexer = { version = "0.3.4", path = "crates/brk_indexer" }
|
||||
brk_iterator = { version = "0.3.4", path = "crates/brk_iterator" }
|
||||
brk_logger = { version = "0.3.4", path = "crates/brk_logger" }
|
||||
brk_mempool = { version = "0.3.4", path = "crates/brk_mempool" }
|
||||
brk_oracle = { version = "0.3.4", path = "crates/brk_oracle" }
|
||||
brk_query = { version = "0.3.4", path = "crates/brk_query", features = ["tokio"] }
|
||||
brk_reader = { version = "0.3.4", path = "crates/brk_reader" }
|
||||
brk_rpc = { version = "0.3.4", path = "crates/brk_rpc" }
|
||||
brk_server = { version = "0.3.4", path = "crates/brk_server" }
|
||||
brk_store = { version = "0.3.4", path = "crates/brk_store" }
|
||||
brk_traversable = { version = "0.3.4", path = "crates/brk_traversable", features = ["pco", "derive"] }
|
||||
brk_traversable_derive = { version = "0.3.4", path = "crates/brk_traversable_derive" }
|
||||
brk_types = { version = "0.3.4", path = "crates/brk_types" }
|
||||
brk_website = { version = "0.3.4", path = "crates/brk_website" }
|
||||
byteview = "0.10.1"
|
||||
color-eyre = "0.6.5"
|
||||
corepc-jsonrpc = { package = "jsonrpc", version = "0.19.0", features = ["simple_http"], default-features = false }
|
||||
corepc-types = { version = "0.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.29", features = ["perf-inline", "tz-system"], default-features = false }
|
||||
owo-colors = "4.3.0"
|
||||
parking_lot = "0.12.5"
|
||||
pco = "1.0.2"
|
||||
@@ -77,9 +77,9 @@ 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"] }
|
||||
|
||||
@@ -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,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
|
||||
*/
|
||||
@@ -403,6 +409,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);
|
||||
|
||||
@@ -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
|
||||
|
||||
+628
-34
@@ -1191,6 +1191,22 @@ pub struct CapCapitalizedGrossLossMvrvNetPeakPriceProfitSellSoprPattern {
|
||||
pub sopr: AdjustedRatioValuePattern,
|
||||
}
|
||||
|
||||
/// Pattern struct for repeated tree structure.
|
||||
pub struct CapCapitalizedGrossLossMvrvNetPeakPriceProfitSellSoprPattern2 {
|
||||
pub cap: CentsDeltaToUsdPattern,
|
||||
pub capitalized: PricePattern,
|
||||
pub gross_pnl: BlockCumulativeSumPattern,
|
||||
pub loss: BlockCumulativeNegativeSumPattern,
|
||||
pub mvrv: SeriesPattern1<StoredF32>,
|
||||
pub net_pnl: BlockChangeCumulativeDeltaSumPattern,
|
||||
pub peak_regret: BlockCumulativeSumPattern,
|
||||
pub price: BpsCentsPercentilesRatioSatsSmaStdUsdPattern,
|
||||
pub profit: BlockCumulativeSumPattern,
|
||||
pub profit_to_loss_ratio: _1m1w1y24hPattern<StoredF64>,
|
||||
pub sell_side_risk_ratio: _1m1w1y24hPattern8,
|
||||
pub sopr: RatioValuePattern2,
|
||||
}
|
||||
|
||||
/// Pattern struct for repeated tree structure.
|
||||
pub struct EmptyOpP2aP2msP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshUnknownPattern2 {
|
||||
pub empty: _1m1w1y24hBpsPercentRatioPattern,
|
||||
@@ -1658,6 +1674,17 @@ pub struct ActiveInputOutputSpendablePattern {
|
||||
pub spendable_output_to_reused_addr_share: _1m1w1y24hBpsPercentRatioPattern,
|
||||
}
|
||||
|
||||
/// Pattern struct for repeated tree structure.
|
||||
pub struct ActivityCostInvestedOutputsRealizedSupplyUnrealizedPattern2 {
|
||||
pub activity: CoindaysCoinyearsDormancyTransferPattern,
|
||||
pub cost_basis: InMaxMinPerSupplyPattern,
|
||||
pub invested_capital: InPattern,
|
||||
pub outputs: SpendingSpentUnspentPattern,
|
||||
pub realized: CapCapitalizedGrossLossMvrvNetPeakPriceProfitSellSoprPattern2,
|
||||
pub supply: DeltaDominanceHalfInTotalPattern2,
|
||||
pub unrealized: CapitalizedGrossInvestedLossNetNuplProfitSentimentPattern2,
|
||||
}
|
||||
|
||||
/// Pattern struct for repeated tree structure.
|
||||
pub struct CapLossMvrvNetPriceProfitSoprPattern {
|
||||
pub cap: CentsDeltaUsdPattern,
|
||||
@@ -3408,6 +3435,22 @@ impl PriceRatioPattern {
|
||||
}
|
||||
}
|
||||
|
||||
/// Pattern struct for repeated tree structure.
|
||||
pub struct RatioValuePattern2 {
|
||||
pub ratio: _1m1w1y24hPattern<StoredF64>,
|
||||
pub value_destroyed: AverageBlockCumulativeSumPattern<Cents>,
|
||||
}
|
||||
|
||||
impl RatioValuePattern2 {
|
||||
/// Create a new pattern node with accumulated series name.
|
||||
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
|
||||
Self {
|
||||
ratio: _1m1w1y24hPattern::new(client.clone(), _m(&acc, "sopr")),
|
||||
value_destroyed: AverageBlockCumulativeSumPattern::new(client.clone(), _m(&acc, "value_destroyed")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Pattern struct for repeated tree structure.
|
||||
pub struct RatioValuePattern {
|
||||
pub ratio: _24hPattern,
|
||||
@@ -3534,7 +3577,7 @@ pub struct SeriesTree {
|
||||
pub investing: SeriesTree_Investing,
|
||||
pub market: SeriesTree_Market,
|
||||
pub pools: SeriesTree_Pools,
|
||||
pub prices: SeriesTree_Prices,
|
||||
pub price: SeriesTree_Price,
|
||||
pub supply: SeriesTree_Supply,
|
||||
pub cohorts: SeriesTree_Cohorts,
|
||||
}
|
||||
@@ -3556,7 +3599,7 @@ impl SeriesTree {
|
||||
investing: SeriesTree_Investing::new(client.clone(), format!("{base_path}_investing")),
|
||||
market: SeriesTree_Market::new(client.clone(), format!("{base_path}_market")),
|
||||
pools: SeriesTree_Pools::new(client.clone(), format!("{base_path}_pools")),
|
||||
prices: SeriesTree_Prices::new(client.clone(), format!("{base_path}_prices")),
|
||||
price: SeriesTree_Price::new(client.clone(), format!("{base_path}_price")),
|
||||
supply: SeriesTree_Supply::new(client.clone(), format!("{base_path}_supply")),
|
||||
cohorts: SeriesTree_Cohorts::new(client.clone(), format!("{base_path}_cohorts")),
|
||||
}
|
||||
@@ -7063,31 +7106,31 @@ impl SeriesTree_Pools_Minor {
|
||||
}
|
||||
|
||||
/// Series tree node.
|
||||
pub struct SeriesTree_Prices {
|
||||
pub split: SeriesTree_Prices_Split,
|
||||
pub ohlc: SeriesTree_Prices_Ohlc,
|
||||
pub spot: SeriesTree_Prices_Spot,
|
||||
pub struct SeriesTree_Price {
|
||||
pub split: SeriesTree_Price_Split,
|
||||
pub ohlc: SeriesTree_Price_Ohlc,
|
||||
pub spot: SeriesTree_Price_Spot,
|
||||
}
|
||||
|
||||
impl SeriesTree_Prices {
|
||||
impl SeriesTree_Price {
|
||||
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
|
||||
Self {
|
||||
split: SeriesTree_Prices_Split::new(client.clone(), format!("{base_path}_split")),
|
||||
ohlc: SeriesTree_Prices_Ohlc::new(client.clone(), format!("{base_path}_ohlc")),
|
||||
spot: SeriesTree_Prices_Spot::new(client.clone(), format!("{base_path}_spot")),
|
||||
split: SeriesTree_Price_Split::new(client.clone(), format!("{base_path}_split")),
|
||||
ohlc: SeriesTree_Price_Ohlc::new(client.clone(), format!("{base_path}_ohlc")),
|
||||
spot: SeriesTree_Price_Spot::new(client.clone(), format!("{base_path}_spot")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Series tree node.
|
||||
pub struct SeriesTree_Prices_Split {
|
||||
pub struct SeriesTree_Price_Split {
|
||||
pub open: CentsSatsUsdPattern3,
|
||||
pub high: CentsSatsUsdPattern3,
|
||||
pub low: CentsSatsUsdPattern3,
|
||||
pub close: CentsSatsUsdPattern3,
|
||||
}
|
||||
|
||||
impl SeriesTree_Prices_Split {
|
||||
impl SeriesTree_Price_Split {
|
||||
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
|
||||
Self {
|
||||
open: CentsSatsUsdPattern3::new(client.clone(), "price_open".to_string()),
|
||||
@@ -7099,13 +7142,13 @@ impl SeriesTree_Prices_Split {
|
||||
}
|
||||
|
||||
/// Series tree node.
|
||||
pub struct SeriesTree_Prices_Ohlc {
|
||||
pub struct SeriesTree_Price_Ohlc {
|
||||
pub usd: SeriesPattern2<OHLCDollars>,
|
||||
pub cents: SeriesPattern2<OHLCCents>,
|
||||
pub sats: SeriesPattern2<OHLCSats>,
|
||||
}
|
||||
|
||||
impl SeriesTree_Prices_Ohlc {
|
||||
impl SeriesTree_Price_Ohlc {
|
||||
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
|
||||
Self {
|
||||
usd: SeriesPattern2::new(client.clone(), "price_ohlc".to_string()),
|
||||
@@ -7116,13 +7159,13 @@ impl SeriesTree_Prices_Ohlc {
|
||||
}
|
||||
|
||||
/// Series tree node.
|
||||
pub struct SeriesTree_Prices_Spot {
|
||||
pub struct SeriesTree_Price_Spot {
|
||||
pub usd: SeriesPattern1<Dollars>,
|
||||
pub cents: SeriesPattern1<Cents>,
|
||||
pub sats: SeriesPattern1<Sats>,
|
||||
}
|
||||
|
||||
impl SeriesTree_Prices_Spot {
|
||||
impl SeriesTree_Price_Spot {
|
||||
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
|
||||
Self {
|
||||
usd: SeriesPattern1::new(client.clone(), "price".to_string()),
|
||||
@@ -7199,6 +7242,7 @@ pub struct SeriesTree_Cohorts_Utxo {
|
||||
pub over_age: SeriesTree_Cohorts_Utxo_OverAge,
|
||||
pub epoch: SeriesTree_Cohorts_Utxo_Epoch,
|
||||
pub class: SeriesTree_Cohorts_Utxo_Class,
|
||||
pub entry: SeriesTree_Cohorts_Utxo_Entry,
|
||||
pub over_amount: SeriesTree_Cohorts_Utxo_OverAmount,
|
||||
pub amount_range: SeriesTree_Cohorts_Utxo_AmountRange,
|
||||
pub under_amount: SeriesTree_Cohorts_Utxo_UnderAmount,
|
||||
@@ -7218,6 +7262,7 @@ impl SeriesTree_Cohorts_Utxo {
|
||||
over_age: SeriesTree_Cohorts_Utxo_OverAge::new(client.clone(), format!("{base_path}_over_age")),
|
||||
epoch: SeriesTree_Cohorts_Utxo_Epoch::new(client.clone(), format!("{base_path}_epoch")),
|
||||
class: SeriesTree_Cohorts_Utxo_Class::new(client.clone(), format!("{base_path}_class")),
|
||||
entry: SeriesTree_Cohorts_Utxo_Entry::new(client.clone(), format!("{base_path}_entry")),
|
||||
over_amount: SeriesTree_Cohorts_Utxo_OverAmount::new(client.clone(), format!("{base_path}_over_amount")),
|
||||
amount_range: SeriesTree_Cohorts_Utxo_AmountRange::new(client.clone(), format!("{base_path}_amount_range")),
|
||||
under_amount: SeriesTree_Cohorts_Utxo_UnderAmount::new(client.clone(), format!("{base_path}_under_amount")),
|
||||
@@ -7999,7 +8044,7 @@ pub struct SeriesTree_Cohorts_Utxo_Lth_Realized {
|
||||
pub price: SeriesTree_Cohorts_Utxo_Lth_Realized_Price,
|
||||
pub mvrv: SeriesPattern1<StoredF32>,
|
||||
pub net_pnl: BlockChangeCumulativeDeltaSumPattern,
|
||||
pub sopr: SeriesTree_Cohorts_Utxo_Lth_Realized_Sopr,
|
||||
pub sopr: RatioValuePattern2,
|
||||
pub gross_pnl: BlockCumulativeSumPattern,
|
||||
pub sell_side_risk_ratio: _1m1w1y24hPattern8,
|
||||
pub peak_regret: BlockCumulativeSumPattern,
|
||||
@@ -8016,7 +8061,7 @@ impl SeriesTree_Cohorts_Utxo_Lth_Realized {
|
||||
price: SeriesTree_Cohorts_Utxo_Lth_Realized_Price::new(client.clone(), format!("{base_path}_price")),
|
||||
mvrv: SeriesPattern1::new(client.clone(), "lth_mvrv".to_string()),
|
||||
net_pnl: BlockChangeCumulativeDeltaSumPattern::new(client.clone(), "lth_net".to_string()),
|
||||
sopr: SeriesTree_Cohorts_Utxo_Lth_Realized_Sopr::new(client.clone(), format!("{base_path}_sopr")),
|
||||
sopr: RatioValuePattern2::new(client.clone(), "lth".to_string()),
|
||||
gross_pnl: BlockCumulativeSumPattern::new(client.clone(), "lth_realized_gross_pnl".to_string()),
|
||||
sell_side_risk_ratio: _1m1w1y24hPattern8::new(client.clone(), "lth_sell_side_risk_ratio".to_string()),
|
||||
peak_regret: BlockCumulativeSumPattern::new(client.clone(), "lth_realized_peak_regret".to_string()),
|
||||
@@ -8236,21 +8281,6 @@ impl SeriesTree_Cohorts_Utxo_Lth_Realized_Price_StdDev_1y {
|
||||
}
|
||||
}
|
||||
|
||||
/// Series tree node.
|
||||
pub struct SeriesTree_Cohorts_Utxo_Lth_Realized_Sopr {
|
||||
pub value_destroyed: AverageBlockCumulativeSumPattern<Cents>,
|
||||
pub ratio: _1m1w1y24hPattern<StoredF64>,
|
||||
}
|
||||
|
||||
impl SeriesTree_Cohorts_Utxo_Lth_Realized_Sopr {
|
||||
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
|
||||
Self {
|
||||
value_destroyed: AverageBlockCumulativeSumPattern::new(client.clone(), "lth_value_destroyed".to_string()),
|
||||
ratio: _1m1w1y24hPattern::new(client.clone(), "lth_sopr".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Series tree node.
|
||||
pub struct SeriesTree_Cohorts_Utxo_AgeRange {
|
||||
pub under_1h: ActivityOutputsRealizedSupplyUnrealizedPattern,
|
||||
@@ -8466,6 +8496,561 @@ impl SeriesTree_Cohorts_Utxo_Class {
|
||||
}
|
||||
}
|
||||
|
||||
/// Series tree node.
|
||||
pub struct SeriesTree_Cohorts_Utxo_Entry {
|
||||
pub discount: SeriesTree_Cohorts_Utxo_Entry_Discount,
|
||||
pub premium: SeriesTree_Cohorts_Utxo_Entry_Premium,
|
||||
}
|
||||
|
||||
impl SeriesTree_Cohorts_Utxo_Entry {
|
||||
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
|
||||
Self {
|
||||
discount: SeriesTree_Cohorts_Utxo_Entry_Discount::new(client.clone(), format!("{base_path}_discount")),
|
||||
premium: SeriesTree_Cohorts_Utxo_Entry_Premium::new(client.clone(), format!("{base_path}_premium")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Series tree node.
|
||||
pub struct SeriesTree_Cohorts_Utxo_Entry_Discount {
|
||||
pub supply: DeltaDominanceHalfInTotalPattern2,
|
||||
pub outputs: SpendingSpentUnspentPattern,
|
||||
pub activity: CoindaysCoinyearsDormancyTransferPattern,
|
||||
pub realized: SeriesTree_Cohorts_Utxo_Entry_Discount_Realized,
|
||||
pub cost_basis: InMaxMinPerSupplyPattern,
|
||||
pub unrealized: CapitalizedGrossInvestedLossNetNuplProfitSentimentPattern2,
|
||||
pub invested_capital: InPattern,
|
||||
}
|
||||
|
||||
impl SeriesTree_Cohorts_Utxo_Entry_Discount {
|
||||
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
|
||||
Self {
|
||||
supply: DeltaDominanceHalfInTotalPattern2::new(client.clone(), "veteran_supply".to_string()),
|
||||
outputs: SpendingSpentUnspentPattern::new(client.clone(), "veteran".to_string()),
|
||||
activity: CoindaysCoinyearsDormancyTransferPattern::new(client.clone(), "veteran".to_string()),
|
||||
realized: SeriesTree_Cohorts_Utxo_Entry_Discount_Realized::new(client.clone(), format!("{base_path}_realized")),
|
||||
cost_basis: InMaxMinPerSupplyPattern::new(client.clone(), "veteran".to_string()),
|
||||
unrealized: CapitalizedGrossInvestedLossNetNuplProfitSentimentPattern2::new(client.clone(), "veteran".to_string()),
|
||||
invested_capital: InPattern::new(client.clone(), "veteran_invested_capital_in".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Series tree node.
|
||||
pub struct SeriesTree_Cohorts_Utxo_Entry_Discount_Realized {
|
||||
pub cap: CentsDeltaToUsdPattern,
|
||||
pub profit: BlockCumulativeSumPattern,
|
||||
pub loss: BlockCumulativeNegativeSumPattern,
|
||||
pub price: SeriesTree_Cohorts_Utxo_Entry_Discount_Realized_Price,
|
||||
pub mvrv: SeriesPattern1<StoredF32>,
|
||||
pub net_pnl: BlockChangeCumulativeDeltaSumPattern,
|
||||
pub sopr: RatioValuePattern2,
|
||||
pub gross_pnl: BlockCumulativeSumPattern,
|
||||
pub sell_side_risk_ratio: _1m1w1y24hPattern8,
|
||||
pub peak_regret: BlockCumulativeSumPattern,
|
||||
pub capitalized: PricePattern,
|
||||
pub profit_to_loss_ratio: _1m1w1y24hPattern<StoredF64>,
|
||||
}
|
||||
|
||||
impl SeriesTree_Cohorts_Utxo_Entry_Discount_Realized {
|
||||
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
|
||||
Self {
|
||||
cap: CentsDeltaToUsdPattern::new(client.clone(), "veteran_realized_cap".to_string()),
|
||||
profit: BlockCumulativeSumPattern::new(client.clone(), "veteran_realized_profit".to_string()),
|
||||
loss: BlockCumulativeNegativeSumPattern::new(client.clone(), "veteran_realized_loss".to_string()),
|
||||
price: SeriesTree_Cohorts_Utxo_Entry_Discount_Realized_Price::new(client.clone(), format!("{base_path}_price")),
|
||||
mvrv: SeriesPattern1::new(client.clone(), "veteran_mvrv".to_string()),
|
||||
net_pnl: BlockChangeCumulativeDeltaSumPattern::new(client.clone(), "veteran_net".to_string()),
|
||||
sopr: RatioValuePattern2::new(client.clone(), "veteran".to_string()),
|
||||
gross_pnl: BlockCumulativeSumPattern::new(client.clone(), "veteran_realized_gross_pnl".to_string()),
|
||||
sell_side_risk_ratio: _1m1w1y24hPattern8::new(client.clone(), "veteran_sell_side_risk_ratio".to_string()),
|
||||
peak_regret: BlockCumulativeSumPattern::new(client.clone(), "veteran_realized_peak_regret".to_string()),
|
||||
capitalized: PricePattern::new(client.clone(), "veteran_capitalized_price".to_string()),
|
||||
profit_to_loss_ratio: _1m1w1y24hPattern::new(client.clone(), "veteran_realized_profit_to_loss_ratio".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Series tree node.
|
||||
pub struct SeriesTree_Cohorts_Utxo_Entry_Discount_Realized_Price {
|
||||
pub usd: SeriesPattern1<Dollars>,
|
||||
pub cents: SeriesPattern1<Cents>,
|
||||
pub sats: SeriesPattern1<SatsFract>,
|
||||
pub bps: SeriesPattern1<BasisPoints32>,
|
||||
pub ratio: SeriesPattern1<StoredF32>,
|
||||
pub percentiles: Pct0Pct1Pct2Pct5Pct95Pct98Pct99Pattern,
|
||||
pub sma: _1m1w1y2y4yAllPattern,
|
||||
pub std_dev: SeriesTree_Cohorts_Utxo_Entry_Discount_Realized_Price_StdDev,
|
||||
}
|
||||
|
||||
impl SeriesTree_Cohorts_Utxo_Entry_Discount_Realized_Price {
|
||||
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
|
||||
Self {
|
||||
usd: SeriesPattern1::new(client.clone(), "veteran_realized_price".to_string()),
|
||||
cents: SeriesPattern1::new(client.clone(), "veteran_realized_price_cents".to_string()),
|
||||
sats: SeriesPattern1::new(client.clone(), "veteran_realized_price_sats".to_string()),
|
||||
bps: SeriesPattern1::new(client.clone(), "veteran_realized_price_ratio_bps".to_string()),
|
||||
ratio: SeriesPattern1::new(client.clone(), "veteran_realized_price_ratio".to_string()),
|
||||
percentiles: Pct0Pct1Pct2Pct5Pct95Pct98Pct99Pattern::new(client.clone(), "veteran_realized_price".to_string()),
|
||||
sma: _1m1w1y2y4yAllPattern::new(client.clone(), "veteran_realized_price_ratio_sma".to_string()),
|
||||
std_dev: SeriesTree_Cohorts_Utxo_Entry_Discount_Realized_Price_StdDev::new(client.clone(), format!("{base_path}_std_dev")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Series tree node.
|
||||
pub struct SeriesTree_Cohorts_Utxo_Entry_Discount_Realized_Price_StdDev {
|
||||
pub all: SeriesTree_Cohorts_Utxo_Entry_Discount_Realized_Price_StdDev_All,
|
||||
pub _4y: SeriesTree_Cohorts_Utxo_Entry_Discount_Realized_Price_StdDev_4y,
|
||||
pub _2y: SeriesTree_Cohorts_Utxo_Entry_Discount_Realized_Price_StdDev_2y,
|
||||
pub _1y: SeriesTree_Cohorts_Utxo_Entry_Discount_Realized_Price_StdDev_1y,
|
||||
}
|
||||
|
||||
impl SeriesTree_Cohorts_Utxo_Entry_Discount_Realized_Price_StdDev {
|
||||
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
|
||||
Self {
|
||||
all: SeriesTree_Cohorts_Utxo_Entry_Discount_Realized_Price_StdDev_All::new(client.clone(), format!("{base_path}_all")),
|
||||
_4y: SeriesTree_Cohorts_Utxo_Entry_Discount_Realized_Price_StdDev_4y::new(client.clone(), format!("{base_path}_4y")),
|
||||
_2y: SeriesTree_Cohorts_Utxo_Entry_Discount_Realized_Price_StdDev_2y::new(client.clone(), format!("{base_path}_2y")),
|
||||
_1y: SeriesTree_Cohorts_Utxo_Entry_Discount_Realized_Price_StdDev_1y::new(client.clone(), format!("{base_path}_1y")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Series tree node.
|
||||
pub struct SeriesTree_Cohorts_Utxo_Entry_Discount_Realized_Price_StdDev_All {
|
||||
pub sd: SeriesPattern1<StoredF32>,
|
||||
pub zscore: SeriesPattern1<StoredF32>,
|
||||
pub _0sd: CentsSatsUsdPattern,
|
||||
pub p0_5sd: PriceRatioPattern,
|
||||
pub p1sd: PriceRatioPattern,
|
||||
pub p1_5sd: PriceRatioPattern,
|
||||
pub p2sd: PriceRatioPattern,
|
||||
pub p2_5sd: PriceRatioPattern,
|
||||
pub p3sd: PriceRatioPattern,
|
||||
pub m0_5sd: PriceRatioPattern,
|
||||
pub m1sd: PriceRatioPattern,
|
||||
pub m1_5sd: PriceRatioPattern,
|
||||
pub m2sd: PriceRatioPattern,
|
||||
pub m2_5sd: PriceRatioPattern,
|
||||
pub m3sd: PriceRatioPattern,
|
||||
}
|
||||
|
||||
impl SeriesTree_Cohorts_Utxo_Entry_Discount_Realized_Price_StdDev_All {
|
||||
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
|
||||
Self {
|
||||
sd: SeriesPattern1::new(client.clone(), "veteran_realized_price_ratio_sd".to_string()),
|
||||
zscore: SeriesPattern1::new(client.clone(), "veteran_realized_price_ratio_zscore".to_string()),
|
||||
_0sd: CentsSatsUsdPattern::new(client.clone(), "veteran_realized_price_0sd".to_string()),
|
||||
p0_5sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "p0_5sd".to_string()),
|
||||
p1sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "p1sd".to_string()),
|
||||
p1_5sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "p1_5sd".to_string()),
|
||||
p2sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "p2sd".to_string()),
|
||||
p2_5sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "p2_5sd".to_string()),
|
||||
p3sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "p3sd".to_string()),
|
||||
m0_5sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "m0_5sd".to_string()),
|
||||
m1sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "m1sd".to_string()),
|
||||
m1_5sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "m1_5sd".to_string()),
|
||||
m2sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "m2sd".to_string()),
|
||||
m2_5sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "m2_5sd".to_string()),
|
||||
m3sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "m3sd".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Series tree node.
|
||||
pub struct SeriesTree_Cohorts_Utxo_Entry_Discount_Realized_Price_StdDev_4y {
|
||||
pub sd: SeriesPattern1<StoredF32>,
|
||||
pub zscore: SeriesPattern1<StoredF32>,
|
||||
pub _0sd: CentsSatsUsdPattern,
|
||||
pub p0_5sd: PriceRatioPattern,
|
||||
pub p1sd: PriceRatioPattern,
|
||||
pub p1_5sd: PriceRatioPattern,
|
||||
pub p2sd: PriceRatioPattern,
|
||||
pub p2_5sd: PriceRatioPattern,
|
||||
pub p3sd: PriceRatioPattern,
|
||||
pub m0_5sd: PriceRatioPattern,
|
||||
pub m1sd: PriceRatioPattern,
|
||||
pub m1_5sd: PriceRatioPattern,
|
||||
pub m2sd: PriceRatioPattern,
|
||||
pub m2_5sd: PriceRatioPattern,
|
||||
pub m3sd: PriceRatioPattern,
|
||||
}
|
||||
|
||||
impl SeriesTree_Cohorts_Utxo_Entry_Discount_Realized_Price_StdDev_4y {
|
||||
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
|
||||
Self {
|
||||
sd: SeriesPattern1::new(client.clone(), "veteran_realized_price_ratio_sd_4y".to_string()),
|
||||
zscore: SeriesPattern1::new(client.clone(), "veteran_realized_price_ratio_zscore_4y".to_string()),
|
||||
_0sd: CentsSatsUsdPattern::new(client.clone(), "veteran_realized_price_0sd_4y".to_string()),
|
||||
p0_5sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "p0_5sd_4y".to_string()),
|
||||
p1sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "p1sd_4y".to_string()),
|
||||
p1_5sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "p1_5sd_4y".to_string()),
|
||||
p2sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "p2sd_4y".to_string()),
|
||||
p2_5sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "p2_5sd_4y".to_string()),
|
||||
p3sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "p3sd_4y".to_string()),
|
||||
m0_5sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "m0_5sd_4y".to_string()),
|
||||
m1sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "m1sd_4y".to_string()),
|
||||
m1_5sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "m1_5sd_4y".to_string()),
|
||||
m2sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "m2sd_4y".to_string()),
|
||||
m2_5sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "m2_5sd_4y".to_string()),
|
||||
m3sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "m3sd_4y".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Series tree node.
|
||||
pub struct SeriesTree_Cohorts_Utxo_Entry_Discount_Realized_Price_StdDev_2y {
|
||||
pub sd: SeriesPattern1<StoredF32>,
|
||||
pub zscore: SeriesPattern1<StoredF32>,
|
||||
pub _0sd: CentsSatsUsdPattern,
|
||||
pub p0_5sd: PriceRatioPattern,
|
||||
pub p1sd: PriceRatioPattern,
|
||||
pub p1_5sd: PriceRatioPattern,
|
||||
pub p2sd: PriceRatioPattern,
|
||||
pub p2_5sd: PriceRatioPattern,
|
||||
pub p3sd: PriceRatioPattern,
|
||||
pub m0_5sd: PriceRatioPattern,
|
||||
pub m1sd: PriceRatioPattern,
|
||||
pub m1_5sd: PriceRatioPattern,
|
||||
pub m2sd: PriceRatioPattern,
|
||||
pub m2_5sd: PriceRatioPattern,
|
||||
pub m3sd: PriceRatioPattern,
|
||||
}
|
||||
|
||||
impl SeriesTree_Cohorts_Utxo_Entry_Discount_Realized_Price_StdDev_2y {
|
||||
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
|
||||
Self {
|
||||
sd: SeriesPattern1::new(client.clone(), "veteran_realized_price_ratio_sd_2y".to_string()),
|
||||
zscore: SeriesPattern1::new(client.clone(), "veteran_realized_price_ratio_zscore_2y".to_string()),
|
||||
_0sd: CentsSatsUsdPattern::new(client.clone(), "veteran_realized_price_0sd_2y".to_string()),
|
||||
p0_5sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "p0_5sd_2y".to_string()),
|
||||
p1sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "p1sd_2y".to_string()),
|
||||
p1_5sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "p1_5sd_2y".to_string()),
|
||||
p2sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "p2sd_2y".to_string()),
|
||||
p2_5sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "p2_5sd_2y".to_string()),
|
||||
p3sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "p3sd_2y".to_string()),
|
||||
m0_5sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "m0_5sd_2y".to_string()),
|
||||
m1sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "m1sd_2y".to_string()),
|
||||
m1_5sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "m1_5sd_2y".to_string()),
|
||||
m2sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "m2sd_2y".to_string()),
|
||||
m2_5sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "m2_5sd_2y".to_string()),
|
||||
m3sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "m3sd_2y".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Series tree node.
|
||||
pub struct SeriesTree_Cohorts_Utxo_Entry_Discount_Realized_Price_StdDev_1y {
|
||||
pub sd: SeriesPattern1<StoredF32>,
|
||||
pub zscore: SeriesPattern1<StoredF32>,
|
||||
pub _0sd: CentsSatsUsdPattern,
|
||||
pub p0_5sd: PriceRatioPattern,
|
||||
pub p1sd: PriceRatioPattern,
|
||||
pub p1_5sd: PriceRatioPattern,
|
||||
pub p2sd: PriceRatioPattern,
|
||||
pub p2_5sd: PriceRatioPattern,
|
||||
pub p3sd: PriceRatioPattern,
|
||||
pub m0_5sd: PriceRatioPattern,
|
||||
pub m1sd: PriceRatioPattern,
|
||||
pub m1_5sd: PriceRatioPattern,
|
||||
pub m2sd: PriceRatioPattern,
|
||||
pub m2_5sd: PriceRatioPattern,
|
||||
pub m3sd: PriceRatioPattern,
|
||||
}
|
||||
|
||||
impl SeriesTree_Cohorts_Utxo_Entry_Discount_Realized_Price_StdDev_1y {
|
||||
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
|
||||
Self {
|
||||
sd: SeriesPattern1::new(client.clone(), "veteran_realized_price_ratio_sd_1y".to_string()),
|
||||
zscore: SeriesPattern1::new(client.clone(), "veteran_realized_price_ratio_zscore_1y".to_string()),
|
||||
_0sd: CentsSatsUsdPattern::new(client.clone(), "veteran_realized_price_0sd_1y".to_string()),
|
||||
p0_5sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "p0_5sd_1y".to_string()),
|
||||
p1sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "p1sd_1y".to_string()),
|
||||
p1_5sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "p1_5sd_1y".to_string()),
|
||||
p2sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "p2sd_1y".to_string()),
|
||||
p2_5sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "p2_5sd_1y".to_string()),
|
||||
p3sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "p3sd_1y".to_string()),
|
||||
m0_5sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "m0_5sd_1y".to_string()),
|
||||
m1sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "m1sd_1y".to_string()),
|
||||
m1_5sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "m1_5sd_1y".to_string()),
|
||||
m2sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "m2sd_1y".to_string()),
|
||||
m2_5sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "m2_5sd_1y".to_string()),
|
||||
m3sd: PriceRatioPattern::new(client.clone(), "veteran_realized_price".to_string(), "m3sd_1y".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Series tree node.
|
||||
pub struct SeriesTree_Cohorts_Utxo_Entry_Premium {
|
||||
pub supply: DeltaDominanceHalfInTotalPattern2,
|
||||
pub outputs: SpendingSpentUnspentPattern,
|
||||
pub activity: CoindaysCoinyearsDormancyTransferPattern,
|
||||
pub realized: SeriesTree_Cohorts_Utxo_Entry_Premium_Realized,
|
||||
pub cost_basis: InMaxMinPerSupplyPattern,
|
||||
pub unrealized: CapitalizedGrossInvestedLossNetNuplProfitSentimentPattern2,
|
||||
pub invested_capital: InPattern,
|
||||
}
|
||||
|
||||
impl SeriesTree_Cohorts_Utxo_Entry_Premium {
|
||||
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
|
||||
Self {
|
||||
supply: DeltaDominanceHalfInTotalPattern2::new(client.clone(), "rookie_supply".to_string()),
|
||||
outputs: SpendingSpentUnspentPattern::new(client.clone(), "rookie".to_string()),
|
||||
activity: CoindaysCoinyearsDormancyTransferPattern::new(client.clone(), "rookie".to_string()),
|
||||
realized: SeriesTree_Cohorts_Utxo_Entry_Premium_Realized::new(client.clone(), format!("{base_path}_realized")),
|
||||
cost_basis: InMaxMinPerSupplyPattern::new(client.clone(), "rookie".to_string()),
|
||||
unrealized: CapitalizedGrossInvestedLossNetNuplProfitSentimentPattern2::new(client.clone(), "rookie".to_string()),
|
||||
invested_capital: InPattern::new(client.clone(), "rookie_invested_capital_in".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Series tree node.
|
||||
pub struct SeriesTree_Cohorts_Utxo_Entry_Premium_Realized {
|
||||
pub cap: CentsDeltaToUsdPattern,
|
||||
pub profit: BlockCumulativeSumPattern,
|
||||
pub loss: BlockCumulativeNegativeSumPattern,
|
||||
pub price: SeriesTree_Cohorts_Utxo_Entry_Premium_Realized_Price,
|
||||
pub mvrv: SeriesPattern1<StoredF32>,
|
||||
pub net_pnl: BlockChangeCumulativeDeltaSumPattern,
|
||||
pub sopr: RatioValuePattern2,
|
||||
pub gross_pnl: BlockCumulativeSumPattern,
|
||||
pub sell_side_risk_ratio: _1m1w1y24hPattern8,
|
||||
pub peak_regret: BlockCumulativeSumPattern,
|
||||
pub capitalized: PricePattern,
|
||||
pub profit_to_loss_ratio: _1m1w1y24hPattern<StoredF64>,
|
||||
}
|
||||
|
||||
impl SeriesTree_Cohorts_Utxo_Entry_Premium_Realized {
|
||||
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
|
||||
Self {
|
||||
cap: CentsDeltaToUsdPattern::new(client.clone(), "rookie_realized_cap".to_string()),
|
||||
profit: BlockCumulativeSumPattern::new(client.clone(), "rookie_realized_profit".to_string()),
|
||||
loss: BlockCumulativeNegativeSumPattern::new(client.clone(), "rookie_realized_loss".to_string()),
|
||||
price: SeriesTree_Cohorts_Utxo_Entry_Premium_Realized_Price::new(client.clone(), format!("{base_path}_price")),
|
||||
mvrv: SeriesPattern1::new(client.clone(), "rookie_mvrv".to_string()),
|
||||
net_pnl: BlockChangeCumulativeDeltaSumPattern::new(client.clone(), "rookie_net".to_string()),
|
||||
sopr: RatioValuePattern2::new(client.clone(), "rookie".to_string()),
|
||||
gross_pnl: BlockCumulativeSumPattern::new(client.clone(), "rookie_realized_gross_pnl".to_string()),
|
||||
sell_side_risk_ratio: _1m1w1y24hPattern8::new(client.clone(), "rookie_sell_side_risk_ratio".to_string()),
|
||||
peak_regret: BlockCumulativeSumPattern::new(client.clone(), "rookie_realized_peak_regret".to_string()),
|
||||
capitalized: PricePattern::new(client.clone(), "rookie_capitalized_price".to_string()),
|
||||
profit_to_loss_ratio: _1m1w1y24hPattern::new(client.clone(), "rookie_realized_profit_to_loss_ratio".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Series tree node.
|
||||
pub struct SeriesTree_Cohorts_Utxo_Entry_Premium_Realized_Price {
|
||||
pub usd: SeriesPattern1<Dollars>,
|
||||
pub cents: SeriesPattern1<Cents>,
|
||||
pub sats: SeriesPattern1<SatsFract>,
|
||||
pub bps: SeriesPattern1<BasisPoints32>,
|
||||
pub ratio: SeriesPattern1<StoredF32>,
|
||||
pub percentiles: Pct0Pct1Pct2Pct5Pct95Pct98Pct99Pattern,
|
||||
pub sma: _1m1w1y2y4yAllPattern,
|
||||
pub std_dev: SeriesTree_Cohorts_Utxo_Entry_Premium_Realized_Price_StdDev,
|
||||
}
|
||||
|
||||
impl SeriesTree_Cohorts_Utxo_Entry_Premium_Realized_Price {
|
||||
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
|
||||
Self {
|
||||
usd: SeriesPattern1::new(client.clone(), "rookie_realized_price".to_string()),
|
||||
cents: SeriesPattern1::new(client.clone(), "rookie_realized_price_cents".to_string()),
|
||||
sats: SeriesPattern1::new(client.clone(), "rookie_realized_price_sats".to_string()),
|
||||
bps: SeriesPattern1::new(client.clone(), "rookie_realized_price_ratio_bps".to_string()),
|
||||
ratio: SeriesPattern1::new(client.clone(), "rookie_realized_price_ratio".to_string()),
|
||||
percentiles: Pct0Pct1Pct2Pct5Pct95Pct98Pct99Pattern::new(client.clone(), "rookie_realized_price".to_string()),
|
||||
sma: _1m1w1y2y4yAllPattern::new(client.clone(), "rookie_realized_price_ratio_sma".to_string()),
|
||||
std_dev: SeriesTree_Cohorts_Utxo_Entry_Premium_Realized_Price_StdDev::new(client.clone(), format!("{base_path}_std_dev")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Series tree node.
|
||||
pub struct SeriesTree_Cohorts_Utxo_Entry_Premium_Realized_Price_StdDev {
|
||||
pub all: SeriesTree_Cohorts_Utxo_Entry_Premium_Realized_Price_StdDev_All,
|
||||
pub _4y: SeriesTree_Cohorts_Utxo_Entry_Premium_Realized_Price_StdDev_4y,
|
||||
pub _2y: SeriesTree_Cohorts_Utxo_Entry_Premium_Realized_Price_StdDev_2y,
|
||||
pub _1y: SeriesTree_Cohorts_Utxo_Entry_Premium_Realized_Price_StdDev_1y,
|
||||
}
|
||||
|
||||
impl SeriesTree_Cohorts_Utxo_Entry_Premium_Realized_Price_StdDev {
|
||||
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
|
||||
Self {
|
||||
all: SeriesTree_Cohorts_Utxo_Entry_Premium_Realized_Price_StdDev_All::new(client.clone(), format!("{base_path}_all")),
|
||||
_4y: SeriesTree_Cohorts_Utxo_Entry_Premium_Realized_Price_StdDev_4y::new(client.clone(), format!("{base_path}_4y")),
|
||||
_2y: SeriesTree_Cohorts_Utxo_Entry_Premium_Realized_Price_StdDev_2y::new(client.clone(), format!("{base_path}_2y")),
|
||||
_1y: SeriesTree_Cohorts_Utxo_Entry_Premium_Realized_Price_StdDev_1y::new(client.clone(), format!("{base_path}_1y")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Series tree node.
|
||||
pub struct SeriesTree_Cohorts_Utxo_Entry_Premium_Realized_Price_StdDev_All {
|
||||
pub sd: SeriesPattern1<StoredF32>,
|
||||
pub zscore: SeriesPattern1<StoredF32>,
|
||||
pub _0sd: CentsSatsUsdPattern,
|
||||
pub p0_5sd: PriceRatioPattern,
|
||||
pub p1sd: PriceRatioPattern,
|
||||
pub p1_5sd: PriceRatioPattern,
|
||||
pub p2sd: PriceRatioPattern,
|
||||
pub p2_5sd: PriceRatioPattern,
|
||||
pub p3sd: PriceRatioPattern,
|
||||
pub m0_5sd: PriceRatioPattern,
|
||||
pub m1sd: PriceRatioPattern,
|
||||
pub m1_5sd: PriceRatioPattern,
|
||||
pub m2sd: PriceRatioPattern,
|
||||
pub m2_5sd: PriceRatioPattern,
|
||||
pub m3sd: PriceRatioPattern,
|
||||
}
|
||||
|
||||
impl SeriesTree_Cohorts_Utxo_Entry_Premium_Realized_Price_StdDev_All {
|
||||
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
|
||||
Self {
|
||||
sd: SeriesPattern1::new(client.clone(), "rookie_realized_price_ratio_sd".to_string()),
|
||||
zscore: SeriesPattern1::new(client.clone(), "rookie_realized_price_ratio_zscore".to_string()),
|
||||
_0sd: CentsSatsUsdPattern::new(client.clone(), "rookie_realized_price_0sd".to_string()),
|
||||
p0_5sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "p0_5sd".to_string()),
|
||||
p1sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "p1sd".to_string()),
|
||||
p1_5sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "p1_5sd".to_string()),
|
||||
p2sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "p2sd".to_string()),
|
||||
p2_5sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "p2_5sd".to_string()),
|
||||
p3sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "p3sd".to_string()),
|
||||
m0_5sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "m0_5sd".to_string()),
|
||||
m1sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "m1sd".to_string()),
|
||||
m1_5sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "m1_5sd".to_string()),
|
||||
m2sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "m2sd".to_string()),
|
||||
m2_5sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "m2_5sd".to_string()),
|
||||
m3sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "m3sd".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Series tree node.
|
||||
pub struct SeriesTree_Cohorts_Utxo_Entry_Premium_Realized_Price_StdDev_4y {
|
||||
pub sd: SeriesPattern1<StoredF32>,
|
||||
pub zscore: SeriesPattern1<StoredF32>,
|
||||
pub _0sd: CentsSatsUsdPattern,
|
||||
pub p0_5sd: PriceRatioPattern,
|
||||
pub p1sd: PriceRatioPattern,
|
||||
pub p1_5sd: PriceRatioPattern,
|
||||
pub p2sd: PriceRatioPattern,
|
||||
pub p2_5sd: PriceRatioPattern,
|
||||
pub p3sd: PriceRatioPattern,
|
||||
pub m0_5sd: PriceRatioPattern,
|
||||
pub m1sd: PriceRatioPattern,
|
||||
pub m1_5sd: PriceRatioPattern,
|
||||
pub m2sd: PriceRatioPattern,
|
||||
pub m2_5sd: PriceRatioPattern,
|
||||
pub m3sd: PriceRatioPattern,
|
||||
}
|
||||
|
||||
impl SeriesTree_Cohorts_Utxo_Entry_Premium_Realized_Price_StdDev_4y {
|
||||
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
|
||||
Self {
|
||||
sd: SeriesPattern1::new(client.clone(), "rookie_realized_price_ratio_sd_4y".to_string()),
|
||||
zscore: SeriesPattern1::new(client.clone(), "rookie_realized_price_ratio_zscore_4y".to_string()),
|
||||
_0sd: CentsSatsUsdPattern::new(client.clone(), "rookie_realized_price_0sd_4y".to_string()),
|
||||
p0_5sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "p0_5sd_4y".to_string()),
|
||||
p1sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "p1sd_4y".to_string()),
|
||||
p1_5sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "p1_5sd_4y".to_string()),
|
||||
p2sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "p2sd_4y".to_string()),
|
||||
p2_5sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "p2_5sd_4y".to_string()),
|
||||
p3sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "p3sd_4y".to_string()),
|
||||
m0_5sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "m0_5sd_4y".to_string()),
|
||||
m1sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "m1sd_4y".to_string()),
|
||||
m1_5sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "m1_5sd_4y".to_string()),
|
||||
m2sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "m2sd_4y".to_string()),
|
||||
m2_5sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "m2_5sd_4y".to_string()),
|
||||
m3sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "m3sd_4y".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Series tree node.
|
||||
pub struct SeriesTree_Cohorts_Utxo_Entry_Premium_Realized_Price_StdDev_2y {
|
||||
pub sd: SeriesPattern1<StoredF32>,
|
||||
pub zscore: SeriesPattern1<StoredF32>,
|
||||
pub _0sd: CentsSatsUsdPattern,
|
||||
pub p0_5sd: PriceRatioPattern,
|
||||
pub p1sd: PriceRatioPattern,
|
||||
pub p1_5sd: PriceRatioPattern,
|
||||
pub p2sd: PriceRatioPattern,
|
||||
pub p2_5sd: PriceRatioPattern,
|
||||
pub p3sd: PriceRatioPattern,
|
||||
pub m0_5sd: PriceRatioPattern,
|
||||
pub m1sd: PriceRatioPattern,
|
||||
pub m1_5sd: PriceRatioPattern,
|
||||
pub m2sd: PriceRatioPattern,
|
||||
pub m2_5sd: PriceRatioPattern,
|
||||
pub m3sd: PriceRatioPattern,
|
||||
}
|
||||
|
||||
impl SeriesTree_Cohorts_Utxo_Entry_Premium_Realized_Price_StdDev_2y {
|
||||
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
|
||||
Self {
|
||||
sd: SeriesPattern1::new(client.clone(), "rookie_realized_price_ratio_sd_2y".to_string()),
|
||||
zscore: SeriesPattern1::new(client.clone(), "rookie_realized_price_ratio_zscore_2y".to_string()),
|
||||
_0sd: CentsSatsUsdPattern::new(client.clone(), "rookie_realized_price_0sd_2y".to_string()),
|
||||
p0_5sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "p0_5sd_2y".to_string()),
|
||||
p1sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "p1sd_2y".to_string()),
|
||||
p1_5sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "p1_5sd_2y".to_string()),
|
||||
p2sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "p2sd_2y".to_string()),
|
||||
p2_5sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "p2_5sd_2y".to_string()),
|
||||
p3sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "p3sd_2y".to_string()),
|
||||
m0_5sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "m0_5sd_2y".to_string()),
|
||||
m1sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "m1sd_2y".to_string()),
|
||||
m1_5sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "m1_5sd_2y".to_string()),
|
||||
m2sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "m2sd_2y".to_string()),
|
||||
m2_5sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "m2_5sd_2y".to_string()),
|
||||
m3sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "m3sd_2y".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Series tree node.
|
||||
pub struct SeriesTree_Cohorts_Utxo_Entry_Premium_Realized_Price_StdDev_1y {
|
||||
pub sd: SeriesPattern1<StoredF32>,
|
||||
pub zscore: SeriesPattern1<StoredF32>,
|
||||
pub _0sd: CentsSatsUsdPattern,
|
||||
pub p0_5sd: PriceRatioPattern,
|
||||
pub p1sd: PriceRatioPattern,
|
||||
pub p1_5sd: PriceRatioPattern,
|
||||
pub p2sd: PriceRatioPattern,
|
||||
pub p2_5sd: PriceRatioPattern,
|
||||
pub p3sd: PriceRatioPattern,
|
||||
pub m0_5sd: PriceRatioPattern,
|
||||
pub m1sd: PriceRatioPattern,
|
||||
pub m1_5sd: PriceRatioPattern,
|
||||
pub m2sd: PriceRatioPattern,
|
||||
pub m2_5sd: PriceRatioPattern,
|
||||
pub m3sd: PriceRatioPattern,
|
||||
}
|
||||
|
||||
impl SeriesTree_Cohorts_Utxo_Entry_Premium_Realized_Price_StdDev_1y {
|
||||
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
|
||||
Self {
|
||||
sd: SeriesPattern1::new(client.clone(), "rookie_realized_price_ratio_sd_1y".to_string()),
|
||||
zscore: SeriesPattern1::new(client.clone(), "rookie_realized_price_ratio_zscore_1y".to_string()),
|
||||
_0sd: CentsSatsUsdPattern::new(client.clone(), "rookie_realized_price_0sd_1y".to_string()),
|
||||
p0_5sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "p0_5sd_1y".to_string()),
|
||||
p1sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "p1sd_1y".to_string()),
|
||||
p1_5sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "p1_5sd_1y".to_string()),
|
||||
p2sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "p2sd_1y".to_string()),
|
||||
p2_5sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "p2_5sd_1y".to_string()),
|
||||
p3sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "p3sd_1y".to_string()),
|
||||
m0_5sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "m0_5sd_1y".to_string()),
|
||||
m1sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "m1sd_1y".to_string()),
|
||||
m1_5sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "m1_5sd_1y".to_string()),
|
||||
m2sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "m2sd_1y".to_string()),
|
||||
m2_5sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "m2_5sd_1y".to_string()),
|
||||
m3sd: PriceRatioPattern::new(client.clone(), "rookie_realized_price".to_string(), "m3sd_1y".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Series tree node.
|
||||
pub struct SeriesTree_Cohorts_Utxo_OverAmount {
|
||||
pub _1sat: ActivityOutputsRealizedSupplyUnrealizedPattern2,
|
||||
@@ -8953,7 +9538,7 @@ pub struct BrkClient {
|
||||
|
||||
impl BrkClient {
|
||||
/// Client version.
|
||||
pub const VERSION: &'static str = "v0.3.0";
|
||||
pub const VERSION: &'static str = "v0.3.4";
|
||||
|
||||
/// Create a new client with the given base URL.
|
||||
pub fn new(base_url: impl Into<String>) -> Self {
|
||||
@@ -9281,6 +9866,15 @@ impl BrkClient {
|
||||
self.base.get_json(&path)
|
||||
}
|
||||
|
||||
/// Address hash-prefix matches
|
||||
///
|
||||
/// Find addresses by address type and address-payload hash prefix. Intended for privacy-preserving client-side wallet discovery without sending raw addresses or xpubs. Fetch metadata for the returned addresses through `/api/address/{address}`.
|
||||
///
|
||||
/// Endpoint: `GET /api/address/hash-prefix/{addr_type}/{prefix}`
|
||||
pub fn get_address_hash_prefix_matches(&self, addr_type: OutputType, prefix: &str) -> Result<AddrHashPrefixMatches> {
|
||||
self.base.get_json(&format!("/api/address/hash-prefix/{addr_type}/{prefix}"))
|
||||
}
|
||||
|
||||
/// Address information
|
||||
///
|
||||
/// Retrieve address information including balance and transaction counts. Supports all standard Bitcoin address types (P2PKH, P2SH, P2WPKH, P2WSH, P2TR).
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
|
||||
@@ -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(_)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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(<h_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(<h, 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,
|
||||
|
||||
@@ -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,
|
||||
×tamps,
|
||||
&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)?;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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<()> {
|
||||
|
||||
@@ -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<()> {
|
||||
|
||||
+9
-19
@@ -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;
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
//! Generate detailed oracle accuracy report for README / documentation.
|
||||
//! Experimental oracle accuracy report from an arbitrary start height with many
|
||||
//! scoring/filter knobs.
|
||||
//!
|
||||
//! Run with: cargo run -p brk_oracle --example report --release
|
||||
//! Use `report.rs` for the canonical README/documentation report.
|
||||
//!
|
||||
//! Run with: cargo run -p brk_oracle --example report_from --release
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use brk_indexer::Indexer;
|
||||
use brk_oracle::{
|
||||
Config, HistogramEma, HistogramRaw, NUM_BINS, PRICES, START_HEIGHT_FAST, bin_to_cents,
|
||||
cents_to_bin, eligible_bin,
|
||||
Config, HistogramEma, HistogramRaw, NUM_BINS, START_HEIGHT_FAST, bin_to_cents, cents_to_bin,
|
||||
pre_oracle_price_cents, PaymentFilter,
|
||||
};
|
||||
use brk_types::{OutputType, Sats, TxIndex, TxOutIndex};
|
||||
use vecdb::{AnyVec, ReadableVec, VecIndex};
|
||||
@@ -579,16 +582,14 @@ fn main() {
|
||||
.map(|ts| (**ts / 86400).saturating_sub(GENESIS_DAY) as usize)
|
||||
.collect();
|
||||
|
||||
// Seed price at height `start - 1`. The baked prices.txt only covers up to
|
||||
// 508k (the cold-start seed); past it we warm-start from the exchange close
|
||||
// so any later start height gets a primed ref_bin without the cold-start
|
||||
// alias zone. start <= 508k stays bit-identical to the old baseline.
|
||||
let start_price: f64 = PRICES
|
||||
.lines()
|
||||
.nth(start - 1)
|
||||
.and_then(|l| l.parse().ok())
|
||||
// Seed price at height `start - 1`. The baked prices.txt only covers the
|
||||
// pre-oracle seed range; past it this experimental harness warm-starts from
|
||||
// exchange OHLC so arbitrary later starts get a primed ref_bin.
|
||||
let seed_height = start.saturating_sub(1);
|
||||
let start_price: f64 = pre_oracle_price_cents(seed_height)
|
||||
.map(|cents| cents.inner() as f64 / 100.0)
|
||||
.unwrap_or_else(|| {
|
||||
let o = height_ohlc.get(start - 1).copied().unwrap_or([0.0; 4]);
|
||||
let o = height_ohlc.get(seed_height).copied().unwrap_or([0.0; 4]);
|
||||
if o[3] > 0.0 {
|
||||
o[3]
|
||||
} else {
|
||||
@@ -912,7 +913,7 @@ fn main() {
|
||||
continue;
|
||||
}
|
||||
for i in lo..hi {
|
||||
if let Some(bin) = eligible_bin(values[i], output_types[i]) {
|
||||
if let Some(bin) = PaymentFilter::eligible_bin(values[i], output_types[i]) {
|
||||
hist.increment(bin as usize);
|
||||
}
|
||||
}
|
||||
@@ -1168,7 +1169,10 @@ fn main() {
|
||||
println!(" brk_oracle accuracy report");
|
||||
println!(" ══════════════════════════");
|
||||
println!();
|
||||
println!(" Config: w12, alpha=2/7, search -9/+11, noisy/dust/round-btc filtered");
|
||||
println!(
|
||||
" Config: w{}, alpha={:.5}, search -{}/+{}, experimental knobs",
|
||||
window_size, config.alpha, config.search_below, config.search_above
|
||||
);
|
||||
println!(
|
||||
" Test range: height {} .. {} ({} blocks), seed ${:.2}",
|
||||
start,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use std::ops::Range;
|
||||
|
||||
/// First height the oracle computes on-chain, with the slow cold-start EMA
|
||||
/// ([`slow`](Config::slow)). Below it, prices come from [`PRICES`](crate::PRICES).
|
||||
/// ([`slow`](Config::slow)). Below it, prices come from
|
||||
/// [`pre_oracle_prices_from`](crate::pre_oracle_prices_from).
|
||||
pub const START_HEIGHT_SLOW: usize = 340_000;
|
||||
|
||||
/// Height where the oracle switches slow -> fast EMA ([`default`](Config::default)).
|
||||
@@ -10,7 +11,7 @@ pub const START_HEIGHT_SLOW: usize = 340_000;
|
||||
/// slow.
|
||||
pub const START_HEIGHT_FAST: usize = 508_000;
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct Config {
|
||||
/// EMA decay: 2/(N+1) where N is span in blocks. 2/7 = 6-block span.
|
||||
pub alpha: f64,
|
||||
@@ -100,4 +101,10 @@ mod tests {
|
||||
Config::segments_for_range(START_HEIGHT_FAST..(START_HEIGHT_FAST + 2)).collect();
|
||||
assert_eq!(fast, vec![START_HEIGHT_FAST..(START_HEIGHT_FAST + 2)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn for_height_selects_regime() {
|
||||
assert_eq!(Config::for_height(START_HEIGHT_FAST - 1), Config::slow());
|
||||
assert_eq!(Config::for_height(START_HEIGHT_FAST), Config::default());
|
||||
}
|
||||
}
|
||||
|
||||
+192
-49
@@ -1,6 +1,6 @@
|
||||
use brk_types::{OutputType, Sats};
|
||||
|
||||
use crate::scale::sats_to_bin;
|
||||
use crate::scale::{sats_to_bin, HistogramRaw};
|
||||
|
||||
/// Dust floor: outputs below this many sats are too small to be payments.
|
||||
const MIN_SATS: u64 = 1000;
|
||||
@@ -10,7 +10,7 @@ const MIN_SATS: u64 = 1000;
|
||||
const EXCLUDED_OUTPUT_TYPES: &[OutputType] = &[OutputType::P2TR];
|
||||
|
||||
/// Bitmask form of [`EXCLUDED_OUTPUT_TYPES`], folded at compile time so
|
||||
/// [`eligible_bin`] checks membership with a single AND.
|
||||
/// [`PaymentFilter::eligible_bin`] checks membership with a single AND.
|
||||
const EXCLUDED_MASK: u16 = {
|
||||
let mut mask = 0u16;
|
||||
let mut i = 0;
|
||||
@@ -21,55 +21,198 @@ const EXCLUDED_MASK: u16 = {
|
||||
mask
|
||||
};
|
||||
|
||||
/// A transaction with more than this many outputs is a batch payout (exchange
|
||||
/// sweep, mixer fan-out), not a round-dollar payment, so it is dropped below
|
||||
/// [`MAX_OUTPUTS_UNTIL_HEIGHT`].
|
||||
pub const MAX_OUTPUTS: usize = 100;
|
||||
|
||||
/// Height below which the [`MAX_OUTPUTS`] cap applies. The thin 2018-2020
|
||||
/// signal needs batch payouts removed to stay locked onto the round-dollar
|
||||
/// pattern. Above this height on-chain volume is dense enough that the cap
|
||||
/// removes more genuine signal than noise, so it is lifted.
|
||||
pub const MAX_OUTPUTS_UNTIL_HEIGHT: usize = 630_000;
|
||||
|
||||
/// Bin index for `(sats, output_type)`, or `None` for an excluded type (P2TR),
|
||||
/// dust, a round-BTC value, or an out-of-range bin. The per-output half of the
|
||||
/// round-dollar payment filter.
|
||||
#[inline(always)]
|
||||
pub fn eligible_bin(sats: Sats, output_type: OutputType) -> Option<u16> {
|
||||
if EXCLUDED_MASK & (1u16 << output_type as u8) != 0 {
|
||||
return None;
|
||||
}
|
||||
if *sats < MIN_SATS || sats.is_common_round_value() {
|
||||
return None;
|
||||
}
|
||||
sats_to_bin(sats).map(|b| b as u16)
|
||||
/// Round-dollar payment filter.
|
||||
///
|
||||
/// Input: transaction outputs. Output: eligible log-scale bins or a fresh block
|
||||
/// histogram. The only state is the transaction-output fan-out cap selected by
|
||||
/// block height, or [`MODERN`](Self::MODERN) for live modern transaction streams.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct PaymentFilter {
|
||||
tx_output_fanout_cap: usize,
|
||||
}
|
||||
|
||||
/// The on-chain round-dollar payment filter, shared by the indexer warm-up,
|
||||
/// per-request reconstruction, and the mempool's live histogram so every path
|
||||
/// bins identically. Calls `emit(bin)` for each eligible output, in order.
|
||||
///
|
||||
/// A whole transaction is dropped when it carries any OP_RETURN output (data
|
||||
/// carriers, not payments) or, below [`MAX_OUTPUTS_UNTIL_HEIGHT`], when it has
|
||||
/// more than [`MAX_OUTPUTS`] outputs (batch payouts). `height` is the block these
|
||||
/// outputs belong to. The mempool, always past the cap window, passes
|
||||
/// `usize::MAX`.
|
||||
#[inline]
|
||||
pub fn for_each_round_dollar_bin(
|
||||
height: usize,
|
||||
outputs: impl ExactSizeIterator<Item = (Sats, OutputType)> + Clone,
|
||||
mut emit: impl FnMut(u16),
|
||||
) {
|
||||
if height < MAX_OUTPUTS_UNTIL_HEIGHT && outputs.len() > MAX_OUTPUTS {
|
||||
return;
|
||||
}
|
||||
if outputs.clone().any(|(_, ty)| ty == OutputType::OpReturn) {
|
||||
return;
|
||||
}
|
||||
for (sats, ty) in outputs {
|
||||
if let Some(bin) = eligible_bin(sats, ty) {
|
||||
emit(bin);
|
||||
impl PaymentFilter {
|
||||
/// Pre-modern transaction-output fan-out cap. Above this, the transaction is
|
||||
/// a batch payout (exchange sweep, mixer fan-out), not a round-dollar
|
||||
/// payment.
|
||||
pub const PRE_MODERN_TX_OUTPUT_FANOUT_CAP: usize = 100;
|
||||
|
||||
/// Modern-chain transaction-output fan-out cap. Dense post-630k blocks can
|
||||
/// carry more genuine payment outputs, but very large fan-outs can still
|
||||
/// dominate one EMA slot and create a false round-dollar ladder.
|
||||
pub const MODERN_TX_OUTPUT_FANOUT_CAP: usize = 250;
|
||||
|
||||
/// Height where [`Self::PRE_MODERN_TX_OUTPUT_FANOUT_CAP`] relaxes to
|
||||
/// [`Self::MODERN_TX_OUTPUT_FANOUT_CAP`].
|
||||
pub const MODERN_TX_OUTPUT_FANOUT_CAP_START_HEIGHT: usize = 630_000;
|
||||
|
||||
/// Filter for live or otherwise guaranteed-modern transaction streams.
|
||||
pub const MODERN: Self = Self::with_fanout_cap(Self::MODERN_TX_OUTPUT_FANOUT_CAP);
|
||||
|
||||
const fn with_fanout_cap(tx_output_fanout_cap: usize) -> Self {
|
||||
Self {
|
||||
tx_output_fanout_cap,
|
||||
}
|
||||
}
|
||||
|
||||
/// Filter for transactions in `height`.
|
||||
pub const fn for_height(height: usize) -> Self {
|
||||
if height < Self::MODERN_TX_OUTPUT_FANOUT_CAP_START_HEIGHT {
|
||||
Self::with_fanout_cap(Self::PRE_MODERN_TX_OUTPUT_FANOUT_CAP)
|
||||
} else {
|
||||
Self::MODERN
|
||||
}
|
||||
}
|
||||
|
||||
/// Bin index for `(sats, output_type)`, or `None` for an excluded type
|
||||
/// (P2TR), dust, a round-BTC value, or an out-of-range bin. The per-output
|
||||
/// half of the round-dollar payment filter.
|
||||
#[inline(always)]
|
||||
pub fn eligible_bin(sats: Sats, output_type: OutputType) -> Option<u16> {
|
||||
if EXCLUDED_MASK & (1u16 << output_type as u8) != 0 {
|
||||
return None;
|
||||
}
|
||||
if *sats < MIN_SATS || sats.is_common_round_value() {
|
||||
return None;
|
||||
}
|
||||
sats_to_bin(sats).map(|b| b as u16)
|
||||
}
|
||||
|
||||
/// Apply the transaction-level payment filter and call `emit(bin)` for each
|
||||
/// eligible output, in order.
|
||||
///
|
||||
/// A whole transaction is dropped when it carries any OP_RETURN output (data
|
||||
/// carriers, not payments) or when it has more outputs than this filter's
|
||||
/// fan-out cap.
|
||||
#[inline]
|
||||
pub fn for_each_bin(
|
||||
self,
|
||||
outputs: impl ExactSizeIterator<Item = (Sats, OutputType)> + Clone,
|
||||
mut emit: impl FnMut(u16),
|
||||
) {
|
||||
if outputs.len() > self.tx_output_fanout_cap {
|
||||
return;
|
||||
}
|
||||
if outputs.clone().any(|(_, ty)| ty == OutputType::OpReturn) {
|
||||
return;
|
||||
}
|
||||
for (sats, ty) in outputs {
|
||||
if let Some(bin) = Self::eligible_bin(sats, ty) {
|
||||
emit(bin);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a fresh eligible round-dollar payment histogram for one block's
|
||||
/// non-coinbase transaction outputs.
|
||||
#[inline]
|
||||
pub fn histogram<Outputs>(self, txs: impl IntoIterator<Item = Outputs>) -> HistogramRaw
|
||||
where
|
||||
Outputs: ExactSizeIterator<Item = (Sats, OutputType)> + Clone,
|
||||
{
|
||||
let mut hist = HistogramRaw::zeros();
|
||||
for outputs in txs {
|
||||
self.for_each_bin(outputs, |bin| hist.increment(bin as usize));
|
||||
}
|
||||
hist
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn payment_outputs(len: usize) -> impl ExactSizeIterator<Item = (Sats, OutputType)> + Clone {
|
||||
std::iter::repeat_n((Sats::new(12_345), OutputType::P2WPKH), len)
|
||||
}
|
||||
|
||||
fn emitted_count(height: usize, len: usize) -> usize {
|
||||
let mut count = 0;
|
||||
PaymentFilter::for_height(height).for_each_bin(payment_outputs(len), |_| count += 1);
|
||||
count
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn early_fanout_cap_is_strict() {
|
||||
assert_eq!(
|
||||
emitted_count(
|
||||
PaymentFilter::MODERN_TX_OUTPUT_FANOUT_CAP_START_HEIGHT - 1,
|
||||
PaymentFilter::PRE_MODERN_TX_OUTPUT_FANOUT_CAP,
|
||||
),
|
||||
PaymentFilter::PRE_MODERN_TX_OUTPUT_FANOUT_CAP
|
||||
);
|
||||
assert_eq!(
|
||||
emitted_count(
|
||||
PaymentFilter::MODERN_TX_OUTPUT_FANOUT_CAP_START_HEIGHT - 1,
|
||||
PaymentFilter::PRE_MODERN_TX_OUTPUT_FANOUT_CAP + 1,
|
||||
),
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn modern_fanout_cap_is_relaxed_but_not_lifted() {
|
||||
assert_eq!(
|
||||
emitted_count(
|
||||
PaymentFilter::MODERN_TX_OUTPUT_FANOUT_CAP_START_HEIGHT,
|
||||
PaymentFilter::MODERN_TX_OUTPUT_FANOUT_CAP,
|
||||
),
|
||||
PaymentFilter::MODERN_TX_OUTPUT_FANOUT_CAP
|
||||
);
|
||||
assert_eq!(
|
||||
emitted_count(
|
||||
PaymentFilter::MODERN_TX_OUTPUT_FANOUT_CAP_START_HEIGHT,
|
||||
PaymentFilter::MODERN_TX_OUTPUT_FANOUT_CAP + 1,
|
||||
),
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
fn emitted_count_modern(len: usize) -> usize {
|
||||
let mut count = 0;
|
||||
PaymentFilter::MODERN.for_each_bin(payment_outputs(len), |_| count += 1);
|
||||
count
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn modern_helper_uses_modern_fanout_cap() {
|
||||
assert_eq!(
|
||||
emitted_count_modern(PaymentFilter::MODERN_TX_OUTPUT_FANOUT_CAP),
|
||||
PaymentFilter::MODERN_TX_OUTPUT_FANOUT_CAP
|
||||
);
|
||||
assert_eq!(
|
||||
emitted_count_modern(PaymentFilter::MODERN_TX_OUTPUT_FANOUT_CAP + 1),
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn payment_histogram_drops_op_return_transaction() {
|
||||
let sats = Sats::new(12_345);
|
||||
let txs = vec![
|
||||
vec![(sats, OutputType::P2WPKH), (sats, OutputType::P2PKH)],
|
||||
vec![
|
||||
(Sats::new(54_321), OutputType::OpReturn),
|
||||
(sats, OutputType::P2WPKH),
|
||||
],
|
||||
];
|
||||
let hist = PaymentFilter::MODERN.histogram(txs.into_iter().map(|tx| tx.into_iter()));
|
||||
|
||||
let bin = PaymentFilter::eligible_bin(sats, OutputType::P2WPKH).unwrap() as usize;
|
||||
assert_eq!(hist[bin], 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builds_fresh_payment_histogram() {
|
||||
let sats = Sats::new(12_345);
|
||||
let txs = vec![vec![
|
||||
(sats, OutputType::P2WPKH),
|
||||
(Sats::new(100_000_000), OutputType::P2WPKH),
|
||||
]];
|
||||
|
||||
let hist = PaymentFilter::MODERN.histogram(txs.into_iter().map(|tx| tx.into_iter()));
|
||||
|
||||
let bin = PaymentFilter::eligible_bin(sats, OutputType::P2WPKH).unwrap() as usize;
|
||||
assert_eq!(hist[bin], 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,42 +6,41 @@
|
||||
//! Behavior changes by height along two independent axes, each in its own module:
|
||||
//!
|
||||
//! - EMA regime (`config`): below [`START_HEIGHT_SLOW`] prices come from the baked
|
||||
//! [`PRICES`]. From there to [`START_HEIGHT_FAST`] a slow cold-start EMA runs with
|
||||
//! a shape-anchoring restoring force. At [`START_HEIGHT_FAST`] it switches to a
|
||||
//! fast EMA that tracks mature-market volatility.
|
||||
//! - Output filter (`filter`): below [`MAX_OUTPUTS_UNTIL_HEIGHT`] batch-payout
|
||||
//! transactions are dropped from the histogram. Above it the cap is lifted.
|
||||
//! pre-oracle tape. From there to [`START_HEIGHT_FAST`] a slow cold-start EMA
|
||||
//! runs with a shape-anchoring restoring force. At [`START_HEIGHT_FAST`] it
|
||||
//! switches to a fast EMA that tracks mature-market volatility.
|
||||
//! - Output filter (`filter`): below
|
||||
//! [`PaymentFilter::MODERN_TX_OUTPUT_FANOUT_CAP_START_HEIGHT`] batch-payout
|
||||
//! transactions are capped strictly. Above it the cap relaxes but still drops
|
||||
//! very large fan-outs.
|
||||
//!
|
||||
//! The two boundaries differ on purpose. The EMA must hand off to fast before the
|
||||
//! 2020 crash, while the output cap helps the thin pre-2020 mix for longer.
|
||||
//! 2020 crash, while the output cap helps the thin pre-2020 mix for longer and
|
||||
//! still prevents modern fan-out clusters from dominating one EMA slot.
|
||||
|
||||
use brk_types::{Cents, Dollars};
|
||||
|
||||
mod config;
|
||||
mod filter;
|
||||
mod scale;
|
||||
mod shape;
|
||||
mod seed;
|
||||
mod stencil;
|
||||
mod window;
|
||||
|
||||
pub use config::{Config, START_HEIGHT_FAST, START_HEIGHT_SLOW};
|
||||
pub use filter::{MAX_OUTPUTS, MAX_OUTPUTS_UNTIL_HEIGHT, eligible_bin, for_each_round_dollar_bin};
|
||||
pub use filter::PaymentFilter;
|
||||
pub use scale::{
|
||||
BINS_PER_DECADE, HistogramEma, HistogramEmaCompact, HistogramRaw, NUM_BINS, bin_to_cents,
|
||||
cents_to_bin, sats_to_bin,
|
||||
bin_to_cents, cents_to_bin, sats_to_bin, HistogramEma, HistogramEmaCompact, HistogramRaw,
|
||||
BINS_PER_DECADE, NUM_BINS,
|
||||
};
|
||||
pub use seed::{pre_oracle_price_cents, pre_oracle_prices_from, seed_bin, seed_price_cents};
|
||||
|
||||
use shape::ShapeAnchor;
|
||||
use stencil::find_best_bin;
|
||||
use stencil::Stencil;
|
||||
use window::EmaWindow;
|
||||
|
||||
/// Oracle algorithm version. Bump on any change that alters computed prices
|
||||
/// so downstream consumers can invalidate cached results.
|
||||
pub const VERSION: u32 = 3;
|
||||
|
||||
/// Pre-oracle dollar prices, one per line, heights 0..340_000. The last entry
|
||||
/// seeds the oracle's first on-chain computation at [`START_HEIGHT_SLOW`].
|
||||
pub const PRICES: &str = include_str!("prices.txt");
|
||||
pub const VERSION: u32 = 4;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Oracle {
|
||||
@@ -49,9 +48,7 @@ pub struct Oracle {
|
||||
ref_bin: f64,
|
||||
config: Config,
|
||||
warmup: bool,
|
||||
/// Shape-anchoring restoring force, inert outside the slow cold-start
|
||||
/// regime (zero weight). See [`ShapeAnchor`](shape::ShapeAnchor).
|
||||
shape: ShapeAnchor,
|
||||
stencil: Stencil,
|
||||
}
|
||||
|
||||
impl Oracle {
|
||||
@@ -60,11 +57,17 @@ impl Oracle {
|
||||
window: EmaWindow::new(config.window_size, config.alpha),
|
||||
ref_bin: start_bin,
|
||||
warmup: false,
|
||||
shape: ShapeAnchor::new(config.shape_weight),
|
||||
stencil: Stencil::new(config.shape_weight),
|
||||
config,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create an oracle ready to process height [`START_HEIGHT_SLOW`], seeded from
|
||||
/// the baked pre-oracle price tape and using the slow cold-start config.
|
||||
pub fn from_seed() -> Self {
|
||||
Self::new(seed_bin(), Config::slow())
|
||||
}
|
||||
|
||||
/// Create an oracle restored from a known price. `fill` should call
|
||||
/// `process_histogram` for the warmup blocks. During warmup the ring
|
||||
/// fills without recomputing EMA or searching, then we recompute once
|
||||
@@ -84,14 +87,12 @@ impl Oracle {
|
||||
if !self.warmup {
|
||||
self.window.recompute();
|
||||
|
||||
self.ref_bin = find_best_bin(
|
||||
self.ref_bin = self.stencil.pick(
|
||||
self.window.ema(),
|
||||
self.ref_bin,
|
||||
self.config.search_below,
|
||||
self.config.search_above,
|
||||
&self.shape,
|
||||
);
|
||||
self.shape.update(self.window.ema(), self.ref_bin.round() as i64);
|
||||
}
|
||||
self.ref_bin
|
||||
}
|
||||
@@ -139,6 +140,21 @@ mod tests {
|
||||
assert_eq!(oracle.price_cents(), bin_to_cents(1600.0).into());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_seed_matches_manual_seed() {
|
||||
let mut seeded = Oracle::from_seed();
|
||||
let mut manual = Oracle::new(seed_bin(), Config::slow());
|
||||
let mut hist = HistogramRaw::zeros();
|
||||
hist.increment(1200);
|
||||
|
||||
assert_eq!(seeded.ref_bin(), manual.ref_bin());
|
||||
assert_eq!(
|
||||
seeded.process_histogram(&hist),
|
||||
manual.process_histogram(&hist)
|
||||
);
|
||||
assert!(seeded.ema().iter().eq(manual.ema().iter()));
|
||||
}
|
||||
|
||||
// reconfigure must leave the oracle in the same state as a fresh warm-up
|
||||
// over the most recent window of raw histograms. The continuous build and
|
||||
// the incremental resume rely on this agreeing at the slow -> fast seam.
|
||||
@@ -158,7 +174,7 @@ mod tests {
|
||||
hists.iter().for_each(|h| {
|
||||
switched.process_histogram(h);
|
||||
});
|
||||
switched.reconfigure(fast.clone());
|
||||
switched.reconfigure(fast);
|
||||
|
||||
let keep = fast.window_size;
|
||||
let fresh = Oracle::from_checkpoint(switched.ref_bin(), fast, |o| {
|
||||
@@ -186,7 +202,7 @@ mod tests {
|
||||
let query_start = config.window_size + 5;
|
||||
let query_end = query_start + 20;
|
||||
let seed = 1600.0;
|
||||
let mut sequential = Oracle::from_checkpoint(seed, config.clone(), |o| {
|
||||
let mut sequential = Oracle::from_checkpoint(seed, config, |o| {
|
||||
hists[query_start + 1 - config.window_size..query_start + 1]
|
||||
.iter()
|
||||
.for_each(|h| {
|
||||
@@ -199,7 +215,7 @@ mod tests {
|
||||
sequential.process_histogram(&hists[height]);
|
||||
}
|
||||
|
||||
let fresh = Oracle::from_checkpoint(seed, config.clone(), |o| {
|
||||
let fresh = Oracle::from_checkpoint(seed, config, |o| {
|
||||
hists[height + 1 - config.window_size..height + 1]
|
||||
.iter()
|
||||
.for_each(|h| {
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
use brk_types::Cents;
|
||||
|
||||
use crate::{config::START_HEIGHT_SLOW, scale::cents_to_bin};
|
||||
|
||||
/// Pre-oracle dollar prices, one per line, heights 0..START_HEIGHT_SLOW.
|
||||
const PRICES: &str = include_str!("prices.txt");
|
||||
|
||||
/// Baked pre-oracle price at `height`, or `None` once on-chain oracle prices
|
||||
/// start.
|
||||
pub fn pre_oracle_price_cents(height: usize) -> Option<Cents> {
|
||||
if height >= START_HEIGHT_SLOW {
|
||||
return None;
|
||||
}
|
||||
PRICES.lines().nth(height).map(parse_price_cents)
|
||||
}
|
||||
|
||||
/// Baked pre-oracle prices starting at `start_height`, as a one-pass iterator.
|
||||
pub fn pre_oracle_prices_from(start_height: usize) -> impl Iterator<Item = Cents> {
|
||||
PRICES
|
||||
.lines()
|
||||
.take(START_HEIGHT_SLOW)
|
||||
.skip(start_height.min(START_HEIGHT_SLOW))
|
||||
.map(parse_price_cents)
|
||||
}
|
||||
|
||||
/// Baked exchange price for the block immediately before on-chain oracle prices
|
||||
/// start.
|
||||
pub fn seed_price_cents() -> Cents {
|
||||
pre_oracle_price_cents(START_HEIGHT_SLOW - 1)
|
||||
.expect("prices.txt must cover height START_HEIGHT_SLOW - 1")
|
||||
}
|
||||
|
||||
/// Initial reference bin for processing height START_HEIGHT_SLOW.
|
||||
pub fn seed_bin() -> f64 {
|
||||
cents_to_bin(seed_price_cents().inner() as f64)
|
||||
}
|
||||
|
||||
fn parse_price_cents(line: &str) -> Cents {
|
||||
let dollars: f64 = line.parse().expect("invalid baked oracle price");
|
||||
Cents::new((dollars * 100.0).round() as u64)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn prices_txt_covers_pre_oracle_range() {
|
||||
assert!(PRICES.lines().count() >= START_HEIGHT_SLOW);
|
||||
assert_eq!(pre_oracle_prices_from(0).count(), START_HEIGHT_SLOW);
|
||||
seed_price_cents();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_oracle_prices_stop_at_onchain_start() {
|
||||
assert!(pre_oracle_price_cents(START_HEIGHT_SLOW - 1).is_some());
|
||||
assert!(pre_oracle_price_cents(START_HEIGHT_SLOW).is_none());
|
||||
assert_eq!(pre_oracle_prices_from(START_HEIGHT_SLOW).count(), 0);
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
use crate::{
|
||||
scale::HistogramEma,
|
||||
stencil::{N_ARMS, normalized_arms_at},
|
||||
};
|
||||
|
||||
/// EMA rate for the adaptive shape template (~250-block time constant), slow
|
||||
/// enough that a transient octave slide can't corrupt the profile before the
|
||||
/// pick recovers.
|
||||
const SHAPE_BETA: f64 = 0.004;
|
||||
|
||||
/// Adaptive shape-anchoring restoring force for the slow cold-start regime.
|
||||
///
|
||||
/// Holds a round-USD shape template (`profile`), re-estimated each block from the
|
||||
/// arm vector at the pick, and adds a per-candidate score pulling the search
|
||||
/// toward the octave whose payment shape looks real. This lets the slow EMA
|
||||
/// resist round-USD octave aliasing in the thin pre-2018 output mix.
|
||||
///
|
||||
/// A zero `weight` makes it inert ([`score`](Self::score) returns 0,
|
||||
/// [`update`](Self::update) is a no-op), so the fast regime carries it for free
|
||||
/// without call sites special-casing the disabled path.
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct ShapeAnchor {
|
||||
weight: f64,
|
||||
/// Seeded flat (every arm equal). The slow EMA learns the real payment shape
|
||||
/// within a few hundred blocks, so no hand-tuned starting guess is needed.
|
||||
profile: [f64; N_ARMS],
|
||||
}
|
||||
|
||||
impl ShapeAnchor {
|
||||
pub(crate) fn new(weight: f64) -> Self {
|
||||
Self {
|
||||
weight,
|
||||
profile: [1.0 / N_ARMS as f64; N_ARMS],
|
||||
}
|
||||
}
|
||||
|
||||
/// Restoring-force contribution to a candidate bin's score: `weight` times the
|
||||
/// shape match against the learned profile. 0 when inert or the bin is empty.
|
||||
pub(crate) fn score(&self, ema: &HistogramEma, bin: i64) -> f64 {
|
||||
if self.weight == 0.0 {
|
||||
return 0.0;
|
||||
}
|
||||
self.weight * self.shape_match(ema, bin)
|
||||
}
|
||||
|
||||
/// Blend the L1-normalized arm shape at `pick` into the profile (slow EMA,
|
||||
/// [`SHAPE_BETA`]). No-op when inert or the pick is empty.
|
||||
pub(crate) fn update(&mut self, ema: &HistogramEma, pick: i64) {
|
||||
if self.weight == 0.0 {
|
||||
return;
|
||||
}
|
||||
if let Some(arms) = normalized_arms_at(ema, pick) {
|
||||
(0..N_ARMS).for_each(|i| {
|
||||
self.profile[i] = (1.0 - SHAPE_BETA) * self.profile[i] + SHAPE_BETA * arms[i];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Shape match `1 - L1distance` between the candidate's L1-normalized arm
|
||||
/// vector and the profile. 1.0 is an identical shape and it falls as mass
|
||||
/// shifts off the round-USD ladder. 0 for an empty (no-mass) center.
|
||||
fn shape_match(&self, ema: &HistogramEma, center: i64) -> f64 {
|
||||
match normalized_arms_at(ema, center) {
|
||||
Some(arms) => {
|
||||
1.0 - (0..N_ARMS)
|
||||
.map(|i| (arms[i] - self.profile[i]).abs())
|
||||
.sum::<f64>()
|
||||
}
|
||||
None => 0.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
use crate::{
|
||||
scale::{HistogramEma, NUM_BINS},
|
||||
shape::ShapeAnchor,
|
||||
};
|
||||
use std::ops::Range;
|
||||
|
||||
use crate::scale::{HistogramEma, NUM_BINS};
|
||||
|
||||
/// Bin offsets for 19 round-USD amounts relative to the $100 reference (offset 0).
|
||||
/// Each offset = log10(amount / 100) * BINS_PER_DECADE.
|
||||
@@ -28,7 +27,13 @@ const STENCIL_OFFSETS: [i32; 19] = [
|
||||
];
|
||||
|
||||
/// Number of round-USD stencil arms.
|
||||
pub(crate) const N_ARMS: usize = STENCIL_OFFSETS.len();
|
||||
const N_ARMS: usize = STENCIL_OFFSETS.len();
|
||||
type Arms = [f64; N_ARMS];
|
||||
|
||||
/// EMA rate for the adaptive shape template (~250-block time constant), slow
|
||||
/// enough that a transient octave slide can't corrupt the profile before the
|
||||
/// pick recovers.
|
||||
const SHAPE_BETA: f64 = 0.004;
|
||||
|
||||
/// EMA mass at `idx`, or 0.0 when the index falls outside the histogram.
|
||||
#[inline(always)]
|
||||
@@ -41,12 +46,12 @@ fn bin_value(ema: &HistogramEma, idx: i64) -> f64 {
|
||||
}
|
||||
|
||||
/// Raw EMA mass on each of the 19 stencil arms at `center`.
|
||||
fn arms_at(ema: &HistogramEma, center: i64) -> [f64; N_ARMS] {
|
||||
fn arms_at(ema: &HistogramEma, center: i64) -> Arms {
|
||||
STENCIL_OFFSETS.map(|offset| bin_value(ema, center + offset as i64))
|
||||
}
|
||||
|
||||
/// [`arms_at`] L1-normalized to sum 1, or `None` when the center carries no mass.
|
||||
pub(crate) fn normalized_arms_at(ema: &HistogramEma, center: i64) -> Option<[f64; N_ARMS]> {
|
||||
fn normalized_arms_at(ema: &HistogramEma, center: i64) -> Option<Arms> {
|
||||
let mut arms = arms_at(ema, center);
|
||||
let sum: f64 = arms.iter().sum();
|
||||
if sum <= 0.0 {
|
||||
@@ -58,72 +63,200 @@ pub(crate) fn normalized_arms_at(ema: &HistogramEma, center: i64) -> Option<[f64
|
||||
Some(arms)
|
||||
}
|
||||
|
||||
/// Round-dollar stencil picker.
|
||||
///
|
||||
/// Input: current EMA histogram, previous reference bin, and search bounds.
|
||||
/// Output: next reference bin. Internal state is only the adaptive shape profile
|
||||
/// used by the slow cold-start regime.
|
||||
#[derive(Clone)]
|
||||
pub(super) struct Stencil {
|
||||
shape: ShapeAnchor,
|
||||
}
|
||||
|
||||
impl Stencil {
|
||||
pub(super) fn new(shape_weight: f64) -> Self {
|
||||
Self {
|
||||
shape: ShapeAnchor::new(shape_weight),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn pick(
|
||||
&mut self,
|
||||
ema: &HistogramEma,
|
||||
prev_bin: f64,
|
||||
search_below: usize,
|
||||
search_above: usize,
|
||||
) -> f64 {
|
||||
let ref_bin = find_best_bin(ema, prev_bin, search_below, search_above, &self.shape);
|
||||
self.shape.update(ema, ref_bin.round() as i64);
|
||||
ref_bin
|
||||
}
|
||||
}
|
||||
|
||||
/// Adaptive shape-anchoring restoring force for the slow cold-start regime.
|
||||
///
|
||||
/// Holds a round-USD shape template (`profile`), re-estimated each block from the
|
||||
/// arm vector at the pick, and adds a per-candidate score pulling the search
|
||||
/// toward the octave whose payment shape looks real. This lets the slow EMA
|
||||
/// resist round-USD octave aliasing in the thin pre-2018 output mix.
|
||||
///
|
||||
/// A zero `weight` makes it inert ([`score`](Self::score) returns 0,
|
||||
/// [`update`](Self::update) is a no-op), so the fast regime carries it for free
|
||||
/// without call sites special-casing the disabled path.
|
||||
#[derive(Clone)]
|
||||
struct ShapeAnchor {
|
||||
weight: f64,
|
||||
/// Seeded flat (every arm equal). The slow EMA learns the real payment shape
|
||||
/// within a few hundred blocks, so no hand-tuned starting guess is needed.
|
||||
profile: Arms,
|
||||
}
|
||||
|
||||
impl ShapeAnchor {
|
||||
fn new(weight: f64) -> Self {
|
||||
Self {
|
||||
weight,
|
||||
profile: [1.0 / N_ARMS as f64; N_ARMS],
|
||||
}
|
||||
}
|
||||
|
||||
/// Restoring-force contribution to a candidate bin's score: `weight` times the
|
||||
/// shape match against the learned profile. 0 when inert or the bin is empty.
|
||||
fn score(&self, ema: &HistogramEma, bin: i64) -> f64 {
|
||||
if self.weight == 0.0 {
|
||||
return 0.0;
|
||||
}
|
||||
self.weight * self.shape_match(ema, bin)
|
||||
}
|
||||
|
||||
/// Blend the L1-normalized arm shape at `pick` into the profile (slow EMA,
|
||||
/// [`SHAPE_BETA`]). No-op when inert or the pick is empty.
|
||||
fn update(&mut self, ema: &HistogramEma, pick: i64) {
|
||||
if self.weight == 0.0 {
|
||||
return;
|
||||
}
|
||||
if let Some(arms) = normalized_arms_at(ema, pick) {
|
||||
(0..N_ARMS).for_each(|i| {
|
||||
self.profile[i] = (1.0 - SHAPE_BETA) * self.profile[i] + SHAPE_BETA * arms[i];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Shape match `1 - L1distance` between the candidate's L1-normalized arm
|
||||
/// vector and the profile. 1.0 is an identical shape and it falls as mass
|
||||
/// shifts off the round-USD ladder. 0 for an empty (no-mass) center.
|
||||
fn shape_match(&self, ema: &HistogramEma, center: i64) -> f64 {
|
||||
match normalized_arms_at(ema, center) {
|
||||
Some(arms) => {
|
||||
1.0 - (0..N_ARMS)
|
||||
.map(|i| (arms[i] - self.profile[i]).abs())
|
||||
.sum::<f64>()
|
||||
}
|
||||
None => 0.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct CandidateScorer<'a> {
|
||||
ema: &'a HistogramEma,
|
||||
shape: &'a ShapeAnchor,
|
||||
range: Range<usize>,
|
||||
arm_peaks: Arms,
|
||||
}
|
||||
|
||||
impl<'a> CandidateScorer<'a> {
|
||||
fn new(ema: &'a HistogramEma, shape: &'a ShapeAnchor, range: Range<usize>) -> Self {
|
||||
Self {
|
||||
ema,
|
||||
shape,
|
||||
arm_peaks: arm_peaks(ema, range.clone()),
|
||||
range,
|
||||
}
|
||||
}
|
||||
|
||||
fn score(&self, bin: usize) -> f64 {
|
||||
let mut total = 0.0;
|
||||
for (i, &offset) in STENCIL_OFFSETS.iter().enumerate() {
|
||||
if self.arm_peaks[i] > 0.0 {
|
||||
total += bin_value(self.ema, bin as i64 + offset as i64) / self.arm_peaks[i];
|
||||
}
|
||||
}
|
||||
total + self.shape.score(self.ema, bin as i64)
|
||||
}
|
||||
|
||||
fn best_bin(&self) -> (usize, f64) {
|
||||
let mut bins = self.range.clone();
|
||||
let mut best_bin = bins.next().expect("candidate range must not be empty");
|
||||
let mut best_score = self.score(best_bin);
|
||||
|
||||
for bin in bins {
|
||||
let candidate = self.score(bin);
|
||||
if candidate > best_score {
|
||||
best_score = candidate;
|
||||
best_bin = bin;
|
||||
}
|
||||
}
|
||||
|
||||
(best_bin, best_score)
|
||||
}
|
||||
|
||||
/// Parabolic sub-bin interpolation for fractional precision.
|
||||
fn interpolated_bin(&self, best_bin: usize, best_score: f64) -> f64 {
|
||||
let score_center = best_score;
|
||||
let score_left = if best_bin > self.range.start {
|
||||
self.score(best_bin - 1)
|
||||
} else {
|
||||
score_center
|
||||
};
|
||||
let score_right = if best_bin + 1 < self.range.end {
|
||||
self.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
|
||||
}
|
||||
}
|
||||
|
||||
fn search_range(prev_bin: f64, search_below: usize, search_above: usize) -> Option<Range<usize>> {
|
||||
let center = prev_bin.round() as usize;
|
||||
let search_start = center.saturating_sub(search_below);
|
||||
let search_end = (center + search_above + 1).min(NUM_BINS);
|
||||
|
||||
(search_start < search_end).then_some(search_start..search_end)
|
||||
}
|
||||
|
||||
fn arm_peaks(ema: &HistogramEma, range: Range<usize>) -> Arms {
|
||||
let mut peaks = [0.0f64; N_ARMS];
|
||||
for (i, &offset) in STENCIL_OFFSETS.iter().enumerate() {
|
||||
for bin in range.clone() {
|
||||
peaks[i] = peaks[i].max(bin_value(ema, bin as i64 + offset as i64));
|
||||
}
|
||||
}
|
||||
peaks
|
||||
}
|
||||
|
||||
/// Scores each candidate bin in the search window by summing normalized stencil
|
||||
/// matches across the EMA histogram, then refines with parabolic interpolation.
|
||||
/// Each candidate also picks up `shape`'s shape-anchoring restoring force, which
|
||||
/// is inert (adds 0) outside the slow cold-start regime.
|
||||
pub(crate) fn find_best_bin(
|
||||
fn find_best_bin(
|
||||
ema: &HistogramEma,
|
||||
prev_bin: f64,
|
||||
search_below: usize,
|
||||
search_above: usize,
|
||||
shape: &ShapeAnchor,
|
||||
) -> f64 {
|
||||
let center = prev_bin.round() as usize;
|
||||
let search_start = center.saturating_sub(search_below);
|
||||
let search_end = (center + search_above + 1).min(NUM_BINS);
|
||||
|
||||
if search_start >= search_end {
|
||||
let Some(range) = search_range(prev_bin, search_below, search_above) else {
|
||||
return prev_bin;
|
||||
}
|
||||
|
||||
// Per-offset peak within the search window (for normalization).
|
||||
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(bin_value(ema, 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 += bin_value(ema, bin as i64 + offset as i64) / arm_peaks[i];
|
||||
}
|
||||
}
|
||||
total += shape.score(ema, bin as i64);
|
||||
total
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// Parabolic sub-bin interpolation for fractional precision.
|
||||
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
|
||||
let scorer = CandidateScorer::new(ema, shape, range);
|
||||
let (best_bin, best_score) = scorer.best_bin();
|
||||
scorer.interpolated_bin(best_bin, best_score)
|
||||
}
|
||||
|
||||
@@ -1,279 +0,0 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use bitcoin::{Network, PublicKey, ScriptBuf};
|
||||
use brk_error::{Error, OptionData, Result};
|
||||
use brk_types::{
|
||||
Addr, AddrBytes, AddrChainStats, AddrHash, AddrIndexOutPoint, AddrIndexTxIndex, AddrStats,
|
||||
AnyAddrDataIndexEnum, Dollars, Height, OutputType, Transaction, TxIndex, TxStatus, Txid,
|
||||
TypeIndex, Unit, Utxo, Vout,
|
||||
};
|
||||
use vecdb::VecIndex;
|
||||
|
||||
use crate::Query;
|
||||
|
||||
impl Query {
|
||||
pub fn addr(&self, addr: Addr) -> Result<AddrStats> {
|
||||
let computer = self.computer();
|
||||
|
||||
let script = if let Ok(addr) = bitcoin::Address::from_str(&addr) {
|
||||
if !addr.is_valid_for_network(Network::Bitcoin) {
|
||||
return Err(Error::InvalidNetwork);
|
||||
}
|
||||
let addr = addr.assume_checked();
|
||||
addr.script_pubkey()
|
||||
} else if let Ok(pubkey) = PublicKey::from_str(&addr) {
|
||||
ScriptBuf::new_p2pk(&pubkey)
|
||||
} else {
|
||||
return Err(Error::InvalidAddr);
|
||||
};
|
||||
|
||||
let output_type = OutputType::from(&script);
|
||||
let Ok(bytes) = AddrBytes::try_from((&script, output_type)) else {
|
||||
return Err(Error::InvalidAddr);
|
||||
};
|
||||
let hash = AddrHash::from(&bytes);
|
||||
let type_index = self.type_index_for(output_type, &hash)?;
|
||||
|
||||
if type_index >= self.safe_lengths().to_type_index(output_type) {
|
||||
return Err(Error::UnknownAddr);
|
||||
}
|
||||
|
||||
let any_addr_index = computer
|
||||
.distribution
|
||||
.any_addr_indexes
|
||||
.get_once(output_type, type_index)?;
|
||||
|
||||
let (addr_data, realized_price) = match any_addr_index.to_enum() {
|
||||
AnyAddrDataIndexEnum::Funded(index) => {
|
||||
let data = computer
|
||||
.distribution
|
||||
.addrs_data
|
||||
.funded
|
||||
.reader()
|
||||
.get(usize::from(index));
|
||||
let price = data.realized_price().to_dollars();
|
||||
(data, price)
|
||||
}
|
||||
AnyAddrDataIndexEnum::Empty(index) => {
|
||||
let data = computer
|
||||
.distribution
|
||||
.addrs_data
|
||||
.empty
|
||||
.reader()
|
||||
.get(usize::from(index))
|
||||
.into();
|
||||
(data, Dollars::default())
|
||||
}
|
||||
};
|
||||
|
||||
Ok(AddrStats {
|
||||
addr,
|
||||
addr_type: output_type,
|
||||
chain_stats: AddrChainStats {
|
||||
type_index,
|
||||
funded_txo_count: addr_data.funded_txo_count,
|
||||
funded_txo_sum: addr_data.received,
|
||||
spent_txo_count: addr_data.spent_txo_count,
|
||||
spent_txo_sum: addr_data.sent,
|
||||
tx_count: addr_data.tx_count,
|
||||
realized_price,
|
||||
},
|
||||
mempool_stats: self
|
||||
.mempool()
|
||||
.and_then(|m| m.addr_stats(&bytes))
|
||||
.unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn addr_txs_chain(
|
||||
&self,
|
||||
addr: &Addr,
|
||||
after_txid: Option<Txid>,
|
||||
limit: usize,
|
||||
) -> Result<Vec<Transaction>> {
|
||||
let txindices = self.addr_txindices(addr, after_txid, limit)?;
|
||||
self.transactions_by_indices(&txindices)
|
||||
}
|
||||
|
||||
pub fn addr_txids(
|
||||
&self,
|
||||
addr: Addr,
|
||||
after_txid: Option<Txid>,
|
||||
limit: usize,
|
||||
) -> Result<Vec<Txid>> {
|
||||
let txindices = self.addr_txindices(&addr, after_txid, limit)?;
|
||||
let txid_reader = self.indexer().vecs.transactions.txid.reader();
|
||||
Ok(txindices
|
||||
.into_iter()
|
||||
.map(|tx_index| txid_reader.get(tx_index.to_usize()))
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn addr_txindices(
|
||||
&self,
|
||||
addr: &Addr,
|
||||
after_txid: Option<Txid>,
|
||||
limit: usize,
|
||||
) -> Result<Vec<TxIndex>> {
|
||||
let stores = &self.indexer().stores;
|
||||
|
||||
let (output_type, type_index) = self.resolve_addr(addr)?;
|
||||
|
||||
let store = stores
|
||||
.addr_type_to_addr_index_and_tx_index
|
||||
.get(output_type)
|
||||
.data()?;
|
||||
|
||||
let tx_index_len = self.safe_lengths().tx_index;
|
||||
|
||||
if let Some(after_txid) = after_txid {
|
||||
let after_tx_index = self.resolve_tx_index(&after_txid)?;
|
||||
let min = AddrIndexTxIndex::min_for_addr(type_index);
|
||||
let cursor = AddrIndexTxIndex::from((type_index, after_tx_index));
|
||||
Ok(store
|
||||
.range(min..cursor)
|
||||
.rev()
|
||||
.map(|(key, _): (AddrIndexTxIndex, Unit)| key.tx_index())
|
||||
.filter(|tx_index| *tx_index < tx_index_len)
|
||||
.take(limit)
|
||||
.collect())
|
||||
} else {
|
||||
Ok(store
|
||||
.prefix(type_index)
|
||||
.rev()
|
||||
.map(|(key, _): (AddrIndexTxIndex, Unit)| key.tx_index())
|
||||
.filter(|tx_index| *tx_index < tx_index_len)
|
||||
.take(limit)
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn addr_utxos(&self, addr: Addr, max_utxos: usize) -> Result<Vec<Utxo>> {
|
||||
let indexer = self.indexer();
|
||||
let stores = &indexer.stores;
|
||||
let vecs = &indexer.vecs;
|
||||
|
||||
let (output_type, type_index) = self.resolve_addr(&addr)?;
|
||||
|
||||
let store = stores
|
||||
.addr_type_to_addr_index_and_unspent_outpoint
|
||||
.get(output_type)
|
||||
.data()?;
|
||||
|
||||
let tx_index_len = self.safe_lengths().tx_index;
|
||||
let outpoints: Vec<(TxIndex, Vout)> = store
|
||||
.prefix(type_index)
|
||||
.map(|(key, _): (AddrIndexOutPoint, Unit)| (key.tx_index(), key.vout()))
|
||||
.filter(|(tx_index, _)| *tx_index < tx_index_len)
|
||||
.take(max_utxos + 1)
|
||||
.collect();
|
||||
if outpoints.len() > max_utxos {
|
||||
return Err(Error::TooManyUtxos);
|
||||
}
|
||||
|
||||
let txid_reader = vecs.transactions.txid.reader();
|
||||
let first_txout_index_reader = vecs.transactions.first_txout_index.reader();
|
||||
let value_reader = vecs.outputs.value.reader();
|
||||
|
||||
let mut cached_status: Option<(Height, TxStatus)> = None;
|
||||
let mut utxos = Vec::with_capacity(outpoints.len());
|
||||
|
||||
for (tx_index, vout) in outpoints {
|
||||
let txid = txid_reader.get(tx_index.to_usize());
|
||||
let first_txout_index = first_txout_index_reader.get(tx_index.to_usize());
|
||||
let value = value_reader.get(usize::from(first_txout_index + vout));
|
||||
|
||||
let height = self.confirmed_status_height(tx_index)?;
|
||||
let status = if let Some((h, ref s)) = cached_status
|
||||
&& h == height
|
||||
{
|
||||
s.clone()
|
||||
} else {
|
||||
let s = self.confirmed_status_at(height)?;
|
||||
cached_status = Some((height, s.clone()));
|
||||
s
|
||||
};
|
||||
|
||||
utxos.push(Utxo {
|
||||
txid,
|
||||
vout,
|
||||
status,
|
||||
value,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(utxos)
|
||||
}
|
||||
|
||||
pub fn addr_mempool_hash(&self, addr: &Addr) -> Option<u64> {
|
||||
let mempool = self.mempool()?;
|
||||
let bytes = AddrBytes::from_str(addr).ok()?;
|
||||
mempool.addr_state_hash(&bytes)
|
||||
}
|
||||
|
||||
pub fn addr_mempool_txs(&self, addr: &Addr, limit: usize) -> Result<Vec<Transaction>> {
|
||||
let bytes = AddrBytes::from_str(addr)?;
|
||||
let mempool = self.mempool().ok_or(Error::MempoolNotAvailable)?;
|
||||
Ok(mempool.addr_txs(&bytes, limit))
|
||||
}
|
||||
|
||||
/// Height of the last on-chain activity for an address (last tx_index to height).
|
||||
/// With `before_txid`, returns the newest activity strictly older than that
|
||||
/// cursor. Used by paginated chain etags so a new tx above the cursor
|
||||
/// doesn't invalidate deeper pages.
|
||||
pub fn addr_last_activity_height(
|
||||
&self,
|
||||
addr: &Addr,
|
||||
before_txid: Option<&Txid>,
|
||||
) -> Result<Height> {
|
||||
let (output_type, type_index) = self.resolve_addr(addr)?;
|
||||
let store = self
|
||||
.indexer()
|
||||
.stores
|
||||
.addr_type_to_addr_index_and_tx_index
|
||||
.get(output_type)
|
||||
.data()?;
|
||||
let tx_index_len = self.safe_lengths().tx_index;
|
||||
let last_tx_index = match before_txid {
|
||||
Some(txid) => {
|
||||
let before_tx_index = self.resolve_tx_index(txid)?;
|
||||
let min = AddrIndexTxIndex::min_for_addr(type_index);
|
||||
let cursor = AddrIndexTxIndex::from((type_index, before_tx_index));
|
||||
store
|
||||
.range(min..cursor)
|
||||
.rev()
|
||||
.map(|(key, _): (AddrIndexTxIndex, Unit)| key.tx_index())
|
||||
.find(|tx_index| *tx_index < tx_index_len)
|
||||
.ok_or(Error::UnknownAddr)?
|
||||
}
|
||||
None => store
|
||||
.prefix(type_index)
|
||||
.rev()
|
||||
.map(|(key, _): (AddrIndexTxIndex, Unit)| key.tx_index())
|
||||
.find(|tx_index| *tx_index < tx_index_len)
|
||||
.ok_or(Error::UnknownAddr)?,
|
||||
};
|
||||
self.confirmed_status_height(last_tx_index)
|
||||
}
|
||||
|
||||
fn resolve_addr(&self, addr: &Addr) -> Result<(OutputType, TypeIndex)> {
|
||||
let bytes = AddrBytes::from_str(addr)?;
|
||||
let output_type = OutputType::from(&bytes);
|
||||
let hash = AddrHash::from(&bytes);
|
||||
let type_index = self.type_index_for(output_type, &hash)?;
|
||||
Ok((output_type, type_index))
|
||||
}
|
||||
|
||||
/// Lookup the per-type index of an address by `(output_type, hash)`.
|
||||
/// Returns `UnknownAddr` if the hash is absent from the type's index.
|
||||
fn type_index_for(&self, output_type: OutputType, hash: &AddrHash) -> Result<TypeIndex> {
|
||||
self.indexer()
|
||||
.stores
|
||||
.addr_type_to_addr_hash_to_addr_index
|
||||
.get(output_type)
|
||||
.data()?
|
||||
.get(hash)?
|
||||
.map(|cow| cow.into_owned())
|
||||
.ok_or(Error::UnknownAddr)
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user