mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-06-08 14:11:56 -07:00
oracle: doc fixes
This commit is contained in:
@@ -52,6 +52,13 @@ struct GuardCfg {
|
||||
raw_margin: f64, // octave neighbor raw mass must be >= raw_margin * current
|
||||
q_margin: usize, // neighbor must have >= q_margin MORE lit arms than current
|
||||
q_min: usize, // neighbor must have at least this many lit arms (looks full)
|
||||
// Lever 2: global re-acquire. Instead of only checking the +-60 octave
|
||||
// neighbors, scan a wide band beyond the local search window for the
|
||||
// strongest true-price peak (most lit arms, raw mass as tiebreak) and snap
|
||||
// to it when it clearly beats the locally-trapped pick. Escapes any
|
||||
// local-max trap, not just the octave alias.
|
||||
global: bool,
|
||||
global_radius: i64, // bins scanned on each side of the local pick
|
||||
}
|
||||
|
||||
impl GuardCfg {
|
||||
@@ -71,6 +78,11 @@ impl GuardCfg {
|
||||
raw_margin: g("GUARD_RAW", 1.0),
|
||||
q_margin: g("GUARD_QMARGIN", 4.0) as usize,
|
||||
q_min: g("GUARD_QMIN", 14.0) as usize,
|
||||
global: std::env::var("GLOBAL_REACQUIRE")
|
||||
.ok()
|
||||
.map(|v| v != "0")
|
||||
.unwrap_or(false),
|
||||
global_radius: g("GLOBAL_RADIUS", 600.0) as i64,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -182,21 +194,54 @@ fn guarded_best_bin(
|
||||
let qb = arm_count(ema, b, guard.tau);
|
||||
let raw_b = ema_stencil_sum(ema, b);
|
||||
let mut target = b;
|
||||
let mut best: Option<(usize, f64)> = None;
|
||||
for &delta in &[-OCTAVE_BINS, OCTAVE_BINS] {
|
||||
let n = b + delta;
|
||||
if n < 0 || n as usize >= brk_oracle::NUM_BINS {
|
||||
continue;
|
||||
}
|
||||
let qn = arm_count(ema, n, guard.tau);
|
||||
let raw_n = ema_stencil_sum(ema, n);
|
||||
if qn >= qb + guard.q_margin && qn >= guard.q_min && raw_n >= guard.raw_margin * raw_b {
|
||||
let better = best.is_none_or(|(sq, sr)| qn > sq || (qn == sq && raw_n > sr));
|
||||
if guard.global {
|
||||
// Scan beyond the local window for the strongest peak by lit-arm
|
||||
// count (raw mass as tiebreak), considering only bins carrying at
|
||||
// least the local pick's raw mass. Snap to it when it lights up
|
||||
// q_margin more arms and looks full (>= q_min), regardless of how
|
||||
// many bins away it sits.
|
||||
let lo = (b - guard.global_radius).max(0);
|
||||
let hi = (b + guard.global_radius).min(brk_oracle::NUM_BINS as i64 - 1);
|
||||
let mut best: Option<(i64, usize, f64)> = None;
|
||||
for n in lo..=hi {
|
||||
if n >= search_start as i64 && n < search_end as i64 {
|
||||
continue; // window interior is owned by the local search
|
||||
}
|
||||
let raw_n = ema_stencil_sum(ema, n);
|
||||
if raw_n < guard.raw_margin * raw_b {
|
||||
continue;
|
||||
}
|
||||
let qn = arm_count(ema, n, guard.tau);
|
||||
let better = best.is_none_or(|(_, sq, sr)| qn > sq || (qn == sq && raw_n > sr));
|
||||
if better {
|
||||
best = Some((qn, raw_n));
|
||||
best = Some((n, qn, raw_n));
|
||||
}
|
||||
}
|
||||
if let Some((n, qn, _)) = best {
|
||||
if qn >= qb + guard.q_margin && qn >= guard.q_min {
|
||||
target = n;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let mut best: Option<(usize, f64)> = None;
|
||||
for &delta in &[-OCTAVE_BINS, OCTAVE_BINS] {
|
||||
let n = b + delta;
|
||||
if n < 0 || n as usize >= brk_oracle::NUM_BINS {
|
||||
continue;
|
||||
}
|
||||
let qn = arm_count(ema, n, guard.tau);
|
||||
let raw_n = ema_stencil_sum(ema, n);
|
||||
if qn >= qb + guard.q_margin
|
||||
&& qn >= guard.q_min
|
||||
&& raw_n >= guard.raw_margin * raw_b
|
||||
{
|
||||
let better = best.is_none_or(|(sq, sr)| qn > sq || (qn == sq && raw_n > sr));
|
||||
if better {
|
||||
best = Some((qn, raw_n));
|
||||
target = n;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if target != b {
|
||||
return target as f64;
|
||||
@@ -480,7 +525,7 @@ fn main() {
|
||||
max_outputs,
|
||||
);
|
||||
eprintln!(
|
||||
" cfg: window_size={} alpha={:.5} (~{:.0}-block span) search -{}/+{} guard={} (tau={} raw={} qm={} qmin={})",
|
||||
" cfg: window_size={} alpha={:.5} (~{:.0}-block span) search -{}/+{} guard={} (tau={} raw={} qm={} qmin={}) global={} radius={}",
|
||||
config.window_size,
|
||||
config.alpha,
|
||||
2.0 / config.alpha - 1.0,
|
||||
@@ -491,6 +536,8 @@ fn main() {
|
||||
guard.raw_margin,
|
||||
guard.q_margin,
|
||||
guard.q_min,
|
||||
guard.global,
|
||||
guard.global_radius,
|
||||
);
|
||||
let (sb, sa) = (config.search_below, config.search_above);
|
||||
let window_size = config.window_size;
|
||||
@@ -504,6 +551,31 @@ fn main() {
|
||||
let mut ema = HistogramEma::zeros();
|
||||
let mut ref_bin = cents_to_bin(start_price * 100.0);
|
||||
|
||||
// Lever 4: a parallel "sharp" detection EMA (fast span, short window) folded
|
||||
// from the same per-block hists. The slow EMA above still sets the price; this
|
||||
// is diagnostic only, used to check whether the true-price stencil holes (the
|
||||
// arm-count contrast that the smeared slow EMA flattens during a crash) survive
|
||||
// when the histogram is not smoothed.
|
||||
let sharp_span: f64 = std::env::var("SHARP_SPAN")
|
||||
.ok()
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or(3.0);
|
||||
let sharp_window: usize = std::env::var("SHARP_WINDOW")
|
||||
.ok()
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or(6);
|
||||
let sharp_alpha = 2.0 / (sharp_span + 1.0);
|
||||
let sharp_weights: Vec<f64> = (0..sharp_window)
|
||||
.map(|i| sharp_alpha * (1.0 - sharp_alpha).powi(i as i32))
|
||||
.collect();
|
||||
let mut sharp_ring: Vec<Vec<f64>> = vec![vec![0.0; NUM_BINS]; sharp_window];
|
||||
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}"
|
||||
);
|
||||
|
||||
let total_txs = indexer.vecs.transactions.txid.len();
|
||||
let total_outputs = indexer.vecs.outputs.value.len();
|
||||
|
||||
@@ -600,6 +672,26 @@ fn main() {
|
||||
ema[b] += w * block[b];
|
||||
}
|
||||
}
|
||||
// Sharp detection EMA (diagnostic only - does not drive the price).
|
||||
{
|
||||
let slot = &mut sharp_ring[sharp_cursor];
|
||||
for b in 0..NUM_BINS {
|
||||
slot[b] = hist[b] as f64 * scale;
|
||||
}
|
||||
}
|
||||
sharp_cursor = (sharp_cursor + 1) % sharp_window;
|
||||
if sharp_filled < sharp_window {
|
||||
sharp_filled += 1;
|
||||
}
|
||||
sharp_ema.fill(0.0);
|
||||
for age in 0..sharp_filled {
|
||||
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];
|
||||
}
|
||||
}
|
||||
ref_bin = guarded_best_bin(&ema, ref_bin, sb, sa, &guard);
|
||||
let oracle_price = bin_to_cents(ref_bin) as f64 / 100.0;
|
||||
|
||||
@@ -632,9 +724,14 @@ fn main() {
|
||||
let qh = arm_count(&ema, true_bin + 60, guard.tau);
|
||||
let qd = arm_count(&ema, true_bin - 60, guard.tau);
|
||||
let pat = arm_pattern(&ema, true_bin, guard.tau);
|
||||
// Same arm-count contrast measured on the sharp detection EMA.
|
||||
let qst = arm_count(&sharp_ema, true_bin, guard.tau);
|
||||
let qsh = arm_count(&sharp_ema, true_bin + 60, guard.tau);
|
||||
let qsd = arm_count(&sharp_ema, true_bin - 60, guard.tau);
|
||||
let spat = arm_pattern(&sharp_ema, true_bin, guard.tau);
|
||||
let ts_secs: u32 = *timestamps[h];
|
||||
eprintln!(
|
||||
"{h}\t{ts_secs}\t{oracle_price:.0}\t{ex_close:.0}\t{band_err:+.2}\t{eligible}\tT={s_true:.1}\tH={s_half:.1}\tD={s_dbl:.1}\tQt={qt}\tQh={qh}\tQd={qd}\t{pat}"
|
||||
"{h}\t{ts_secs}\t{oracle_price:.0}\t{ex_close:.0}\t{band_err:+.2}\t{eligible}\tT={s_true:.1}\tH={s_half:.1}\tD={s_dbl:.1}\tQt={qt}\tQh={qh}\tQd={qd}\t{pat}\t|sharp Qt={qst} Qh={qsh} Qd={qsd}\t{spat}"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -342,9 +342,10 @@ impl Oracle {
|
||||
let idx = (self.cursor + self.config.window_size - 1 - age) % self.config.window_size;
|
||||
let weight = self.weights[age];
|
||||
let h = &self.histograms[idx];
|
||||
(0..NUM_BINS).for_each(|bin| {
|
||||
self.ema[bin] += weight * h[bin] as f64;
|
||||
});
|
||||
self.ema
|
||||
.iter_mut()
|
||||
.zip(h.iter())
|
||||
.for_each(|(e, &c)| *e += weight * c as f64);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,6 +177,20 @@ All errors return structured JSON with a consistent format:
|
||||
),
|
||||
..Default::default()
|
||||
},
|
||||
Tag {
|
||||
name: "Oracle".to_string(),
|
||||
description: Some(
|
||||
"On-chain BTC/USD price derived purely from round-dollar payment patterns in \
|
||||
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`."
|
||||
.to_string(),
|
||||
),
|
||||
..Default::default()
|
||||
},
|
||||
Tag {
|
||||
name: "URPD".to_string(),
|
||||
description: Some(
|
||||
|
||||
@@ -31,10 +31,9 @@ impl OracleRoutes for ApiRouter<AppState> {
|
||||
.oracle_tag()
|
||||
.summary("Live BTC/USD price")
|
||||
.description(
|
||||
"Current BTC/USD price in dollars, derived purely from on-chain \
|
||||
round-dollar output patterns over the last 12 blocks plus the \
|
||||
forming mempool block. Same value as `/api/mempool/price`. \
|
||||
Confirmed per-height history is available at `/api/vecs/height-to-price`.",
|
||||
"Current BTC/USD price in dollars. Same value as \
|
||||
`/api/mempool/price`. Confirmed per-height history is available at \
|
||||
`/api/vecs/height-to-price`.",
|
||||
)
|
||||
.json_response::<Dollars>()
|
||||
.not_modified()
|
||||
@@ -58,9 +57,8 @@ impl OracleRoutes for ApiRouter<AppState> {
|
||||
.summary("Live EMA histogram")
|
||||
.description(
|
||||
"Smoothed round-dollar payment histogram at the live tip: the \
|
||||
committed 12-block EMA with the forming mempool block blended in \
|
||||
as a final slot. A flat array of 2400 log-scale bins, quantized \
|
||||
to `u16` for the wire. This is the heatmap column you render.",
|
||||
committed EMA with the forming mempool block blended in. \
|
||||
A flat array of log-scale bins.",
|
||||
)
|
||||
.json_response::<HistogramEmaCompact>()
|
||||
.not_modified()
|
||||
@@ -88,11 +86,8 @@ impl OracleRoutes for ApiRouter<AppState> {
|
||||
.oracle_tag()
|
||||
.summary("EMA histogram at height")
|
||||
.description(
|
||||
"Smoothed round-dollar payment histogram for a confirmed height, \
|
||||
deterministically reconstructed by replaying the 12-block window \
|
||||
ending at that height. Immutable once buried, so repeated requests \
|
||||
return byte-identical results. A flat array of 2400 log-scale bins, \
|
||||
quantized to `u16`.",
|
||||
"Smoothed round-dollar payment histogram for a confirmed height. \
|
||||
A flat array of log-scale bins.",
|
||||
)
|
||||
.json_response::<HistogramEmaCompact>()
|
||||
.not_modified()
|
||||
@@ -118,8 +113,7 @@ impl OracleRoutes for ApiRouter<AppState> {
|
||||
.summary("Live raw histogram")
|
||||
.description(
|
||||
"Un-smoothed per-block round-dollar counts for the forming mempool \
|
||||
block: the spiky primitive the EMA smooths over. A flat array of \
|
||||
2400 log-scale bins (`u32` counts), all zero when no mempool is \
|
||||
block. A flat array of log-scale bins, all zero when no mempool is \
|
||||
configured.",
|
||||
)
|
||||
.json_response::<HistogramRaw>()
|
||||
@@ -149,7 +143,7 @@ impl OracleRoutes for ApiRouter<AppState> {
|
||||
.summary("Raw histogram at height")
|
||||
.description(
|
||||
"Un-smoothed round-dollar counts for a single confirmed block. A \
|
||||
flat array of 2400 log-scale bins (`u32` counts).",
|
||||
flat array of log-scale bins.",
|
||||
)
|
||||
.json_response::<HistogramRaw>()
|
||||
.not_modified()
|
||||
|
||||
Reference in New Issue
Block a user