global: snapshot

This commit is contained in:
nym21
2026-02-27 23:00:43 +01:00
parent d5ec291579
commit 85c7933ad6
41 changed files with 534 additions and 583 deletions
@@ -81,7 +81,7 @@ function indexToDate(index, i) {{
case 'hour4': return new Date(_EPOCH_MS + i * 14400000);
case 'hour12': return new Date(_EPOCH_MS + i * 43200000);
case 'day1': return i === 0 ? _GENESIS : new Date(_DAY_ONE.getTime() + (i - 1) * _MS_PER_DAY);
case 'day3': return new Date(_EPOCH_MS + i * 259200000);
case 'day3': return new Date(_EPOCH_MS - 86400000 + i * 259200000);
case 'week1': return new Date(_GENESIS.getTime() + i * _MS_PER_WEEK);
case 'month1': return _addMonths(i);
case 'month3': return _addMonths(i * 3);
@@ -113,7 +113,7 @@ function dateToIndex(index, d) {{
if (ms < _DAY_ONE.getTime()) return 0;
return 1 + Math.floor((ms - _DAY_ONE.getTime()) / _MS_PER_DAY);
}}
case 'day3': return Math.floor((ms - _EPOCH_MS) / 259200000);
case 'day3': return Math.floor((ms - _EPOCH_MS + 86400000) / 259200000);
case 'week1': return Math.floor((ms - _GENESIS.getTime()) / _MS_PER_WEEK);
case 'month1': return (d.getFullYear() - 2009) * 12 + d.getMonth();
case 'month3': return (d.getFullYear() - 2009) * 4 + Math.floor(d.getMonth() / 3);
@@ -162,7 +162,7 @@ def _index_to_date(index: str, i: int) -> Union[date, datetime]:
elif index == 'day1':
return _GENESIS if i == 0 else _DAY_ONE + timedelta(days=i - 1)
elif index == 'day3':
return _EPOCH.date() + timedelta(days=i * 3)
return _EPOCH.date() - timedelta(days=1) + timedelta(days=i * 3)
elif index == 'week1':
return _GENESIS + timedelta(weeks=i)
elif index == 'month1':
@@ -202,7 +202,7 @@ def _date_to_index(index: str, d: Union[date, datetime]) -> int:
return 0
return 1 + (dd - _DAY_ONE).days
elif index == 'day3':
return (dd - date(2009, 1, 1)).days // 3
return (dd - date(2008, 12, 31)).days // 3
elif index == 'week1':
return (dd - _GENESIS).days // 7
elif index == 'month1':
+27 -103
View File
@@ -2101,52 +2101,6 @@ impl GreedInvestedInvestorNegNetPainPeakSupplyTotalUnrealizedPattern {
}
}
/// Pattern struct for repeated tree structure.
pub struct Day1Day3DifficultyepochHalvingepochHour1Hour12Hour4Minute1Minute10Minute30Minute5Month1Month3Month6Week1Year1Year10Pattern<T> {
pub day1: MetricPattern10<T>,
pub day3: MetricPattern11<T>,
pub difficultyepoch: MetricPattern19<T>,
pub halvingepoch: MetricPattern18<T>,
pub hour1: MetricPattern7<T>,
pub hour12: MetricPattern9<T>,
pub hour4: MetricPattern8<T>,
pub minute1: MetricPattern3<T>,
pub minute10: MetricPattern5<T>,
pub minute30: MetricPattern6<T>,
pub minute5: MetricPattern4<T>,
pub month1: MetricPattern13<T>,
pub month3: MetricPattern14<T>,
pub month6: MetricPattern15<T>,
pub week1: MetricPattern12<T>,
pub year1: MetricPattern16<T>,
pub year10: MetricPattern17<T>,
}
impl<T: DeserializeOwned> Day1Day3DifficultyepochHalvingepochHour1Hour12Hour4Minute1Minute10Minute30Minute5Month1Month3Month6Week1Year1Year10Pattern<T> {
/// Create a new pattern node with accumulated metric name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
day1: MetricPattern10::new(client.clone(), _m(&acc, "day1")),
day3: MetricPattern11::new(client.clone(), _m(&acc, "day3")),
difficultyepoch: MetricPattern19::new(client.clone(), _m(&acc, "difficultyepoch")),
halvingepoch: MetricPattern18::new(client.clone(), _m(&acc, "halvingepoch")),
hour1: MetricPattern7::new(client.clone(), _m(&acc, "hour1")),
hour12: MetricPattern9::new(client.clone(), _m(&acc, "hour12")),
hour4: MetricPattern8::new(client.clone(), _m(&acc, "hour4")),
minute1: MetricPattern3::new(client.clone(), _m(&acc, "minute1")),
minute10: MetricPattern5::new(client.clone(), _m(&acc, "minute10")),
minute30: MetricPattern6::new(client.clone(), _m(&acc, "minute30")),
minute5: MetricPattern4::new(client.clone(), _m(&acc, "minute5")),
month1: MetricPattern13::new(client.clone(), _m(&acc, "month1")),
month3: MetricPattern14::new(client.clone(), _m(&acc, "month3")),
month6: MetricPattern15::new(client.clone(), _m(&acc, "month6")),
week1: MetricPattern12::new(client.clone(), _m(&acc, "week1")),
year1: MetricPattern16::new(client.clone(), _m(&acc, "year1")),
year10: MetricPattern17::new(client.clone(), _m(&acc, "year10")),
}
}
}
/// Pattern struct for repeated tree structure.
pub struct GreedInvestedInvestorNegNetPainSupplyTotalUnrealizedPattern {
pub greed_index: MetricPattern1<Dollars>,
@@ -3093,18 +3047,18 @@ impl BtcSatsUsdPattern {
/// Pattern struct for repeated tree structure.
pub struct CentsSatsUsdPattern {
pub cents: Day1Day3DifficultyepochHalvingepochHour1Hour12Hour4Minute1Minute10Minute30Minute5Month1Month3Month6Week1Year1Year10Pattern<OHLCCents>,
pub sats: Day1Day3DifficultyepochHalvingepochHour1Hour12Hour4Minute1Minute10Minute30Minute5Month1Month3Month6Week1Year1Year10Pattern<OHLCSats>,
pub usd: Day1Day3DifficultyepochHalvingepochHour1Hour12Hour4Minute1Minute10Minute30Minute5Month1Month3Month6Week1Year1Year10Pattern<OHLCDollars>,
pub cents: MetricPattern2<Cents>,
pub sats: MetricPattern2<Sats>,
pub usd: MetricPattern2<Dollars>,
}
impl CentsSatsUsdPattern {
/// Create a new pattern node with accumulated metric name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
cents: Day1Day3DifficultyepochHalvingepochHour1Hour12Hour4Minute1Minute10Minute30Minute5Month1Month3Month6Week1Year1Year10Pattern::new(client.clone(), _m(&acc, "cents")),
sats: Day1Day3DifficultyepochHalvingepochHour1Hour12Hour4Minute1Minute10Minute30Minute5Month1Month3Month6Week1Year1Year10Pattern::new(client.clone(), _m(&acc, "sats")),
usd: Day1Day3DifficultyepochHalvingepochHour1Hour12Hour4Minute1Minute10Minute30Minute5Month1Month3Month6Week1Year1Year10Pattern::new(client.clone(), acc.clone()),
cents: MetricPattern2::new(client.clone(), _m(&acc, "cents")),
sats: MetricPattern2::new(client.clone(), _m(&acc, "sats")),
usd: MetricPattern2::new(client.clone(), acc.clone()),
}
}
}
@@ -3390,7 +3344,7 @@ impl MetricsTree_Blocks_Difficulty {
/// Metrics tree node.
pub struct MetricsTree_Blocks_Time {
pub timestamp: MetricsTree_Blocks_Time_Timestamp,
pub timestamp: MetricPattern1<Timestamp>,
pub date: MetricPattern20<Date>,
pub timestamp_monotonic: MetricPattern20<Timestamp>,
}
@@ -3398,60 +3352,13 @@ pub struct MetricsTree_Blocks_Time {
impl MetricsTree_Blocks_Time {
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
Self {
timestamp: MetricsTree_Blocks_Time_Timestamp::new(client.clone(), format!("{base_path}_timestamp")),
timestamp: MetricPattern1::new(client.clone(), "timestamp".to_string()),
date: MetricPattern20::new(client.clone(), "date".to_string()),
timestamp_monotonic: MetricPattern20::new(client.clone(), "timestamp_monotonic".to_string()),
}
}
}
/// Metrics tree node.
pub struct MetricsTree_Blocks_Time_Timestamp {
pub base: MetricPattern20<Timestamp>,
pub minute1: MetricPattern3<Timestamp>,
pub minute5: MetricPattern4<Timestamp>,
pub minute10: MetricPattern5<Timestamp>,
pub minute30: MetricPattern6<Timestamp>,
pub hour1: MetricPattern7<Timestamp>,
pub hour4: MetricPattern8<Timestamp>,
pub hour12: MetricPattern9<Timestamp>,
pub day1: MetricPattern10<Timestamp>,
pub day3: MetricPattern11<Timestamp>,
pub week1: MetricPattern12<Timestamp>,
pub month1: MetricPattern13<Timestamp>,
pub month3: MetricPattern14<Timestamp>,
pub month6: MetricPattern15<Timestamp>,
pub year1: MetricPattern16<Timestamp>,
pub year10: MetricPattern17<Timestamp>,
pub halvingepoch: MetricPattern18<Timestamp>,
pub difficultyepoch: MetricPattern19<Timestamp>,
}
impl MetricsTree_Blocks_Time_Timestamp {
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
Self {
base: MetricPattern20::new(client.clone(), "timestamp".to_string()),
minute1: MetricPattern3::new(client.clone(), "timestamp_minute1".to_string()),
minute5: MetricPattern4::new(client.clone(), "timestamp_minute5".to_string()),
minute10: MetricPattern5::new(client.clone(), "timestamp_minute10".to_string()),
minute30: MetricPattern6::new(client.clone(), "timestamp_minute30".to_string()),
hour1: MetricPattern7::new(client.clone(), "timestamp_hour1".to_string()),
hour4: MetricPattern8::new(client.clone(), "timestamp_hour4".to_string()),
hour12: MetricPattern9::new(client.clone(), "timestamp_hour12".to_string()),
day1: MetricPattern10::new(client.clone(), "timestamp_day1".to_string()),
day3: MetricPattern11::new(client.clone(), "timestamp_day3".to_string()),
week1: MetricPattern12::new(client.clone(), "timestamp_week1".to_string()),
month1: MetricPattern13::new(client.clone(), "timestamp_month1".to_string()),
month3: MetricPattern14::new(client.clone(), "timestamp_month3".to_string()),
month6: MetricPattern15::new(client.clone(), "timestamp_month6".to_string()),
year1: MetricPattern16::new(client.clone(), "timestamp_year1".to_string()),
year10: MetricPattern17::new(client.clone(), "timestamp_year10".to_string()),
halvingepoch: MetricPattern18::new(client.clone(), "timestamp_halvingepoch".to_string()),
difficultyepoch: MetricPattern19::new(client.clone(), "timestamp_difficultyepoch".to_string()),
}
}
}
/// Metrics tree node.
pub struct MetricsTree_Blocks_Weight {
pub base: MetricPattern20<Weight>,
@@ -6039,7 +5946,7 @@ impl MetricsTree_Pools_Vecs {
/// Metrics tree node.
pub struct MetricsTree_Prices {
pub split: MetricsTree_Prices_Split,
pub ohlc: CentsSatsUsdPattern,
pub ohlc: MetricsTree_Prices_Ohlc,
pub price: MetricsTree_Prices_Price,
}
@@ -6047,7 +5954,7 @@ impl MetricsTree_Prices {
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
Self {
split: MetricsTree_Prices_Split::new(client.clone(), format!("{base_path}_split")),
ohlc: CentsSatsUsdPattern::new(client.clone(), "price_ohlc".to_string()),
ohlc: MetricsTree_Prices_Ohlc::new(client.clone(), format!("{base_path}_ohlc")),
price: MetricsTree_Prices_Price::new(client.clone(), format!("{base_path}_price")),
}
}
@@ -6089,6 +5996,23 @@ impl MetricsTree_Prices_Split_Close {
}
}
/// Metrics tree node.
pub struct MetricsTree_Prices_Ohlc {
pub cents: MetricPattern2<OHLCCents>,
pub usd: MetricPattern2<OHLCDollars>,
pub sats: MetricPattern2<OHLCSats>,
}
impl MetricsTree_Prices_Ohlc {
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
Self {
cents: MetricPattern2::new(client.clone(), "price_ohlc_cents".to_string()),
usd: MetricPattern2::new(client.clone(), "price_ohlc".to_string()),
sats: MetricPattern2::new(client.clone(), "price_ohlc_sats".to_string()),
}
}
}
/// Metrics tree node.
pub struct MetricsTree_Prices_Price {
pub cents: MetricPattern20<Cents>,
+43 -6
View File
@@ -14,12 +14,49 @@ impl Vecs {
starting_indexes: &ComputeIndexes,
exit: &Exit,
) -> Result<()> {
self.time.timestamp.compute_first(
starting_indexes,
&indexer.vecs.blocks.timestamp,
indexes,
exit,
)?;
{
let ts = &mut self.time.timestamp;
macro_rules! period {
($field:ident) => {
ts.$field.compute_transform(
starting_indexes.$field,
&indexes.$field.first_height,
|(idx, _, _)| (idx, idx.to_timestamp()),
exit,
)?;
};
}
period!(minute1);
period!(minute5);
period!(minute10);
period!(minute30);
period!(hour1);
period!(hour4);
period!(hour12);
period!(day1);
period!(day3);
period!(week1);
period!(month1);
period!(month3);
period!(month6);
period!(year1);
period!(year10);
ts.halvingepoch.compute_indirect(
starting_indexes.halvingepoch,
&indexes.halvingepoch.first_height,
&indexer.vecs.blocks.timestamp,
exit,
)?;
ts.difficultyepoch.compute_indirect(
starting_indexes.difficultyepoch,
&indexes.difficultyepoch.first_height,
&indexer.vecs.blocks.timestamp,
exit,
)?;
}
self.count
.compute(indexer, &self.time, starting_indexes, exit)?;
self.interval
@@ -204,7 +204,7 @@ pub(crate) fn process_blocks(
debug!("AddressCache created, entering main loop");
// Cache for day1 lookups - same day1 repeats ~140 times per day
let mut cached_day1 = Day1::default();
let mut cached_day1: Option<Day1> = None;
let mut cached_date_first_height = Height::ZERO;
let mut cached_date_height_count = StoredU64::default();
@@ -428,12 +428,12 @@ pub(crate) fn process_blocks(
// avoiding redundant PcoVec page decompressions.
let date = height_to_date_vec[offset];
let day1 = Day1::try_from(date).unwrap();
let (date_first_height, date_height_count) = if day1 == cached_day1 {
let (date_first_height, date_height_count) = if cached_day1 == Some(day1) {
(cached_date_first_height, cached_date_height_count)
} else {
let fh: Height = day1_to_first_height.collect_one(day1).unwrap();
let hc = day1_to_height_count.collect_one(day1).unwrap();
cached_day1 = day1;
cached_day1 = Some(day1);
cached_date_first_height = fh;
cached_date_height_count = hc;
(fh, hc)
@@ -13,7 +13,8 @@ use brk_types::{
use derive_more::{Deref, DerefMut};
use schemars::JsonSchema;
use vecdb::{
Database, EagerVec, Exit, ImportableVec, PcoVec, ReadableVec, Rw, StorageMode, VecIndex,
AnyVec, Database, EagerVec, Exit, ImportableVec, PcoVec, ReadableVec, Rw, StorageMode,
VecIndex, WritableVec,
};
use crate::{
@@ -59,7 +60,7 @@ where
macro_rules! period {
($idx:ident) => {
ImportableVec::forced_import(db, &format!("{name}_{}", stringify!($idx)), v)?
ImportableVec::forced_import(db, name, v)?
};
}
@@ -76,15 +77,10 @@ where
) -> Result<()> {
macro_rules! period {
($field:ident) => {
self.0.$field.compute_transform(
self.0.$field.compute_indirect(
starting_indexes.$field,
&indexes.$field.first_height,
|(idx, first_h, _)| {
let v = height_source
.collect_one(first_h)
.unwrap_or_else(|| T::from(0_usize));
(idx, v)
},
height_source,
exit,
)?;
};
@@ -122,25 +118,17 @@ where
let src_len = height_source.len();
macro_rules! period {
($field:ident) => {{
let fh = &indexes.$field.first_height;
self.0.$field.compute_transform(
($field:ident) => {
compute_period_extremum(
&mut self.0.$field,
starting_indexes.$field,
fh,
|(idx, first_h, _)| {
let end_h = Height::from(
fh.collect_one_at(idx.to_usize() + 1)
.map(|h: Height| h.to_usize())
.unwrap_or(src_len),
);
let v = height_source
.max(first_h, end_h)
.unwrap_or_else(|| T::from(0_usize));
(idx, v)
},
&indexes.$field.first_height,
height_source,
src_len,
T::max,
exit,
)?;
}};
};
}
period!(minute1);
@@ -175,25 +163,17 @@ where
let src_len = height_source.len();
macro_rules! period {
($field:ident) => {{
let fh = &indexes.$field.first_height;
self.0.$field.compute_transform(
($field:ident) => {
compute_period_extremum(
&mut self.0.$field,
starting_indexes.$field,
fh,
|(idx, first_h, _)| {
let end_h = Height::from(
fh.collect_one_at(idx.to_usize() + 1)
.map(|h: Height| h.to_usize())
.unwrap_or(src_len),
);
let v = height_source
.min(first_h, end_h)
.unwrap_or_else(|| T::from(0_usize));
(idx, v)
},
&indexes.$field.first_height,
height_source,
src_len,
T::min,
exit,
)?;
}};
};
}
period!(minute1);
@@ -217,3 +197,60 @@ where
Ok(())
}
}
/// Compute per-period extremum (max or min) of height_source values.
///
/// Each period's range is `[fh[i]..fh[i+1])` of height_source.
/// Uses a cursor on height_source so each page is decompressed at most once.
fn compute_period_extremum<I: VecIndex, T: ComputedVecValue + JsonSchema>(
out: &mut EagerVec<PcoVec<I, T>>,
starting_index: I,
fh: &impl ReadableVec<I, Height>,
height_source: &impl ReadableVec<Height, T>,
src_len: usize,
better: fn(T, T) -> T,
exit: &Exit,
) -> Result<()> {
out.validate_and_truncate(fh.version() + height_source.version(), starting_index)?;
let mut cursor = height_source.cursor();
Ok(out.repeat_until_complete(exit, |this| {
let skip = this.len();
let end = fh.len();
if skip >= end {
return Ok(());
}
let fh_batch: Vec<Height> = fh.collect_range_at(skip, (end + 1).min(fh.len()));
if cursor.position() < fh_batch[0].to_usize() {
cursor.advance(fh_batch[0].to_usize() - cursor.position());
}
for j in 0..(end - skip) {
let first_h = fh_batch[j].to_usize();
let end_h = fh_batch.get(j + 1).map_or(src_len, |h| h.to_usize());
if cursor.position() < first_h {
cursor.advance(first_h - cursor.position());
}
let range_len = end_h.saturating_sub(first_h);
let v = if range_len > 0 {
cursor
.fold(range_len, None, |acc, b| {
Some(match acc {
Some(a) => better(a, b),
None => b,
})
})
.unwrap_or_else(|| T::from(0_usize))
} else {
T::from(0_usize)
};
this.checked_push_at(skip + j, v)?;
}
Ok(())
})?)
}
@@ -59,7 +59,7 @@ where
macro_rules! period {
($idx:ident) => {
LazyVecFrom1::transformed::<Transform>(
&format!("{name}_{}", stringify!($idx)),
name,
version,
source.$idx.read_only_boxed_clone(),
)
+66 -20
View File
@@ -65,10 +65,17 @@ impl Computer {
let i = Instant::now();
let (indexes, positions) = thread::scope(|s| -> Result<_> {
let positions_handle = big_thread().spawn_scoped(s, || -> Result<_> {
Ok(Box::new(positions::Vecs::forced_import(&computed_path, VERSION)?))
Ok(Box::new(positions::Vecs::forced_import(
&computed_path,
VERSION,
)?))
})?;
let indexes = Box::new(indexes::Vecs::forced_import(&computed_path, VERSION, indexer)?);
let indexes = Box::new(indexes::Vecs::forced_import(
&computed_path,
VERSION,
indexer,
)?);
let positions = positions_handle.join().unwrap()?;
Ok((indexes, positions))
@@ -79,11 +86,19 @@ impl Computer {
let i = Instant::now();
let (inputs, outputs) = thread::scope(|s| -> Result<_> {
let inputs_handle = big_thread().spawn_scoped(s, || -> Result<_> {
Ok(Box::new(inputs::Vecs::forced_import(&computed_path, VERSION, &indexes)?))
Ok(Box::new(inputs::Vecs::forced_import(
&computed_path,
VERSION,
&indexes,
)?))
})?;
let outputs_handle = big_thread().spawn_scoped(s, || -> Result<_> {
Ok(Box::new(outputs::Vecs::forced_import(&computed_path, VERSION, &indexes)?))
Ok(Box::new(outputs::Vecs::forced_import(
&computed_path,
VERSION,
&indexes,
)?))
})?;
let inputs = inputs_handle.join().unwrap()?;
@@ -96,7 +111,11 @@ impl Computer {
let i = Instant::now();
let constants = Box::new(constants::Vecs::new(VERSION, &indexes));
// Price must be created before market since market's lazy vecs reference price
let prices = Box::new(prices::Vecs::forced_import(&computed_path, VERSION, &indexes)?);
let prices = Box::new(prices::Vecs::forced_import(
&computed_path,
VERSION,
&indexes,
)?);
info!("Imported price/constants in {:?}", i.elapsed());
let i = Instant::now();
@@ -104,12 +123,21 @@ impl Computer {
thread::scope(|s| -> Result<_> {
// Import blocks module (no longer needs prices)
let blocks_handle = big_thread().spawn_scoped(s, || -> Result<_> {
Ok(Box::new(blocks::Vecs::forced_import(&computed_path, VERSION, indexer, &indexes)?))
Ok(Box::new(blocks::Vecs::forced_import(
&computed_path,
VERSION,
indexer,
&indexes,
)?))
})?;
// Import mining module (separate database)
let mining_handle = big_thread().spawn_scoped(s, || -> Result<_> {
Ok(Box::new(mining::Vecs::forced_import(&computed_path, VERSION, &indexes)?))
Ok(Box::new(mining::Vecs::forced_import(
&computed_path,
VERSION,
&indexes,
)?))
})?;
// Import transactions module
@@ -130,9 +158,11 @@ impl Computer {
)?))
})?;
let cointime = Box::new(
cointime::Vecs::forced_import(&computed_path, VERSION, &indexes)?
);
let cointime = Box::new(cointime::Vecs::forced_import(
&computed_path,
VERSION,
&indexes,
)?);
let blocks = blocks_handle.join().unwrap()?;
let mining = mining_handle.join().unwrap()?;
@@ -154,16 +184,21 @@ impl Computer {
// Threads inside
let i = Instant::now();
let distribution = Box::new(
distribution::Vecs::forced_import(&computed_path, VERSION, &indexes)?
);
let distribution = Box::new(distribution::Vecs::forced_import(
&computed_path,
VERSION,
&indexes,
)?);
info!("Imported distribution in {:?}", i.elapsed());
// Supply must be imported after distribution (references distribution's supply)
let i = Instant::now();
let supply = Box::new(
supply::Vecs::forced_import(&computed_path, VERSION, &indexes, &distribution)?
);
let supply = Box::new(supply::Vecs::forced_import(
&computed_path,
VERSION,
&indexes,
&distribution,
)?);
info!("Imported supply in {:?}", i.elapsed());
let i = Instant::now();
@@ -286,14 +321,25 @@ impl Computer {
// Inputs → scripts → outputs (sequential)
info!("Computing inputs...");
let i = Instant::now();
self.inputs
.compute(indexer, &self.indexes, &self.blocks, &starting_indexes, exit)?;
self.inputs.compute(
indexer,
&self.indexes,
&self.blocks,
&starting_indexes,
exit,
)?;
info!("Computed inputs in {:?}", i.elapsed());
info!("Computing scripts...");
let i = Instant::now();
self.scripts
.compute(indexer, &self.blocks, &self.outputs, &self.prices, &starting_indexes, exit)?;
self.scripts.compute(
indexer,
&self.blocks,
&self.outputs,
&self.prices,
&starting_indexes,
exit,
)?;
info!("Computed scripts in {:?}", i.elapsed());
info!("Computing outputs...");
@@ -37,7 +37,6 @@ impl Vecs {
&self.split.high.cents,
&self.split.low.cents,
&self.split.close.cents,
indexes,
exit,
)?;
+19 -23
View File
@@ -10,18 +10,18 @@ use schemars::JsonSchema;
use serde::Serialize;
use vecdb::{
BytesVec, BytesVecValue, Database, EagerVec, Exit, Formattable, ImportableVec, LazyVecFrom1,
ReadableCloneableVec, ReadableVec, Rw, StorageMode, UnaryTransform,
ReadableCloneableVec, Rw, StorageMode, UnaryTransform,
};
use crate::{
ComputeIndexes, indexes, indexes_from,
ComputeIndexes, indexes_from,
internal::{ComputedHeightDerivedLast, EagerIndexes, Indexes},
};
// ── EagerOhlcIndexes ─────────────────────────────────────────────────
#[derive(Deref, DerefMut, Traversable)]
#[traversable(transparent)]
#[traversable(merge)]
pub struct OhlcVecs<T, M: StorageMode = Rw>(
#[allow(clippy::type_complexity)]
pub Indexes<
@@ -58,7 +58,7 @@ where
macro_rules! period {
($idx:ident) => {
ImportableVec::forced_import(db, &format!("{name}_{}", stringify!($idx)), v)?
ImportableVec::forced_import(db, name, v)?
};
}
@@ -67,7 +67,6 @@ where
}
impl OhlcVecs<OHLCCents> {
#[allow(clippy::too_many_arguments)]
pub(crate) fn compute_from_split(
&mut self,
starting_indexes: &ComputeIndexes,
@@ -75,26 +74,24 @@ impl OhlcVecs<OHLCCents> {
high: &EagerIndexes<Cents>,
low: &EagerIndexes<Cents>,
close: &ComputedHeightDerivedLast<Cents>,
indexes: &indexes::Vecs,
exit: &Exit,
) -> Result<()> {
macro_rules! period {
($field:ident) => {
self.0.$field.compute_transform(
self.0.$field.compute_transform4(
starting_indexes.$field,
&indexes.$field.first_height,
|(idx, _first_h, _)| {
let o = open.$field.collect_one(idx).unwrap_or_default();
let h = high.$field.collect_one(idx).unwrap_or_default();
let l = low.$field.collect_one(idx).unwrap_or_default();
let c = close.$field.collect_one(idx).flatten().unwrap_or_default();
&open.$field,
&high.$field,
&low.$field,
&close.$field,
|(idx, o, h, l, c, _)| {
(
idx,
OHLCCents {
open: Open::new(o),
high: High::new(h),
low: Low::new(l),
close: Close::new(c),
close: Close::new(c.unwrap_or_default()),
},
)
},
@@ -105,14 +102,13 @@ impl OhlcVecs<OHLCCents> {
macro_rules! epoch {
($field:ident) => {
self.0.$field.compute_transform(
self.0.$field.compute_transform4(
starting_indexes.$field,
&indexes.$field.first_height,
|(idx, _first_h, _)| {
let o = open.$field.collect_one(idx).unwrap_or_default();
let h = high.$field.collect_one(idx).unwrap_or_default();
let l = low.$field.collect_one(idx).unwrap_or_default();
let c = close.$field.collect_one(idx).unwrap_or_default();
&open.$field,
&high.$field,
&low.$field,
&close.$field,
|(idx, o, h, l, c, _)| {
(
idx,
OHLCCents {
@@ -153,7 +149,7 @@ impl OhlcVecs<OHLCCents> {
// ── LazyOhlcIndexes ──────────────────────────────────────────────────
#[derive(Clone, Deref, DerefMut, Traversable)]
#[traversable(transparent)]
#[traversable(merge)]
pub struct LazyOhlcVecs<T, S>(
#[allow(clippy::type_complexity)]
pub Indexes<
@@ -193,7 +189,7 @@ where
macro_rules! period {
($idx:ident) => {
LazyVecFrom1::transformed::<Transform>(
&format!("{name}_{}", stringify!($idx)),
name,
version,
source.$idx.read_only_boxed_clone(),
)
+25 -32
View File
@@ -14,10 +14,8 @@ use super::{Day1, Year10, Month1, Month3, Month6, Timestamp, Week1, Year1};
pub struct Date(u32);
impl Date {
pub const INDEX_ZERO: Self = Self(20090103);
pub const INDEX_ZERO_: Date_ = Date_::constant(2009, 1, 3);
pub const INDEX_ONE: Self = Self(20090109);
pub const INDEX_ONE_: Date_ = Date_::constant(2009, 1, 9);
pub const INDEX_ZERO: Self = Self(20090101);
pub const INDEX_ZERO_: Date_ = Date_::constant(2009, 1, 1);
pub const MIN_RATIO: Self = Self(20120101);
pub fn new(year: u16, month: u8, day: u8) -> Self {
@@ -99,22 +97,18 @@ impl From<Timestamp> for Date {
impl From<Day1> for Date {
#[inline]
fn from(value: Day1) -> Self {
if value == Day1::default() {
Date::INDEX_ZERO
} else {
Self::from(
Self::INDEX_ONE_
.checked_add(Span::new().days(i64::from(value) - 1))
.unwrap(),
)
}
Self::from(
Self::INDEX_ZERO_
.checked_add(Span::new().days(i64::from(value)))
.unwrap(),
)
}
}
impl From<Week1> for Date {
#[inline]
fn from(value: Week1) -> Self {
// Week 0 starts at genesis (2009-01-03), add i weeks
// Week 0 starts at 2009-01-01, add i weeks
Self::from(
Self::INDEX_ZERO_
.checked_add(Span::new().weeks(i64::from(u16::from(value))))
@@ -279,49 +273,48 @@ mod tests {
#[test]
fn test_date_from_day1_zero() {
// Day1 0 is genesis: Jan 3, 2009
// Day1 0 is Jan 1, 2009
let date = Date::from(Day1::from(0_usize));
assert_eq!(date, Date::INDEX_ZERO);
assert_eq!(date.year(), 2009);
assert_eq!(date.month(), 1);
assert_eq!(date.day(), 1);
}
#[test]
fn test_date_from_day1_two() {
// Day1 2 is Jan 3, 2009 (genesis)
let date = Date::from(Day1::from(2_usize));
assert_eq!(date.year(), 2009);
assert_eq!(date.month(), 1);
assert_eq!(date.day(), 3);
}
#[test]
fn test_date_from_day1_one() {
// Day1 1 is Jan 9, 2009 (6 day gap after genesis)
let date = Date::from(Day1::from(1_usize));
assert_eq!(date, Date::INDEX_ONE);
fn test_date_from_day1_eight() {
// Day1 8 is Jan 9, 2009
let date = Date::from(Day1::from(8_usize));
assert_eq!(date.year(), 2009);
assert_eq!(date.month(), 1);
assert_eq!(date.day(), 9);
}
#[test]
fn test_date_from_day1_two() {
// Day1 2 is Jan 10, 2009
let date = Date::from(Day1::from(2_usize));
assert_eq!(date.year(), 2009);
assert_eq!(date.month(), 1);
assert_eq!(date.day(), 10);
}
#[test]
fn test_date_from_week1_zero() {
// Week1 0 starts at genesis: Jan 3, 2009
// Week1 0 starts at Jan 1, 2009
let date = Date::from(Week1::from(0_usize));
assert_eq!(date.year(), 2009);
assert_eq!(date.month(), 1);
assert_eq!(date.day(), 3);
assert_eq!(date.day(), 1);
}
#[test]
fn test_date_from_week1_one() {
// Week1 1 is Jan 10, 2009 (one week after genesis)
// Week1 1 is Jan 8, 2009 (one week after epoch)
let date = Date::from(Week1::from(1_usize));
assert_eq!(date.year(), 2009);
assert_eq!(date.month(), 1);
assert_eq!(date.day(), 10);
assert_eq!(date.day(), 8);
}
#[test]
+27 -62
View File
@@ -11,7 +11,7 @@ use vecdb::{CheckedSub, Formattable, Pco, PrintableIndex};
use crate::{FromCoarserIndex, Month1, Month3, Month6, Week1, Year1, Year10};
use super::Date;
use super::{Date, Timestamp};
#[derive(
Debug,
@@ -31,6 +31,10 @@ pub struct Day1(u16);
impl Day1 {
pub const BYTES: usize = size_of::<Self>();
pub fn to_timestamp(&self) -> Timestamp {
Timestamp::from(Date::from(*self))
}
}
impl From<Day1> for usize {
@@ -74,14 +78,8 @@ impl TryFrom<Date> for Day1 {
let value_ = jiff::civil::Date::from(value);
if value_ < Date::INDEX_ZERO_ {
Err(Error::UnindexableDate)
} else if value == Date::INDEX_ZERO {
Ok(Self(0))
} else if value_ < Date::INDEX_ONE_ {
Err(Error::UnindexableDate)
} else if value == Date::INDEX_ONE {
Ok(Self(1))
} else {
Ok(Self(Date::INDEX_ONE_.until(value_)?.get_days() as u16 + 1))
Ok(Self(Date::INDEX_ZERO_.until(value_)?.get_days() as u16))
}
}
}
@@ -101,40 +99,21 @@ impl Rem<usize> for Day1 {
impl FromCoarserIndex<Week1> for Day1 {
fn min_from(coarser: Week1) -> usize {
let coarser = usize::from(coarser);
if coarser == 0 {
0
} else if coarser == 1 {
1
} else {
4 + (coarser - 2) * 7
}
usize::from(coarser) * 7
}
fn max_from_(coarser: Week1) -> usize {
let coarser = usize::from(coarser);
if coarser == 0 {
0
} else if coarser == 1 {
3
} else {
3 + (coarser - 1) * 7
}
usize::from(coarser) * 7 + 6
}
}
impl FromCoarserIndex<Month1> for Day1 {
fn min_from(coarser: Month1) -> usize {
let coarser = u16::from(coarser);
if coarser == 0 {
0
} else {
let d = Date::new(2009, 1, 1)
.into_jiff()
.checked_add(Span::new().months(coarser))
.unwrap();
Day1::try_from(Date::from(d)).unwrap().into()
}
let d = Date::new(2009, 1, 1)
.into_jiff()
.checked_add(Span::new().months(u16::from(coarser)))
.unwrap();
Day1::try_from(Date::from(d)).unwrap().into()
}
fn max_from_(coarser: Month1) -> usize {
@@ -148,16 +127,11 @@ impl FromCoarserIndex<Month1> for Day1 {
impl FromCoarserIndex<Month3> for Day1 {
fn min_from(coarser: Month3) -> usize {
let coarser = u8::from(coarser);
if coarser == 0 {
0
} else {
let d = Date::new(2009, 1, 1)
.into_jiff()
.checked_add(Span::new().months(3 * coarser))
.unwrap();
Day1::try_from(Date::from(d)).unwrap().into()
}
let d = Date::new(2009, 1, 1)
.into_jiff()
.checked_add(Span::new().months(3 * u8::from(coarser)))
.unwrap();
Day1::try_from(Date::from(d)).unwrap().into()
}
fn max_from_(coarser: Month3) -> usize {
@@ -171,16 +145,11 @@ impl FromCoarserIndex<Month3> for Day1 {
impl FromCoarserIndex<Month6> for Day1 {
fn min_from(coarser: Month6) -> usize {
let coarser = u8::from(coarser);
if coarser == 0 {
0
} else {
let d = Date::new(2009, 1, 1)
.into_jiff()
.checked_add(Span::new().months(6 * coarser))
.unwrap();
Day1::try_from(Date::from(d)).unwrap().into()
}
let d = Date::new(2009, 1, 1)
.into_jiff()
.checked_add(Span::new().months(6 * u8::from(coarser)))
.unwrap();
Day1::try_from(Date::from(d)).unwrap().into()
}
fn max_from_(coarser: Month6) -> usize {
@@ -194,14 +163,9 @@ impl FromCoarserIndex<Month6> for Day1 {
impl FromCoarserIndex<Year1> for Day1 {
fn min_from(coarser: Year1) -> usize {
let coarser = u8::from(coarser);
if coarser == 0 {
0
} else {
Self::try_from(Date::new(2009 + coarser as u16, 1, 1))
.unwrap()
.into()
}
Self::try_from(Date::new(2009 + u8::from(coarser) as u16, 1, 1))
.unwrap()
.into()
}
fn max_from_(coarser: Year1) -> usize {
@@ -215,6 +179,7 @@ impl FromCoarserIndex<Year10> for Day1 {
fn min_from(coarser: Year10) -> usize {
let coarser = u8::from(coarser);
if coarser == 0 {
// Decade 0 starts at 2000, before INDEX_ZERO (2009-01-01)
0
} else {
Self::try_from(Date::new(2000 + 10 * coarser as u16, 1, 1))
+5 -1
View File
@@ -26,7 +26,11 @@ pub struct Day3(u16);
impl Day3 {
pub fn from_timestamp(ts: Timestamp) -> Self {
Self(((*ts - INDEX_EPOCH) / DAY3_INTERVAL) as u16)
Self(((*ts - INDEX_EPOCH + 86400) / DAY3_INTERVAL) as u16)
}
pub fn to_timestamp(&self) -> Timestamp {
Timestamp::new(INDEX_EPOCH - 86400 + self.0 as u32 * DAY3_INTERVAL)
}
}
+1 -1
View File
@@ -96,7 +96,7 @@ impl PrintableIndex for DifficultyEpoch {
}
fn to_possible_strings() -> &'static [&'static str] {
&["difficulty", "difficultyepoch"]
&["difficulty", "difficultyepoch", "diff"]
}
}
+1 -1
View File
@@ -102,7 +102,7 @@ impl PrintableIndex for HalvingEpoch {
}
fn to_possible_strings() -> &'static [&'static str] {
&["halving", "halvingepoch"]
&["halving", "halvingepoch", "halv"]
}
}
+1 -1
View File
@@ -272,7 +272,7 @@ impl PrintableIndex for Height {
}
fn to_possible_strings() -> &'static [&'static str] {
&["h", "height"]
&["h", "height", "blk", "block"]
}
}
+4
View File
@@ -28,6 +28,10 @@ impl Hour1 {
pub fn from_timestamp(ts: Timestamp) -> Self {
Self((*ts - INDEX_EPOCH) / HOUR1_INTERVAL)
}
pub fn to_timestamp(&self) -> Timestamp {
Timestamp::new(INDEX_EPOCH + self.0 * HOUR1_INTERVAL)
}
}
impl From<Hour1> for usize {
+4
View File
@@ -28,6 +28,10 @@ impl Hour12 {
pub fn from_timestamp(ts: Timestamp) -> Self {
Self((*ts - INDEX_EPOCH) / HOUR12_INTERVAL)
}
pub fn to_timestamp(&self) -> Timestamp {
Timestamp::new(INDEX_EPOCH + self.0 * HOUR12_INTERVAL)
}
}
impl From<Hour12> for usize {
+4
View File
@@ -28,6 +28,10 @@ impl Hour4 {
pub fn from_timestamp(ts: Timestamp) -> Self {
Self((*ts - INDEX_EPOCH) / HOUR4_INTERVAL)
}
pub fn to_timestamp(&self) -> Timestamp {
Timestamp::new(INDEX_EPOCH + self.0 * HOUR4_INTERVAL)
}
}
impl From<Hour4> for usize {
+10 -10
View File
@@ -238,7 +238,7 @@ impl Index {
Self::Hour1 => HOUR1_INTERVAL,
Self::Hour4 => HOUR4_INTERVAL,
Self::Hour12 => HOUR12_INTERVAL,
Self::Day3 => DAY3_INTERVAL,
Self::Day3 => return Some(Day3::from(i).to_timestamp()),
_ => return self.index_to_date(i).map(|d| d.into()),
};
Some(Timestamp::new(INDEX_EPOCH + i as u32 * interval))
@@ -276,7 +276,7 @@ impl Index {
Self::Hour1 => HOUR1_INTERVAL,
Self::Hour4 => HOUR4_INTERVAL,
Self::Hour12 => HOUR12_INTERVAL,
Self::Day3 => DAY3_INTERVAL,
Self::Day3 => return Some(usize::from(Day3::from_timestamp(ts))),
_ => return self.date_to_index(Date::from(ts)),
};
Some(((*ts - INDEX_EPOCH) / interval) as usize)
@@ -442,13 +442,13 @@ mod tests {
}
#[test]
fn test_date_to_index_day1_genesis() {
fn test_date_to_index_day1_zero() {
assert_eq!(Index::Day1.date_to_index(Date::INDEX_ZERO), Some(0));
}
#[test]
fn test_date_to_index_day1_one() {
assert_eq!(Index::Day1.date_to_index(Date::INDEX_ONE), Some(1));
fn test_date_to_index_day1_genesis() {
assert_eq!(Index::Day1.date_to_index(Date::new(2009, 1, 3)), Some(2));
}
#[test]
@@ -494,11 +494,11 @@ mod tests {
}
#[test]
fn test_date_to_index_pre_genesis_returns_none() {
let pre_genesis = Date::new(2009, 1, 2);
assert!(Index::Day1.date_to_index(pre_genesis).is_none());
assert!(Index::Week1.date_to_index(pre_genesis).is_none());
assert!(Index::Month1.date_to_index(pre_genesis).is_none());
fn test_date_to_index_pre_epoch_returns_none() {
let pre_epoch = Date::new(2008, 12, 31);
assert!(Index::Day1.date_to_index(pre_epoch).is_none());
assert!(Index::Week1.date_to_index(pre_epoch).is_none());
assert!(Index::Month1.date_to_index(pre_epoch).is_none());
}
#[test]
+11 -11
View File
@@ -237,14 +237,14 @@ mod tests {
let metric = date_based_metric();
let dates: Vec<_> = metric.dates().unwrap().collect();
assert_eq!(dates.len(), 5);
// Day1 0 = Jan 3, 2009 (genesis)
// Day1 0 = Jan 1, 2009
assert_eq!(dates[0].year(), 2009);
assert_eq!(dates[0].month(), 1);
assert_eq!(dates[0].day(), 3);
// Day1 1 = Jan 9, 2009 (day one)
assert_eq!(dates[0].day(), 1);
// Day1 1 = Jan 2, 2009
assert_eq!(dates[1].year(), 2009);
assert_eq!(dates[1].month(), 1);
assert_eq!(dates[1].day(), 9);
assert_eq!(dates[1].day(), 2);
}
#[test]
@@ -271,13 +271,13 @@ mod tests {
let metric = date_based_metric();
let pairs: Vec<_> = metric.iter_dates().unwrap().collect();
assert_eq!(pairs.len(), 5);
// First pair: (Jan 3 2009, 100)
// First pair: (Jan 1 2009, 100)
assert_eq!(pairs[0].0.year(), 2009);
assert_eq!(pairs[0].0.month(), 1);
assert_eq!(pairs[0].0.day(), 3);
assert_eq!(pairs[0].0.day(), 1);
assert_eq!(pairs[0].1, &100);
// Second pair: (Jan 9 2009, 200)
assert_eq!(pairs[1].0.day(), 9);
// Second pair: (Jan 2 2009, 200)
assert_eq!(pairs[1].0.day(), 2);
assert_eq!(pairs[1].1, &200);
}
@@ -315,7 +315,7 @@ mod tests {
let date_metric = DateMetricData::try_new(metric).unwrap();
let pairs: Vec<_> = date_metric.iter_dates().unwrap().collect();
assert_eq!(pairs.len(), 5);
assert_eq!(pairs[0].0.day(), 3);
assert_eq!(pairs[0].0.day(), 1);
assert_eq!(pairs[0].1, &100);
}
@@ -545,8 +545,8 @@ mod tests {
#[test]
fn test_timestamp_to_index_day1_via_date_fallback() {
// Day1 goes through date_to_index fallback
// 2009-01-09 = Day1 index 1
// 2009-01-09 = Day1 index 8
let ts = Timestamp::from(Date::new(2009, 1, 9));
assert_eq!(Index::Day1.timestamp_to_index(ts), Some(1));
assert_eq!(Index::Day1.timestamp_to_index(ts), Some(8));
}
}
+4
View File
@@ -28,6 +28,10 @@ impl Minute1 {
pub fn from_timestamp(ts: Timestamp) -> Self {
Self((*ts - INDEX_EPOCH) / MINUTE1_INTERVAL)
}
pub fn to_timestamp(&self) -> Timestamp {
Timestamp::new(INDEX_EPOCH + self.0 * MINUTE1_INTERVAL)
}
}
impl From<Minute1> for usize {
+4
View File
@@ -28,6 +28,10 @@ impl Minute10 {
pub fn from_timestamp(ts: Timestamp) -> Self {
Self((*ts - INDEX_EPOCH) / MINUTE10_INTERVAL)
}
pub fn to_timestamp(&self) -> Timestamp {
Timestamp::new(INDEX_EPOCH + self.0 * MINUTE10_INTERVAL)
}
}
impl From<Minute10> for usize {
+4
View File
@@ -28,6 +28,10 @@ impl Minute30 {
pub fn from_timestamp(ts: Timestamp) -> Self {
Self((*ts - INDEX_EPOCH) / MINUTE30_INTERVAL)
}
pub fn to_timestamp(&self) -> Timestamp {
Timestamp::new(INDEX_EPOCH + self.0 * MINUTE30_INTERVAL)
}
}
impl From<Minute30> for usize {
+4
View File
@@ -28,6 +28,10 @@ impl Minute5 {
pub fn from_timestamp(ts: Timestamp) -> Self {
Self((*ts - INDEX_EPOCH) / MINUTE5_INTERVAL)
}
pub fn to_timestamp(&self) -> Timestamp {
Timestamp::new(INDEX_EPOCH + self.0 * MINUTE5_INTERVAL)
}
}
impl From<Minute5> for usize {
+8 -2
View File
@@ -7,7 +7,7 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use vecdb::{CheckedSub, Formattable, Pco, PrintableIndex};
use super::{Date, Day1, Year1};
use super::{Date, Day1, Timestamp, Year1};
#[derive(
Debug,
@@ -25,6 +25,12 @@ use super::{Date, Day1, Year1};
)]
pub struct Month1(u16);
impl Month1 {
pub fn to_timestamp(&self) -> Timestamp {
Timestamp::from(Date::from(*self))
}
}
impl From<u16> for Month1 {
#[inline]
fn from(value: u16) -> Self {
@@ -115,7 +121,7 @@ impl PrintableIndex for Month1 {
}
fn to_possible_strings() -> &'static [&'static str] {
&["month", "m", "monthly", "month1", "monthindex"]
&["month", "m", "monthly", "month1", "monthindex", "1m", "1mo"]
}
}
+8 -2
View File
@@ -7,7 +7,7 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use vecdb::{CheckedSub, Formattable, Pco, PrintableIndex};
use super::Month1;
use super::{Date, Month1, Timestamp};
#[derive(
Debug,
@@ -25,6 +25,12 @@ use super::Month1;
)]
pub struct Month3(u8);
impl Month3 {
pub fn to_timestamp(&self) -> Timestamp {
Timestamp::from(Date::from(*self))
}
}
impl From<u8> for Month3 {
#[inline]
fn from(value: u8) -> Self {
@@ -101,7 +107,7 @@ impl PrintableIndex for Month3 {
}
fn to_possible_strings() -> &'static [&'static str] {
&["quarter", "q", "quarterly", "month3", "quarterindex"]
&["quarter", "q", "quarterly", "month3", "quarterindex", "3m", "3mo"]
}
}
+8 -2
View File
@@ -7,7 +7,7 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use vecdb::{CheckedSub, Formattable, Pco, PrintableIndex};
use super::Month1;
use super::{Date, Month1, Timestamp};
#[derive(
Debug,
@@ -25,6 +25,12 @@ use super::Month1;
)]
pub struct Month6(u8);
impl Month6 {
pub fn to_timestamp(&self) -> Timestamp {
Timestamp::from(Date::from(*self))
}
}
impl From<u8> for Month6 {
#[inline]
fn from(value: u8) -> Self {
@@ -101,7 +107,7 @@ impl PrintableIndex for Month6 {
}
fn to_possible_strings() -> &'static [&'static str] {
&["semester", "s", "month6", "semesterindex"]
&["semester", "s", "month6", "semesterindex", "6m", "6mo"]
}
}
+7 -1
View File
@@ -7,7 +7,7 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use vecdb::{CheckedSub, Formattable, Pco, PrintableIndex};
use super::{Date, Day1};
use super::{Date, Day1, Timestamp};
#[derive(
Debug,
@@ -25,6 +25,12 @@ use super::{Date, Day1};
)]
pub struct Week1(u16);
impl Week1 {
pub fn to_timestamp(&self) -> Timestamp {
Timestamp::from(Date::from(*self))
}
}
impl From<u16> for Week1 {
#[inline]
fn from(value: u16) -> Self {
+7 -1
View File
@@ -7,7 +7,7 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use vecdb::{CheckedSub, Formattable, Pco, PrintableIndex};
use super::{Date, Day1, Month1};
use super::{Date, Day1, Month1, Timestamp};
#[derive(
Debug,
@@ -25,6 +25,12 @@ use super::{Date, Day1, Month1};
)]
pub struct Year1(u8);
impl Year1 {
pub fn to_timestamp(&self) -> Timestamp {
Timestamp::from(Date::from(*self))
}
}
impl From<u8> for Year1 {
#[inline]
fn from(value: u8) -> Self {
+7 -1
View File
@@ -7,7 +7,7 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use vecdb::{CheckedSub, Formattable, Pco, PrintableIndex};
use super::{Date, Day1, Year1};
use super::{Date, Day1, Timestamp, Year1};
#[derive(
Debug,
@@ -25,6 +25,12 @@ use super::{Date, Day1, Year1};
)]
pub struct Year10(u8);
impl Year10 {
pub fn to_timestamp(&self) -> Timestamp {
Timestamp::from(Date::from(*self))
}
}
impl From<u8> for Year10 {
#[inline]
fn from(value: u8) -> Self {
+23 -104
View File
@@ -951,7 +951,7 @@ function indexToDate(index, i) {
case 'hour4': return new Date(_EPOCH_MS + i * 14400000);
case 'hour12': return new Date(_EPOCH_MS + i * 43200000);
case 'day1': return i === 0 ? _GENESIS : new Date(_DAY_ONE.getTime() + (i - 1) * _MS_PER_DAY);
case 'day3': return new Date(_EPOCH_MS + i * 259200000);
case 'day3': return new Date(_EPOCH_MS - 86400000 + i * 259200000);
case 'week1': return new Date(_GENESIS.getTime() + i * _MS_PER_WEEK);
case 'month1': return _addMonths(i);
case 'month3': return _addMonths(i * 3);
@@ -983,7 +983,7 @@ function dateToIndex(index, d) {
if (ms < _DAY_ONE.getTime()) return 0;
return 1 + Math.floor((ms - _DAY_ONE.getTime()) / _MS_PER_DAY);
}
case 'day3': return Math.floor((ms - _EPOCH_MS) / 259200000);
case 'day3': return Math.floor((ms - _EPOCH_MS + 86400000) / 259200000);
case 'week1': return Math.floor((ms - _GENESIS.getTime()) / _MS_PER_WEEK);
case 'month1': return (d.getFullYear() - 2009) * 12 + d.getMonth();
case 'month3': return (d.getFullYear() - 2009) * 4 + Math.floor(d.getMonth() / 3);
@@ -2710,57 +2710,6 @@ function createGreedInvestedInvestorNegNetPainPeakSupplyTotalUnrealizedPattern(c
};
}
/**
* @template T
* @typedef {Object} Day1Day3DifficultyepochHalvingepochHour1Hour12Hour4Minute1Minute10Minute30Minute5Month1Month3Month6Week1Year1Year10Pattern
* @property {MetricPattern10<T>} day1
* @property {MetricPattern11<T>} day3
* @property {MetricPattern19<T>} difficultyepoch
* @property {MetricPattern18<T>} halvingepoch
* @property {MetricPattern7<T>} hour1
* @property {MetricPattern9<T>} hour12
* @property {MetricPattern8<T>} hour4
* @property {MetricPattern3<T>} minute1
* @property {MetricPattern5<T>} minute10
* @property {MetricPattern6<T>} minute30
* @property {MetricPattern4<T>} minute5
* @property {MetricPattern13<T>} month1
* @property {MetricPattern14<T>} month3
* @property {MetricPattern15<T>} month6
* @property {MetricPattern12<T>} week1
* @property {MetricPattern16<T>} year1
* @property {MetricPattern17<T>} year10
*/
/**
* Create a Day1Day3DifficultyepochHalvingepochHour1Hour12Hour4Minute1Minute10Minute30Minute5Month1Month3Month6Week1Year1Year10Pattern pattern node
* @template T
* @param {BrkClientBase} client
* @param {string} acc - Accumulated metric name
* @returns {Day1Day3DifficultyepochHalvingepochHour1Hour12Hour4Minute1Minute10Minute30Minute5Month1Month3Month6Week1Year1Year10Pattern<T>}
*/
function createDay1Day3DifficultyepochHalvingepochHour1Hour12Hour4Minute1Minute10Minute30Minute5Month1Month3Month6Week1Year1Year10Pattern(client, acc) {
return {
day1: createMetricPattern10(client, _m(acc, 'day1')),
day3: createMetricPattern11(client, _m(acc, 'day3')),
difficultyepoch: createMetricPattern19(client, _m(acc, 'difficultyepoch')),
halvingepoch: createMetricPattern18(client, _m(acc, 'halvingepoch')),
hour1: createMetricPattern7(client, _m(acc, 'hour1')),
hour12: createMetricPattern9(client, _m(acc, 'hour12')),
hour4: createMetricPattern8(client, _m(acc, 'hour4')),
minute1: createMetricPattern3(client, _m(acc, 'minute1')),
minute10: createMetricPattern5(client, _m(acc, 'minute10')),
minute30: createMetricPattern6(client, _m(acc, 'minute30')),
minute5: createMetricPattern4(client, _m(acc, 'minute5')),
month1: createMetricPattern13(client, _m(acc, 'month1')),
month3: createMetricPattern14(client, _m(acc, 'month3')),
month6: createMetricPattern15(client, _m(acc, 'month6')),
week1: createMetricPattern12(client, _m(acc, 'week1')),
year1: createMetricPattern16(client, _m(acc, 'year1')),
year10: createMetricPattern17(client, _m(acc, 'year10')),
};
}
/**
* @typedef {Object} GreedInvestedInvestorNegNetPainSupplyTotalUnrealizedPattern
* @property {MetricPattern1<Dollars>} greedIndex
@@ -3818,9 +3767,9 @@ function createBtcSatsUsdPattern(client, acc) {
/**
* @typedef {Object} CentsSatsUsdPattern
* @property {Day1Day3DifficultyepochHalvingepochHour1Hour12Hour4Minute1Minute10Minute30Minute5Month1Month3Month6Week1Year1Year10Pattern<OHLCCents>} cents
* @property {Day1Day3DifficultyepochHalvingepochHour1Hour12Hour4Minute1Minute10Minute30Minute5Month1Month3Month6Week1Year1Year10Pattern<OHLCSats>} sats
* @property {Day1Day3DifficultyepochHalvingepochHour1Hour12Hour4Minute1Minute10Minute30Minute5Month1Month3Month6Week1Year1Year10Pattern<OHLCDollars>} usd
* @property {MetricPattern2<Cents>} cents
* @property {MetricPattern2<Sats>} sats
* @property {MetricPattern2<Dollars>} usd
*/
/**
@@ -3831,9 +3780,9 @@ function createBtcSatsUsdPattern(client, acc) {
*/
function createCentsSatsUsdPattern(client, acc) {
return {
cents: createDay1Day3DifficultyepochHalvingepochHour1Hour12Hour4Minute1Minute10Minute30Minute5Month1Month3Month6Week1Year1Year10Pattern(client, _m(acc, 'cents')),
sats: createDay1Day3DifficultyepochHalvingepochHour1Hour12Hour4Minute1Minute10Minute30Minute5Month1Month3Month6Week1Year1Year10Pattern(client, _m(acc, 'sats')),
usd: createDay1Day3DifficultyepochHalvingepochHour1Hour12Hour4Minute1Minute10Minute30Minute5Month1Month3Month6Week1Year1Year10Pattern(client, acc),
cents: createMetricPattern2(client, _m(acc, 'cents')),
sats: createMetricPattern2(client, _m(acc, 'sats')),
usd: createMetricPattern2(client, acc),
};
}
@@ -4099,33 +4048,11 @@ function createRatioPattern2(client, acc) {
/**
* @typedef {Object} MetricsTree_Blocks_Time
* @property {MetricsTree_Blocks_Time_Timestamp} timestamp
* @property {MetricPattern1<Timestamp>} timestamp
* @property {MetricPattern20<Date>} date
* @property {MetricPattern20<Timestamp>} timestampMonotonic
*/
/**
* @typedef {Object} MetricsTree_Blocks_Time_Timestamp
* @property {MetricPattern20<Timestamp>} base
* @property {MetricPattern3<Timestamp>} minute1
* @property {MetricPattern4<Timestamp>} minute5
* @property {MetricPattern5<Timestamp>} minute10
* @property {MetricPattern6<Timestamp>} minute30
* @property {MetricPattern7<Timestamp>} hour1
* @property {MetricPattern8<Timestamp>} hour4
* @property {MetricPattern9<Timestamp>} hour12
* @property {MetricPattern10<Timestamp>} day1
* @property {MetricPattern11<Timestamp>} day3
* @property {MetricPattern12<Timestamp>} week1
* @property {MetricPattern13<Timestamp>} month1
* @property {MetricPattern14<Timestamp>} month3
* @property {MetricPattern15<Timestamp>} month6
* @property {MetricPattern16<Timestamp>} year1
* @property {MetricPattern17<Timestamp>} year10
* @property {MetricPattern18<Timestamp>} halvingepoch
* @property {MetricPattern19<Timestamp>} difficultyepoch
*/
/**
* @typedef {Object} MetricsTree_Blocks_Weight
* @property {MetricPattern20<Weight>} base
@@ -5280,7 +5207,7 @@ function createRatioPattern2(client, acc) {
/**
* @typedef {Object} MetricsTree_Prices
* @property {MetricsTree_Prices_Split} split
* @property {CentsSatsUsdPattern} ohlc
* @property {MetricsTree_Prices_Ohlc} ohlc
* @property {MetricsTree_Prices_Price} price
*/
@@ -5299,6 +5226,13 @@ function createRatioPattern2(client, acc) {
* @property {MetricPattern2<Sats>} sats
*/
/**
* @typedef {Object} MetricsTree_Prices_Ohlc
* @property {MetricPattern2<OHLCCents>} cents
* @property {MetricPattern2<OHLCDollars>} usd
* @property {MetricPattern2<OHLCSats>} sats
*/
/**
* @typedef {Object} MetricsTree_Prices_Price
* @property {MetricPattern20<Cents>} cents
@@ -6660,26 +6594,7 @@ class BrkClient extends BrkClientBase {
daysBeforeNextAdjustment: createMetricPattern1(this, 'days_before_next_difficulty_adjustment'),
},
time: {
timestamp: {
base: createMetricPattern20(this, 'timestamp'),
minute1: createMetricPattern3(this, 'timestamp_minute1'),
minute5: createMetricPattern4(this, 'timestamp_minute5'),
minute10: createMetricPattern5(this, 'timestamp_minute10'),
minute30: createMetricPattern6(this, 'timestamp_minute30'),
hour1: createMetricPattern7(this, 'timestamp_hour1'),
hour4: createMetricPattern8(this, 'timestamp_hour4'),
hour12: createMetricPattern9(this, 'timestamp_hour12'),
day1: createMetricPattern10(this, 'timestamp_day1'),
day3: createMetricPattern11(this, 'timestamp_day3'),
week1: createMetricPattern12(this, 'timestamp_week1'),
month1: createMetricPattern13(this, 'timestamp_month1'),
month3: createMetricPattern14(this, 'timestamp_month3'),
month6: createMetricPattern15(this, 'timestamp_month6'),
year1: createMetricPattern16(this, 'timestamp_year1'),
year10: createMetricPattern17(this, 'timestamp_year10'),
halvingepoch: createMetricPattern18(this, 'timestamp_halvingepoch'),
difficultyepoch: createMetricPattern19(this, 'timestamp_difficultyepoch'),
},
timestamp: createMetricPattern1(this, 'timestamp'),
date: createMetricPattern20(this, 'date'),
timestampMonotonic: createMetricPattern20(this, 'timestamp_monotonic'),
},
@@ -7584,7 +7499,11 @@ class BrkClient extends BrkClientBase {
sats: createMetricPattern2(this, 'price_close_sats'),
},
},
ohlc: createCentsSatsUsdPattern(this, 'price_ohlc'),
ohlc: {
cents: createMetricPattern2(this, 'price_ohlc_cents'),
usd: createMetricPattern2(this, 'price_ohlc'),
sats: createMetricPattern2(this, 'price_ohlc_sats'),
},
price: {
cents: createMetricPattern20(this, 'price_cents'),
usd: createMetricPattern20(this, 'price'),
+15 -53
View File
@@ -1140,7 +1140,7 @@ def _index_to_date(index: str, i: int) -> Union[date, datetime]:
elif index == 'day1':
return _GENESIS if i == 0 else _DAY_ONE + timedelta(days=i - 1)
elif index == 'day3':
return _EPOCH.date() + timedelta(days=i * 3)
return _EPOCH.date() - timedelta(days=1) + timedelta(days=i * 3)
elif index == 'week1':
return _GENESIS + timedelta(weeks=i)
elif index == 'month1':
@@ -1180,7 +1180,7 @@ def _date_to_index(index: str, d: Union[date, datetime]) -> int:
return 0
return 1 + (dd - _DAY_ONE).days
elif index == 'day3':
return (dd - date(2009, 1, 1)).days // 3
return (dd - date(2008, 12, 31)).days // 3
elif index == 'week1':
return (dd - _GENESIS).days // 7
elif index == 'month1':
@@ -2645,29 +2645,6 @@ class GreedInvestedInvestorNegNetPainPeakSupplyTotalUnrealizedPattern:
self.unrealized_loss: MetricPattern1[Dollars] = MetricPattern1(client, _m(acc, 'unrealized_loss'))
self.unrealized_profit: MetricPattern1[Dollars] = MetricPattern1(client, _m(acc, 'unrealized_profit'))
class Day1Day3DifficultyepochHalvingepochHour1Hour12Hour4Minute1Minute10Minute30Minute5Month1Month3Month6Week1Year1Year10Pattern(Generic[T]):
"""Pattern struct for repeated tree structure."""
def __init__(self, client: BrkClientBase, acc: str):
"""Create pattern node with accumulated metric name."""
self.day1: MetricPattern10[T] = MetricPattern10(client, _m(acc, 'day1'))
self.day3: MetricPattern11[T] = MetricPattern11(client, _m(acc, 'day3'))
self.difficultyepoch: MetricPattern19[T] = MetricPattern19(client, _m(acc, 'difficultyepoch'))
self.halvingepoch: MetricPattern18[T] = MetricPattern18(client, _m(acc, 'halvingepoch'))
self.hour1: MetricPattern7[T] = MetricPattern7(client, _m(acc, 'hour1'))
self.hour12: MetricPattern9[T] = MetricPattern9(client, _m(acc, 'hour12'))
self.hour4: MetricPattern8[T] = MetricPattern8(client, _m(acc, 'hour4'))
self.minute1: MetricPattern3[T] = MetricPattern3(client, _m(acc, 'minute1'))
self.minute10: MetricPattern5[T] = MetricPattern5(client, _m(acc, 'minute10'))
self.minute30: MetricPattern6[T] = MetricPattern6(client, _m(acc, 'minute30'))
self.minute5: MetricPattern4[T] = MetricPattern4(client, _m(acc, 'minute5'))
self.month1: MetricPattern13[T] = MetricPattern13(client, _m(acc, 'month1'))
self.month3: MetricPattern14[T] = MetricPattern14(client, _m(acc, 'month3'))
self.month6: MetricPattern15[T] = MetricPattern15(client, _m(acc, 'month6'))
self.week1: MetricPattern12[T] = MetricPattern12(client, _m(acc, 'week1'))
self.year1: MetricPattern16[T] = MetricPattern16(client, _m(acc, 'year1'))
self.year10: MetricPattern17[T] = MetricPattern17(client, _m(acc, 'year10'))
class GreedInvestedInvestorNegNetPainSupplyTotalUnrealizedPattern:
"""Pattern struct for repeated tree structure."""
@@ -3145,9 +3122,9 @@ class CentsSatsUsdPattern:
def __init__(self, client: BrkClientBase, acc: str):
"""Create pattern node with accumulated metric name."""
self.cents: Day1Day3DifficultyepochHalvingepochHour1Hour12Hour4Minute1Minute10Minute30Minute5Month1Month3Month6Week1Year1Year10Pattern[OHLCCents] = Day1Day3DifficultyepochHalvingepochHour1Hour12Hour4Minute1Minute10Minute30Minute5Month1Month3Month6Week1Year1Year10Pattern(client, _m(acc, 'cents'))
self.sats: Day1Day3DifficultyepochHalvingepochHour1Hour12Hour4Minute1Minute10Minute30Minute5Month1Month3Month6Week1Year1Year10Pattern[OHLCSats] = Day1Day3DifficultyepochHalvingepochHour1Hour12Hour4Minute1Minute10Minute30Minute5Month1Month3Month6Week1Year1Year10Pattern(client, _m(acc, 'sats'))
self.usd: Day1Day3DifficultyepochHalvingepochHour1Hour12Hour4Minute1Minute10Minute30Minute5Month1Month3Month6Week1Year1Year10Pattern[OHLCDollars] = Day1Day3DifficultyepochHalvingepochHour1Hour12Hour4Minute1Minute10Minute30Minute5Month1Month3Month6Week1Year1Year10Pattern(client, acc)
self.cents: MetricPattern2[Cents] = MetricPattern2(client, _m(acc, 'cents'))
self.sats: MetricPattern2[Sats] = MetricPattern2(client, _m(acc, 'sats'))
self.usd: MetricPattern2[Dollars] = MetricPattern2(client, acc)
class HistogramLineSignalPattern:
"""Pattern struct for repeated tree structure."""
@@ -3251,34 +3228,11 @@ class MetricsTree_Blocks_Difficulty:
self.blocks_before_next_adjustment: MetricPattern1[StoredU32] = MetricPattern1(client, 'blocks_before_next_difficulty_adjustment')
self.days_before_next_adjustment: MetricPattern1[StoredF32] = MetricPattern1(client, 'days_before_next_difficulty_adjustment')
class MetricsTree_Blocks_Time_Timestamp:
"""Metrics tree node."""
def __init__(self, client: BrkClientBase, base_path: str = ''):
self.base: MetricPattern20[Timestamp] = MetricPattern20(client, 'timestamp')
self.minute1: MetricPattern3[Timestamp] = MetricPattern3(client, 'timestamp_minute1')
self.minute5: MetricPattern4[Timestamp] = MetricPattern4(client, 'timestamp_minute5')
self.minute10: MetricPattern5[Timestamp] = MetricPattern5(client, 'timestamp_minute10')
self.minute30: MetricPattern6[Timestamp] = MetricPattern6(client, 'timestamp_minute30')
self.hour1: MetricPattern7[Timestamp] = MetricPattern7(client, 'timestamp_hour1')
self.hour4: MetricPattern8[Timestamp] = MetricPattern8(client, 'timestamp_hour4')
self.hour12: MetricPattern9[Timestamp] = MetricPattern9(client, 'timestamp_hour12')
self.day1: MetricPattern10[Timestamp] = MetricPattern10(client, 'timestamp_day1')
self.day3: MetricPattern11[Timestamp] = MetricPattern11(client, 'timestamp_day3')
self.week1: MetricPattern12[Timestamp] = MetricPattern12(client, 'timestamp_week1')
self.month1: MetricPattern13[Timestamp] = MetricPattern13(client, 'timestamp_month1')
self.month3: MetricPattern14[Timestamp] = MetricPattern14(client, 'timestamp_month3')
self.month6: MetricPattern15[Timestamp] = MetricPattern15(client, 'timestamp_month6')
self.year1: MetricPattern16[Timestamp] = MetricPattern16(client, 'timestamp_year1')
self.year10: MetricPattern17[Timestamp] = MetricPattern17(client, 'timestamp_year10')
self.halvingepoch: MetricPattern18[Timestamp] = MetricPattern18(client, 'timestamp_halvingepoch')
self.difficultyepoch: MetricPattern19[Timestamp] = MetricPattern19(client, 'timestamp_difficultyepoch')
class MetricsTree_Blocks_Time:
"""Metrics tree node."""
def __init__(self, client: BrkClientBase, base_path: str = ''):
self.timestamp: MetricsTree_Blocks_Time_Timestamp = MetricsTree_Blocks_Time_Timestamp(client)
self.timestamp: MetricPattern1[Timestamp] = MetricPattern1(client, 'timestamp')
self.date: MetricPattern20[Date] = MetricPattern20(client, 'date')
self.timestamp_monotonic: MetricPattern20[Timestamp] = MetricPattern20(client, 'timestamp_monotonic')
@@ -4560,6 +4514,14 @@ class MetricsTree_Prices_Split:
self.low: CentsSatsUsdPattern = CentsSatsUsdPattern(client, 'price_low')
self.close: MetricsTree_Prices_Split_Close = MetricsTree_Prices_Split_Close(client)
class MetricsTree_Prices_Ohlc:
"""Metrics tree node."""
def __init__(self, client: BrkClientBase, base_path: str = ''):
self.cents: MetricPattern2[OHLCCents] = MetricPattern2(client, 'price_ohlc_cents')
self.usd: MetricPattern2[OHLCDollars] = MetricPattern2(client, 'price_ohlc')
self.sats: MetricPattern2[OHLCSats] = MetricPattern2(client, 'price_ohlc_sats')
class MetricsTree_Prices_Price:
"""Metrics tree node."""
@@ -4573,7 +4535,7 @@ class MetricsTree_Prices:
def __init__(self, client: BrkClientBase, base_path: str = ''):
self.split: MetricsTree_Prices_Split = MetricsTree_Prices_Split(client)
self.ohlc: CentsSatsUsdPattern = CentsSatsUsdPattern(client, 'price_ohlc')
self.ohlc: MetricsTree_Prices_Ohlc = MetricsTree_Prices_Ohlc(client)
self.price: MetricsTree_Prices_Price = MetricsTree_Prices_Price(client)
class MetricsTree_Distribution_AnyAddressIndexes:
@@ -291,9 +291,9 @@ class TestIndexToDate:
def test_day3(self, day3_metric):
dates = day3_metric.dates()
assert dates[0] == date(2009, 1, 1) # epoch
assert dates[1] == date(2009, 1, 4) # +3 days
assert dates[2] == date(2009, 1, 7) # +6 days
assert dates[0] == date(2008, 12, 31) # epoch - 1 day (TradingView-aligned)
assert dates[1] == date(2009, 1, 3) # +3 days
assert dates[2] == date(2009, 1, 6) # +6 days
def test_hour1_returns_datetime(self, hour1_metric):
"""Sub-daily indexes return datetime, not date."""
+19 -10
View File
@@ -13,7 +13,7 @@ import { createRadios, createSelect } from "../utils/dom.js";
import { createPersistedValue } from "../utils/persisted.js";
import { onChange as onThemeChange } from "../utils/theme.js";
import { throttle, debounce } from "../utils/timing.js";
import { serdeBool, serdeChartableIndex } from "../utils/serde.js";
import { serdeBool, INDEX_FROM_LABEL } from "../utils/serde.js";
import { stringToId, numberToShortUSFormat } from "../utils/format.js";
import { style } from "../utils/elements.js";
import { Unit } from "../utils/units.js";
@@ -87,22 +87,23 @@ export function createChart({ parent, brk, fitContent }) {
const getTimeEndpoint = (idx) =>
idx === "height"
? brk.metrics.blocks.time.timestampMonotonic.by[idx]
: /** @type {any} */ (brk.metrics.blocks.time.timestamp)[idx].by[idx];
: brk.metrics.blocks.time.timestamp.by[idx];
const index = {
/** @type {Set<(index: ChartableIndex) => void>} */
onChange: new Set(),
get() {
return serdeChartableIndex.deserialize(index.name.value);
return INDEX_FROM_LABEL[index.name.value];
},
name: createPersistedValue({
defaultValue: /** @type {ChartableIndexName} */ ("date"),
defaultValue: /** @type {IndexLabel} */ ("1d"),
storageKey: "chart-index",
urlKey: "i",
serialize: (v) => v,
deserialize: (s) => /** @type {ChartableIndexName} */ (s),
deserialize: (s) =>
/** @type {IndexLabel} */ (s in INDEX_FROM_LABEL ? s : "1d"),
onChange: () => {
range.set(null);
index.onChange.forEach((cb) => cb(index.get()));
@@ -323,7 +324,12 @@ export function createChart({ parent, brk, fitContent }) {
ichart.applyOptions({
timeScale: {
timeVisible: index === "height",
timeVisible:
index === "height" ||
index === "difficultyepoch" ||
index === "halvingepoch" ||
index.startsWith("minute") ||
index.startsWith("hour"),
...(!fitContent
? {
minBarSpacing,
@@ -1420,20 +1426,22 @@ export function createChart({ parent, brk, fitContent }) {
/** @type {HTMLElement | null} */
let indexField = null;
const lastTd = ichart.chartElement().querySelector("table > tr:last-child > td:nth-child(2)");
const lastTd = ichart
.chartElement()
.querySelector("table > tr:last-child > td:nth-child(2)");
const chart = {
get panes() {
return blueprints.panes;
},
/** @param {ChartableIndexName[]} choices */
setIndexChoices(choices) {
/** @param {{ choices: IndexLabel[], groups: { label: string, items: IndexLabel[] }[] }} arg */
setIndexChoices({ choices, groups }) {
if (indexField) indexField.remove();
let currentValue = choices.includes(preferredIndex)
? preferredIndex
: (choices[0] ?? "date");
: (choices[0] ?? "1d");
if (currentValue !== index.name.value) {
index.name.set(currentValue);
@@ -1446,6 +1454,7 @@ export function createChart({ parent, brk, fitContent }) {
index.name.set(v);
},
choices,
groups,
id: "index",
});
const sep = document.createElement("span");
+2 -2
View File
@@ -1,5 +1,5 @@
import { localhost } from "../utils/env.js";
import { serdeChartableIndex } from "../utils/serde.js";
import { INDEX_LABEL } from "../utils/serde.js";
/**
* Check if a metric pattern has at least one chartable index
@@ -8,7 +8,7 @@ import { serdeChartableIndex } from "../utils/serde.js";
*/
function hasChartableIndex(node) {
const indexes = node.indexes();
return indexes.some((idx) => serdeChartableIndex.serialize(idx) !== null);
return indexes.some((idx) => idx in INDEX_LABEL);
}
/**
+34 -18
View File
@@ -1,6 +1,6 @@
import { createHeader } from "../utils/dom.js";
import { chartElement } from "../utils/elements.js";
import { serdeChartableIndex } from "../utils/serde.js";
import { INDEX_FROM_LABEL } from "../utils/serde.js";
import { Unit } from "../utils/units.js";
import { createChart } from "../chart/index.js";
import { colors } from "../utils/colors.js";
@@ -45,7 +45,7 @@ export function init() {
const usdPrice = {
type: "Candlestick",
title: "Price",
metric: brk.metrics.prices.ohlc.usd.day1,
metric: brk.metrics.prices.ohlc.usd,
};
result.set(Unit.usd, [usdPrice, ...(optionTop.get(Unit.usd) ?? [])]);
@@ -54,7 +54,7 @@ export function init() {
const satsPrice = {
type: "Candlestick",
title: "Price",
metric: brk.metrics.prices.ohlc.sats.day1,
metric: brk.metrics.prices.ohlc.sats,
colors: /** @type {const} */ ([colors.bi.p1[1], colors.bi.p1[0]]),
};
result.set(Unit.sats, [satsPrice, ...(optionTop.get(Unit.sats) ?? [])]);
@@ -104,24 +104,32 @@ export function init() {
onPrice(updatePriceWithLatest);
}
const ALL_CHOICES = /** @satisfies {ChartableIndexName[]} */ ([
"timestamp",
"date",
"week",
"month",
"month3",
"month6",
"year",
"year10",
]);
/** @type {{ label: string, items: IndexLabel[] }[]} */
const ALL_GROUPS = [
{ label: "Height", items: ["blk", "halv", "diff"] },
{
label: "Time",
items: [
"1mn", "5mn", "10mn", "30mn",
"1h", "4h", "12h",
"1d", "3d", "1w",
"1m", "3m", "6m",
"1y", "10y",
],
},
];
const ALL_CHOICES = /** @satisfies {IndexLabel[]} */ (
ALL_GROUPS.flatMap((g) => g.items)
);
/**
* @param {ChartOption} opt
* @returns {ChartableIndexName[]}
* @returns {{ choices: IndexLabel[], groups: { label: string, items: IndexLabel[] }[] }}
*/
function computeChoices(opt) {
if (!opt.top().size && !opt.bottom().size) {
return [...ALL_CHOICES];
return { choices: [...ALL_CHOICES], groups: ALL_GROUPS };
}
const rawIndexes = new Set(
[Array.from(opt.top().values()), Array.from(opt.bottom().values())]
@@ -133,8 +141,16 @@ function computeChoices(opt) {
.flatMap((blueprint) => blueprint.metric.indexes()),
);
return ALL_CHOICES.filter((choice) =>
rawIndexes.has(serdeChartableIndex.deserialize(choice)),
);
const groups = ALL_GROUPS
.map(({ label, items }) => ({
label,
items: items.filter((choice) => rawIndexes.has(INDEX_FROM_LABEL[choice])),
}))
.filter(({ items }) => items.length > 0);
return {
choices: groups.flatMap((g) => g.items),
groups,
};
}
+1 -3
View File
@@ -17,7 +17,7 @@
*
* @import { UnitObject as Unit } from "./utils/units.js"
*
* @import { ChartableIndexName } from "./utils/serde.js";
* @import { ChartableIndex, IndexLabel } from "./utils/serde.js";
*/
// import uFuzzy = require("./modules/leeoniya-ufuzzy/1.0.19/dist/uFuzzy.d.ts");
@@ -215,6 +215,4 @@
* Generic tree node type for walking
* @typedef {AnyMetricPattern | Record<string, unknown>} TreeNode
*
* Chartable index IDs (subset of IndexName that can be charted)
* @typedef {"height" | "day1" | "week1" | "month1" | "month3" | "month6" | "year1" | "year10"} ChartableIndex
*/
+9
View File
@@ -6,6 +6,15 @@
*/
export const entries = (obj) => /** @type {[keyof T & string, T[keyof T & string]][]} */ (Object.entries(obj));
/**
* Typed Object.fromEntries that preserves key/value types
* @template {string} K
* @template V
* @param {Iterable<readonly [K, V]>} pairs
* @returns {Record<K, V>}
*/
export const fromEntries = (pairs) => /** @type {Record<K, V>} */ (Object.fromEntries(pairs));
/**
* Type-safe includes that narrows the value type
* @template T
+17 -3
View File
@@ -238,10 +238,12 @@ export function createRadios({
* @param {(choice: T) => string} [args.toKey]
* @param {(choice: T) => string} [args.toLabel]
* @param {boolean} [args.sorted]
* @param {{ label: string, items: T[] }[]} [args.groups]
*/
export function createSelect({
id,
choices: unsortedChoices,
groups,
initialValue,
onChange,
sorted,
@@ -271,15 +273,27 @@ export function createSelect({
select.name = id ?? "";
field.append(select);
choices.forEach((choice) => {
/** @param {T} choice */
const createOption = (choice) => {
const option = window.document.createElement("option");
option.value = toKey(choice);
option.textContent = toLabel(choice);
if (toKey(choice) === initialKey) {
option.selected = true;
}
select.append(option);
});
return option;
};
if (groups) {
groups.forEach(({ label, items }) => {
const optgroup = window.document.createElement("optgroup");
optgroup.label = label;
items.forEach((choice) => optgroup.append(createOption(choice)));
select.append(optgroup);
});
} else {
choices.forEach((choice) => select.append(createOption(choice)));
}
select.addEventListener("change", () => {
onChange?.(fromKey(select.value));
+16 -57
View File
@@ -1,3 +1,5 @@
import { entries, fromEntries } from "./array.js";
export const serdeBool = {
/**
* @param {boolean} v
@@ -17,64 +19,21 @@ export const serdeBool = {
},
};
/**
* @typedef {"timestamp" | "date" | "week" | "month" | "month3" | "month6" | "year" | "year10"} ChartableIndexName
*/
export const INDEX_LABEL = /** @type {const} */ ({
height: "blk",
minute1: "1mn", minute5: "5mn", minute10: "10mn", minute30: "30mn",
hour1: "1h", hour4: "4h", hour12: "12h",
day1: "1d", day3: "3d", week1: "1w",
month1: "1m", month3: "3m", month6: "6m",
year1: "1y", year10: "10y",
halvingepoch: "halv", difficultyepoch: "diff",
});
export const serdeChartableIndex = {
/**
* @param {IndexName} v
* @returns {ChartableIndexName | null}
*/
serialize(v) {
switch (v) {
case "day1":
return "date";
case "year10":
return "year10";
case "height":
return "timestamp";
case "month1":
return "month";
case "month3":
return "month3";
case "month6":
return "month6";
case "week1":
return "week";
case "year1":
return "year";
default:
return null;
}
},
/**
* @param {ChartableIndexName} v
* @returns {ChartableIndex}
*/
deserialize(v) {
switch (v) {
case "timestamp":
return "height";
case "date":
return "day1";
case "week":
return "week1";
case "month":
return "month1";
case "month3":
return "month3";
case "month6":
return "month6";
case "year":
return "year1";
case "year10":
return "year10";
default:
throw Error("todo");
}
},
};
/** @typedef {typeof INDEX_LABEL} IndexLabelMap */
/** @typedef {keyof IndexLabelMap} ChartableIndex */
/** @typedef {IndexLabelMap[ChartableIndex]} IndexLabel */
export const INDEX_FROM_LABEL = fromEntries(entries(INDEX_LABEL).map(([k, v]) => [v, k]));
/**
* @typedef {"" |