global: snapshot

This commit is contained in:
nym21
2026-01-15 23:34:43 +01:00
parent b0d933a7ab
commit 967d2c7f35
67 changed files with 6854 additions and 5210 deletions

View File

@@ -7,7 +7,7 @@ use brk_types::Version;
use schemars::JsonSchema;
use vecdb::{LazyVecFrom1, UnaryTransform, VecIndex};
use crate::internal::{ComputedVecValue, Full};
use crate::internal::{ComputedVecValue, Distribution, Full};
use super::LazyPercentiles;
@@ -61,4 +61,33 @@ where
),
}
}
pub fn from_distribution<F: UnaryTransform<S1T, T>>(
name: &str,
version: Version,
source: &Distribution<I, S1T>,
) -> Self {
Self {
average: LazyVecFrom1::transformed::<F>(
&format!("{name}_average"),
version,
source.boxed_average(),
),
min: LazyVecFrom1::transformed::<F>(
&format!("{name}_min"),
version,
source.boxed_min(),
),
max: LazyVecFrom1::transformed::<F>(
&format!("{name}_max"),
version,
source.boxed_max(),
),
percentiles: LazyPercentiles::from_percentiles::<F>(
name,
version,
&source.percentiles,
),
}
}
}

View File

@@ -0,0 +1,11 @@
use brk_types::{Cents, Dollars};
use vecdb::UnaryTransform;
pub struct CentsToDollars;
impl UnaryTransform<Cents, Dollars> for CentsToDollars {
#[inline(always)]
fn apply(cents: Cents) -> Dollars {
Dollars::from(cents)
}
}

View File

@@ -1,3 +1,4 @@
mod cents_to_dollars;
mod close_price_times_ratio;
mod close_price_times_sats;
mod difference_f32;
@@ -37,6 +38,7 @@ mod volatility_sqrt365;
mod volatility_sqrt7;
mod weight_to_fullness;
pub use cents_to_dollars::*;
pub use close_price_times_ratio::*;
pub use close_price_times_sats::*;
pub use difference_f32::*;

View File

@@ -1,10 +1,50 @@
//! # Phase Oracle - On-chain Price Discovery
//!
//! Uses `frac(log10(sats))` to bin outputs into 100 bins per block.
//! The peak bin indicates the price decade (cyclical: $6.3, $63, $630, $6300 all map to same bin).
//! Monthly/yearly calibration anchors resolve the decade ambiguity.
//!
//! ## What Worked
//!
//! **Transaction filters (in `compute_pair_index`):**
//! - `output_count == 2` - payment + change pattern
//! - `input_count <= 5` - matches Python UTXOracle
//! - `witness_size <= 2500` bytes total
//! - No OP_RETURN outputs
//! - No P2TR (taproot) outputs - significantly cleaned up 2021+ data
//! - No P2MS, Empty, Unknown outputs - allowlist approach
//! - No same-day spends - inputs must spend outputs confirmed on earlier days
//! - No both-outputs-round - skip tx if both outputs are round BTC amounts (±0.1%)
//!
//! **Output filters (in `OracleBins::sats_to_bin`):**
//! - Per-output min/max: 1k sats to 100k BTC (matches Python's 1e-5 to 1e5 BTC)
//!
//! **Peak finding:**
//! - Skip bin 0 when finding peak - round BTC amounts (0.001, 0.01, 0.1, 1.0 BTC) cluster there
//!
//! **Anchors:**
//! - Monthly anchors 2010-2020 for better decade selection in volatile early years
//! - Yearly anchors 2021+ when prices are more stable
//!
//! ## What Didn't Work
//!
//! - **Skip all round bins (0, 10, 20, ..., 90) before 2020** - made results worse, not better
//! - **Top-N tie-breaking with prev_price** - caused drift
//! - **50% margin threshold for round bin avoidance** - still had issues
//! - **Transaction-level min sats filter** - Python filters per-output, not per-tx
//!
//! ## Known Limitations
//!
//! - Pre-2017 data is noisy due to low transaction volume (weak signal)
//! - 2017 SegWit activation era has some spikes
use std::collections::VecDeque;
use brk_error::Result;
use brk_indexer::Indexer;
use brk_types::{
Cents, Close, Date, DateIndex, Height, High, Low, OHLCCents, Open, OutputType, Sats, StoredU32,
StoredU64, TxIndex,
Cents, Close, Date, DateIndex, Height, High, Low, OHLCCents, Open, OracleBins, OutputType,
PHASE_BINS, PairOutputIndex, Sats, StoredU32, StoredU64, TxIndex,
};
use tracing::info;
use vecdb::{
@@ -31,6 +71,653 @@ impl Vecs {
indexes: &indexes::Vecs,
starting_indexes: &ComputeIndexes,
exit: &Exit,
) -> Result<()> {
// Step 1: Compute pair output index (all 2-output transactions)
self.compute_pair_index(indexer, indexes, starting_indexes, exit)?;
// Step 2: Compute phase histograms (Layer 4)
self.compute_phase_histograms(starting_indexes, exit)?;
// Step 3: Compute phase oracle prices (Layer 5)
self.compute_phase_prices(starting_indexes, exit)?;
// Step 4: Compute phase daily average
self.compute_phase_daily_average(indexes, starting_indexes, exit)?;
// Step 6: Compute UTXOracle prices (Python port)
self.compute_prices(indexer, indexes, starting_indexes, exit)?;
// Step 7: Aggregate to daily OHLC
self.compute_daily_ohlc(indexes, starting_indexes, exit)?;
Ok(())
}
/// Compute the pair output index: all transactions with exactly 2 outputs
///
/// This is Layer 1 of the oracle computation - identifies all candidate
/// transactions for the payment+change pattern.
fn compute_pair_index(
&mut self,
indexer: &Indexer,
indexes: &indexes::Vecs,
starting_indexes: &ComputeIndexes,
exit: &Exit,
) -> Result<()> {
// Validate version - combine all source vec versions
let source_version = indexes.txindex.output_count.version()
+ indexes.txindex.input_count.version()
+ indexer.vecs.transactions.base_size.version()
+ indexer.vecs.transactions.total_size.version()
+ indexer.vecs.outputs.outputtype.version()
+ indexer.vecs.outputs.value.version()
+ indexer.vecs.inputs.outpoint.version()
+ indexes.height.dateindex.version();
self.pairoutputindex_to_txindex
.validate_computed_version_or_reset(source_version)?;
self.height_to_first_pairoutputindex
.validate_computed_version_or_reset(source_version)?;
self.output0_value
.validate_computed_version_or_reset(source_version)?;
self.output1_value
.validate_computed_version_or_reset(source_version)?;
let total_heights = indexer.vecs.blocks.timestamp.len();
let total_txs = indexer.vecs.transactions.height.len();
// Determine starting height (handle rollback + sync)
let start_height = self
.height_to_first_pairoutputindex
.len()
.min(starting_indexes.height.to_usize());
// Truncation point for pair vecs: first_pairoutputindex of start_height block
// (i.e., keep all pairs from blocks before start_height)
let pair_truncate_len =
if start_height > 0 && start_height <= self.height_to_first_pairoutputindex.len() {
self.height_to_first_pairoutputindex
.iter()?
.get(Height::from(start_height))
.map(|idx| idx.to_usize())
.unwrap_or(self.pairoutputindex_to_txindex.len())
} else if start_height == 0 {
0
} else {
self.pairoutputindex_to_txindex.len()
}
.min(self.pairoutputindex_to_txindex.len())
.min(self.output0_value.len())
.min(self.output1_value.len());
// Truncate all vecs together
self.height_to_first_pairoutputindex
.truncate_if_needed_at(start_height)?;
self.pairoutputindex_to_txindex
.truncate_if_needed_at(pair_truncate_len)?;
self.output0_value
.truncate_if_needed_at(pair_truncate_len)?;
self.output1_value
.truncate_if_needed_at(pair_truncate_len)?;
if start_height >= total_heights {
return Ok(());
}
info!(
"Computing pair index from height {} to {}",
start_height, total_heights
);
let mut height_to_first_txindex_iter = indexer.vecs.transactions.first_txindex.into_iter();
let mut txindex_to_output_count_iter = indexes.txindex.output_count.iter();
let mut txindex_to_input_count_iter = indexes.txindex.input_count.iter();
let mut txindex_to_base_size_iter = indexer.vecs.transactions.base_size.into_iter();
let mut txindex_to_total_size_iter = indexer.vecs.transactions.total_size.into_iter();
let mut txindex_to_first_txoutindex_iter =
indexer.vecs.transactions.first_txoutindex.into_iter();
let mut txindex_to_first_txinindex_iter =
indexer.vecs.transactions.first_txinindex.into_iter();
let mut txoutindex_to_outputtype_iter = indexer.vecs.outputs.outputtype.into_iter();
let mut txoutindex_to_value_iter = indexer.vecs.outputs.value.into_iter();
let mut txinindex_to_outpoint_iter = indexer.vecs.inputs.outpoint.into_iter();
let mut height_to_dateindex_iter = indexes.height.dateindex.iter();
let mut dateindex_to_first_height_iter = indexes.dateindex.first_height.iter();
// Track current date for same-day spend check
let mut current_dateindex = DateIndex::from(0usize);
let mut current_date_first_txindex = TxIndex::from(0usize);
let mut last_progress = (start_height * 100 / total_heights.max(1)) as u8;
for height in start_height..total_heights {
// Record first pairoutputindex for this block
let first_pairoutputindex =
PairOutputIndex::from(self.pairoutputindex_to_txindex.len());
self.height_to_first_pairoutputindex
.push(first_pairoutputindex);
// Get transaction range for this block
let first_txindex = height_to_first_txindex_iter.get_at_unwrap(height);
let next_first_txindex = height_to_first_txindex_iter
.get_at(height + 1)
.unwrap_or(TxIndex::from(total_txs));
// Update current date tracking for same-day spend check
let block_dateindex = height_to_dateindex_iter.get_unwrap(Height::from(height));
if block_dateindex != current_dateindex {
current_dateindex = block_dateindex;
if let Some(first_height) = dateindex_to_first_height_iter.get(block_dateindex) {
current_date_first_txindex = height_to_first_txindex_iter
.get_at(first_height.to_usize())
.unwrap_or(first_txindex);
}
}
// Skip coinbase (first tx in block)
let tx_start = first_txindex.to_usize() + 1;
let tx_end = next_first_txindex.to_usize();
for txindex in tx_start..tx_end {
// Check output count first (most common filter)
let output_count: StoredU64 =
txindex_to_output_count_iter.get_unwrap(TxIndex::from(txindex));
if *output_count != 2 {
continue;
}
// Filter: 1-5 inputs (same as UTXOracle)
let input_count: StoredU64 =
txindex_to_input_count_iter.get_unwrap(TxIndex::from(txindex));
if *input_count == 0 || *input_count > 5 {
continue;
}
// Filter: max 2500 bytes total witness size
let base_size: StoredU32 = txindex_to_base_size_iter.get_at_unwrap(txindex);
let total_size: StoredU32 = txindex_to_total_size_iter.get_at_unwrap(txindex);
let witness_size = *total_size - *base_size;
if witness_size > 2500 {
continue;
}
// Filter: only standard payment types (no OP_RETURN, P2TR, P2MS, Empty, Unknown)
let first_txoutindex = txindex_to_first_txoutindex_iter.get_at_unwrap(txindex);
let out0_type =
txoutindex_to_outputtype_iter.get_at_unwrap(first_txoutindex.to_usize());
let out1_type =
txoutindex_to_outputtype_iter.get_at_unwrap(first_txoutindex.to_usize() + 1);
if !matches!(
out0_type,
OutputType::P2PK65
| OutputType::P2PK33
| OutputType::P2PKH
| OutputType::P2SH
| OutputType::P2WPKH
| OutputType::P2WSH
| OutputType::P2A
) || !matches!(
out1_type,
OutputType::P2PK65
| OutputType::P2PK33
| OutputType::P2PKH
| OutputType::P2SH
| OutputType::P2WPKH
| OutputType::P2WSH
| OutputType::P2A
) {
continue;
}
// Filter: no same-day spends (input spending output confirmed today)
let first_txinindex = txindex_to_first_txinindex_iter.get_at_unwrap(txindex);
let mut has_same_day_spend = false;
for i in 0..*input_count as usize {
let txinindex = first_txinindex.to_usize() + i;
let outpoint = txinindex_to_outpoint_iter.get_at_unwrap(txinindex);
if !outpoint.is_coinbase() && outpoint.txindex() >= current_date_first_txindex {
has_same_day_spend = true;
break;
}
}
if has_same_day_spend {
continue;
}
// Get output values (Layer 3)
let value0: Sats =
txoutindex_to_value_iter.get_at_unwrap(first_txoutindex.to_usize());
let value1: Sats =
txoutindex_to_value_iter.get_at_unwrap(first_txoutindex.to_usize() + 1);
// Filter: skip if BOTH outputs are round BTC amounts (not price-related)
if value0.is_round_btc() && value1.is_round_btc() {
continue;
}
// Store Layer 1 & 3 data
// Note: min/max sats filtering done per-output in OracleBins::sats_to_bin
self.pairoutputindex_to_txindex.push(TxIndex::from(txindex));
self.output0_value.push(value0);
self.output1_value.push(value1);
}
// Log and flush every 1%
let progress = (height * 100 / total_heights.max(1)) as u8;
if progress > last_progress {
last_progress = progress;
info!("Pair index computation: {}%", progress);
let _lock = exit.lock();
self.pairoutputindex_to_txindex.write()?;
self.height_to_first_pairoutputindex.write()?;
self.output0_value.write()?;
self.output1_value.write()?;
}
}
// Final write
{
let _lock = exit.lock();
self.pairoutputindex_to_txindex.write()?;
self.height_to_first_pairoutputindex.write()?;
self.output0_value.write()?;
self.output1_value.write()?;
}
info!(
"Pair index complete: {} pairs across {} blocks",
self.pairoutputindex_to_txindex.len(),
self.height_to_first_pairoutputindex.len()
);
Ok(())
}
/// Compute phase histograms per block (Layer 4)
///
/// Bins output values by frac(log10(sats)) into 100 bins per block.
fn compute_phase_histograms(
&mut self,
starting_indexes: &ComputeIndexes,
exit: &Exit,
) -> Result<()> {
let source_version = self.pairoutputindex_to_txindex.version();
self.phase_histogram
.validate_computed_version_or_reset(source_version)?;
let total_heights = self.height_to_first_pairoutputindex.len();
let total_pairs = self.pairoutputindex_to_txindex.len();
let start_height = self
.phase_histogram
.len()
.min(starting_indexes.height.to_usize());
self.phase_histogram.truncate_if_needed_at(start_height)?;
if start_height >= total_heights {
return Ok(());
}
info!(
"Computing phase histograms from height {} to {}",
start_height, total_heights
);
let mut output0_iter = self.output0_value.iter()?;
let mut output1_iter = self.output1_value.iter()?;
let mut height_to_first_pair_iter = self.height_to_first_pairoutputindex.iter()?;
let mut last_progress = (start_height * 100 / total_heights.max(1)) as u8;
for height in start_height..total_heights {
// Get pair range for this block
let first_pair = height_to_first_pair_iter
.get_unwrap(Height::from(height))
.to_usize();
let next_first_pair = height_to_first_pair_iter
.get(Height::from(height + 1))
.map(|p| p.to_usize())
.unwrap_or(total_pairs);
// Build phase histogram
let mut histogram = OracleBins::ZERO;
for pair_idx in first_pair..next_first_pair {
let pair_idx = PairOutputIndex::from(pair_idx);
let sats0: Sats = output0_iter.get_unwrap(pair_idx);
let sats1: Sats = output1_iter.get_unwrap(pair_idx);
histogram.add(sats0);
histogram.add(sats1);
}
self.phase_histogram.push(histogram);
// Progress logging
let progress = (height * 100 / total_heights.max(1)) as u8;
if progress > last_progress {
last_progress = progress;
info!("Phase histogram computation: {}%", progress);
let _lock = exit.lock();
self.phase_histogram.write()?;
}
}
// Final write
{
let _lock = exit.lock();
self.phase_histogram.write()?;
}
info!(
"Phase histograms complete: {} blocks",
self.phase_histogram.len()
);
Ok(())
}
/// Compute phase oracle prices (Layer 5)
///
/// Derives prices from phase histograms using peak finding.
/// Uses monthly calibration anchors (2010-2020) then yearly (2021+).
fn compute_phase_prices(
&mut self,
starting_indexes: &ComputeIndexes,
exit: &Exit,
) -> Result<()> {
/// Monthly calibration anchors 2010-2020, then yearly 2021+
/// Format: (first_height_of_period, open_price)
const ANCHORS: [(u32, f64); 129] = [
// 2010 (monthly from Oct)
(82_998, 0.06), // 2010-10-01
(88_893, 0.19), // 2010-11-01
(94_802, 0.20), // 2010-12-01
// 2011
(100_410, 0.30), // 2011-01-01
(105_571, 0.55), // 2011-02-01
(111_137, 0.86), // 2011-03-01
(116_039, 0.78), // 2011-04-01
(121_127, 3.50), // 2011-05-01
(127_866, 8.74), // 2011-06-01
(134_122, 16.10), // 2011-07-01
(139_036, 13.35), // 2011-08-01
(143_409, 8.19), // 2011-09-01
(147_566, 5.14), // 2011-10-01
(151_315, 3.24), // 2011-11-01
(155_452, 2.97), // 2011-12-01
// 2012
(160_037, 4.72), // 2012-01-01
(164_781, 5.48), // 2012-02-01
(169_136, 4.86), // 2012-03-01
(173_805, 4.90), // 2012-04-01
(178_015, 4.94), // 2012-05-01
(182_429, 5.18), // 2012-06-01
(186_964, 6.68), // 2012-07-01
(191_737, 9.35), // 2012-08-01
(196_616, 10.16), // 2012-09-01
(201_311, 12.40), // 2012-10-01
(205_919, 11.20), // 2012-11-01
(210_350, 12.56), // 2012-12-01
// 2013
(214_563, 13.51), // 2013-01-01
(219_007, 20.41), // 2013-02-01
(223_665, 33.38), // 2013-03-01
(229_008, 93.03), // 2013-04-01
(233_975, 139.22), // 2013-05-01
(238_952, 128.81), // 2013-06-01
(244_160, 97.51), // 2013-07-01
(249_525, 106.21), // 2013-08-01
(255_362, 141.00), // 2013-09-01
(260_989, 141.89), // 2013-10-01
(267_188, 211.17), // 2013-11-01
(272_375, 1205.80), // 2013-12-01
// 2014
(277_996, 739.28), // 2014-01-01
(283_468, 805.22), // 2014-02-01
(288_370, 549.99), // 2014-03-01
(293_483, 456.98), // 2014-04-01
(298_513, 449.02), // 2014-05-01
(303_552, 626.21), // 2014-06-01
(308_672, 640.79), // 2014-07-01
(313_404, 580.00), // 2014-08-01
(318_531, 477.81), // 2014-09-01
(323_269, 387.00), // 2014-10-01
(327_939, 336.82), // 2014-11-01
(332_363, 379.89), // 2014-12-01
// 2015
(336_861, 322.30), // 2015-01-01
(341_392, 215.80), // 2015-02-01
(345_611, 255.70), // 2015-03-01
(350_162, 244.51), // 2015-04-01
(354_416, 236.11), // 2015-05-01
(358_881, 228.70), // 2015-06-01
(363_263, 262.89), // 2015-07-01
(367_846, 284.45), // 2015-08-01
(372_441, 231.35), // 2015-09-01
(376_910, 236.49), // 2015-10-01
(381_470, 316.00), // 2015-11-01
(386_119, 376.88), // 2015-12-01
// 2016
(391_182, 429.02), // 2016-01-01
(396_049, 365.52), // 2016-02-01
(400_601, 438.99), // 2016-03-01
(405_179, 416.02), // 2016-04-01
(409_638, 446.60), // 2016-05-01
(414_258, 530.69), // 2016-06-01
(418_723, 671.91), // 2016-07-01
(423_088, 624.22), // 2016-08-01
(427_737, 573.80), // 2016-09-01
(432_284, 609.67), // 2016-10-01
(436_828, 697.69), // 2016-11-01
(441_341, 742.33), // 2016-12-01
// 2017
(446_033, 970.41), // 2017-01-01
(450_945, 968.74), // 2017-02-01
(455_200, 1190.37), // 2017-03-01
(459_832, 1080.82), // 2017-04-01
(464_270, 1362.02), // 2017-05-01
(469_122, 2299.05), // 2017-06-01
(473_593, 2455.42), // 2017-07-01
(478_479, 2865.02), // 2017-08-01
(482_885, 4737.93), // 2017-09-01
(487_740, 4334.18), // 2017-10-01
(492_558, 6439.52), // 2017-11-01
(496_932, 9968.39), // 2017-12-01
// 2018
(501_961, 13888.32), // 2018-01-01
(507_016, 10115.79), // 2018-02-01
(511_385, 10306.80), // 2018-03-01
(516_040, 6922.18), // 2018-04-01
(520_650, 9243.39), // 2018-05-01
(525_367, 7486.93), // 2018-06-01
(529_967, 6386.45), // 2018-07-01
(534_613, 7725.93), // 2018-08-01
(539_416, 7016.31), // 2018-09-01
(543_835, 6565.64), // 2018-10-01
(548_214, 6305.13), // 2018-11-01
(552_084, 3971.61), // 2018-12-01
// 2019
(556_459, 3692.35), // 2019-01-01
(560_984, 3411.57), // 2019-02-01
(565_109, 3792.17), // 2019-03-01
(569_659, 4095.32), // 2019-04-01
(573_997, 5269.55), // 2019-05-01
(578_718, 8542.59), // 2019-06-01
(583_237, 10754.91), // 2019-07-01
(588_007, 10085.57), // 2019-08-01
(592_683, 9600.93), // 2019-09-01
(597_318, 8303.79), // 2019-10-01
(601_842, 9152.56), // 2019-11-01
(606_088, 7554.92), // 2019-12-01
// 2020
(610_691, 7167.07), // 2020-01-01
(615_428, 9333.17), // 2020-02-01
(619_582, 8526.76), // 2020-03-01
(623_837, 6424.03), // 2020-04-01
(628_350, 8627.93), // 2020-05-01
(632_542, 9448.95), // 2020-06-01
(637_091, 9134.01), // 2020-07-01
(641_680, 11354.08), // 2020-08-01
(646_201, 11657.26), // 2020-09-01
(650_732, 10779.19), // 2020-10-01
(654_933, 13809.85), // 2020-11-01
(658_977, 19698.14), // 2020-12-01
// 2021+ (yearly)
(663_913, 28_980.45), // 2021-01-01
(716_599, 46_195.56), // 2022-01-01
(769_787, 16_528.89), // 2023-01-01
(823_786, 42_241.10), // 2024-01-01
(877_259, 93_576.00), // 2025-01-01
(930_341, 87_648.22), // 2026-01-01
];
/// Find the calibration price for a given height
fn anchor_price_for_height(height: usize) -> Option<f64> {
let mut result = None;
for &(anchor_height, price) in &ANCHORS {
if height >= anchor_height as usize {
result = Some(price);
} else {
break;
}
}
result
}
let source_version = self.phase_histogram.version();
self.phase_price_cents
.validate_computed_version_or_reset(source_version)?;
let total_heights = self.phase_histogram.len();
let start_height = self
.phase_price_cents
.len()
.min(starting_indexes.height.to_usize());
self.phase_price_cents.truncate_if_needed_at(start_height)?;
if start_height >= total_heights {
return Ok(());
}
info!(
"Computing phase prices from height {} to {}",
start_height, total_heights
);
let mut histogram_iter = self.phase_histogram.iter()?;
let mut last_progress = (start_height * 100 / total_heights.max(1)) as u8;
// Fixed exponent calibrated for ~$63,000 (ceil(log10(63000)) = 5)
const EXPONENT: f64 = 5.0;
/// Convert a bin to price using anchor for decade selection
fn bin_to_price(bin: usize, anchor_price: f64) -> f64 {
let peak = (bin as f64 + 0.5) / PHASE_BINS as f64;
let raw_price = 10.0_f64.powf(EXPONENT - peak);
let decade_ratio = (anchor_price / raw_price).log10().round();
raw_price * 10.0_f64.powf(decade_ratio)
}
for height in start_height..total_heights {
// Before first anchor (pre-Oct 2010), output 0
let anchor_price = match anchor_price_for_height(height) {
Some(price) => price,
None => {
self.phase_price_cents.push(Cents::ZERO);
continue;
}
};
let histogram = histogram_iter.get_unwrap(Height::from(height));
// Skip empty histograms, use anchor price
if histogram.total_count() == 0 {
let price_cents = Cents::from((anchor_price * 100.0) as i64);
self.phase_price_cents.push(price_cents);
continue;
}
// Find peak bin, skipping bin 0 (round BTC amounts cluster there)
let peak_bin = histogram
.bins
.iter()
.enumerate()
.filter(|(bin, _)| *bin != 0)
.max_by_key(|(_, count)| *count)
.map(|(bin, _)| bin)
.unwrap_or(0);
let price = bin_to_price(peak_bin, anchor_price);
// Clamp to reasonable range ($0.001 to $10M)
let price = price.clamp(0.001, 10_000_000.0);
let price_cents = Cents::from((price * 100.0) as i64);
self.phase_price_cents.push(price_cents);
// Progress logging
let progress = (height * 100 / total_heights.max(1)) as u8;
if progress > last_progress {
last_progress = progress;
info!("Phase price computation: {}%", progress);
let _lock = exit.lock();
self.phase_price_cents.write()?;
}
}
// Final write
{
let _lock = exit.lock();
self.phase_price_cents.write()?;
}
info!(
"Phase prices complete: {} blocks",
self.phase_price_cents.len()
);
Ok(())
}
/// Compute daily distribution (min, max, average, percentiles) from phase oracle prices
fn compute_phase_daily_average(
&mut self,
indexes: &indexes::Vecs,
starting_indexes: &ComputeIndexes,
exit: &Exit,
) -> Result<()> {
info!("Computing phase daily distribution");
self.phase_daily_cents.compute(
starting_indexes.dateindex,
&self.phase_price_cents,
&indexes.dateindex.first_height,
&indexes.dateindex.height_count,
exit,
)?;
info!(
"Phase daily distribution complete: {} days",
self.phase_daily_cents.len()
);
Ok(())
}
/// Compute oracle prices from on-chain data (UTXOracle port)
fn compute_prices(
&mut self,
indexer: &Indexer,
indexes: &indexes::Vecs,
starting_indexes: &ComputeIndexes,
exit: &Exit,
) -> Result<()> {
// Validate versions
self.price_cents
@@ -92,8 +779,8 @@ impl Vecs {
};
// Progress tracking
let total_blocks = last_height.to_usize() - start_height.to_usize();
let mut last_progress = 0u8;
let mut last_progress =
(start_height.to_usize() * 100 / last_height.to_usize().max(1)) as u8;
let total_txs = indexer.vecs.transactions.height.len();
// Sparse entries for current block (reused buffer)
@@ -109,8 +796,7 @@ impl Vecs {
let height = Height::from(height);
// Log progress every 1%
let progress =
((height.to_usize() - start_height.to_usize()) * 100 / total_blocks.max(1)) as u8;
let progress = (height.to_usize() * 100 / last_height.to_usize().max(1)) as u8;
if progress > last_progress {
last_progress = progress;
info!("Oracle price computation: {}%", progress);
@@ -174,14 +860,14 @@ impl Vecs {
// Check outputs: no OP_RETURN, collect values
let mut has_opreturn = false;
let mut values: [Sats; 2] = [Sats::ZERO; 2];
for i in 0..2usize {
for (i, value) in values.iter_mut().enumerate() {
let txoutindex = first_txoutindex.to_usize() + i;
let outputtype = txoutindex_to_outputtype_iter.get_at_unwrap(txoutindex);
if outputtype == OutputType::OpReturn {
has_opreturn = true;
break;
}
values[i] = txoutindex_to_value_iter.get_at_unwrap(txoutindex);
*value = txoutindex_to_value_iter.get_at_unwrap(txoutindex);
}
if has_opreturn {
continue;

View File

@@ -3,12 +3,38 @@ use brk_types::{DateIndex, OHLCCents, OHLCDollars, Version};
use vecdb::{BytesVec, Database, ImportableVec, IterableCloneableVec, LazyVecFrom1, PcoVec};
use super::Vecs;
use crate::internal::{CentsToDollars, Distribution, LazyTransformDistribution};
impl Vecs {
pub fn forced_import(db: &Database, parent_version: Version) -> Result<Self> {
// v2: Fixed spike stencil positions and Gaussian center to match Python's empirical data
let version = parent_version + Version::TWO;
// v12: Add both-outputs-round filter
let version = parent_version + Version::new(12);
// Layer 1: Pair output index
let pairoutputindex_to_txindex =
PcoVec::forced_import(db, "pairoutputindex_to_txindex", version)?;
let height_to_first_pairoutputindex =
PcoVec::forced_import(db, "height_to_first_pairoutputindex", version)?;
// Layer 3: Output values
let output0_value = PcoVec::forced_import(db, "pair_output0_value", version)?;
let output1_value = PcoVec::forced_import(db, "pair_output1_value", version)?;
// Layer 4: Phase histograms (depends on Layer 1)
let phase_histogram = BytesVec::forced_import(db, "phase_histogram", version)?;
// Layer 5: Phase Oracle prices
// v20: Skip only bin 0 (reverted round bin skip)
let phase_version = version + Version::new(13);
let phase_price_cents = PcoVec::forced_import(db, "phase_price_cents", phase_version)?;
let phase_daily_cents = Distribution::forced_import(db, "phase_daily", phase_version)?;
let phase_daily_dollars = LazyTransformDistribution::from_distribution::<CentsToDollars>(
"phase_daily_dollars",
phase_version,
&phase_daily_cents,
);
// UTXOracle (Python port)
let price_cents = PcoVec::forced_import(db, "oracle_price_cents", version)?;
let ohlc_cents = BytesVec::forced_import(db, "oracle_ohlc_cents", version)?;
let tx_count = PcoVec::forced_import(db, "oracle_tx_count", version)?;
@@ -21,6 +47,14 @@ impl Vecs {
);
Ok(Self {
pairoutputindex_to_txindex,
height_to_first_pairoutputindex,
output0_value,
output1_value,
phase_histogram,
phase_price_cents,
phase_daily_cents,
phase_daily_dollars,
price_cents,
ohlc_cents,
ohlc_dollars,

View File

@@ -1,16 +1,53 @@
use brk_traversable::Traversable;
use brk_types::{Cents, DateIndex, Height, OHLCCents, OHLCDollars, StoredU32};
use brk_types::{
Cents, DateIndex, Dollars, Height, OHLCCents, OHLCDollars, OracleBins, PairOutputIndex, Sats,
StoredU32, TxIndex,
};
use vecdb::{BytesVec, LazyVecFrom1, PcoVec};
/// Vectors storing UTXOracle-derived price data
use crate::internal::{Distribution, LazyTransformDistribution};
/// Vectors storing oracle-derived price data
#[derive(Clone, Traversable)]
pub struct Vecs {
/// Per-block price estimate in cents
/// This enables OHLC derivation for any time period
// ========== Layer 1: Pair identification (requires chain scan) ==========
/// Maps PairOutputIndex to TxIndex for all 2-output transactions
/// This is the base index for oracle candidates (~400M entries)
pub pairoutputindex_to_txindex: PcoVec<PairOutputIndex, TxIndex>,
/// Maps Height to first PairOutputIndex in that block
/// Enables efficient per-block iteration over pairs
pub height_to_first_pairoutputindex: PcoVec<Height, PairOutputIndex>,
// ========== Layer 3: Output values (enables any price algorithm) ==========
/// First output value for each pair (index 0)
pub output0_value: PcoVec<PairOutputIndex, Sats>,
/// Second output value for each pair (index 1)
pub output1_value: PcoVec<PairOutputIndex, Sats>,
// ========== Layer 4: Phase histograms (per block) ==========
/// Phase histogram per block: frac(log10(sats)) binned into 100 bins
/// ~200 bytes per block, ~175 MB total
pub phase_histogram: BytesVec<Height, OracleBins>,
// ========== Layer 5: Phase Oracle prices (derived from histograms) ==========
/// Per-block price in cents from phase histogram analysis
/// Calibrated at block 840,000 (~$63,000)
/// TODO: Add interpolation for sub-bin precision
pub phase_price_cents: PcoVec<Height, Cents>,
/// Daily distribution (min, max, average, percentiles) from phase oracle in cents
pub phase_daily_cents: Distribution<DateIndex, Cents>,
/// Daily distribution in dollars (lazy conversion from cents)
pub phase_daily_dollars: LazyTransformDistribution<DateIndex, Dollars, Cents>,
// ========== UTXOracle (Python port) ==========
/// Per-block price estimate in cents (sliding window + stencil matching)
pub price_cents: PcoVec<Height, Cents>,
/// Daily OHLC derived from height_to_price
/// Uses BytesVec because OHLCCents is a complex type
/// Daily OHLC derived from price_cents
pub ohlc_cents: BytesVec<DateIndex, OHLCCents>,
/// Daily OHLC in dollars (lazy conversion from cents)