From 66037c862f005fdb6ebe21a255a3fe26166ad1dc Mon Sep 17 00:00:00 2001 From: nym21 Date: Mon, 25 May 2026 16:44:09 +0200 Subject: [PATCH] global: added support for oracle histograms --- crates/brk_client/src/lib.rs | 26 +-- .../src/distribution/compute/recover.rs | 79 +++++---- crates/brk_computer/src/distribution/vecs.rs | 11 +- crates/brk_mempool/src/api/histogram.rs | 22 ++- .../brk_mempool/src/stores/live_histograms.rs | 61 +++++++ crates/brk_mempool/src/stores/mod.rs | 4 +- crates/brk_mempool/src/stores/output_bins.rs | 26 --- crates/brk_mempool/src/stores/tx_store.rs | 80 ++++++--- crates/brk_oracle/examples/report_from.rs | 59 ++++--- crates/brk_query/src/impl/oracle.rs | 154 +++++++++++------- crates/brk_server/src/api/openapi/mod.rs | 7 +- crates/brk_server/src/api/oracle.rs | 87 +++++++--- crates/brk_server/src/api/series.rs | 7 +- .../src/params/height_or_date_param.rs | 38 +++++ crates/brk_server/src/params/mod.rs | 2 + crates/brk_types/src/histogram.rs | 7 + modules/brk-client/index.js | 52 +++--- packages/brk_client/brk_client/__init__.py | 36 ++-- 18 files changed, 493 insertions(+), 265 deletions(-) create mode 100644 crates/brk_mempool/src/stores/live_histograms.rs delete mode 100644 crates/brk_mempool/src/stores/output_bins.rs create mode 100644 crates/brk_server/src/params/height_or_date_param.rs diff --git a/crates/brk_client/src/lib.rs b/crates/brk_client/src/lib.rs index 74b501622..96f5c73e9 100644 --- a/crates/brk_client/src/lib.rs +++ b/crates/brk_client/src/lib.rs @@ -9870,35 +9870,35 @@ impl BrkClient { /// Smoothed round-dollar payment histogram at the live tip: the committed EMA with the forming mempool block blended in. A flat array of log-scale bins. /// /// Endpoint: `GET /api/oracle/histogram/ema/live` - pub fn get_oracle_histogram_ema_live(&self) -> Result { + pub fn get_oracle_histogram_ema_live(&self) -> Result> { self.base.get_json(&format!("/api/oracle/histogram/ema/live")) } - /// EMA histogram at height + /// EMA histogram at height or day /// - /// Smoothed round-dollar payment histogram for a confirmed height. A flat array of log-scale bins. + /// Smoothed round-dollar payment histogram for a confirmed point: a block height (`840000`) gives that block's EMA, a calendar date (`YYYY-MM-DD`) gives the average of that day's per-block EMAs. A flat array of log-scale bins. /// - /// Endpoint: `GET /api/oracle/histogram/ema/{height}` - pub fn get_oracle_histogram_ema(&self, height: Height) -> Result { - self.base.get_json(&format!("/api/oracle/histogram/ema/{height}")) + /// Endpoint: `GET /api/oracle/histogram/ema/{point}` + pub fn get_oracle_histogram_ema(&self, point: &str) -> Result> { + self.base.get_json(&format!("/api/oracle/histogram/ema/{point}")) } /// Live raw histogram /// - /// Un-smoothed per-block round-dollar counts for the forming mempool block. A flat array of log-scale bins, all zero when no mempool is configured. + /// Unfiltered output histogram for the forming mempool block: every live output binned by value, with none of the round-dollar payment filters applied. A flat array of log-scale bins, all zero when no mempool is configured. /// /// Endpoint: `GET /api/oracle/histogram/raw/live` - pub fn get_oracle_histogram_raw_live(&self) -> Result { + pub fn get_oracle_histogram_raw_live(&self) -> Result> { self.base.get_json(&format!("/api/oracle/histogram/raw/live")) } - /// Raw histogram at height + /// Raw histogram at height or day /// - /// Un-smoothed round-dollar counts for a single confirmed block. A flat array of log-scale bins. + /// Unfiltered output histogram for a confirmed point: a block height (`840000`) gives that block's outputs, coinbase included, binned by value with no payment filtering; a calendar date (`YYYY-MM-DD`) sums every block that day. A flat array of log-scale bins. /// - /// Endpoint: `GET /api/oracle/histogram/raw/{height}` - pub fn get_oracle_histogram_raw(&self, height: Height) -> Result { - self.base.get_json(&format!("/api/oracle/histogram/raw/{height}")) + /// Endpoint: `GET /api/oracle/histogram/raw/{point}` + pub fn get_oracle_histogram_raw(&self, point: &str) -> Result> { + self.base.get_json(&format!("/api/oracle/histogram/raw/{point}")) } /// Txid by index diff --git a/crates/brk_computer/src/distribution/compute/recover.rs b/crates/brk_computer/src/distribution/compute/recover.rs index b199c876a..f6f683671 100644 --- a/crates/brk_computer/src/distribution/compute/recover.rs +++ b/crates/brk_computer/src/distribution/compute/recover.rs @@ -24,51 +24,60 @@ pub struct RecoveredState { /// Returns Height::ZERO if any validation fails (triggers fresh start). pub(crate) fn recover_state( height: Height, - chain_state_rollback: vecdb::Result, + chain_state_rollback: Option>, any_addr_indexes: &mut AnyAddrIndexesVecs, addrs_data: &mut AddrsDataVecs, utxo_cohorts: &mut UTXOCohorts, addr_cohorts: &mut AddrCohorts, ) -> Result { - let stamp = Stamp::from(height); + // `None`: clean resume, already at the checkpoint, nothing to undo. + // `Some`: reorg, undo state past the resume point. + let consistent_height = match chain_state_rollback { + None => height, + Some(chain_state_rollback) => { + let stamp = Stamp::from(height); - // Rollback address state vectors - let addr_indexes_rollback = any_addr_indexes.rollback_before(stamp); - let addr_data_rollback = addrs_data.rollback_before(stamp); + // Rollback address state vectors + let addr_indexes_rollback = any_addr_indexes.rollback_before(stamp); + let addr_data_rollback = addrs_data.rollback_before(stamp); - // Verify rollback consistency - all must agree on the same height - let consistent_height = rollback_states( - chain_state_rollback, - addr_indexes_rollback, - addr_data_rollback, - ); + // Verify rollback consistency - all must agree on the same height + let consistent_height = rollback_states( + chain_state_rollback, + addr_indexes_rollback, + addr_data_rollback, + ); - // If rollbacks are inconsistent, start fresh - if consistent_height.is_zero() { - warn!("Rollback consistency check failed: inconsistent heights"); - return Ok(RecoveredState { - starting_height: Height::ZERO, - }); - } + // If rollbacks are inconsistent, start fresh + if consistent_height.is_zero() { + warn!("Rollback consistency check failed: inconsistent heights"); + return Ok(RecoveredState { + starting_height: Height::ZERO, + }); + } - // Rollback can land at an earlier height (multi-block change file), which is fine. - // But if it lands AHEAD of target, that means rollback failed (missing change files). - if consistent_height > height { - warn!( - "Rollback failed: still at {} but target was {}, falling back to fresh start", - consistent_height, height - ); - return Ok(RecoveredState { - starting_height: Height::ZERO, - }); - } + // Rollback can land at an earlier height (multi-block change file), which is fine. + // But if it lands AHEAD of target, that means rollback failed (missing change files). + if consistent_height > height { + warn!( + "Rollback failed: still at {} but target was {}, falling back to fresh start", + consistent_height, height + ); + return Ok(RecoveredState { + starting_height: Height::ZERO, + }); + } - if consistent_height != height { - debug!( - "Rollback landed at {} instead of {}, will resume from there", - consistent_height, height - ); - } + if consistent_height != height { + debug!( + "Rollback landed at {} instead of {}, will resume from there", + consistent_height, height + ); + } + + consistent_height + } + }; // Import UTXO cohort states - all must succeed debug!( diff --git a/crates/brk_computer/src/distribution/vecs.rs b/crates/brk_computer/src/distribution/vecs.rs index ec64e1b0c..c87d158d9 100644 --- a/crates/brk_computer/src/distribution/vecs.rs +++ b/crates/brk_computer/src/distribution/vecs.rs @@ -341,12 +341,13 @@ impl Vecs { // Try to resume from checkpoint, fall back to fresh start if needed let recovered_height = match start_mode { StartMode::Resume(height) => { - let stamp = Stamp::from(height); + // Roll back only on a reorg. A clean resume has nothing to undo, and an + // interrupted run wrote no rollback metadata (periodic flushes use + // with_changes=false; only the final write creates the `changes/` dir), + // so `rollback_before` would fail with `NotFound`. + let chain_state_rollback = (height < current_height) + .then(|| self.supply_state.rollback_before(Stamp::from(height))); - // Rollback BytesVec state and capture results for validation - let chain_state_rollback = self.supply_state.rollback_before(stamp); - - // Validate all rollbacks and imports are consistent let recovered = recover_state( height, chain_state_rollback, diff --git a/crates/brk_mempool/src/api/histogram.rs b/crates/brk_mempool/src/api/histogram.rs index 2d610fcbf..f91ba8c4c 100644 --- a/crates/brk_mempool/src/api/histogram.rs +++ b/crates/brk_mempool/src/api/histogram.rs @@ -11,13 +11,21 @@ impl Mempool { self.read().info.clone() } - /// Snapshot of pre-bucketed oracle bins across all live mempool tx - /// outputs. The total is maintained incrementally by `TxStore` on - /// every insert/remove, so this hot path is `O(NUM_BINS)` regardless - /// of pool size. Used by `live_price` to blend the mempool into the - /// committed oracle without re-parsing scripts per request. + /// Snapshot of pre-bucketed round-dollar-eligible bins across all live + /// mempool tx outputs. Maintained incrementally by `TxStore` on every + /// insert/remove, so this hot path is `O(NUM_BINS)` regardless of pool + /// size. Used by `live_price` to blend the mempool into the committed + /// oracle without re-parsing scripts per request. #[must_use] - pub fn live_histogram(&self) -> HistogramRaw { - self.read().txs.live_histogram() + pub fn live_eligible_histogram(&self) -> HistogramRaw { + self.read().txs.live_eligible_histogram() + } + + /// Snapshot of the raw histogram: every live mempool output binned by + /// value with no payment filtering. Backs the `histogram/raw/live` + /// endpoint. + #[must_use] + pub fn live_raw_histogram(&self) -> HistogramRaw { + self.read().txs.live_raw_histogram() } } diff --git a/crates/brk_mempool/src/stores/live_histograms.rs b/crates/brk_mempool/src/stores/live_histograms.rs new file mode 100644 index 000000000..0c24cf260 --- /dev/null +++ b/crates/brk_mempool/src/stores/live_histograms.rs @@ -0,0 +1,61 @@ +use brk_oracle::{HistogramRaw, for_each_round_dollar_bin, sats_to_bin}; +use brk_types::Transaction; + +use crate::stores::tx_store::TxRecord; + +/// The two live per-bin histograms the pool maintains incrementally as txs +/// enter and leave: `eligible` applies the round-dollar payment filter (it +/// feeds the oracle blend), `raw` bins every output by value with no filtering. +/// Add and remove run through the same code so the two stay symmetric. +#[derive(Default)] +pub struct LiveHistograms { + eligible: HistogramRaw, + raw: HistogramRaw, +} + +impl LiveHistograms { + /// Fold a record's outputs into both histograms. + pub fn add(&mut self, record: &TxRecord) { + Self::eligible_bins(&record.tx, |bin| self.eligible[bin as usize] += 1); + for bin in Self::raw_bins(&record.tx) { + self.raw[bin] += 1; + } + } + + /// Reverse a previous `add` for the same record. + pub fn remove(&mut self, record: &TxRecord) { + Self::eligible_bins(&record.tx, |bin| self.eligible[bin as usize] -= 1); + for bin in Self::raw_bins(&record.tx) { + self.raw[bin] -= 1; + } + } + + /// Round-dollar-eligible bins, blended into the oracle by `live_price`. + pub fn eligible(&self) -> HistogramRaw { + self.eligible.clone() + } + + /// Every live output binned by value, no payment filtering. + pub fn raw(&self) -> HistogramRaw { + self.raw.clone() + } + + /// Round-dollar-eligible bins, applying the oracle payment filter. Calls + /// `emit(bin)` per eligible output. Deterministic over a tx's outputs, + /// which are never mutated after insert, so add and remove recompute it + /// identically rather than caching. Live mempool txs are post-tip, always + /// above the historical max-outputs cap window, so the cap never applies. + 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, + ); + } + + /// Raw bin index per output, dropping only values outside the bin domain + /// (zero / out-of-range). + fn raw_bins(tx: &Transaction) -> impl Iterator + '_ { + tx.output.iter().filter_map(|o| sats_to_bin(o.value)) + } +} diff --git a/crates/brk_mempool/src/stores/mod.rs b/crates/brk_mempool/src/stores/mod.rs index 74d31a8fa..a54a34935 100644 --- a/crates/brk_mempool/src/stores/mod.rs +++ b/crates/brk_mempool/src/stores/mod.rs @@ -4,13 +4,13 @@ //! one lock-order discipline. mod addr_tracker; +mod live_histograms; mod outpoint_spends; -mod output_bins; mod tx_graveyard; mod tx_store; pub use addr_tracker::AddrTracker; +pub use live_histograms::LiveHistograms; pub use outpoint_spends::OutpointSpends; -pub use output_bins::OutputBins; pub use tx_graveyard::{TxGraveyard, TxTombstone}; pub use tx_store::TxStore; diff --git a/crates/brk_mempool/src/stores/output_bins.rs b/crates/brk_mempool/src/stores/output_bins.rs deleted file mode 100644 index 57171bcfa..000000000 --- a/crates/brk_mempool/src/stores/output_bins.rs +++ /dev/null @@ -1,26 +0,0 @@ -use brk_oracle::for_each_round_dollar_bin; -use brk_types::Transaction; -use smallvec::SmallVec; - -/// Pre-bucketed oracle bins for a tx's eligible outputs. Computed once on -/// insert so `Mempool::live_histogram` can bin all live outputs without -/// re-parsing scripts or recomputing eligibility per request. -pub struct OutputBins(SmallVec<[u16; 4]>); - -impl OutputBins { - pub fn from_tx(tx: &Transaction) -> Self { - let mut bins = SmallVec::new(); - // Live mempool txs are post-tip, always above the historical max-outputs - // cap window, so the cap never applies here. - for_each_round_dollar_bin( - usize::MAX, - tx.output.iter().map(|o| (o.value, o.type_())), - |bin| bins.push(bin), - ); - Self(bins) - } - - pub fn iter(&self) -> impl Iterator + '_ { - self.0.iter().copied() - } -} diff --git a/crates/brk_mempool/src/stores/tx_store.rs b/crates/brk_mempool/src/stores/tx_store.rs index 4f9f462f7..4745518e9 100644 --- a/crates/brk_mempool/src/stores/tx_store.rs +++ b/crates/brk_mempool/src/stores/tx_store.rs @@ -2,27 +2,20 @@ use brk_oracle::HistogramRaw; use brk_types::{MempoolRecentTx, Transaction, TxOut, Txid, TxidPrefix, Vin}; use rustc_hash::{FxHashMap, FxHashSet}; -use crate::{state::TxEntry, stores::OutputBins}; +use crate::{state::TxEntry, stores::LiveHistograms}; const RECENT_CAP: usize = 10; -/// Per-tx record: live tx body, its mempool entry, and the pre-bucketed -/// oracle bins for its outputs. Kept under one key so a single map probe -/// returns everything readers need. +/// Per-tx record: live tx body and its mempool entry, kept under one key +/// so a single map probe returns everything readers need. pub struct TxRecord { pub tx: Transaction, pub entry: TxEntry, - pub output_bins: OutputBins, } impl TxRecord { pub fn new(tx: Transaction, entry: TxEntry) -> Self { - let output_bins = OutputBins::from_tx(&tx); - Self { - tx, - entry, - output_bins, - } + Self { tx, entry } } } @@ -32,15 +25,15 @@ impl TxRecord { /// set of prefixes whose tx still has at least one `prevout: None`, /// maintained on every `insert` / `remove_by_prefix` / `apply_fills` /// so the post-update prevout filler can early-exit when empty. -/// `live_histogram` mirrors the union of each record's `OutputBins`, -/// kept in sync on `insert` / `remove_by_prefix` so the oracle-blend -/// read path is a single array clone, not a full pool walk. +/// `histograms` holds the eligible (oracle-blend) and raw per-bin output +/// histograms, kept in sync on `insert` / `remove_by_prefix` so each read +/// path is a single array clone, not a full pool walk. #[derive(Default)] pub struct TxStore { records: FxHashMap, recent: Vec, unresolved: FxHashSet, - live_histogram: HistogramRaw, + histograms: LiveHistograms, } impl TxStore { @@ -92,9 +85,7 @@ impl TxStore { self.unresolved.insert(prefix); } let record = TxRecord::new(tx, entry); - for bin in record.output_bins.iter() { - self.live_histogram[bin as usize] += 1; - } + self.histograms.add(&record); self.records.insert(prefix, record); } @@ -112,16 +103,21 @@ impl TxStore { pub fn remove_by_prefix(&mut self, prefix: &TxidPrefix) -> Option { let record = self.records.remove(prefix)?; self.unresolved.remove(prefix); - for bin in record.output_bins.iter() { - self.live_histogram[bin as usize] -= 1; - } + self.histograms.remove(&record); Some(record) } - /// Snapshot the live oracle-bin histogram. Maintained incrementally - /// on insert/remove, so this is `O(NUM_BINS)`, not `O(live_outputs)`. - pub fn live_histogram(&self) -> HistogramRaw { - self.live_histogram.clone() + /// Snapshot the round-dollar-eligible histogram that feeds the oracle + /// blend. Maintained incrementally, so this is `O(NUM_BINS)`, not + /// `O(live_outputs)`. + pub fn live_eligible_histogram(&self) -> HistogramRaw { + self.histograms.eligible() + } + + /// Snapshot the raw histogram: every live output binned by value with no + /// payment filtering. Maintained incrementally alongside the eligible one. + pub fn live_raw_histogram(&self) -> HistogramRaw { + self.histograms.raw() } /// Set of prefixes with at least one unfilled prevout. Used by the @@ -338,11 +334,41 @@ mod tests { store.insert(tx_a, entry_a); store.insert(tx_b, entry_b); - let total_after_both: u32 = store.live_histogram().iter().sum(); + let total_after_both: u32 = store.live_eligible_histogram().iter().sum(); assert_eq!(total_after_both, 3, "two outputs + one output"); store.remove_by_prefix(&prefix_a); - let total_after_remove: u32 = store.live_histogram().iter().sum(); + let total_after_remove: u32 = store.live_eligible_histogram().iter().sum(); assert_eq!(total_after_remove, 1); } + + #[test] + fn raw_histogram_bins_outputs_the_eligible_filter_drops() { + let mut store = TxStore::default(); + // 2_345 sats is a round-dollar-eligible payment; 100_000_000 sats (1 BTC) + // is a round-BTC value the eligible filter drops but raw still bins. + let tx = fake_tx( + 30, + &[Some(TxOut::from((p2wpkh_script(1), Sats::from(50_000u64))))], + &[(p2wpkh_script(2), 2_345), (p2wpkh_script(3), 100_000_000)], + ); + let entry = entry_for(&tx, 100, 100); + let prefix = entry.txid_prefix(); + store.insert(tx, entry); + + assert_eq!( + store.live_eligible_histogram().iter().sum::(), + 1, + "round-BTC output filtered out of the eligible histogram" + ); + assert_eq!( + store.live_raw_histogram().iter().sum::(), + 2, + "raw histogram bins every output" + ); + + store.remove_by_prefix(&prefix); + assert_eq!(store.live_eligible_histogram().iter().sum::(), 0); + assert_eq!(store.live_raw_histogram().iter().sum::(), 0); + } } diff --git a/crates/brk_oracle/examples/report_from.rs b/crates/brk_oracle/examples/report_from.rs index 9d3ae091d..bd229c4e7 100644 --- a/crates/brk_oracle/examples/report_from.rs +++ b/crates/brk_oracle/examples/report_from.rs @@ -253,6 +253,7 @@ fn arm_pattern(ema: &HistogramEma, center: i64, tau: f64) -> String { /// guard: if the half- or double-price bin lights up strictly more stencil arms /// and carries comparable mass, snap to it. This escapes a ½×/2× alias lock that /// the ±window can never climb the 60 bins out of on its own. +#[allow(clippy::too_many_arguments)] fn guarded_best_bin( ema: &HistogramEma, prev_bin: f64, @@ -340,10 +341,11 @@ fn guarded_best_bin( best = Some((n, qn, raw_n)); } } - if let Some((n, qn, _)) = best { - if qn >= qb + guard.q_margin && qn >= guard.q_min { - target = n; - } + if let Some((n, qn, _)) = best + && qn >= qb + guard.q_margin + && qn >= guard.q_min + { + target = n; } } else { let mut best: Option<(usize, f64)> = None; @@ -587,12 +589,18 @@ fn main() { .and_then(|l| l.parse().ok()) .unwrap_or_else(|| { let o = height_ohlc.get(start - 1).copied().unwrap_or([0.0; 4]); - if o[3] > 0.0 { o[3] } else { (o[1] + o[2]) / 2.0 } + if o[3] > 0.0 { + o[3] + } else { + (o[1] + o[2]) / 2.0 + } }); // Exact seed override (reproduce the committed prices.txt seed at a start the // truncated working-tree prices.txt no longer covers). - let start_price = - std::env::var("SEED").ok().and_then(|s| s.parse().ok()).unwrap_or(start_price); + let start_price = std::env::var("SEED") + .ok() + .and_then(|s| s.parse().ok()) + .unwrap_or(start_price); let mut config = Config::default(); if let Some(w) = std::env::var("EMA_WINDOW") @@ -664,8 +672,7 @@ fn main() { // ARM_PROFILE. let profile_seed = std::env::var("PROFILE_SEED").ok(); let bootstrap_profile = profile_seed.as_deref() == Some("bootstrap"); - let uniform_profile = - matches!(profile_seed.as_deref(), Some("uniform") | Some("flat")); + let uniform_profile = matches!(profile_seed.as_deref(), Some("uniform") | Some("flat")); // Stencil-sum weight (default 1). Set 0 for SHAPE-ONLY scoring: the shape match // does both within-octave localization and octave discrimination, no stencil // term and no cw balance to tune. @@ -708,7 +715,9 @@ fn main() { for &i in &ALIAS_ARMS { arm_weights[i] = alias_weight; } - eprintln!(" disc_weight={disc_weight} on {DISC_ARMS:?}; alias_weight={alias_weight} on {ALIAS_ARMS:?}; corr_weight={corr_weight}"); + eprintln!( + " disc_weight={disc_weight} on {DISC_ARMS:?}; alias_weight={alias_weight} on {ALIAS_ARMS:?}; corr_weight={corr_weight}" + ); let anom_thresh: f64 = std::env::var("ANOM_THRESH") .ok() .and_then(|s| s.parse().ok()) @@ -765,7 +774,9 @@ fn main() { guard.global_radius, ); if switch_at != 0 { - eprintln!(" switch: at height {switch_at} -> window={switch_window} alpha={switch_alpha:.5}"); + eprintln!( + " switch: at height {switch_at} -> window={switch_window} alpha={switch_alpha:.5}" + ); } let (sb, sa) = (config.search_below, config.search_above); let mut window_size = config.window_size; @@ -821,9 +832,7 @@ fn main() { let mut sharp_cursor = 0usize; let mut sharp_filled = 0usize; let mut sharp_ema = HistogramEma::zeros(); - eprintln!( - " sharp: span={sharp_span:.0} window={sharp_window} alpha={sharp_alpha:.5}" - ); + eprintln!(" sharp: span={sharp_span:.0} window={sharp_window} alpha={sharp_alpha:.5}"); let total_txs = indexer.vecs.transactions.txid.len(); let total_outputs = indexer.vecs.outputs.value.len(); @@ -922,14 +931,14 @@ fn main() { filled += 1; } ema.fill(0.0); - for age in 0..filled { + (0..filled).for_each(|age| { let idx = (ring_cursor + window_size - 1 - age) % window_size; let w = weights[age]; let block = &ring[idx]; for b in 0..NUM_BINS { ema[b] += w * block[b]; } - } + }); // Sharp detection EMA (diagnostic only - does not drive the price). { let slot = &mut sharp_ring[sharp_cursor]; @@ -942,17 +951,27 @@ fn main() { sharp_filled += 1; } sharp_ema.fill(0.0); - for age in 0..sharp_filled { + (0..sharp_filled).for_each(|age| { let idx = (sharp_cursor + sharp_window - 1 - age) % sharp_window; let w = sharp_weights[age]; let block = &sharp_ring[idx]; for b in 0..NUM_BINS { sharp_ema[b] += w * block[b]; } - } + }); let cw = if h < corr_until { corr_weight } else { 0.0 }; - ref_bin = - guarded_best_bin(&ema, ref_bin, sb, sa, &guard, &arm_weights, cw, &profile, metric, stencil_weight); + ref_bin = guarded_best_bin( + &ema, + ref_bin, + sb, + sa, + &guard, + &arm_weights, + cw, + &profile, + metric, + stencil_weight, + ); let oracle_price = bin_to_cents(ref_bin) as f64 / 100.0; if verify_prod { diff --git a/crates/brk_query/src/impl/oracle.rs b/crates/brk_query/src/impl/oracle.rs index 369eaa808..535e5eeb6 100644 --- a/crates/brk_query/src/impl/oracle.rs +++ b/crates/brk_query/src/impl/oracle.rs @@ -1,13 +1,13 @@ -use std::sync::Arc; +use std::{ops::Range, sync::Arc}; use brk_computer::prices::Vecs as PricesVecs; use brk_error::{Error, Result}; use brk_indexer::Lengths; use brk_oracle::{ - Config, HistogramEmaCompact, HistogramRaw, Oracle, START_HEIGHT_SLOW, cents_to_bin, - for_each_round_dollar_bin, + Config, HistogramEma, HistogramEmaCompact, HistogramRaw, Oracle, START_HEIGHT_SLOW, + cents_to_bin, sats_to_bin, }; -use brk_types::{Dollars, OutputType, Sats, TxIndex, TxOutIndex}; +use brk_types::{Day1, Dollars, Sats, TxOutIndex}; use vecdb::{AnyVec, ReadableVec, VecIndex}; use crate::Query; @@ -27,31 +27,67 @@ impl Query { /// seed-independent, so the result is exact. pub fn confirmed_histogram_ema(&self, height: usize) -> Result { let safe = self.check_histogram_height(height)?; - let ref_bin = self.seed_bin_at(height)?; - Ok(self.warm_oracle(ref_bin, height + 1, &safe).ema().to_compact()) + Ok(self.ema_oracle_at(height, &safe)?.ema().to_compact()) } - /// Un-smoothed per-block round-dollar counts at the live tip: the mempool's - /// forming-block histogram, or zeros when no mempool is configured. + /// Smoothed EMA histogram for a calendar `day`: the bin-by-bin average of + /// every confirmed block's per-block EMA. Each block's EMA is reconstructed + /// independently (seed-independent, so exact); averaging keeps the result an + /// intensive per-block rate rather than letting a busy day dominate. + pub fn confirmed_histogram_ema_day(&self, day: Day1) -> Result { + let safe = self.safe_lengths(); + let range = self.day_block_range(day, &safe)?; + let count = range.len() as f64; + let mut acc = HistogramEma::zeros(); + for height in range { + let oracle = self.ema_oracle_at(height, &safe)?; + acc.iter_mut() + .zip(oracle.ema().iter()) + .for_each(|(a, &e)| *a += e); + } + acc.iter_mut().for_each(|a| *a /= count); + Ok(acc.to_compact()) + } + + /// Unfiltered per-bin output counts at the live tip: every forming-block + /// mempool output binned by value, with none of the round-dollar payment + /// filters applied. Zeros when no mempool is configured. pub fn live_histogram_raw(&self) -> Result { Ok(match self.mempool() { - Some(mempool) => mempool.live_histogram(), + Some(mempool) => mempool.live_raw_histogram(), None => HistogramRaw::zeros(), }) } - /// Un-smoothed per-block round-dollar counts for a confirmed `height`. + /// Unfiltered per-bin output counts for a confirmed `height`: every output + /// in the block binned by value, with no payment filtering. pub fn confirmed_histogram_raw(&self, height: usize) -> Result { let safe = self.check_histogram_height(height)?; Ok(self.block_raw_histogram(height, &safe)) } + /// Unfiltered per-bin output counts for a calendar `day`: every block's raw + /// histogram summed bin-by-bin. Raw counts are additive, so the day total is + /// just the sum across its confirmed blocks. + pub fn confirmed_histogram_raw_day(&self, day: Day1) -> Result { + let safe = self.safe_lengths(); + let range = self.day_block_range(day, &safe)?; + let mut acc = HistogramRaw::zeros(); + for height in range { + let block = self.block_raw_histogram(height, &safe); + acc.iter_mut() + .zip(block.iter()) + .for_each(|(a, &v)| *a += v); + } + Ok(acc) + } + /// The live tip oracle: the cached committed base, with the forming block's /// mempool outputs blended in as a final slot when a mempool is configured. fn live_oracle(&self) -> Result { let mut oracle = (*self.cached_oracle()?).clone(); if let Some(mempool) = self.mempool() { - oracle.process_histogram(&mempool.live_histogram()); + oracle.process_histogram(&mempool.live_eligible_histogram()); } Ok(oracle) } @@ -86,6 +122,14 @@ impl Query { Ok(oracle) } + /// Oracle warmed to just after `height`, ready for its per-block EMA. Seeds + /// from the stored spot price at `height`, though the EMA is seed-independent + /// so the seed only sets the price read-out, not the window contents. + fn ema_oracle_at(&self, height: usize, safe: &Lengths) -> Result { + let seed_bin = self.seed_bin_at(height)?; + Ok(self.warm_oracle(seed_bin, height + 1, safe)) + } + /// An oracle seeded at `seed_bin` and warmed by replaying the `window_size` /// committed blocks ending just before `end`. Reads are capped at `safe` so /// concurrent indexer writes past the cap stay invisible. @@ -133,72 +177,62 @@ impl Query { Ok(safe) } - /// One confirmed block's round-dollar histogram, built from batched columnar - /// reads and the shared `for_each_round_dollar_bin` filter. Kept separate from - /// the hot-path `feed_blocks` (cursor + reusable buffers over a block range). + /// The confirmed block heights `[first, end)` of calendar `day`, clamped to + /// the same histogram-available bound as `check_histogram_height`. 404 when + /// the day has no committed blocks in range. + fn day_block_range(&self, day: Day1, safe: &Lengths) -> Result> { + let first_height = &self.computer().indexes.day1.first_height; + let bound = self + .computer() + .prices + .spot + .cents + .height + .len() + .min(safe.height.to_usize()); + let start = first_height + .collect_one(day) + .map_or(usize::MAX, |h| h.to_usize()) + .max(START_HEIGHT_SLOW); + let end = first_height + .collect_one(day + 1) + .map_or(bound, |h| h.to_usize()) + .min(bound); + if start >= end { + return Err(Error::NotFound(format!( + "oracle histogram unavailable for day {day}" + ))); + } + Ok(start..end) + } + + /// One confirmed block's unfiltered histogram: every output in the block, + /// coinbase included, binned by value via `sats_to_bin` with no payment + /// filtering. Built from a single batched columnar read of the block's + /// output-value range. fn block_raw_histogram(&self, height: usize, safe: &Lengths) -> HistogramRaw { let indexer = self.indexer(); - let total_txs = safe.tx_index.to_usize(); let total_outputs = safe.txout_index.to_usize(); let next_height = (height + 2).min(safe.height.to_usize()); - let first_tx_indexes: Vec = indexer - .vecs - .transactions - .first_tx_index - .collect_range_at(height, next_height); let out_firsts: Vec = indexer .vecs .outputs .first_txout_index .collect_range_at(height, next_height); - - let block_first_tx = first_tx_indexes[0].to_usize() + 1; - let next_first_tx = first_tx_indexes - .get(1) - .copied() - .unwrap_or(TxIndex::from(total_txs)) - .to_usize(); - let tx_count = next_first_tx - block_first_tx; - - let mut hist = HistogramRaw::zeros(); - if tx_count == 0 { - return hist; - } - + let out_start = out_firsts[0].to_usize(); let out_end = out_firsts .get(1) .copied() .unwrap_or(TxOutIndex::from(total_outputs)) .to_usize(); - let tx_starts: Vec = indexer - .vecs - .transactions - .first_txout_index - .collect_range_at(block_first_tx, next_first_tx) - .into_iter() - .map(|t| t.to_usize()) - .collect(); - let out_start = tx_starts.first().copied().unwrap_or(out_end); + let mut hist = HistogramRaw::zeros(); let values: Vec = indexer.vecs.outputs.value.collect_range_at(out_start, out_end); - let output_types: Vec = indexer - .vecs - .outputs - .output_type - .collect_range_at(out_start, out_end); - - for tx in 0..tx_count { - let lo = tx_starts[tx] - out_start; - let hi = tx_starts - .get(tx + 1) - .map(|s| s - out_start) - .unwrap_or(out_end - out_start); - let outputs = values[lo..hi] - .iter() - .copied() - .zip(output_types[lo..hi].iter().copied()); - for_each_round_dollar_bin(height, outputs, |bin| hist.increment(bin as usize)); + for sats in values { + if let Some(bin) = sats_to_bin(sats) { + hist.increment(bin); + } } hist } diff --git a/crates/brk_server/src/api/openapi/mod.rs b/crates/brk_server/src/api/openapi/mod.rs index eb72b7575..4cef744ab 100644 --- a/crates/brk_server/src/api/openapi/mod.rs +++ b/crates/brk_server/src/api/openapi/mod.rs @@ -184,9 +184,10 @@ All errors return structured JSON with a consistent format: transaction outputs, with no external price feed. Payment activity is binned on a \ log scale, and a smoothed EMA over recent blocks locates the price.\n\n\ Histograms come in two flavors, each available at the live tip (mempool-blended) \ - or at any confirmed height: `raw` (per-block counts) and `ema` (the smoothed \ - window). The live price is also at `/api/mempool/price`. Confirmed per-height \ - price history is at `/api/vecs/height-to-price`." + or at any confirmed height: `raw` bins every output by value with no filtering, \ + while `ema` is the smoothed round-dollar window the price is read from. The live \ + price is also at `/api/mempool/price`. Confirmed per-height price history is at \ + `/api/vecs/height-to-price`." .to_string(), ), ..Default::default() diff --git a/crates/brk_server/src/api/oracle.rs b/crates/brk_server/src/api/oracle.rs index e41082d75..3294946de 100644 --- a/crates/brk_server/src/api/oracle.rs +++ b/crates/brk_server/src/api/oracle.rs @@ -2,14 +2,15 @@ use aide::axum::{ApiRouter, routing::get_with}; use axum::{ extract::{Path, State}, http::{HeaderMap, Uri}, + response::IntoResponse, }; use brk_oracle::{HistogramEmaCompact, HistogramRaw}; -use brk_types::{Dollars, Version}; +use brk_types::{Day1, Dollars, Version}; use crate::{ AppState, extended::TransformResponseExtended, - params::{Empty, HeightParam}, + params::{Empty, HeightOrDate, HeightOrDateParam}, }; pub trait OracleRoutes { @@ -67,26 +68,42 @@ impl OracleRoutes for ApiRouter { ), ) .api_route( - "/api/oracle/histogram/ema/{height}", + "/api/oracle/histogram/ema/{point}", get_with( async |uri: Uri, headers: HeaderMap, - Path(path): Path, + Path(path): Path, _: Empty, State(state): State| { - let strategy = state.height_strategy(Version::new(brk_oracle::VERSION), path.height); - state - .respond_json(&headers, strategy, &uri, move |q| { - q.confirmed_histogram_ema(usize::from(path.height)) - }) - .await + let version = Version::new(brk_oracle::VERSION); + match path.resolve() { + Ok(HeightOrDate::Date(date)) => { + let strategy = state.date_strategy(version, date); + state + .respond_json(&headers, strategy, &uri, move |q| { + q.confirmed_histogram_ema_day(Day1::try_from(date)?) + }) + .await + } + Ok(HeightOrDate::Height(height)) => { + let strategy = state.height_strategy(version, height); + state + .respond_json(&headers, strategy, &uri, move |q| { + q.confirmed_histogram_ema(usize::from(height)) + }) + .await + } + Err(e) => e.into_response(), + } }, |op| { op.id("get_oracle_histogram_ema") .oracle_tag() - .summary("EMA histogram at height") + .summary("EMA histogram at height or day") .description( - "Smoothed round-dollar payment histogram for a confirmed height. \ + "Smoothed round-dollar payment histogram for a confirmed point: a \ + block height (`840000`) gives that block's EMA, a calendar date \ + (`YYYY-MM-DD`) gives the average of that day's per-block EMAs. \ A flat array of log-scale bins.", ) .json_response::() @@ -112,9 +129,10 @@ impl OracleRoutes for ApiRouter { .oracle_tag() .summary("Live raw histogram") .description( - "Un-smoothed per-block round-dollar counts for the forming mempool \ - block. A flat array of log-scale bins, all zero when no mempool is \ - configured.", + "Unfiltered output histogram for the forming mempool block: every \ + live output binned by value, with none of the round-dollar payment \ + filters applied. A flat array of log-scale bins, all zero when no \ + mempool is configured.", ) .json_response::() .not_modified() @@ -123,27 +141,44 @@ impl OracleRoutes for ApiRouter { ), ) .api_route( - "/api/oracle/histogram/raw/{height}", + "/api/oracle/histogram/raw/{point}", get_with( async |uri: Uri, headers: HeaderMap, - Path(path): Path, + Path(path): Path, _: Empty, State(state): State| { - let strategy = state.height_strategy(Version::new(brk_oracle::VERSION), path.height); - state - .respond_json(&headers, strategy, &uri, move |q| { - q.confirmed_histogram_raw(usize::from(path.height)) - }) - .await + let version = Version::new(brk_oracle::VERSION); + + match path.resolve() { + Ok(HeightOrDate::Date(date)) => { + let strategy = state.date_strategy(version, date); + state + .respond_json(&headers, strategy, &uri, move |q| { + q.confirmed_histogram_raw_day(Day1::try_from(date)?) + }) + .await + } + Ok(HeightOrDate::Height(height)) => { + let strategy = state.height_strategy(version, height); + state + .respond_json(&headers, strategy, &uri, move |q| { + q.confirmed_histogram_raw(usize::from(height)) + }) + .await + } + Err(e) => e.into_response(), + } }, |op| { op.id("get_oracle_histogram_raw") .oracle_tag() - .summary("Raw histogram at height") + .summary("Raw histogram at height or day") .description( - "Un-smoothed round-dollar counts for a single confirmed block. A \ - flat array of log-scale bins.", + "Unfiltered output histogram for a confirmed point: a block height \ + (`840000`) gives that block's outputs, coinbase included, binned by \ + value with no payment filtering; a calendar date (`YYYY-MM-DD`) sums \ + every block that day. A flat array of log-scale bins.", ) .json_response::() .not_modified() diff --git a/crates/brk_server/src/api/series.rs b/crates/brk_server/src/api/series.rs index 876ab719a..62ecf38b5 100644 --- a/crates/brk_server/src/api/series.rs +++ b/crates/brk_server/src/api/series.rs @@ -16,7 +16,8 @@ use brk_query::{Query as BrkQuery, ResolvedQuery}; use brk_traversable::TreeNode; use brk_types::{ DataRangeFormat, Format, IndexInfo, Output, PaginatedSeries, Pagination, SearchQuery, - SeriesCount, SeriesData, SeriesInfo, SeriesNameWithIndex, SeriesSelection, + SeriesCount, SeriesData, SeriesInfo, SeriesNameWithIndex, SeriesOutput, SeriesSelection, + Version, }; use crate::{ @@ -67,7 +68,7 @@ pub(super) async fn serve( .await) } -fn output_to_bytes(out: brk_types::SeriesOutput) -> BrkResult { +fn output_to_bytes(out: SeriesOutput) -> BrkResult { Ok(match out.output { Output::CSV(s) => Bytes::from(s), Output::Json(v) => Bytes::from(v), @@ -365,7 +366,7 @@ impl ApiSeriesRoutes for ApiRouter { .series_tag() .summary("Get series version") .description("Returns the current version of a series. Changes when the series data is updated.") - .json_response::() + .json_response::() .not_modified() .not_found(), ), diff --git a/crates/brk_server/src/params/height_or_date_param.rs b/crates/brk_server/src/params/height_or_date_param.rs new file mode 100644 index 000000000..6283e6cfe --- /dev/null +++ b/crates/brk_server/src/params/height_or_date_param.rs @@ -0,0 +1,38 @@ +use brk_types::{Date, Height}; +use schemars::JsonSchema; +use serde::Deserialize; + +use crate::Error; + +/// Path parameter accepting either a block height (`840000`) or a calendar date +/// (`YYYY-MM-DD`). The handler resolves it and dispatches to the per-height or +/// per-day variant, choosing the matching cache strategy. +#[derive(Deserialize, JsonSchema)] +pub struct HeightOrDateParam { + #[schemars(example = &"840000")] + pub point: String, +} + +/// A resolved [`HeightOrDateParam`]: a confirmed block height or a calendar day. +pub enum HeightOrDate { + Height(Height), + Date(Date), +} + +impl HeightOrDateParam { + /// Parses the raw `point`: a `YYYY-MM-DD` string is a [`Date`], an all-digit + /// string is a [`Height`], anything else is a 400. Dates are tried first + /// because their dashes keep them from parsing as a height. + pub fn resolve(&self) -> Result { + if let Ok(date) = self.point.parse::() { + Ok(HeightOrDate::Date(date)) + } else if let Ok(height) = self.point.parse::() { + Ok(HeightOrDate::Height(Height::from(height))) + } else { + Err(Error::bad_request(format!( + "expected a block height or YYYY-MM-DD date, got `{}`", + self.point + ))) + } + } +} diff --git a/crates/brk_server/src/params/mod.rs b/crates/brk_server/src/params/mod.rs index 804109e75..f95b97c1b 100644 --- a/crates/brk_server/src/params/mod.rs +++ b/crates/brk_server/src/params/mod.rs @@ -5,6 +5,7 @@ mod blockhash_param; mod blockhash_start_index; mod blockhash_tx_index; mod empty; +mod height_or_date_param; mod height_param; mod next_block_hash_param; mod pool_slug_param; @@ -25,6 +26,7 @@ pub use blockhash_param::*; pub use blockhash_start_index::*; pub use blockhash_tx_index::*; pub use empty::*; +pub use height_or_date_param::*; pub use height_param::*; pub use next_block_hash_param::*; pub use pool_slug_param::*; diff --git a/crates/brk_types/src/histogram.rs b/crates/brk_types/src/histogram.rs index 89bf95638..ed9929444 100644 --- a/crates/brk_types/src/histogram.rs +++ b/crates/brk_types/src/histogram.rs @@ -112,4 +112,11 @@ impl JsonSchema for Histogram { fn json_schema(generator: &mut SchemaGenerator) -> schemars::Schema { Vec::::json_schema(generator) } + + /// Inline as a plain array rather than registering a named `Histogram_uintN` + /// component: the wire shape is just a flat array of counts, and the synthetic + /// generic-mangled name has no real type for the Rust client to resolve to. + fn inline_schema() -> bool { + true + } } diff --git a/modules/brk-client/index.js b/modules/brk-client/index.js index 5113c65bf..459daa3ef 100644 --- a/modules/brk-client/index.js +++ b/modules/brk-client/index.js @@ -646,6 +646,14 @@ ancestors and no descendants (matches mempool.space). * * @typedef {number} Height */ +/** + * Path parameter accepting either a block height (`840000`) or a calendar date + * (`YYYY-MM-DD`). The handler resolves it and dispatches to the per-height or + * per-day variant, choosing the matching cache strategy. + * + * @typedef {Object} HeightOrDateParam + * @property {string} point + */ /** * Block height path parameter * @@ -664,8 +672,6 @@ ancestors and no descendants (matches mempool.space). * * @typedef {Dollars} High */ -/** @typedef {number[]} Histogram_uint16 */ -/** @typedef {number[]} Histogram_uint32 */ /** * Historical price response * @@ -11887,8 +11893,8 @@ class BrkClient extends BrkClientBase { * Smoothed round-dollar payment histogram at the live tip: the committed EMA with the forming mempool block blended in. A flat array of log-scale bins. * * Endpoint: `GET /api/oracle/histogram/ema/live` - * @param {{ signal?: AbortSignal, onValue?: (value: Histogram_uint16) => void }} [options] - * @returns {Promise} + * @param {{ signal?: AbortSignal, onValue?: (value: number[]) => void }} [options] + * @returns {Promise} */ async getOracleHistogramEmaLive({ signal, onValue } = {}) { const path = `/api/oracle/histogram/ema/live`; @@ -11896,29 +11902,29 @@ class BrkClient extends BrkClientBase { } /** - * EMA histogram at height + * EMA histogram at height or day * - * Smoothed round-dollar payment histogram for a confirmed height. A flat array of log-scale bins. + * Smoothed round-dollar payment histogram for a confirmed point: a block height (`840000`) gives that block's EMA, a calendar date (`YYYY-MM-DD`) gives the average of that day's per-block EMAs. A flat array of log-scale bins. * - * Endpoint: `GET /api/oracle/histogram/ema/{height}` + * Endpoint: `GET /api/oracle/histogram/ema/{point}` * - * @param {Height} height - * @param {{ signal?: AbortSignal, onValue?: (value: Histogram_uint16) => void }} [options] - * @returns {Promise} + * @param {string} point + * @param {{ signal?: AbortSignal, onValue?: (value: number[]) => void }} [options] + * @returns {Promise} */ - async getOracleHistogramEma(height, { signal, onValue } = {}) { - const path = `/api/oracle/histogram/ema/${height}`; + async getOracleHistogramEma(point, { signal, onValue } = {}) { + const path = `/api/oracle/histogram/ema/${point}`; return this.getJson(path, { signal, onValue }); } /** * Live raw histogram * - * Un-smoothed per-block round-dollar counts for the forming mempool block. A flat array of log-scale bins, all zero when no mempool is configured. + * Unfiltered output histogram for the forming mempool block: every live output binned by value, with none of the round-dollar payment filters applied. A flat array of log-scale bins, all zero when no mempool is configured. * * Endpoint: `GET /api/oracle/histogram/raw/live` - * @param {{ signal?: AbortSignal, onValue?: (value: Histogram_uint32) => void }} [options] - * @returns {Promise} + * @param {{ signal?: AbortSignal, onValue?: (value: number[]) => void }} [options] + * @returns {Promise} */ async getOracleHistogramRawLive({ signal, onValue } = {}) { const path = `/api/oracle/histogram/raw/live`; @@ -11926,18 +11932,18 @@ class BrkClient extends BrkClientBase { } /** - * Raw histogram at height + * Raw histogram at height or day * - * Un-smoothed round-dollar counts for a single confirmed block. A flat array of log-scale bins. + * Unfiltered output histogram for a confirmed point: a block height (`840000`) gives that block's outputs, coinbase included, binned by value with no payment filtering; a calendar date (`YYYY-MM-DD`) sums every block that day. A flat array of log-scale bins. * - * Endpoint: `GET /api/oracle/histogram/raw/{height}` + * Endpoint: `GET /api/oracle/histogram/raw/{point}` * - * @param {Height} height - * @param {{ signal?: AbortSignal, onValue?: (value: Histogram_uint32) => void }} [options] - * @returns {Promise} + * @param {string} point + * @param {{ signal?: AbortSignal, onValue?: (value: number[]) => void }} [options] + * @returns {Promise} */ - async getOracleHistogramRaw(height, { signal, onValue } = {}) { - const path = `/api/oracle/histogram/raw/${height}`; + async getOracleHistogramRaw(point, { signal, onValue } = {}) { + const path = `/api/oracle/histogram/raw/${point}`; return this.getJson(path, { signal, onValue }); } diff --git a/packages/brk_client/brk_client/__init__.py b/packages/brk_client/brk_client/__init__.py index 90f0f8fa8..3a00a5fcd 100644 --- a/packages/brk_client/brk_client/__init__.py +++ b/packages/brk_client/brk_client/__init__.py @@ -174,8 +174,6 @@ Halving = int Hex = str # Highest price value for a time period High = Dollars -Histogram_uint16 = List[int] -Histogram_uint32 = List[int] Hour1 = int Hour12 = int Hour4 = int @@ -1134,6 +1132,14 @@ class Health(TypedDict): last_indexed_at: str last_indexed_at_unix: Timestamp +class HeightOrDateParam(TypedDict): + """ + Path parameter accepting either a block height (`840000`) or a calendar date + (`YYYY-MM-DD`). The handler resolves it and dispatches to the per-height or + per-day variant, choosing the matching cache strategy. + """ + point: str + class HeightParam(TypedDict): """ Block height path parameter @@ -8668,7 +8674,7 @@ class BrkClient(BrkClientBase): Endpoint: `GET /api/oracle/price`""" return self.get_json('/api/oracle/price') - def get_oracle_histogram_ema_live(self) -> Histogram_uint16: + def get_oracle_histogram_ema_live(self) -> List[int]: """Live EMA histogram. Smoothed round-dollar payment histogram at the live tip: the committed EMA with the forming mempool block blended in. A flat array of log-scale bins. @@ -8676,29 +8682,29 @@ class BrkClient(BrkClientBase): Endpoint: `GET /api/oracle/histogram/ema/live`""" return self.get_json('/api/oracle/histogram/ema/live') - def get_oracle_histogram_ema(self, height: Height) -> Histogram_uint16: - """EMA histogram at height. + def get_oracle_histogram_ema(self, point: str) -> List[int]: + """EMA histogram at height or day. - Smoothed round-dollar payment histogram for a confirmed height. A flat array of log-scale bins. + Smoothed round-dollar payment histogram for a confirmed point: a block height (`840000`) gives that block's EMA, a calendar date (`YYYY-MM-DD`) gives the average of that day's per-block EMAs. A flat array of log-scale bins. - Endpoint: `GET /api/oracle/histogram/ema/{height}`""" - return self.get_json(f'/api/oracle/histogram/ema/{height}') + Endpoint: `GET /api/oracle/histogram/ema/{point}`""" + return self.get_json(f'/api/oracle/histogram/ema/{point}') - def get_oracle_histogram_raw_live(self) -> Histogram_uint32: + def get_oracle_histogram_raw_live(self) -> List[int]: """Live raw histogram. - Un-smoothed per-block round-dollar counts for the forming mempool block. A flat array of log-scale bins, all zero when no mempool is configured. + Unfiltered output histogram for the forming mempool block: every live output binned by value, with none of the round-dollar payment filters applied. A flat array of log-scale bins, all zero when no mempool is configured. Endpoint: `GET /api/oracle/histogram/raw/live`""" return self.get_json('/api/oracle/histogram/raw/live') - def get_oracle_histogram_raw(self, height: Height) -> Histogram_uint32: - """Raw histogram at height. + def get_oracle_histogram_raw(self, point: str) -> List[int]: + """Raw histogram at height or day. - Un-smoothed round-dollar counts for a single confirmed block. A flat array of log-scale bins. + Unfiltered output histogram for a confirmed point: a block height (`840000`) gives that block's outputs, coinbase included, binned by value with no payment filtering; a calendar date (`YYYY-MM-DD`) sums every block that day. A flat array of log-scale bins. - Endpoint: `GET /api/oracle/histogram/raw/{height}`""" - return self.get_json(f'/api/oracle/histogram/raw/{height}') + Endpoint: `GET /api/oracle/histogram/raw/{point}`""" + return self.get_json(f'/api/oracle/histogram/raw/{point}') def get_tx_by_index(self, index: TxIndex) -> Txid: """Txid by index.