mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-06-08 14:11:56 -07:00
heatmaps: part 12
This commit is contained in:
Generated
+20
-14
@@ -221,7 +221,7 @@ dependencies = [
|
||||
"quote",
|
||||
"regex",
|
||||
"rustc-hash",
|
||||
"shlex",
|
||||
"shlex 1.3.0",
|
||||
"syn",
|
||||
]
|
||||
|
||||
@@ -757,14 +757,14 @@ checksum = "1c53ba0f290bfc610084c05582d9c5d421662128fc69f4bf236707af6fd321b9"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.62"
|
||||
version = "1.2.63"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98"
|
||||
checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"jobserver",
|
||||
"libc",
|
||||
"shlex",
|
||||
"shlex 2.0.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1567,9 +1567,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "1.10.0"
|
||||
version = "1.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eb92f162bf56536459fc83c79b974bb12837acfed43d6bc370a7916d0ae15ecc"
|
||||
checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498"
|
||||
dependencies = [
|
||||
"atomic-waker",
|
||||
"bytes",
|
||||
@@ -1838,9 +1838,9 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "jiff"
|
||||
version = "0.2.27"
|
||||
version = "0.2.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "392c70591e8749fe235ddaf513e6f58b26bce3dcc16524cecc8936f75afa161e"
|
||||
checksum = "4603d3033e49e2b0e31229fcab20a5d40089c607d975cd9c80551dc69eed9102"
|
||||
dependencies = [
|
||||
"jiff-static",
|
||||
"log",
|
||||
@@ -1852,9 +1852,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "jiff-static"
|
||||
version = "0.2.27"
|
||||
version = "0.2.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47b605b0c050d845fc355bb11eb3f9a8deddc218ea60c76e61aa1f2adfb2c96a"
|
||||
checksum = "782d32378dddf207193ac91cefb848ad41abb58195c95168e1291227a0832b47"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -2839,6 +2839,12 @@ version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
|
||||
|
||||
[[package]]
|
||||
name = "simd-adler32"
|
||||
version = "0.3.9"
|
||||
@@ -3840,18 +3846,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.49"
|
||||
version = "0.8.50"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bce33a6288fa3f072a8c2c7d0f2fdbb90e28298f0135c1f99b96c3db2efcc60b"
|
||||
checksum = "3b065d4f0e55f82fae73202e189638116a87c55ab6b8e6c2721e13dd9d854ad1"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.8.49"
|
||||
version = "0.8.50"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fd425244944f4ab65ccff928e7323354c5a018c75838362fdce749dfad2ee1e"
|
||||
checksum = "0b631b19d36a892ab55420c92dbc83ccd79274f25be714855d3074aa71cab639"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
||||
+1
-1
@@ -66,7 +66,7 @@ corepc-types = { version = "0.14.0", features = ["std"], default-features = fals
|
||||
derive_more = { version = "2.1.1", features = ["deref", "deref_mut"] }
|
||||
fjall = "3.1.4"
|
||||
indexmap = { version = "2.14.0", features = ["serde"] }
|
||||
jiff = { version = "0.2.27", features = ["perf-inline", "tz-system"], default-features = false }
|
||||
jiff = { version = "0.2.28", features = ["perf-inline", "tz-system"], default-features = false }
|
||||
owo-colors = "4.3.0"
|
||||
parking_lot = "0.12.5"
|
||||
pco = "1.0.2"
|
||||
|
||||
@@ -115,7 +115,13 @@ impl Vecs {
|
||||
let seed_bin = cents_to_bin(prev_cents.inner() as f64);
|
||||
let warmup = config.window_size.min(committed - START_HEIGHT_SLOW);
|
||||
let mut oracle = Oracle::from_checkpoint(seed_bin, config, |o| {
|
||||
Self::feed_blocks(o, indexer, (committed - warmup)..committed, None);
|
||||
Self::feed_blocks_with(
|
||||
o,
|
||||
indexer,
|
||||
(committed - warmup)..committed,
|
||||
None,
|
||||
|_, _, _| {},
|
||||
);
|
||||
});
|
||||
|
||||
let num_new = total_heights - committed;
|
||||
|
||||
@@ -6,7 +6,7 @@ use std::path::PathBuf;
|
||||
|
||||
use brk_indexer::Indexer;
|
||||
use brk_oracle::{
|
||||
Config, Oracle, PRICES, HistogramRaw, START_HEIGHT_FAST, bin_to_cents, cents_to_bin,
|
||||
Config, HistogramRaw, Oracle, PRICES, START_HEIGHT_FAST, bin_to_cents, cents_to_bin,
|
||||
eligible_bin,
|
||||
};
|
||||
use brk_types::{OutputType, Sats, TxIndex, TxOutIndex};
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
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).
|
||||
pub const START_HEIGHT_SLOW: usize = 340_000;
|
||||
@@ -61,4 +63,41 @@ impl Config {
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Split a block range into sub-ranges with a single EMA configuration.
|
||||
pub fn segments_for_range(range: Range<usize>) -> impl Iterator<Item = Range<usize>> {
|
||||
let split = START_HEIGHT_FAST.clamp(range.start, range.end);
|
||||
[range.start..split, split..range.end]
|
||||
.into_iter()
|
||||
.filter(|range| !range.is_empty())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn segments_for_range_splits_at_fast_start() {
|
||||
let segments: Vec<_> =
|
||||
Config::segments_for_range((START_HEIGHT_FAST - 2)..(START_HEIGHT_FAST + 2)).collect();
|
||||
assert_eq!(
|
||||
segments,
|
||||
vec![
|
||||
(START_HEIGHT_FAST - 2)..START_HEIGHT_FAST,
|
||||
START_HEIGHT_FAST..(START_HEIGHT_FAST + 2),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn segments_for_range_omits_empty_sides() {
|
||||
let slow: Vec<_> =
|
||||
Config::segments_for_range((START_HEIGHT_FAST - 2)..START_HEIGHT_FAST).collect();
|
||||
assert_eq!(slow, vec![(START_HEIGHT_FAST - 2)..START_HEIGHT_FAST]);
|
||||
|
||||
let fast: Vec<_> =
|
||||
Config::segments_for_range(START_HEIGHT_FAST..(START_HEIGHT_FAST + 2)).collect();
|
||||
assert_eq!(fast, vec![START_HEIGHT_FAST..(START_HEIGHT_FAST + 2)]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,4 +169,46 @@ mod tests {
|
||||
|
||||
assert!(switched.ema().iter().eq(fresh.ema().iter()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sequential_ema_matches_fresh_warmups() {
|
||||
let hists: Vec<HistogramRaw> = (0..80)
|
||||
.map(|i| {
|
||||
let mut h = HistogramRaw::zeros();
|
||||
h.increment(1000 + i % 11);
|
||||
h.increment(1300 + i % 13);
|
||||
h.increment(1700 + i % 17);
|
||||
h
|
||||
})
|
||||
.collect();
|
||||
|
||||
for config in [Config::slow(), Config::default()] {
|
||||
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| {
|
||||
hists[query_start + 1 - config.window_size..query_start + 1]
|
||||
.iter()
|
||||
.for_each(|h| {
|
||||
o.process_histogram(h);
|
||||
});
|
||||
});
|
||||
|
||||
for height in query_start..query_end {
|
||||
if height != query_start {
|
||||
sequential.process_histogram(&hists[height]);
|
||||
}
|
||||
|
||||
let fresh = Oracle::from_checkpoint(seed, config.clone(), |o| {
|
||||
hists[height + 1 - config.window_size..height + 1]
|
||||
.iter()
|
||||
.for_each(|h| {
|
||||
o.process_histogram(h);
|
||||
});
|
||||
});
|
||||
|
||||
assert!(sequential.ema().iter().eq(fresh.ema().iter()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +61,11 @@ impl ShapeAnchor {
|
||||
/// 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>(),
|
||||
Some(arms) => {
|
||||
1.0 - (0..N_ARMS)
|
||||
.map(|i| (arms[i] - self.profile[i]).abs())
|
||||
.sum::<f64>()
|
||||
}
|
||||
None => 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,14 +4,15 @@ use brk_computer::prices::Vecs as PricesVecs;
|
||||
use brk_error::{Error, Result};
|
||||
use brk_indexer::Lengths;
|
||||
use brk_oracle::{
|
||||
Config, HistogramEma, HistogramEmaCompact, HistogramRaw, Oracle, START_HEIGHT_FAST,
|
||||
cents_to_bin, sats_to_bin,
|
||||
Config, HistogramEma, HistogramEmaCompact, HistogramRaw, Oracle, cents_to_bin, sats_to_bin,
|
||||
};
|
||||
use brk_types::{Day1, Dollars, Sats, TxOutIndex};
|
||||
use vecdb::{AnyVec, ReadableVec, VecIndex};
|
||||
|
||||
use crate::Query;
|
||||
|
||||
const RAW_HISTOGRAM_VALUE_CHUNK: usize = 1_000_000;
|
||||
|
||||
impl Query {
|
||||
pub fn live_price(&self) -> Result<Dollars> {
|
||||
Ok(self.live_oracle()?.price_dollars())
|
||||
@@ -48,9 +49,9 @@ impl Query {
|
||||
let count = range.len();
|
||||
let mut acc = HistogramEma::zeros();
|
||||
|
||||
for segment in ema_config_segments(range) {
|
||||
for segment in Config::segments_for_range(range) {
|
||||
let mut oracle = self.ema_oracle_at(segment.start, safe)?;
|
||||
add_ema(&mut acc, oracle.ema());
|
||||
acc.add_from(oracle.ema());
|
||||
|
||||
let feed_start = segment.start + 1;
|
||||
if feed_start < segment.end {
|
||||
@@ -59,13 +60,12 @@ impl Query {
|
||||
self.indexer(),
|
||||
feed_start..segment.end,
|
||||
Some(safe),
|
||||
|_, oracle, _| add_ema(&mut acc, oracle.ema()),
|
||||
|_, oracle, _| acc.add_from(oracle.ema()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let count = count as f64;
|
||||
acc.iter_mut().for_each(|a| *a /= count);
|
||||
acc.divide_by(count as f64);
|
||||
Ok(acc)
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ impl Query {
|
||||
/// in the block binned by value, with no payment filtering.
|
||||
pub fn confirmed_histogram_raw(&self, height: usize) -> Result<HistogramRaw> {
|
||||
let safe = self.check_histogram_height(height)?;
|
||||
Ok(self.block_raw_histogram(height, &safe))
|
||||
Ok(self.raw_histogram_for_blocks(height..height + 1, &safe))
|
||||
}
|
||||
|
||||
/// Unfiltered per-bin output counts for a calendar `day`: every block's raw
|
||||
@@ -92,14 +92,7 @@ impl Query {
|
||||
pub fn confirmed_histogram_raw_day(&self, day: Day1) -> Result<HistogramRaw> {
|
||||
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)
|
||||
Ok(self.raw_histogram_for_blocks(range, &safe))
|
||||
}
|
||||
|
||||
/// The live tip oracle: the cached committed base, with the forming block's
|
||||
@@ -225,49 +218,51 @@ impl Query {
|
||||
Ok(start..end)
|
||||
}
|
||||
|
||||
/// One confirmed block's unfiltered histogram: every output in the block,
|
||||
/// Unfiltered histogram for a contiguous confirmed block range: every output,
|
||||
/// 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 {
|
||||
/// filtering. Raw counts are additive, so a day can be read as one output
|
||||
/// range instead of one block at a time.
|
||||
fn raw_histogram_for_blocks(&self, range: Range<usize>, safe: &Lengths) -> HistogramRaw {
|
||||
let indexer = self.indexer();
|
||||
let total_outputs = safe.txout_index.to_usize();
|
||||
let next_height = (height + 2).min(safe.height.to_usize());
|
||||
let collect_end = (range.end + 1).min(safe.height.to_usize());
|
||||
|
||||
let out_firsts: Vec<TxOutIndex> = indexer
|
||||
.vecs
|
||||
.outputs
|
||||
.first_txout_index
|
||||
.collect_range_at(height, next_height);
|
||||
.collect_range_at(range.start, collect_end);
|
||||
let out_start = out_firsts[0].to_usize();
|
||||
let out_end = out_firsts
|
||||
.get(1)
|
||||
.get(range.end - range.start)
|
||||
.copied()
|
||||
.unwrap_or(TxOutIndex::from(total_outputs))
|
||||
.to_usize();
|
||||
|
||||
let mut hist = HistogramRaw::zeros();
|
||||
let values: Vec<Sats> = indexer.vecs.outputs.value.collect_range_at(out_start, out_end);
|
||||
for sats in values {
|
||||
if let Some(bin) = sats_to_bin(sats) {
|
||||
hist.increment(bin);
|
||||
}
|
||||
let mut values: Vec<Sats> = Vec::new();
|
||||
let mut start = out_start;
|
||||
while start < out_end {
|
||||
let end = (start + RAW_HISTOGRAM_VALUE_CHUNK).min(out_end);
|
||||
values.clear();
|
||||
indexer
|
||||
.vecs
|
||||
.outputs
|
||||
.value
|
||||
.collect_range_into_at(start, end, &mut values);
|
||||
add_sats_to_raw_histogram(&mut hist, &values);
|
||||
start = end;
|
||||
}
|
||||
hist
|
||||
}
|
||||
}
|
||||
|
||||
fn add_ema(acc: &mut HistogramEma, ema: &HistogramEma) {
|
||||
acc.iter_mut()
|
||||
.zip(ema.iter())
|
||||
.for_each(|(a, &e)| *a += e);
|
||||
}
|
||||
|
||||
fn ema_config_segments(range: Range<usize>) -> impl Iterator<Item = Range<usize>> {
|
||||
let split = START_HEIGHT_FAST.clamp(range.start, range.end);
|
||||
[range.start..split, split..range.end]
|
||||
.into_iter()
|
||||
.filter(|range| !range.is_empty())
|
||||
fn add_sats_to_raw_histogram(hist: &mut HistogramRaw, values: &[Sats]) {
|
||||
for &sats in values {
|
||||
if let Some(bin) = sats_to_bin(sats) {
|
||||
hist.increment(bin);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -275,25 +270,24 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn ema_config_segments_split_at_fast_start() {
|
||||
let segments: Vec<_> =
|
||||
ema_config_segments((START_HEIGHT_FAST - 2)..(START_HEIGHT_FAST + 2)).collect();
|
||||
assert_eq!(
|
||||
segments,
|
||||
vec![
|
||||
(START_HEIGHT_FAST - 2)..START_HEIGHT_FAST,
|
||||
START_HEIGHT_FAST..(START_HEIGHT_FAST + 2),
|
||||
]
|
||||
);
|
||||
}
|
||||
fn raw_histogram_accumulation_is_additive() {
|
||||
let values = [
|
||||
Sats::ZERO,
|
||||
Sats::new(1),
|
||||
Sats::new(10),
|
||||
Sats::new(100_000_000),
|
||||
Sats::new(1_000_000_000_000),
|
||||
Sats::new(5_000_000_000),
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn ema_config_segments_omits_empty_sides() {
|
||||
let slow: Vec<_> = ema_config_segments((START_HEIGHT_FAST - 2)..START_HEIGHT_FAST).collect();
|
||||
assert_eq!(slow, vec![(START_HEIGHT_FAST - 2)..START_HEIGHT_FAST]);
|
||||
let mut one_shot = HistogramRaw::zeros();
|
||||
add_sats_to_raw_histogram(&mut one_shot, &values);
|
||||
|
||||
let fast: Vec<_> =
|
||||
ema_config_segments(START_HEIGHT_FAST..(START_HEIGHT_FAST + 2)).collect();
|
||||
assert_eq!(fast, vec![START_HEIGHT_FAST..(START_HEIGHT_FAST + 2)]);
|
||||
let mut chunked = HistogramRaw::zeros();
|
||||
for chunk in values.chunks(2) {
|
||||
add_sats_to_raw_histogram(&mut chunked, chunk);
|
||||
}
|
||||
|
||||
assert!(one_shot.iter().eq(chunked.iter()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,6 +66,21 @@ impl<const N: usize> Histogram<f64, N> {
|
||||
}
|
||||
Histogram(out)
|
||||
}
|
||||
|
||||
/// Add another floating-point histogram bin-by-bin.
|
||||
#[inline]
|
||||
pub fn add_from(&mut self, rhs: &Self) {
|
||||
self.0
|
||||
.iter_mut()
|
||||
.zip(rhs.0.iter())
|
||||
.for_each(|(a, &b)| *a += b);
|
||||
}
|
||||
|
||||
/// Divide every bin by `rhs`.
|
||||
#[inline]
|
||||
pub fn divide_by(&mut self, rhs: f64) {
|
||||
self.0.iter_mut().for_each(|v| *v /= rhs);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Serialize, const N: usize> Serialize for Histogram<T, N> {
|
||||
@@ -120,3 +135,26 @@ impl<T: JsonSchema, const N: usize> JsonSchema for Histogram<T, N> {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn float_histogram_add_and_divide_are_binwise() {
|
||||
let mut a = Histogram::<f64, 3>::zeros();
|
||||
a[0] = 1.0;
|
||||
a[1] = 2.0;
|
||||
a[2] = 3.0;
|
||||
|
||||
let mut b = Histogram::<f64, 3>::zeros();
|
||||
b[0] = 3.0;
|
||||
b[1] = 4.0;
|
||||
b[2] = 5.0;
|
||||
|
||||
a.add_from(&b);
|
||||
a.divide_by(2.0);
|
||||
|
||||
assert_eq!(*a, [2.0, 3.0, 4.0]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,9 @@ mod block_rewards_entry;
|
||||
mod block_size_entry;
|
||||
mod block_sizes_weights;
|
||||
mod block_status;
|
||||
mod block_template;
|
||||
mod block_template_diff;
|
||||
mod block_template_diff_entry;
|
||||
mod block_timestamp;
|
||||
mod block_tx_index;
|
||||
mod block_weight_entry;
|
||||
@@ -81,9 +84,6 @@ mod hour4;
|
||||
mod index;
|
||||
mod index_info;
|
||||
mod limit;
|
||||
mod block_template;
|
||||
mod block_template_diff;
|
||||
mod block_template_diff_entry;
|
||||
mod mempool_block;
|
||||
mod mempool_entry_info;
|
||||
mod mempool_info;
|
||||
|
||||
@@ -59,7 +59,12 @@ impl UrpdAggregation {
|
||||
let size = self.linear_size_cents().unwrap();
|
||||
(price_cents / size) * size
|
||||
}
|
||||
Self::Log10 | Self::Log50 | Self::Log100 | Self::Log200 | Self::Log500 | Self::Log1000 => {
|
||||
Self::Log10
|
||||
| Self::Log50
|
||||
| Self::Log100
|
||||
| Self::Log200
|
||||
| Self::Log500
|
||||
| Self::Log1000 => {
|
||||
if price_cents == Cents::ZERO {
|
||||
return Cents::ZERO;
|
||||
}
|
||||
|
||||
@@ -306,7 +306,7 @@ export function createPartialOptions() {
|
||||
tree: [
|
||||
demoHeatmapOption,
|
||||
{
|
||||
name: "Oracle",
|
||||
name: "output values",
|
||||
tree: [oracleRawHeatmapOption, oracleEmaHeatmapOption],
|
||||
},
|
||||
],
|
||||
|
||||
+121
-10
@@ -3,6 +3,7 @@
|
||||
|
||||
import { createHeader, createSelect } from "../../scripts/utils/dom.js";
|
||||
import { heatmapElement } from "../../scripts/utils/elements.js";
|
||||
import { createPersistedValue } from "../../scripts/utils/persisted.js";
|
||||
import { debounce, next } from "../../scripts/utils/timing.js";
|
||||
import { dark, onChange as onThemeChange } from "../../scripts/utils/theme.js";
|
||||
import { createRenderer } from "./renderer.js";
|
||||
@@ -87,7 +88,9 @@ export function setOption(option) {
|
||||
if (currentOption !== option) {
|
||||
currentOption = option;
|
||||
pointsByDate = new Map();
|
||||
updateDateControls(option);
|
||||
updateYControls(option);
|
||||
renderRangeControls();
|
||||
if (headingElement) headingElement.textContent = option.title;
|
||||
if (canvas) canvas.removeAttribute("title");
|
||||
}
|
||||
@@ -331,11 +334,49 @@ function createRangeControls() {
|
||||
|
||||
statusElement = document.createElement("small");
|
||||
|
||||
return fieldset;
|
||||
}
|
||||
|
||||
/** @param {HeatmapOption} option */
|
||||
function updateDateControls(option) {
|
||||
const currentYear = new Date().getUTCFullYear();
|
||||
const fromChoices = createFromChoices(currentYear);
|
||||
const toChoices = createToChoices(currentYear);
|
||||
let fromChoice = fromChoices.at(-1) ?? fromChoices[0];
|
||||
let toChoice = toChoices[0];
|
||||
const defaultFromChoice = fromChoices.at(-1) ?? fromChoices[0];
|
||||
const defaultToChoice = toChoices[0];
|
||||
|
||||
const persistedFrom = createHeatmapPersistedValue(
|
||||
option,
|
||||
"from",
|
||||
"hm_from",
|
||||
rangeChoiceLabel(defaultFromChoice),
|
||||
);
|
||||
const persistedTo = createHeatmapPersistedValue(
|
||||
option,
|
||||
"to",
|
||||
"hm_to",
|
||||
rangeChoiceLabel(defaultToChoice),
|
||||
);
|
||||
|
||||
let fromChoice = findChoiceByKey(
|
||||
fromChoices,
|
||||
persistedFrom.value,
|
||||
defaultFromChoice,
|
||||
rangeChoiceLabel,
|
||||
);
|
||||
let toChoice = findChoiceByKey(
|
||||
toChoices,
|
||||
persistedTo.value,
|
||||
defaultToChoice,
|
||||
rangeChoiceLabel,
|
||||
);
|
||||
|
||||
if (fromChoice.date > toChoice.date) {
|
||||
toChoice = findSameLabelChoice(toChoices, fromChoice, defaultToChoice);
|
||||
}
|
||||
from = fromChoice.date;
|
||||
to = toChoice.date;
|
||||
persistDateChoices();
|
||||
|
||||
const fromSelect = createSelect({
|
||||
id: "heatmap-from",
|
||||
@@ -345,9 +386,10 @@ function createRangeControls() {
|
||||
onChange(choice) {
|
||||
fromChoice = choice;
|
||||
if (fromChoice.date > toChoice.date) {
|
||||
toChoice = findSameLabelChoice(toChoices, fromChoice, toChoices[0]);
|
||||
toChoice = findSameLabelChoice(toChoices, fromChoice, defaultToChoice);
|
||||
toSelect.set(toChoice);
|
||||
}
|
||||
persistDateChoices();
|
||||
setRange(fromChoice.date, toChoice.date);
|
||||
},
|
||||
toKey: rangeChoiceLabel,
|
||||
@@ -361,9 +403,10 @@ function createRangeControls() {
|
||||
onChange(choice) {
|
||||
toChoice = choice;
|
||||
if (fromChoice.date > toChoice.date) {
|
||||
fromChoice = findSameLabelChoice(fromChoices, toChoice, fromChoices[0]);
|
||||
fromChoice = findSameLabelChoice(fromChoices, toChoice, defaultFromChoice);
|
||||
fromSelect.set(fromChoice);
|
||||
}
|
||||
persistDateChoices();
|
||||
setRange(fromChoice.date, toChoice.date);
|
||||
},
|
||||
toKey: rangeChoiceLabel,
|
||||
@@ -371,9 +414,11 @@ function createRangeControls() {
|
||||
});
|
||||
|
||||
dateControlElements = [fromSelect.element, toSelect.element];
|
||||
renderRangeControls();
|
||||
|
||||
return fieldset;
|
||||
function persistDateChoices() {
|
||||
persistedFrom.setImmediate(rangeChoiceLabel(fromChoice));
|
||||
persistedTo.setImmediate(rangeChoiceLabel(toChoice));
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {HeatmapOption} option */
|
||||
@@ -384,14 +429,42 @@ function updateYControls(option) {
|
||||
yMin = undefined;
|
||||
yMax = undefined;
|
||||
yControlElements = [];
|
||||
renderRangeControls();
|
||||
return;
|
||||
}
|
||||
|
||||
let minChoice = choices[0];
|
||||
let maxChoice = choices.at(-1) ?? choices[0];
|
||||
const defaultMinChoice = choices[0];
|
||||
const defaultMaxChoice = choices.at(-1) ?? choices[0];
|
||||
const persistedMin = createHeatmapPersistedValue(
|
||||
option,
|
||||
"y-min",
|
||||
"hm_y_min",
|
||||
axisChoiceKey(defaultMinChoice),
|
||||
);
|
||||
const persistedMax = createHeatmapPersistedValue(
|
||||
option,
|
||||
"y-max",
|
||||
"hm_y_max",
|
||||
axisChoiceKey(defaultMaxChoice),
|
||||
);
|
||||
|
||||
let minChoice = findChoiceByKey(
|
||||
choices,
|
||||
persistedMin.value,
|
||||
defaultMinChoice,
|
||||
axisChoiceKey,
|
||||
);
|
||||
let maxChoice = findChoiceByKey(
|
||||
choices,
|
||||
persistedMax.value,
|
||||
defaultMaxChoice,
|
||||
axisChoiceKey,
|
||||
);
|
||||
if (minChoice.value > maxChoice.value) {
|
||||
maxChoice = minChoice;
|
||||
}
|
||||
yMin = minChoice.value;
|
||||
yMax = maxChoice.value;
|
||||
persistYChoices();
|
||||
|
||||
const minSelect = createSelect({
|
||||
id: "heatmap-y-min",
|
||||
@@ -404,6 +477,7 @@ function updateYControls(option) {
|
||||
maxChoice = minChoice;
|
||||
maxSelect.set(maxChoice);
|
||||
}
|
||||
persistYChoices();
|
||||
setYRange(minChoice.value, maxChoice.value);
|
||||
},
|
||||
toKey: axisChoiceKey,
|
||||
@@ -420,6 +494,7 @@ function updateYControls(option) {
|
||||
minChoice = maxChoice;
|
||||
minSelect.set(minChoice);
|
||||
}
|
||||
persistYChoices();
|
||||
setYRange(minChoice.value, maxChoice.value);
|
||||
},
|
||||
toKey: axisChoiceKey,
|
||||
@@ -427,7 +502,11 @@ function updateYControls(option) {
|
||||
});
|
||||
|
||||
yControlElements = [minSelect.element, maxSelect.element];
|
||||
renderRangeControls();
|
||||
|
||||
function persistYChoices() {
|
||||
persistedMin.setImmediate(axisChoiceKey(minChoice));
|
||||
persistedMax.setImmediate(axisChoiceKey(maxChoice));
|
||||
}
|
||||
}
|
||||
|
||||
function renderRangeControls() {
|
||||
@@ -513,6 +592,38 @@ function axisChoiceLabel(choice) {
|
||||
return choice.label;
|
||||
}
|
||||
|
||||
/** @param {HeatmapOption} option */
|
||||
function heatmapStoragePrefix(option) {
|
||||
return `heatmap-${option.path.join("-")}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HeatmapOption} option
|
||||
* @param {string} key
|
||||
* @param {string} urlKey
|
||||
* @param {string} defaultValue
|
||||
*/
|
||||
function createHeatmapPersistedValue(option, key, urlKey, defaultValue) {
|
||||
return createPersistedValue({
|
||||
defaultValue,
|
||||
storageKey: `${heatmapStoragePrefix(option)}-${key}`,
|
||||
urlKey,
|
||||
serialize: (value) => value,
|
||||
deserialize: (value) => value,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param {readonly T[]} choices
|
||||
* @param {string} key
|
||||
* @param {T} fallback
|
||||
* @param {(choice: T) => string} toKey
|
||||
*/
|
||||
function findChoiceByKey(choices, key, fallback, toKey) {
|
||||
return choices.find((candidate) => toKey(candidate) === key) ?? fallback;
|
||||
}
|
||||
|
||||
/** @param {number} year */
|
||||
function yearStartISODate(year) {
|
||||
return toISODate(new Date(Date.UTC(year, 0, 1)));
|
||||
|
||||
@@ -25,8 +25,11 @@ const AMOUNT_CHOICES = [
|
||||
{ label: "10k BTC", value: 4 },
|
||||
];
|
||||
|
||||
export const oracleRawHeatmapOption = createOracleHeatmapOption("raw", "Raw");
|
||||
export const oracleEmaHeatmapOption = createOracleHeatmapOption("ema", "EMA");
|
||||
export const oracleRawHeatmapOption = createOracleHeatmapOption("raw", "raw");
|
||||
export const oracleEmaHeatmapOption = createOracleHeatmapOption(
|
||||
"ema",
|
||||
"smoothed",
|
||||
);
|
||||
|
||||
/**
|
||||
* @param {"raw" | "ema"} mode
|
||||
@@ -37,7 +40,7 @@ function createOracleHeatmapOption(mode, name) {
|
||||
return {
|
||||
kind: "heatmap",
|
||||
name,
|
||||
title: `Oracle ${name} Histogram`,
|
||||
title: `${capitalize(name)} Output Value Distribution`,
|
||||
points: {
|
||||
fetch: (date, signal, onPoints) =>
|
||||
fetchOraclePoints(mode, date, signal, onPoints),
|
||||
@@ -135,3 +138,8 @@ function formatNumber(value) {
|
||||
function trimNumber(value) {
|
||||
return value.replace(/\.?0+$/, "");
|
||||
}
|
||||
|
||||
/** @param {string} value */
|
||||
function capitalize(value) {
|
||||
return value.charAt(0).toUpperCase() + value.slice(1);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user