mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-04-24 06:39:58 -07:00
global: snapshot
This commit is contained in:
4
Cargo.lock
generated
4
Cargo.lock
generated
@@ -2460,9 +2460,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quickmatch"
|
name = "quickmatch"
|
||||||
version = "0.3.2"
|
version = "0.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "28628ced26de8cc0e8ce04e4c2e718bc8d84e9b99616a2a6abd2096e9a55f728"
|
checksum = "848244615004bddb7273545dfe909ead495ed734f9faf130c43a7daccca2bf99"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rustc-hash",
|
"rustc-hash",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -4470,8 +4470,8 @@ impl SeriesTree_Cointime_Adjusted {
|
|||||||
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
|
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
|
||||||
Self {
|
Self {
|
||||||
inflation_rate: BpsPercentRatioPattern::new(client.clone(), "cointime_adj_inflation_rate".to_string()),
|
inflation_rate: BpsPercentRatioPattern::new(client.clone(), "cointime_adj_inflation_rate".to_string()),
|
||||||
tx_velocity_native: SeriesPattern1::new(client.clone(), "cointime_adj_tx_velocity".to_string()),
|
tx_velocity_native: SeriesPattern1::new(client.clone(), "cointime_adj_tx_velocity_btc".to_string()),
|
||||||
tx_velocity_fiat: SeriesPattern1::new(client.clone(), "cointime_adj_tx_velocity_fiat".to_string()),
|
tx_velocity_fiat: SeriesPattern1::new(client.clone(), "cointime_adj_tx_velocity_usd".to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6345,8 +6345,8 @@ pub struct SeriesTree_Supply_Velocity {
|
|||||||
impl SeriesTree_Supply_Velocity {
|
impl SeriesTree_Supply_Velocity {
|
||||||
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
|
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
|
||||||
Self {
|
Self {
|
||||||
native: SeriesPattern1::new(client.clone(), "velocity".to_string()),
|
native: SeriesPattern1::new(client.clone(), "velocity_btc".to_string()),
|
||||||
fiat: SeriesPattern1::new(client.clone(), "velocity_fiat".to_string()),
|
fiat: SeriesPattern1::new(client.clone(), "velocity_usd".to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -141,4 +141,18 @@ impl<T> Loss<T> {
|
|||||||
&mut self._80pct,
|
&mut self._80pct,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Iterate from narrowest (_80pct) to broadest (all), yielding each threshold
|
||||||
|
/// with a growing suffix slice of `ranges` (1 range, 2 ranges, ..., LOSS_COUNT).
|
||||||
|
pub fn iter_mut_with_growing_suffix<'a, R>(
|
||||||
|
&'a mut self,
|
||||||
|
ranges: &'a [R],
|
||||||
|
) -> impl Iterator<Item = (&'a mut T, &'a [R])> {
|
||||||
|
let len = ranges.len();
|
||||||
|
self.as_array_mut()
|
||||||
|
.into_iter()
|
||||||
|
.rev()
|
||||||
|
.enumerate()
|
||||||
|
.map(move |(n, threshold)| (threshold, &ranges[len - 1 - n..]))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -181,4 +181,17 @@ impl<T> Profit<T> {
|
|||||||
&mut self._500pct,
|
&mut self._500pct,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Iterate from narrowest (_500pct) to broadest (all), yielding each threshold
|
||||||
|
/// with a growing prefix slice of `ranges` (1 range, 2 ranges, ..., PROFIT_COUNT).
|
||||||
|
pub fn iter_mut_with_growing_prefix<'a, R>(
|
||||||
|
&'a mut self,
|
||||||
|
ranges: &'a [R],
|
||||||
|
) -> impl Iterator<Item = (&'a mut T, &'a [R])> {
|
||||||
|
self.as_array_mut()
|
||||||
|
.into_iter()
|
||||||
|
.rev()
|
||||||
|
.enumerate()
|
||||||
|
.map(move |(n, threshold)| (threshold, &ranges[..n + 1]))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -253,32 +253,37 @@ impl<T> ProfitabilityRange<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut T> {
|
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut T> {
|
||||||
|
self.iter_mut_with_is_profit().map(|(_, v)| v)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Iterate mutably, yielding `(is_profit, &mut T)` for each range.
|
||||||
|
pub fn iter_mut_with_is_profit(&mut self) -> impl Iterator<Item = (bool, &mut T)> {
|
||||||
[
|
[
|
||||||
&mut self.over_1000pct_in_profit,
|
(true, &mut self.over_1000pct_in_profit),
|
||||||
&mut self._500pct_to_1000pct_in_profit,
|
(true, &mut self._500pct_to_1000pct_in_profit),
|
||||||
&mut self._300pct_to_500pct_in_profit,
|
(true, &mut self._300pct_to_500pct_in_profit),
|
||||||
&mut self._200pct_to_300pct_in_profit,
|
(true, &mut self._200pct_to_300pct_in_profit),
|
||||||
&mut self._100pct_to_200pct_in_profit,
|
(true, &mut self._100pct_to_200pct_in_profit),
|
||||||
&mut self._90pct_to_100pct_in_profit,
|
(true, &mut self._90pct_to_100pct_in_profit),
|
||||||
&mut self._80pct_to_90pct_in_profit,
|
(true, &mut self._80pct_to_90pct_in_profit),
|
||||||
&mut self._70pct_to_80pct_in_profit,
|
(true, &mut self._70pct_to_80pct_in_profit),
|
||||||
&mut self._60pct_to_70pct_in_profit,
|
(true, &mut self._60pct_to_70pct_in_profit),
|
||||||
&mut self._50pct_to_60pct_in_profit,
|
(true, &mut self._50pct_to_60pct_in_profit),
|
||||||
&mut self._40pct_to_50pct_in_profit,
|
(true, &mut self._40pct_to_50pct_in_profit),
|
||||||
&mut self._30pct_to_40pct_in_profit,
|
(true, &mut self._30pct_to_40pct_in_profit),
|
||||||
&mut self._20pct_to_30pct_in_profit,
|
(true, &mut self._20pct_to_30pct_in_profit),
|
||||||
&mut self._10pct_to_20pct_in_profit,
|
(true, &mut self._10pct_to_20pct_in_profit),
|
||||||
&mut self._0pct_to_10pct_in_profit,
|
(true, &mut self._0pct_to_10pct_in_profit),
|
||||||
&mut self._0pct_to_10pct_in_loss,
|
(false, &mut self._0pct_to_10pct_in_loss),
|
||||||
&mut self._10pct_to_20pct_in_loss,
|
(false, &mut self._10pct_to_20pct_in_loss),
|
||||||
&mut self._20pct_to_30pct_in_loss,
|
(false, &mut self._20pct_to_30pct_in_loss),
|
||||||
&mut self._30pct_to_40pct_in_loss,
|
(false, &mut self._30pct_to_40pct_in_loss),
|
||||||
&mut self._40pct_to_50pct_in_loss,
|
(false, &mut self._40pct_to_50pct_in_loss),
|
||||||
&mut self._50pct_to_60pct_in_loss,
|
(false, &mut self._50pct_to_60pct_in_loss),
|
||||||
&mut self._60pct_to_70pct_in_loss,
|
(false, &mut self._60pct_to_70pct_in_loss),
|
||||||
&mut self._70pct_to_80pct_in_loss,
|
(false, &mut self._70pct_to_80pct_in_loss),
|
||||||
&mut self._80pct_to_90pct_in_loss,
|
(false, &mut self._80pct_to_90pct_in_loss),
|
||||||
&mut self._90pct_to_100pct_in_loss,
|
(false, &mut self._90pct_to_100pct_in_loss),
|
||||||
]
|
]
|
||||||
.into_iter()
|
.into_iter()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,13 +23,13 @@ impl Vecs {
|
|||||||
)?,
|
)?,
|
||||||
tx_velocity_native: PerBlock::forced_import(
|
tx_velocity_native: PerBlock::forced_import(
|
||||||
db,
|
db,
|
||||||
"cointime_adj_tx_velocity",
|
"cointime_adj_tx_velocity_btc",
|
||||||
version,
|
version,
|
||||||
indexes,
|
indexes,
|
||||||
)?,
|
)?,
|
||||||
tx_velocity_fiat: PerBlock::forced_import(
|
tx_velocity_fiat: PerBlock::forced_import(
|
||||||
db,
|
db,
|
||||||
"cointime_adj_tx_velocity_fiat",
|
"cointime_adj_tx_velocity_usd",
|
||||||
version,
|
version,
|
||||||
indexes,
|
indexes,
|
||||||
)?,
|
)?,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use std::{cmp::Reverse, collections::BinaryHeap, fs, path::Path};
|
use std::{cmp::Reverse, collections::BinaryHeap, fs, path::Path};
|
||||||
|
|
||||||
use brk_cohort::{Filtered, PROFIT_COUNT, PROFITABILITY_RANGE_COUNT, TERM_NAMES};
|
use brk_cohort::{Filtered, PROFITABILITY_RANGE_COUNT, TERM_NAMES};
|
||||||
use brk_error::Result;
|
use brk_error::Result;
|
||||||
use brk_types::{BasisPoints16, Cents, CentsCompact, CostBasisDistribution, Date, Dollars, Sats};
|
use brk_types::{BasisPoints16, Cents, CentsCompact, CostBasisDistribution, Date, Dollars, Sats};
|
||||||
|
|
||||||
@@ -50,7 +50,7 @@ impl UTXOCohorts {
|
|||||||
push_cost_basis(<h, lth_d, &mut self.lth.metrics.cost_basis);
|
push_cost_basis(<h, lth_d, &mut self.lth.metrics.cost_basis);
|
||||||
|
|
||||||
let prof = self.fenwick.profitability(spot_price);
|
let prof = self.fenwick.profitability(spot_price);
|
||||||
push_profitability(&prof, spot_price, &mut self.profitability);
|
push_profitability(&prof, &mut self.profitability);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// K-way merge only for writing daily cost basis distributions to disk.
|
/// K-way merge only for writing daily cost basis distributions to disk.
|
||||||
@@ -100,93 +100,22 @@ fn push_cost_basis(percentiles: &PercentileResult, density_bps: u16, cost_basis:
|
|||||||
cost_basis.push_density(BasisPoints16::from(density_bps));
|
cost_basis.push_density(BasisPoints16::from(density_bps));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert raw (cents × sats) accumulator to Dollars (÷ 100 for cents→dollars, ÷ 1e8 for sats).
|
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
fn raw_usd_to_dollars(raw: u128) -> Dollars {
|
fn raw_usd_to_dollars(raw: u128) -> Dollars {
|
||||||
Dollars::from(raw as f64 / 1e10)
|
Dollars::from(raw as f64 / 1e10)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Number of profit ranges (0..=14 are profit, 15..=24 are loss).
|
|
||||||
const PROFIT_RANGE_COUNT: usize = 15;
|
|
||||||
|
|
||||||
/// Compute unrealized P&L from raw sats/usd for a given range.
|
|
||||||
/// Profit ranges: market_value - cost_basis. Loss ranges: cost_basis - market_value.
|
|
||||||
#[inline(always)]
|
|
||||||
fn compute_unrealized_pnl(spot_cents: u128, sats: u64, usd: u128, is_profit: bool) -> Dollars {
|
|
||||||
let market_value = spot_cents * sats as u128;
|
|
||||||
let raw = if is_profit {
|
|
||||||
market_value.saturating_sub(usd)
|
|
||||||
} else {
|
|
||||||
usd.saturating_sub(market_value)
|
|
||||||
};
|
|
||||||
raw_usd_to_dollars(raw)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Push profitability range + profit/loss aggregate values to vecs.
|
|
||||||
fn push_profitability(
|
fn push_profitability(
|
||||||
buckets: &[ProfitabilityRangeResult; PROFITABILITY_RANGE_COUNT],
|
buckets: &[ProfitabilityRangeResult; PROFITABILITY_RANGE_COUNT],
|
||||||
spot_price: Cents,
|
|
||||||
metrics: &mut ProfitabilityMetrics,
|
metrics: &mut ProfitabilityMetrics,
|
||||||
) {
|
) {
|
||||||
let spot_cents = spot_price.as_u128();
|
|
||||||
|
|
||||||
// Push 25 range buckets
|
|
||||||
for (i, bucket) in metrics.range.as_array_mut().into_iter().enumerate() {
|
for (i, bucket) in metrics.range.as_array_mut().into_iter().enumerate() {
|
||||||
let r = &buckets[i];
|
let r = &buckets[i];
|
||||||
let is_profit = i < PROFIT_RANGE_COUNT;
|
|
||||||
bucket.push(
|
bucket.push(
|
||||||
Sats::from(r.all_sats),
|
Sats::from(r.all_sats),
|
||||||
Sats::from(r.sth_sats),
|
Sats::from(r.sth_sats),
|
||||||
raw_usd_to_dollars(r.all_usd),
|
raw_usd_to_dollars(r.all_usd),
|
||||||
raw_usd_to_dollars(r.sth_usd),
|
raw_usd_to_dollars(r.sth_usd),
|
||||||
compute_unrealized_pnl(spot_cents, r.all_sats, r.all_usd, is_profit),
|
|
||||||
compute_unrealized_pnl(spot_cents, r.sth_sats, r.sth_usd, is_profit),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Profit: forward cumulative sum over ranges[0..15], pushed in reverse.
|
|
||||||
// profit[0] (breakeven) = sum(0..=13), ..., profit[13] (_500pct) = ranges[0]
|
|
||||||
let profit_arr = metrics.profit.as_array_mut();
|
|
||||||
let mut cum_sats = 0u64;
|
|
||||||
let mut cum_sth_sats = 0u64;
|
|
||||||
let mut cum_usd = 0u128;
|
|
||||||
let mut cum_sth_usd = 0u128;
|
|
||||||
for i in 0..PROFIT_COUNT {
|
|
||||||
cum_sats += buckets[i].all_sats;
|
|
||||||
cum_sth_sats += buckets[i].sth_sats;
|
|
||||||
cum_usd += buckets[i].all_usd;
|
|
||||||
cum_sth_usd += buckets[i].sth_usd;
|
|
||||||
profit_arr[PROFIT_COUNT - 1 - i].push(
|
|
||||||
Sats::from(cum_sats),
|
|
||||||
Sats::from(cum_sth_sats),
|
|
||||||
raw_usd_to_dollars(cum_usd),
|
|
||||||
raw_usd_to_dollars(cum_sth_usd),
|
|
||||||
compute_unrealized_pnl(spot_cents, cum_sats, cum_usd, true),
|
|
||||||
compute_unrealized_pnl(spot_cents, cum_sth_sats, cum_sth_usd, true),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loss: backward cumulative sum over ranges[15..25], pushed in reverse.
|
|
||||||
// loss[0] (breakeven) = sum(15..=24), ..., loss[8] (_80pct) = ranges[24]
|
|
||||||
let loss_arr = metrics.loss.as_array_mut();
|
|
||||||
let loss_count = loss_arr.len();
|
|
||||||
cum_sats = 0;
|
|
||||||
cum_sth_sats = 0;
|
|
||||||
cum_usd = 0;
|
|
||||||
cum_sth_usd = 0;
|
|
||||||
for i in 0..loss_count {
|
|
||||||
let r = &buckets[PROFITABILITY_RANGE_COUNT - 1 - i];
|
|
||||||
cum_sats += r.all_sats;
|
|
||||||
cum_sth_sats += r.sth_sats;
|
|
||||||
cum_usd += r.all_usd;
|
|
||||||
cum_sth_usd += r.sth_usd;
|
|
||||||
loss_arr[loss_count - 1 - i].push(
|
|
||||||
Sats::from(cum_sats),
|
|
||||||
Sats::from(cum_sth_sats),
|
|
||||||
raw_usd_to_dollars(cum_usd),
|
|
||||||
raw_usd_to_dollars(cum_sth_usd),
|
|
||||||
compute_unrealized_pnl(spot_cents, cum_sats, cum_usd, false),
|
|
||||||
compute_unrealized_pnl(spot_cents, cum_sth_sats, cum_sth_usd, false),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use brk_cohort::{Loss, Profit, ProfitabilityRange};
|
use brk_cohort::{Loss, Profit, ProfitabilityRange};
|
||||||
use brk_error::Result;
|
use brk_error::Result;
|
||||||
use brk_traversable::Traversable;
|
use brk_traversable::Traversable;
|
||||||
use brk_types::{BasisPointsSigned32, Cents, Dollars, Indexes, Sats, Version};
|
use brk_types::{BasisPointsSigned32, Bitcoin, Cents, Dollars, Indexes, Sats, Version};
|
||||||
use vecdb::{AnyStoredVec, AnyVec, Database, Exit, Rw, StorageMode, WritableVec};
|
use vecdb::{AnyStoredVec, AnyVec, Database, Exit, Rw, StorageMode, WritableVec};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -32,7 +32,6 @@ impl<M: StorageMode> ProfitabilityBucket<M> {
|
|||||||
.height
|
.height
|
||||||
.len()
|
.len()
|
||||||
.min(self.realized_cap.all.height.len())
|
.min(self.realized_cap.all.height.len())
|
||||||
.min(self.unrealized_pnl.all.height.len())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,21 +103,18 @@ impl ProfitabilityBucket {
|
|||||||
sth_supply: Sats,
|
sth_supply: Sats,
|
||||||
realized_cap: Dollars,
|
realized_cap: Dollars,
|
||||||
sth_realized_cap: Dollars,
|
sth_realized_cap: Dollars,
|
||||||
unrealized_pnl: Dollars,
|
|
||||||
sth_unrealized_pnl: Dollars,
|
|
||||||
) {
|
) {
|
||||||
self.supply.all.sats.height.push(supply);
|
self.supply.all.sats.height.push(supply);
|
||||||
self.supply.sth.sats.height.push(sth_supply);
|
self.supply.sth.sats.height.push(sth_supply);
|
||||||
self.realized_cap.all.height.push(realized_cap);
|
self.realized_cap.all.height.push(realized_cap);
|
||||||
self.realized_cap.sth.height.push(sth_realized_cap);
|
self.realized_cap.sth.height.push(sth_realized_cap);
|
||||||
self.unrealized_pnl.all.height.push(unrealized_pnl);
|
|
||||||
self.unrealized_pnl.sth.height.push(sth_unrealized_pnl);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn compute(
|
pub(crate) fn compute(
|
||||||
&mut self,
|
&mut self,
|
||||||
prices: &prices::Vecs,
|
prices: &prices::Vecs,
|
||||||
starting_indexes: &Indexes,
|
starting_indexes: &Indexes,
|
||||||
|
is_profit: bool,
|
||||||
exit: &Exit,
|
exit: &Exit,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let max_from = starting_indexes.height;
|
let max_from = starting_indexes.height;
|
||||||
@@ -126,8 +122,33 @@ impl ProfitabilityBucket {
|
|||||||
self.supply.all.compute(prices, max_from, exit)?;
|
self.supply.all.compute(prices, max_from, exit)?;
|
||||||
self.supply.sth.compute(prices, max_from, exit)?;
|
self.supply.sth.compute(prices, max_from, exit)?;
|
||||||
|
|
||||||
// NUPL = (spot - realized_price) / spot
|
self.unrealized_pnl.all.height.compute_transform3(
|
||||||
// where realized_price = realized_cap_cents × ONE_BTC / supply_sats
|
max_from,
|
||||||
|
&prices.spot.cents.height,
|
||||||
|
&self.realized_cap.all.height,
|
||||||
|
&self.supply.all.sats.height,
|
||||||
|
|(i, spot, cap, supply, ..)| {
|
||||||
|
let mv = f64::from(Dollars::from(spot)) * f64::from(Bitcoin::from(supply));
|
||||||
|
let rc = f64::from(cap);
|
||||||
|
let pnl = if is_profit { mv - rc } else { rc - mv }.max(0.0);
|
||||||
|
(i, Dollars::from(pnl))
|
||||||
|
},
|
||||||
|
exit,
|
||||||
|
)?;
|
||||||
|
self.unrealized_pnl.sth.height.compute_transform3(
|
||||||
|
max_from,
|
||||||
|
&prices.spot.cents.height,
|
||||||
|
&self.realized_cap.sth.height,
|
||||||
|
&self.supply.sth.sats.height,
|
||||||
|
|(i, spot, cap, supply, ..)| {
|
||||||
|
let mv = f64::from(Dollars::from(spot)) * f64::from(Bitcoin::from(supply));
|
||||||
|
let rc = f64::from(cap);
|
||||||
|
let pnl = if is_profit { mv - rc } else { rc - mv }.max(0.0);
|
||||||
|
(i, Dollars::from(pnl))
|
||||||
|
},
|
||||||
|
exit,
|
||||||
|
)?;
|
||||||
|
|
||||||
self.nupl.bps.height.compute_transform3(
|
self.nupl.bps.height.compute_transform3(
|
||||||
max_from,
|
max_from,
|
||||||
&prices.spot.cents.height,
|
&prices.spot.cents.height,
|
||||||
@@ -150,6 +171,40 @@ impl ProfitabilityBucket {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn compute_from_ranges(
|
||||||
|
&mut self,
|
||||||
|
prices: &prices::Vecs,
|
||||||
|
starting_indexes: &Indexes,
|
||||||
|
is_profit: bool,
|
||||||
|
sources: &[&ProfitabilityBucket],
|
||||||
|
exit: &Exit,
|
||||||
|
) -> Result<()> {
|
||||||
|
let max_from = starting_indexes.height;
|
||||||
|
|
||||||
|
self.supply.all.sats.height.compute_sum_of_others(
|
||||||
|
max_from,
|
||||||
|
&sources.iter().map(|s| &s.supply.all.sats.height).collect::<Vec<_>>(),
|
||||||
|
exit,
|
||||||
|
)?;
|
||||||
|
self.supply.sth.sats.height.compute_sum_of_others(
|
||||||
|
max_from,
|
||||||
|
&sources.iter().map(|s| &s.supply.sth.sats.height).collect::<Vec<_>>(),
|
||||||
|
exit,
|
||||||
|
)?;
|
||||||
|
self.realized_cap.all.height.compute_sum_of_others(
|
||||||
|
max_from,
|
||||||
|
&sources.iter().map(|s| &s.realized_cap.all.height).collect::<Vec<_>>(),
|
||||||
|
exit,
|
||||||
|
)?;
|
||||||
|
self.realized_cap.sth.height.compute_sum_of_others(
|
||||||
|
max_from,
|
||||||
|
&sources.iter().map(|s| &s.realized_cap.sth.height).collect::<Vec<_>>(),
|
||||||
|
exit,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
self.compute(prices, starting_indexes, is_profit, exit)
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn collect_all_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
|
pub(crate) fn collect_all_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
|
||||||
vec![
|
vec![
|
||||||
&mut self.supply.all.inner.sats.height as &mut dyn AnyStoredVec,
|
&mut self.supply.all.inner.sats.height as &mut dyn AnyStoredVec,
|
||||||
@@ -189,7 +244,7 @@ impl<M: StorageMode> ProfitabilityMetrics<M> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn min_stateful_len(&self) -> usize {
|
pub(crate) fn min_stateful_len(&self) -> usize {
|
||||||
self.iter().map(|b| b.min_len()).min().unwrap_or(0)
|
self.range.iter().map(|b| b.min_len()).min().unwrap_or(0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,12 +259,14 @@ impl ProfitabilityMetrics {
|
|||||||
ProfitabilityBucket::forced_import(db, name, version, indexes, cached_starts)
|
ProfitabilityBucket::forced_import(db, name, version, indexes, cached_starts)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
let aggregate_version = version + Version::ONE;
|
||||||
|
|
||||||
let profit = Profit::try_new(|name| {
|
let profit = Profit::try_new(|name| {
|
||||||
ProfitabilityBucket::forced_import(db, name, version, indexes, cached_starts)
|
ProfitabilityBucket::forced_import(db, name, aggregate_version, indexes, cached_starts)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let loss = Loss::try_new(|name| {
|
let loss = Loss::try_new(|name| {
|
||||||
ProfitabilityBucket::forced_import(db, name, version, indexes, cached_starts)
|
ProfitabilityBucket::forced_import(db, name, aggregate_version, indexes, cached_starts)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
@@ -225,8 +282,20 @@ impl ProfitabilityMetrics {
|
|||||||
starting_indexes: &Indexes,
|
starting_indexes: &Indexes,
|
||||||
exit: &Exit,
|
exit: &Exit,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
self.iter_mut()
|
for (is_profit, bucket) in self.range.iter_mut_with_is_profit() {
|
||||||
.try_for_each(|b| b.compute(prices, starting_indexes, exit))
|
bucket.compute(prices, starting_indexes, is_profit, exit)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let range_arr = self.range.as_array();
|
||||||
|
|
||||||
|
for (threshold, sources) in self.profit.iter_mut_with_growing_prefix(&range_arr) {
|
||||||
|
threshold.compute_from_ranges(prices, starting_indexes, true, sources, exit)?;
|
||||||
|
}
|
||||||
|
for (threshold, sources) in self.loss.iter_mut_with_growing_suffix(&range_arr) {
|
||||||
|
threshold.compute_from_ranges(prices, starting_indexes, false, sources, exit)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn collect_all_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
|
pub(crate) fn collect_all_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ impl Vecs {
|
|||||||
indexes: &indexes::Vecs,
|
indexes: &indexes::Vecs,
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
native: PerBlock::forced_import(db, "velocity", version, indexes)?,
|
native: PerBlock::forced_import(db, "velocity_btc", version, indexes)?,
|
||||||
fiat: PerBlock::forced_import(db, "velocity_fiat", version, indexes)?,
|
fiat: PerBlock::forced_import(db, "velocity_usd", version, indexes)?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ derive_more = { workspace = true }
|
|||||||
jiff = { workspace = true }
|
jiff = { workspace = true }
|
||||||
parking_lot = { workspace = true }
|
parking_lot = { workspace = true }
|
||||||
# quickmatch = { path = "../../../quickmatch" }
|
# quickmatch = { path = "../../../quickmatch" }
|
||||||
quickmatch = "0.3.2"
|
quickmatch = "0.4.0"
|
||||||
tokio = { workspace = true, optional = true }
|
tokio = { workspace = true, optional = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
vecdb = { workspace = true }
|
vecdb = { workspace = true }
|
||||||
|
|||||||
@@ -7926,8 +7926,8 @@ class BrkClient extends BrkClientBase {
|
|||||||
},
|
},
|
||||||
adjusted: {
|
adjusted: {
|
||||||
inflationRate: createBpsPercentRatioPattern(this, 'cointime_adj_inflation_rate'),
|
inflationRate: createBpsPercentRatioPattern(this, 'cointime_adj_inflation_rate'),
|
||||||
txVelocityNative: createSeriesPattern1(this, 'cointime_adj_tx_velocity'),
|
txVelocityNative: createSeriesPattern1(this, 'cointime_adj_tx_velocity_btc'),
|
||||||
txVelocityFiat: createSeriesPattern1(this, 'cointime_adj_tx_velocity_fiat'),
|
txVelocityFiat: createSeriesPattern1(this, 'cointime_adj_tx_velocity_usd'),
|
||||||
},
|
},
|
||||||
reserveRisk: {
|
reserveRisk: {
|
||||||
value: createSeriesPattern1(this, 'reserve_risk'),
|
value: createSeriesPattern1(this, 'reserve_risk'),
|
||||||
@@ -8531,8 +8531,8 @@ class BrkClient extends BrkClientBase {
|
|||||||
burned: createBlockCumulativePattern(this, 'unspendable_supply'),
|
burned: createBlockCumulativePattern(this, 'unspendable_supply'),
|
||||||
inflationRate: createBpsPercentRatioPattern(this, 'inflation_rate'),
|
inflationRate: createBpsPercentRatioPattern(this, 'inflation_rate'),
|
||||||
velocity: {
|
velocity: {
|
||||||
native: createSeriesPattern1(this, 'velocity'),
|
native: createSeriesPattern1(this, 'velocity_btc'),
|
||||||
fiat: createSeriesPattern1(this, 'velocity_fiat'),
|
fiat: createSeriesPattern1(this, 'velocity_usd'),
|
||||||
},
|
},
|
||||||
marketCap: createCentsDeltaUsdPattern(this, 'market_cap'),
|
marketCap: createCentsDeltaUsdPattern(this, 'market_cap'),
|
||||||
marketMinusRealizedCapGrowthRate: create_1m1w1y24hPattern(this, 'market_minus_realized_cap_growth_rate'),
|
marketMinusRealizedCapGrowthRate: create_1m1w1y24hPattern(this, 'market_minus_realized_cap_growth_rate'),
|
||||||
|
|||||||
@@ -1,441 +0,0 @@
|
|||||||
const DEFAULT_SEPARATORS = "_- ,:";
|
|
||||||
const DEFAULT_TRIGRAM_BUDGET = 6;
|
|
||||||
const DEFAULT_LIMIT = 100;
|
|
||||||
const DEFAULT_MIN_SCORE = 2;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configuration for QuickMatch.
|
|
||||||
*/
|
|
||||||
export class QuickMatchConfig {
|
|
||||||
/** @type {string} Characters used to split items into words */
|
|
||||||
separators = DEFAULT_SEPARATORS;
|
|
||||||
|
|
||||||
/** @type {number} Maximum number of results to return */
|
|
||||||
limit = DEFAULT_LIMIT;
|
|
||||||
|
|
||||||
/** @type {number} Number of trigram lookups for fuzzy matching (0-20) */
|
|
||||||
trigramBudget = DEFAULT_TRIGRAM_BUDGET;
|
|
||||||
|
|
||||||
/** @type {number} Minimum trigram score required for fuzzy matches */
|
|
||||||
minScore = DEFAULT_MIN_SCORE;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set maximum number of results.
|
|
||||||
* @param {number} n
|
|
||||||
*/
|
|
||||||
withLimit(n) {
|
|
||||||
this.limit = Math.max(1, n);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set trigram budget for fuzzy matching.
|
|
||||||
* Higher values find more typos but cost more.
|
|
||||||
* @param {number} n - Budget (0-20, default: 6)
|
|
||||||
*/
|
|
||||||
withTrigramBudget(n) {
|
|
||||||
this.trigramBudget = Math.max(0, Math.min(20, n));
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set word separator characters.
|
|
||||||
* @param {string} s - Separator characters (default: '_- ')
|
|
||||||
*/
|
|
||||||
withSeparators(s) {
|
|
||||||
this.separators = s;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set minimum trigram score for fuzzy matches.
|
|
||||||
* Higher values require more trigram overlap, reducing noise.
|
|
||||||
* @param {number} n - Minimum score (default: 2, min: 1)
|
|
||||||
*/
|
|
||||||
withMinScore(n) {
|
|
||||||
this.minScore = Math.max(1, n);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fast fuzzy string matcher using word and trigram indexing.
|
|
||||||
*/
|
|
||||||
export class QuickMatch {
|
|
||||||
/**
|
|
||||||
* Create a new matcher.
|
|
||||||
* @param {string[]} items - Items to index (should be lowercase)
|
|
||||||
* @param {QuickMatchConfig} [config] - Optional configuration
|
|
||||||
*/
|
|
||||||
constructor(items, config = new QuickMatchConfig()) {
|
|
||||||
this.config = config;
|
|
||||||
this.items = items;
|
|
||||||
/** @type {Map<string, number[]>} */
|
|
||||||
this.wordIndex = new Map();
|
|
||||||
/** @type {Map<string, number[]>} */
|
|
||||||
this.trigramIndex = new Map();
|
|
||||||
|
|
||||||
let maxWordLength = 0;
|
|
||||||
let maxQueryLength = 0;
|
|
||||||
let maxWordCount = 0;
|
|
||||||
|
|
||||||
const { separators } = config;
|
|
||||||
|
|
||||||
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
|
|
||||||
const item = items[itemIndex];
|
|
||||||
|
|
||||||
if (item.length > maxQueryLength) {
|
|
||||||
maxQueryLength = item.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
let wordCount = 0;
|
|
||||||
let wordStart = 0;
|
|
||||||
|
|
||||||
for (let i = 0; i <= item.length; i++) {
|
|
||||||
const isEndOfWord = i === item.length || separators.includes(item[i]);
|
|
||||||
|
|
||||||
if (isEndOfWord && i > wordStart) {
|
|
||||||
wordCount++;
|
|
||||||
const word = item.slice(wordStart, i);
|
|
||||||
|
|
||||||
if (word.length > maxWordLength) {
|
|
||||||
maxWordLength = word.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
addToIndex(this.wordIndex, word, itemIndex);
|
|
||||||
addTrigramsToIndex(this.trigramIndex, word, itemIndex);
|
|
||||||
|
|
||||||
wordStart = i + 1;
|
|
||||||
} else if (isEndOfWord) {
|
|
||||||
wordStart = i + 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (wordCount > maxWordCount) {
|
|
||||||
maxWordCount = wordCount;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.maxWordLength = maxWordLength + 4;
|
|
||||||
this.maxQueryLength = maxQueryLength + 6;
|
|
||||||
this.maxWordCount = maxWordCount + 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find matching items. Returns items sorted by relevance.
|
|
||||||
* @param {string} query - Search query
|
|
||||||
*/
|
|
||||||
matches(query) {
|
|
||||||
return this.matchesWith(query, this.config);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find matching items with custom config. Returns items sorted by relevance.
|
|
||||||
* @param {string} query - Search query
|
|
||||||
* @param {QuickMatchConfig} config - Configuration to use
|
|
||||||
*/
|
|
||||||
matchesWith(query, config) {
|
|
||||||
const { limit, trigramBudget, separators } = config;
|
|
||||||
|
|
||||||
const normalizedQuery = normalizeQuery(query);
|
|
||||||
|
|
||||||
if (!normalizedQuery || normalizedQuery.length > this.maxQueryLength) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const queryWords = parseWords(
|
|
||||||
normalizedQuery,
|
|
||||||
separators,
|
|
||||||
this.maxWordLength,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!queryWords.length || queryWords.length > this.maxWordCount) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const knownWords = [];
|
|
||||||
const unknownWords = [];
|
|
||||||
|
|
||||||
for (const word of queryWords) {
|
|
||||||
const matchingItems = this.wordIndex.get(word);
|
|
||||||
|
|
||||||
if (matchingItems) {
|
|
||||||
knownWords.push(matchingItems);
|
|
||||||
} else if (word.length >= 3 && unknownWords.length < trigramBudget) {
|
|
||||||
unknownWords.push(word);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const exactMatches = intersectAll(knownWords);
|
|
||||||
const hasExactMatches = exactMatches.length > 0;
|
|
||||||
const needsFuzzyMatching = unknownWords.length > 0 && trigramBudget > 0;
|
|
||||||
|
|
||||||
if (!needsFuzzyMatching) {
|
|
||||||
if (!hasExactMatches) return [];
|
|
||||||
return this.sortedByLength(exactMatches, limit);
|
|
||||||
}
|
|
||||||
|
|
||||||
const scores = new Map();
|
|
||||||
|
|
||||||
if (hasExactMatches) {
|
|
||||||
for (const index of exactMatches) {
|
|
||||||
scores.set(index, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const minItemLength = Math.max(0, normalizedQuery.length - 3);
|
|
||||||
|
|
||||||
const hitCount = this.scoreByTrigrams({
|
|
||||||
unknownWords,
|
|
||||||
budget: trigramBudget,
|
|
||||||
scores,
|
|
||||||
hasExactMatches,
|
|
||||||
minItemLength,
|
|
||||||
});
|
|
||||||
|
|
||||||
const minScoreToInclude = Math.max(config.minScore, Math.ceil(hitCount / 2));
|
|
||||||
|
|
||||||
return this.rankedResults(scores, minScoreToInclude, limit);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @private
|
|
||||||
* @param {{unknownWords: string[], budget: number, scores: Map<number, number>, hasExactMatches: boolean, minItemLength: number}} args
|
|
||||||
*/
|
|
||||||
scoreByTrigrams({
|
|
||||||
unknownWords,
|
|
||||||
budget,
|
|
||||||
scores,
|
|
||||||
hasExactMatches,
|
|
||||||
minItemLength,
|
|
||||||
}) {
|
|
||||||
const visitedTrigrams = new Set();
|
|
||||||
let budgetRemaining = budget;
|
|
||||||
let hitCount = 0;
|
|
||||||
|
|
||||||
outer: for (let round = 0; round < budget; round++) {
|
|
||||||
for (const word of unknownWords) {
|
|
||||||
if (budgetRemaining <= 0) break outer;
|
|
||||||
|
|
||||||
const position = pickTrigramPosition(word.length, round);
|
|
||||||
if (position < 0) continue;
|
|
||||||
|
|
||||||
const trigram =
|
|
||||||
word[position] + word[position + 1] + word[position + 2];
|
|
||||||
|
|
||||||
if (visitedTrigrams.has(trigram)) continue;
|
|
||||||
visitedTrigrams.add(trigram);
|
|
||||||
|
|
||||||
budgetRemaining--;
|
|
||||||
|
|
||||||
const matchingItems = this.trigramIndex.get(trigram);
|
|
||||||
if (!matchingItems) continue;
|
|
||||||
|
|
||||||
hitCount++;
|
|
||||||
|
|
||||||
for (const itemIndex of matchingItems) {
|
|
||||||
if (hasExactMatches) {
|
|
||||||
const currentScore = scores.get(itemIndex);
|
|
||||||
if (currentScore !== undefined) {
|
|
||||||
scores.set(itemIndex, currentScore + 1);
|
|
||||||
}
|
|
||||||
} else if (this.items[itemIndex].length >= minItemLength) {
|
|
||||||
scores.set(itemIndex, (scores.get(itemIndex) || 0) + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return hitCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @private
|
|
||||||
* @param {number[]} indices
|
|
||||||
* @param {number} limit
|
|
||||||
*/
|
|
||||||
sortedByLength(indices, limit) {
|
|
||||||
const { items } = this;
|
|
||||||
indices.sort((a, b) => items[a].length - items[b].length);
|
|
||||||
if (indices.length > limit) indices.length = limit;
|
|
||||||
return indices.map((i) => items[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @private
|
|
||||||
* @param {Map<number, number>} scores
|
|
||||||
* @param {number} minScore
|
|
||||||
* @param {number} limit
|
|
||||||
*/
|
|
||||||
rankedResults(scores, minScore, limit) {
|
|
||||||
const { items } = this;
|
|
||||||
const results = [];
|
|
||||||
|
|
||||||
for (const [index, score] of scores) {
|
|
||||||
if (score >= minScore) {
|
|
||||||
results.push({ index, score });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
results.sort((a, b) => {
|
|
||||||
if (b.score !== a.score) return b.score - a.score;
|
|
||||||
return items[a.index].length - items[b.index].length;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (results.length > limit) results.length = limit;
|
|
||||||
|
|
||||||
return results.map((r) => items[r.index]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @param {string} query */
|
|
||||||
function normalizeQuery(query) {
|
|
||||||
let result = "";
|
|
||||||
let start = 0;
|
|
||||||
let end = query.length;
|
|
||||||
|
|
||||||
while (start < end && query.charCodeAt(start) <= 32) start++;
|
|
||||||
while (end > start && query.charCodeAt(end - 1) <= 32) end--;
|
|
||||||
|
|
||||||
for (let i = start; i < end; i++) {
|
|
||||||
const code = query.charCodeAt(i);
|
|
||||||
if (code >= 128) continue;
|
|
||||||
result +=
|
|
||||||
code >= 65 && code <= 90 ? String.fromCharCode(code + 32) : query[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} text
|
|
||||||
* @param {string} separators
|
|
||||||
* @param {number} maxLength
|
|
||||||
*/
|
|
||||||
function parseWords(text, separators, maxLength) {
|
|
||||||
/** @type {string[]} */
|
|
||||||
const words = [];
|
|
||||||
let start = 0;
|
|
||||||
|
|
||||||
for (let i = 0; i <= text.length; i++) {
|
|
||||||
const isEnd = i === text.length || separators.includes(text[i]);
|
|
||||||
|
|
||||||
if (isEnd && i > start) {
|
|
||||||
const word = text.slice(start, i);
|
|
||||||
if (word.length <= maxLength && !words.includes(word)) {
|
|
||||||
words.push(word);
|
|
||||||
}
|
|
||||||
start = i + 1;
|
|
||||||
} else if (isEnd) {
|
|
||||||
start = i + 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return words;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Map<string, number[]>} index
|
|
||||||
* @param {string} key
|
|
||||||
* @param {number} value
|
|
||||||
*/
|
|
||||||
function addToIndex(index, key, value) {
|
|
||||||
const existing = index.get(key);
|
|
||||||
if (existing) {
|
|
||||||
existing.push(value);
|
|
||||||
} else {
|
|
||||||
index.set(key, [value]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Map<string, number[]>} index
|
|
||||||
* @param {string} word
|
|
||||||
* @param {number} itemIndex
|
|
||||||
*/
|
|
||||||
function addTrigramsToIndex(index, word, itemIndex) {
|
|
||||||
if (word.length < 3) return;
|
|
||||||
|
|
||||||
for (let i = 0; i <= word.length - 3; i++) {
|
|
||||||
const trigram = word[i] + word[i + 1] + word[i + 2];
|
|
||||||
const existing = index.get(trigram);
|
|
||||||
|
|
||||||
if (!existing) {
|
|
||||||
index.set(trigram, [itemIndex]);
|
|
||||||
} else if (existing[existing.length - 1] !== itemIndex) {
|
|
||||||
existing.push(itemIndex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @param {number[][]} arrays */
|
|
||||||
function intersectAll(arrays) {
|
|
||||||
if (!arrays.length) return [];
|
|
||||||
|
|
||||||
let smallestIndex = 0;
|
|
||||||
for (let i = 1; i < arrays.length; i++) {
|
|
||||||
if (arrays[i].length < arrays[smallestIndex].length) {
|
|
||||||
smallestIndex = i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = arrays[smallestIndex].slice();
|
|
||||||
|
|
||||||
for (let i = 0; i < arrays.length && result.length > 0; i++) {
|
|
||||||
if (i === smallestIndex) continue;
|
|
||||||
|
|
||||||
let writeIndex = 0;
|
|
||||||
for (let j = 0; j < result.length; j++) {
|
|
||||||
if (binarySearch(arrays[i], result[j])) {
|
|
||||||
result[writeIndex++] = result[j];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result.length = writeIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {number[]} sortedArray
|
|
||||||
* @param {number} value
|
|
||||||
*/
|
|
||||||
function binarySearch(sortedArray, value) {
|
|
||||||
let low = 0;
|
|
||||||
let high = sortedArray.length - 1;
|
|
||||||
|
|
||||||
while (low <= high) {
|
|
||||||
const mid = (low + high) >> 1;
|
|
||||||
const midValue = sortedArray[mid];
|
|
||||||
|
|
||||||
if (midValue === value) return true;
|
|
||||||
if (midValue < value) low = mid + 1;
|
|
||||||
else high = mid - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {number} wordLength
|
|
||||||
* @param {number} round
|
|
||||||
*/
|
|
||||||
function pickTrigramPosition(wordLength, round) {
|
|
||||||
const maxPosition = wordLength - 3;
|
|
||||||
if (maxPosition < 0) return -1;
|
|
||||||
|
|
||||||
if (round === 0) return 0;
|
|
||||||
if (round === 1 && maxPosition > 0) return maxPosition;
|
|
||||||
if (round === 2 && maxPosition > 1) return maxPosition >> 1;
|
|
||||||
if (maxPosition <= 2) return -1;
|
|
||||||
|
|
||||||
const middle = maxPosition >> 1;
|
|
||||||
const offset = (round - 2) >> 1;
|
|
||||||
const position = round & 1 ? Math.max(0, middle - offset) : middle + offset;
|
|
||||||
|
|
||||||
if (position === 0 || position >= maxPosition || position === middle) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return position;
|
|
||||||
}
|
|
||||||
417
modules/quickmatch-js/0.4.0/src/index.js
Normal file
417
modules/quickmatch-js/0.4.0/src/index.js
Normal file
@@ -0,0 +1,417 @@
|
|||||||
|
const DEFAULT_SEPARATORS = "_- :/";
|
||||||
|
const DEFAULT_TRIGRAM_BUDGET = 6;
|
||||||
|
const DEFAULT_LIMIT = 100;
|
||||||
|
const DEFAULT_MIN_SCORE = 2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search configuration.
|
||||||
|
*
|
||||||
|
* Defaults work well for most use cases.
|
||||||
|
* Tweak `trigramBudget` to trade speed for typo tolerance.
|
||||||
|
*/
|
||||||
|
export class QuickMatchConfig {
|
||||||
|
/** Characters that separate words in items (e.g. "hash_rate" → ["hash", "rate"]).
|
||||||
|
* @type {string} */
|
||||||
|
separators = DEFAULT_SEPARATORS;
|
||||||
|
|
||||||
|
/** Max results returned per query.
|
||||||
|
* @type {number} */
|
||||||
|
limit = DEFAULT_LIMIT;
|
||||||
|
|
||||||
|
/** How hard to try matching typos (0 = off, 3–6 = fast, 9–15 = thorough, max 20).
|
||||||
|
* @type {number} */
|
||||||
|
trigramBudget = DEFAULT_TRIGRAM_BUDGET;
|
||||||
|
|
||||||
|
/** Min overlap required for a typo match. Higher = fewer false positives.
|
||||||
|
* @type {number} */
|
||||||
|
minScore = DEFAULT_MIN_SCORE;
|
||||||
|
|
||||||
|
/** @param {number} n - Max results (default: 100, min: 1) */
|
||||||
|
withLimit(n) {
|
||||||
|
this.limit = Math.max(1, n);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {number} n - Trigram budget (0-20, default: 6) */
|
||||||
|
withTrigramBudget(n) {
|
||||||
|
this.trigramBudget = Math.max(0, Math.min(20, n));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {string} s - Separator characters (default: '_- :/') */
|
||||||
|
withSeparators(s) {
|
||||||
|
this.separators = s;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {number} n - Min trigram score (default: 2, min: 1) */
|
||||||
|
withMinScore(n) {
|
||||||
|
this.minScore = Math.max(1, n);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instant search over a list of strings.
|
||||||
|
*
|
||||||
|
* Supports exact words, prefixes ("dom" → "dominance"), joined words
|
||||||
|
* ("hashrate" → "hash_rate"), and typo tolerance ("suply" → "supply").
|
||||||
|
* Results are ranked: exact matches first, then by specificity.
|
||||||
|
*/
|
||||||
|
export class QuickMatch {
|
||||||
|
/** @param {string[]} items - Searchable items (lowercase) @param {QuickMatchConfig} [config] */
|
||||||
|
constructor(items, config = new QuickMatchConfig()) {
|
||||||
|
this.config = config;
|
||||||
|
this.items = items;
|
||||||
|
/** @type {Map<string, number[]>} */
|
||||||
|
this.wordIndex = new Map();
|
||||||
|
/** @type {Map<string, number[]>} */
|
||||||
|
this.trigramIndex = new Map();
|
||||||
|
this._sepLookup = sepLookup(config.separators);
|
||||||
|
this._scores = new Uint32Array(items.length);
|
||||||
|
/** @type {number[]} */
|
||||||
|
this._dirty = [];
|
||||||
|
|
||||||
|
let maxWordLen = 0;
|
||||||
|
let maxQueryLen = 0;
|
||||||
|
let maxWords = 0;
|
||||||
|
const sep = this._sepLookup;
|
||||||
|
|
||||||
|
for (let idx = 0; idx < items.length; idx++) {
|
||||||
|
const item = items[idx];
|
||||||
|
if (item.length > maxQueryLen) maxQueryLen = item.length;
|
||||||
|
|
||||||
|
const words = [];
|
||||||
|
let start = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i <= item.length; i++) {
|
||||||
|
if (i < item.length && !sep[item.charCodeAt(i)]) continue;
|
||||||
|
if (i > start) {
|
||||||
|
const word = item.slice(start, i);
|
||||||
|
words.push(word);
|
||||||
|
if (word.length > maxWordLen) maxWordLen = word.length;
|
||||||
|
for (let len = 1; len <= word.length; len++) {
|
||||||
|
addToIndex(this.wordIndex, word.slice(0, len), idx);
|
||||||
|
}
|
||||||
|
for (let k = 0; k <= word.length - 3; k++) {
|
||||||
|
addToIndex(this.trigramIndex, word[k] + word[k + 1] + word[k + 2], idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
start = i + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < words.length - 1; i++) {
|
||||||
|
const compound = words[i] + words[i + 1];
|
||||||
|
const from = words[i].length + 1;
|
||||||
|
for (let len = from; len <= compound.length; len++) {
|
||||||
|
addToIndex(this.wordIndex, compound.slice(0, len), idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (words.length > maxWords) maxWords = words.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.maxWordLen = maxWordLen + 4;
|
||||||
|
this.maxQueryLen = maxQueryLen + 6;
|
||||||
|
this.maxWords = maxWords + 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {string} query */
|
||||||
|
matches(query) {
|
||||||
|
return this.matchesWith(query, this.config);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} query
|
||||||
|
* @param {QuickMatchConfig} config
|
||||||
|
*/
|
||||||
|
matchesWith(query, config) {
|
||||||
|
const { limit, trigramBudget } = config;
|
||||||
|
const sep =
|
||||||
|
config.separators === this.config.separators
|
||||||
|
? this._sepLookup
|
||||||
|
: sepLookup(config.separators);
|
||||||
|
|
||||||
|
const q = normalize(query);
|
||||||
|
if (!q || q.length > this.maxQueryLen) return [];
|
||||||
|
|
||||||
|
const qwords = splitWords(q, sep, this.maxWordLen);
|
||||||
|
if (!qwords.length || qwords.length > this.maxWords) return [];
|
||||||
|
|
||||||
|
const known = [];
|
||||||
|
const unknown = [];
|
||||||
|
|
||||||
|
for (const w of qwords) {
|
||||||
|
const hits = this.wordIndex.get(w);
|
||||||
|
if (hits) {
|
||||||
|
known.push(hits);
|
||||||
|
} else if (w.length >= 3 && unknown.length < trigramBudget) {
|
||||||
|
unknown.push(w);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const pool = intersect(known);
|
||||||
|
|
||||||
|
// Try typo matching for unknown words
|
||||||
|
if (unknown.length && trigramBudget) {
|
||||||
|
const { _scores: scores, _dirty: dirty } = this;
|
||||||
|
|
||||||
|
if (pool) {
|
||||||
|
for (const i of pool) {
|
||||||
|
scores[i] = 1;
|
||||||
|
dirty.push(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const hitCount = this._scoreTrigrams(
|
||||||
|
unknown,
|
||||||
|
trigramBudget,
|
||||||
|
pool !== null,
|
||||||
|
Math.max(0, q.length - 3),
|
||||||
|
);
|
||||||
|
const minScore = Math.max(config.minScore, Math.ceil(hitCount / 2));
|
||||||
|
const result = this._rank(dirty, minScore, qwords, sep, limit);
|
||||||
|
|
||||||
|
for (const i of dirty) scores[i] = 0;
|
||||||
|
dirty.length = 0;
|
||||||
|
|
||||||
|
if (result.length > 0) return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rank known candidates (intersection, or union as fallback)
|
||||||
|
const candidates = pool || union(known);
|
||||||
|
return candidates.length > 0
|
||||||
|
? this._rank(candidates, null, qwords, sep, limit)
|
||||||
|
: [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @private @param {string[]} unknown @param {number} budget @param {boolean} poolOnly @param {number} minLen */
|
||||||
|
_scoreTrigrams(unknown, budget, poolOnly, minLen) {
|
||||||
|
const { _scores: scores, _dirty: dirty, items } = this;
|
||||||
|
const visited = new Set();
|
||||||
|
const maxRounds = budget;
|
||||||
|
let hits = 0;
|
||||||
|
|
||||||
|
outer: for (let round = 0; round < maxRounds; round++) {
|
||||||
|
for (const word of unknown) {
|
||||||
|
if (budget <= 0) break outer;
|
||||||
|
|
||||||
|
const pos = trigramPosition(word.length, round);
|
||||||
|
if (pos < 0) continue;
|
||||||
|
|
||||||
|
const tri = word[pos] + word[pos + 1] + word[pos + 2];
|
||||||
|
if (visited.has(tri)) continue;
|
||||||
|
visited.add(tri);
|
||||||
|
budget--;
|
||||||
|
|
||||||
|
const matched = this.trigramIndex.get(tri);
|
||||||
|
if (!matched) continue;
|
||||||
|
hits++;
|
||||||
|
|
||||||
|
if (poolOnly) {
|
||||||
|
for (let j = 0; j < matched.length; j++) {
|
||||||
|
const i = matched[j];
|
||||||
|
if (scores[i] > 0) scores[i]++;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (let j = 0; j < matched.length; j++) {
|
||||||
|
const i = matched[j];
|
||||||
|
if (items[i].length >= minLen) {
|
||||||
|
if (scores[i] === 0) dirty.push(i);
|
||||||
|
scores[i]++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return hits;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* @param {number[]} indices
|
||||||
|
* @param {number|null} minScore
|
||||||
|
* @param {string[]} qwords
|
||||||
|
* @param {Uint8Array} sep
|
||||||
|
* @param {number} limit
|
||||||
|
*/
|
||||||
|
_rank(indices, minScore, qwords, sep, limit) {
|
||||||
|
const { items, _scores: scores } = this;
|
||||||
|
const buckets = [[], [], []]; // ps=0, ps=1, ps=2
|
||||||
|
|
||||||
|
for (let i = 0; i < indices.length; i++) {
|
||||||
|
const idx = indices[i];
|
||||||
|
if (minScore !== null && scores[idx] < minScore) continue;
|
||||||
|
buckets[prefixScore(items[idx], qwords, sep)].push(idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = [];
|
||||||
|
for (let ps = 2; ps >= 0 && results.length < limit; ps--) {
|
||||||
|
const bucket = buckets[ps];
|
||||||
|
if (!bucket.length) continue;
|
||||||
|
bucket.sort(
|
||||||
|
(a, b) => scores[b] - scores[a] || items[a].length - items[b].length,
|
||||||
|
);
|
||||||
|
const take = Math.min(bucket.length, limit - results.length);
|
||||||
|
for (let i = 0; i < take; i++) results.push(items[bucket[i]]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Helpers ---
|
||||||
|
|
||||||
|
/** @param {string} query */
|
||||||
|
function normalize(query) {
|
||||||
|
let out = "";
|
||||||
|
let start = 0;
|
||||||
|
let end = query.length;
|
||||||
|
while (start < end && query.charCodeAt(start) <= 32) start++;
|
||||||
|
while (end > start && query.charCodeAt(end - 1) <= 32) end--;
|
||||||
|
for (let i = start; i < end; i++) {
|
||||||
|
const c = query.charCodeAt(i);
|
||||||
|
if (c >= 128) continue;
|
||||||
|
out += c >= 65 && c <= 90 ? String.fromCharCode(c + 32) : query[i];
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {string} separators */
|
||||||
|
function sepLookup(separators) {
|
||||||
|
const t = new Uint8Array(128);
|
||||||
|
for (let i = 0; i < separators.length; i++) {
|
||||||
|
const c = separators.charCodeAt(i);
|
||||||
|
if (c < 128) t[c] = 1;
|
||||||
|
}
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} text
|
||||||
|
* @param {Uint8Array} sep
|
||||||
|
* @param {number} maxLen
|
||||||
|
*/
|
||||||
|
function splitWords(text, sep, maxLen) {
|
||||||
|
/** @type {string[]} */
|
||||||
|
const words = [];
|
||||||
|
let start = 0;
|
||||||
|
for (let i = 0; i <= text.length; i++) {
|
||||||
|
if (i < text.length && !sep[text.charCodeAt(i)]) continue;
|
||||||
|
if (i > start) {
|
||||||
|
const w = text.slice(start, i);
|
||||||
|
if (w.length <= maxLen && !words.includes(w)) words.push(w);
|
||||||
|
}
|
||||||
|
start = i + 1;
|
||||||
|
}
|
||||||
|
return words;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Map<string, number[]>} index
|
||||||
|
* @param {string} key
|
||||||
|
* @param {number} value
|
||||||
|
*/
|
||||||
|
function addToIndex(index, key, value) {
|
||||||
|
const arr = index.get(key);
|
||||||
|
if (arr) {
|
||||||
|
if (arr[arr.length - 1] !== value) arr.push(value);
|
||||||
|
} else {
|
||||||
|
index.set(key, [value]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {number[][]} arrays */
|
||||||
|
function union(arrays) {
|
||||||
|
if (arrays.length <= 1) return arrays[0] || [];
|
||||||
|
const seen = new Set();
|
||||||
|
const result = [];
|
||||||
|
for (const arr of arrays) {
|
||||||
|
for (const idx of arr) {
|
||||||
|
if (!seen.has(idx)) {
|
||||||
|
seen.add(idx);
|
||||||
|
result.push(idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {number[][]} arrays @returns {number[]|null} */
|
||||||
|
function intersect(arrays) {
|
||||||
|
if (arrays.length <= 1) return arrays[0] || null;
|
||||||
|
|
||||||
|
let si = 0;
|
||||||
|
for (let i = 1; i < arrays.length; i++) {
|
||||||
|
if (arrays[i].length < arrays[si].length) si = i;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = arrays[si].slice();
|
||||||
|
for (let i = 0; i < arrays.length; i++) {
|
||||||
|
if (i === si) continue;
|
||||||
|
let w = 0;
|
||||||
|
for (let j = 0; j < result.length; j++) {
|
||||||
|
if (bsearch(arrays[i], result[j])) result[w++] = result[j];
|
||||||
|
}
|
||||||
|
result.length = w;
|
||||||
|
if (!w) return null;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number[]} arr
|
||||||
|
* @param {number} val
|
||||||
|
*/
|
||||||
|
function bsearch(arr, val) {
|
||||||
|
let lo = 0,
|
||||||
|
hi = arr.length - 1;
|
||||||
|
while (lo <= hi) {
|
||||||
|
const mid = (lo + hi) >> 1;
|
||||||
|
if (arr[mid] === val) return true;
|
||||||
|
if (arr[mid] < val) lo = mid + 1;
|
||||||
|
else hi = mid - 1;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {string} item @param {string[]} qwords @param {Uint8Array} sep */
|
||||||
|
function prefixScore(item, qwords, sep) {
|
||||||
|
let qi = 0,
|
||||||
|
pos = 0;
|
||||||
|
const len = item.length;
|
||||||
|
|
||||||
|
while (qi < qwords.length) {
|
||||||
|
while (pos < len && sep[item.charCodeAt(pos)]) pos++;
|
||||||
|
if (pos >= len) return 0;
|
||||||
|
|
||||||
|
const ws = pos;
|
||||||
|
while (pos < len && !sep[item.charCodeAt(pos)]) pos++;
|
||||||
|
|
||||||
|
const qw = qwords[qi];
|
||||||
|
if (pos - ws < qw.length) return 0;
|
||||||
|
for (let j = 0; j < qw.length; j++) {
|
||||||
|
if (item.charCodeAt(ws + j) !== qw.charCodeAt(j)) return 0;
|
||||||
|
}
|
||||||
|
qi++;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (pos < len && sep[item.charCodeAt(pos)]) pos++;
|
||||||
|
return pos >= len ? 2 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {number} len @param {number} round */
|
||||||
|
function trigramPosition(len, round) {
|
||||||
|
const max = len - 3;
|
||||||
|
if (max < 0) return -1;
|
||||||
|
if (round === 0) return 0;
|
||||||
|
if (round === 1 && max > 0) return max;
|
||||||
|
if (round === 2 && max > 1) return max >> 1;
|
||||||
|
if (max <= 2) return -1;
|
||||||
|
|
||||||
|
const mid = max >> 1;
|
||||||
|
const off = (round - 2) >> 1;
|
||||||
|
const pos = round & 1 ? Math.max(0, mid - off) : mid + off;
|
||||||
|
return pos === 0 || pos >= max || pos === mid ? -1 : pos;
|
||||||
|
}
|
||||||
@@ -3850,8 +3850,8 @@ class SeriesTree_Cointime_Adjusted:
|
|||||||
|
|
||||||
def __init__(self, client: BrkClientBase, base_path: str = ''):
|
def __init__(self, client: BrkClientBase, base_path: str = ''):
|
||||||
self.inflation_rate: BpsPercentRatioPattern = BpsPercentRatioPattern(client, 'cointime_adj_inflation_rate')
|
self.inflation_rate: BpsPercentRatioPattern = BpsPercentRatioPattern(client, 'cointime_adj_inflation_rate')
|
||||||
self.tx_velocity_native: SeriesPattern1[StoredF64] = SeriesPattern1(client, 'cointime_adj_tx_velocity')
|
self.tx_velocity_native: SeriesPattern1[StoredF64] = SeriesPattern1(client, 'cointime_adj_tx_velocity_btc')
|
||||||
self.tx_velocity_fiat: SeriesPattern1[StoredF64] = SeriesPattern1(client, 'cointime_adj_tx_velocity_fiat')
|
self.tx_velocity_fiat: SeriesPattern1[StoredF64] = SeriesPattern1(client, 'cointime_adj_tx_velocity_usd')
|
||||||
|
|
||||||
class SeriesTree_Cointime_ReserveRisk:
|
class SeriesTree_Cointime_ReserveRisk:
|
||||||
"""Series tree node."""
|
"""Series tree node."""
|
||||||
@@ -4749,8 +4749,8 @@ class SeriesTree_Supply_Velocity:
|
|||||||
"""Series tree node."""
|
"""Series tree node."""
|
||||||
|
|
||||||
def __init__(self, client: BrkClientBase, base_path: str = ''):
|
def __init__(self, client: BrkClientBase, base_path: str = ''):
|
||||||
self.native: SeriesPattern1[StoredF64] = SeriesPattern1(client, 'velocity')
|
self.native: SeriesPattern1[StoredF64] = SeriesPattern1(client, 'velocity_btc')
|
||||||
self.fiat: SeriesPattern1[StoredF64] = SeriesPattern1(client, 'velocity_fiat')
|
self.fiat: SeriesPattern1[StoredF64] = SeriesPattern1(client, 'velocity_usd')
|
||||||
|
|
||||||
class SeriesTree_Supply:
|
class SeriesTree_Supply:
|
||||||
"""Series tree node."""
|
"""Series tree node."""
|
||||||
|
|||||||
@@ -52,7 +52,8 @@ import { Unit } from "../utils/units.js";
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {Series<any>} AnySeries
|
* @typedef {SingleValueData | CandlestickData | LineData | BaselineData | HistogramData | WhitespaceData} AnyChartData
|
||||||
|
* @typedef {Series<AnyChartData>} AnySeries
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -69,7 +70,7 @@ import { Unit } from "../utils/units.js";
|
|||||||
* @property {function(number): void} removeFrom
|
* @property {function(number): void} removeFrom
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const lineWidth = /** @type {any} */ (1.5);
|
const lineWidth = /** @type {1} */ (/** @type {unknown} */ (1.5));
|
||||||
|
|
||||||
const MAX_SIZE = 10_000;
|
const MAX_SIZE = 10_000;
|
||||||
|
|
||||||
@@ -140,7 +141,7 @@ export function createChart({ parent, brk, fitContent }) {
|
|||||||
if (cached) {
|
if (cached) {
|
||||||
this.data = cached;
|
this.data = cached;
|
||||||
}
|
}
|
||||||
endpoint.slice(-MAX_SIZE).fetch((/** @type {any} */ result) => {
|
endpoint.slice(-MAX_SIZE).fetch((/** @type {AnySeriesData} */ result) => {
|
||||||
if (currentGen !== generation) return;
|
if (currentGen !== generation) return;
|
||||||
cache.set(endpoint.path, result);
|
cache.set(endpoint.path, result);
|
||||||
this.data = result;
|
this.data = result;
|
||||||
@@ -150,7 +151,7 @@ export function createChart({ parent, brk, fitContent }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Memory cache for instant index switching
|
// Memory cache for instant index switching
|
||||||
/** @type {Map<string, SeriesData<any>>} */
|
/** @type {Map<string, AnySeriesData>} */
|
||||||
const cache = new Map();
|
const cache = new Map();
|
||||||
|
|
||||||
// Range state: localStorage stores all ranges per-index, URL stores current range only
|
// Range state: localStorage stores all ranges per-index, URL stores current range only
|
||||||
@@ -432,9 +433,9 @@ export function createChart({ parent, brk, fitContent }) {
|
|||||||
* @param {boolean} [args.defaultActive]
|
* @param {boolean} [args.defaultActive]
|
||||||
* @param {(order: number) => void} args.setOrder
|
* @param {(order: number) => void} args.setOrder
|
||||||
* @param {(active: boolean, highlighted: boolean) => void} args.applyOptions
|
* @param {(active: boolean, highlighted: boolean) => void} args.applyOptions
|
||||||
* @param {() => readonly any[]} args.getData
|
* @param {() => readonly AnyChartData[]} args.getData
|
||||||
* @param {(data: any[]) => void} args.setData
|
* @param {(data: AnyChartData[]) => void} args.setData
|
||||||
* @param {(data: any) => void} args.update
|
* @param {(data: AnyChartData) => void} args.update
|
||||||
* @param {() => void} args.onRemove
|
* @param {() => void} args.onRemove
|
||||||
*/
|
*/
|
||||||
create({
|
create({
|
||||||
@@ -791,8 +792,7 @@ export function createChart({ parent, brk, fitContent }) {
|
|||||||
const upColor = customColors?.[0] ?? colors.bi.p1[0];
|
const upColor = customColors?.[0] ?? colors.bi.p1[0];
|
||||||
const downColor = customColors?.[1] ?? colors.bi.p1[1];
|
const downColor = customColors?.[1] ?? colors.bi.p1[1];
|
||||||
|
|
||||||
/** @type {CandlestickISeries} */
|
const candlestickISeries = /** @type {CandlestickISeries} */ (
|
||||||
const candlestickISeries = /** @type {any} */ (
|
|
||||||
ichart.addSeries(
|
ichart.addSeries(
|
||||||
/** @type {SeriesDefinition<'Candlestick'>} */ (CandlestickSeries),
|
/** @type {SeriesDefinition<'Candlestick'>} */ (CandlestickSeries),
|
||||||
{ visible: false, borderVisible: false, ...options },
|
{ visible: false, borderVisible: false, ...options },
|
||||||
@@ -800,8 +800,7 @@ export function createChart({ parent, brk, fitContent }) {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
/** @type {LineISeries} */
|
const lineISeries = /** @type {LineISeries} */ (
|
||||||
const lineISeries = /** @type {any} */ (
|
|
||||||
ichart.addSeries(
|
ichart.addSeries(
|
||||||
/** @type {SeriesDefinition<'Line'>} */ (LineSeries),
|
/** @type {SeriesDefinition<'Line'>} */ (LineSeries),
|
||||||
{ visible: false, lineWidth, priceLineVisible: true },
|
{ visible: false, lineWidth, priceLineVisible: true },
|
||||||
@@ -851,9 +850,10 @@ export function createChart({ parent, brk, fitContent }) {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
setData: (data) => {
|
setData: (data) => {
|
||||||
candlestickISeries.setData(data);
|
const cdata = /** @type {CandlestickData[]} */ (data);
|
||||||
|
candlestickISeries.setData(cdata);
|
||||||
lineISeries.setData(
|
lineISeries.setData(
|
||||||
data.map((d) => ({ time: d.time, value: d.close })),
|
cdata.map((d) => ({ time: d.time, value: d.close })),
|
||||||
);
|
);
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
if (generation !== series.generation) return;
|
if (generation !== series.generation) return;
|
||||||
@@ -862,8 +862,9 @@ export function createChart({ parent, brk, fitContent }) {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
update: (data) => {
|
update: (data) => {
|
||||||
candlestickISeries.update(data);
|
const cd = /** @type {CandlestickData} */ (data);
|
||||||
lineISeries.update({ time: data.time, value: data.close });
|
candlestickISeries.update(cd);
|
||||||
|
lineISeries.update({ time: cd.time, value: cd.close });
|
||||||
},
|
},
|
||||||
getData: () => candlestickISeries.data(),
|
getData: () => candlestickISeries.data(),
|
||||||
onRemove: () => {
|
onRemove: () => {
|
||||||
@@ -903,8 +904,7 @@ export function createChart({ parent, brk, fitContent }) {
|
|||||||
const positiveColor = isDualColor ? color[0] : color;
|
const positiveColor = isDualColor ? color[0] : color;
|
||||||
const negativeColor = isDualColor ? color[1] : color;
|
const negativeColor = isDualColor ? color[1] : color;
|
||||||
|
|
||||||
/** @type {HistogramISeries} */
|
const iseries = /** @type {HistogramISeries} */ (
|
||||||
const iseries = /** @type {any} */ (
|
|
||||||
ichart.addSeries(
|
ichart.addSeries(
|
||||||
/** @type {SeriesDefinition<'Histogram'>} */ (HistogramSeries),
|
/** @type {SeriesDefinition<'Histogram'>} */ (HistogramSeries),
|
||||||
{ priceLineVisible: false, ...options },
|
{ priceLineVisible: false, ...options },
|
||||||
@@ -972,8 +972,7 @@ export function createChart({ parent, brk, fitContent }) {
|
|||||||
defaultActive,
|
defaultActive,
|
||||||
options,
|
options,
|
||||||
}) {
|
}) {
|
||||||
/** @type {LineISeries} */
|
const iseries = /** @type {LineISeries} */ (
|
||||||
const iseries = /** @type {any} */ (
|
|
||||||
ichart.addSeries(
|
ichart.addSeries(
|
||||||
/** @type {SeriesDefinition<'Line'>} */ (LineSeries),
|
/** @type {SeriesDefinition<'Line'>} */ (LineSeries),
|
||||||
{ lineWidth, priceLineVisible: false, ...options },
|
{ lineWidth, priceLineVisible: false, ...options },
|
||||||
@@ -1029,8 +1028,7 @@ export function createChart({ parent, brk, fitContent }) {
|
|||||||
defaultActive,
|
defaultActive,
|
||||||
options,
|
options,
|
||||||
}) {
|
}) {
|
||||||
/** @type {LineISeries} */
|
const iseries = /** @type {LineISeries} */ (
|
||||||
const iseries = /** @type {any} */ (
|
|
||||||
ichart.addSeries(
|
ichart.addSeries(
|
||||||
/** @type {SeriesDefinition<'Line'>} */ (LineSeries),
|
/** @type {SeriesDefinition<'Line'>} */ (LineSeries),
|
||||||
{
|
{
|
||||||
@@ -1110,8 +1108,7 @@ export function createChart({ parent, brk, fitContent }) {
|
|||||||
bottomColor = colors.bi.p1[1],
|
bottomColor = colors.bi.p1[1],
|
||||||
options,
|
options,
|
||||||
}) {
|
}) {
|
||||||
/** @type {BaselineISeries} */
|
const iseries = /** @type {BaselineISeries} */ (
|
||||||
const iseries = /** @type {any} */ (
|
|
||||||
ichart.addSeries(
|
ichart.addSeries(
|
||||||
/** @type {SeriesDefinition<'Baseline'>} */ (BaselineSeries),
|
/** @type {SeriesDefinition<'Baseline'>} */ (BaselineSeries),
|
||||||
{
|
{
|
||||||
@@ -1182,8 +1179,7 @@ export function createChart({ parent, brk, fitContent }) {
|
|||||||
bottomColor = colors.bi.p1[1],
|
bottomColor = colors.bi.p1[1],
|
||||||
options,
|
options,
|
||||||
}) {
|
}) {
|
||||||
/** @type {BaselineISeries} */
|
const iseries = /** @type {BaselineISeries} */ (
|
||||||
const iseries = /** @type {any} */ (
|
|
||||||
ichart.addSeries(
|
ichart.addSeries(
|
||||||
/** @type {SeriesDefinition<'Baseline'>} */ (BaselineSeries),
|
/** @type {SeriesDefinition<'Baseline'>} */ (BaselineSeries),
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ function volumeFolder(activity, color, title) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {{ transferVolume: TransferVolumePattern }} activity
|
* @param {{ transferVolume: TransferVolumePattern }} activity
|
||||||
* @param {CountPattern<any>} adjustedTransferVolume
|
* @param {CountPattern<number>} adjustedTransferVolume
|
||||||
* @param {Color} color
|
* @param {Color} color
|
||||||
* @param {(name: string) => string} title
|
* @param {(name: string) => string} title
|
||||||
* @returns {PartialOptionsGroup}
|
* @returns {PartialOptionsGroup}
|
||||||
@@ -171,7 +171,7 @@ function singleRollingSoprTree(ratio, title, prefix = "") {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {CountPattern<any>} valueDestroyed
|
* @param {CountPattern<number>} valueDestroyed
|
||||||
* @param {(name: string) => string} title
|
* @param {(name: string) => string} title
|
||||||
* @returns {PartialOptionsTree}
|
* @returns {PartialOptionsTree}
|
||||||
*/
|
*/
|
||||||
@@ -180,7 +180,7 @@ function valueDestroyedTree(valueDestroyed, title) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {CountPattern<any>} valueDestroyed
|
* @param {CountPattern<number>} valueDestroyed
|
||||||
* @param {(name: string) => string} title
|
* @param {(name: string) => string} title
|
||||||
* @returns {PartialOptionsGroup}
|
* @returns {PartialOptionsGroup}
|
||||||
*/
|
*/
|
||||||
@@ -189,8 +189,8 @@ function valueDestroyedFolder(valueDestroyed, title) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {CountPattern<any>} valueDestroyed
|
* @param {CountPattern<number>} valueDestroyed
|
||||||
* @param {CountPattern<any>} adjusted
|
* @param {CountPattern<number>} adjusted
|
||||||
* @param {(name: string) => string} title
|
* @param {(name: string) => string} title
|
||||||
* @returns {PartialOptionsGroup}
|
* @returns {PartialOptionsGroup}
|
||||||
*/
|
*/
|
||||||
@@ -471,7 +471,7 @@ function groupedVolumeFolder(list, all, title, getTransferVolume) {
|
|||||||
* @param {A} all
|
* @param {A} all
|
||||||
* @param {(name: string) => string} title
|
* @param {(name: string) => string} title
|
||||||
* @param {(c: T | A) => { sum: Record<string, AnyValuePattern>, cumulative: AnyValuePattern, inProfit: { sum: Record<string, AnyValuePattern>, cumulative: AnyValuePattern }, inLoss: { sum: Record<string, AnyValuePattern>, cumulative: AnyValuePattern } }} getTransferVolume
|
* @param {(c: T | A) => { sum: Record<string, AnyValuePattern>, cumulative: AnyValuePattern, inProfit: { sum: Record<string, AnyValuePattern>, cumulative: AnyValuePattern }, inLoss: { sum: Record<string, AnyValuePattern>, cumulative: AnyValuePattern } }} getTransferVolume
|
||||||
* @param {(c: T | A) => CountPattern<any>} getAdjustedTransferVolume
|
* @param {(c: T | A) => CountPattern<number>} getAdjustedTransferVolume
|
||||||
* @returns {PartialOptionsGroup}
|
* @returns {PartialOptionsGroup}
|
||||||
*/
|
*/
|
||||||
function groupedVolumeFolderWithAdjusted(list, all, title, getTransferVolume, getAdjustedTransferVolume) {
|
function groupedVolumeFolderWithAdjusted(list, all, title, getTransferVolume, getAdjustedTransferVolume) {
|
||||||
@@ -528,7 +528,7 @@ function groupedSoprCharts(list, all, getRatio, title, prefix = "") {
|
|||||||
* @param {readonly T[]} list
|
* @param {readonly T[]} list
|
||||||
* @param {A} all
|
* @param {A} all
|
||||||
* @param {(name: string) => string} title
|
* @param {(name: string) => string} title
|
||||||
* @param {(c: T | A) => CountPattern<any>} getValueDestroyed
|
* @param {(c: T | A) => CountPattern<number>} getValueDestroyed
|
||||||
* @returns {PartialOptionsTree}
|
* @returns {PartialOptionsTree}
|
||||||
*/
|
*/
|
||||||
function groupedValueDestroyedTree(list, all, title, getValueDestroyed) {
|
function groupedValueDestroyedTree(list, all, title, getValueDestroyed) {
|
||||||
@@ -546,7 +546,7 @@ function groupedValueDestroyedTree(list, all, title, getValueDestroyed) {
|
|||||||
* @param {readonly T[]} list
|
* @param {readonly T[]} list
|
||||||
* @param {A} all
|
* @param {A} all
|
||||||
* @param {(name: string) => string} title
|
* @param {(name: string) => string} title
|
||||||
* @param {(c: T | A) => CountPattern<any>} getValueDestroyed
|
* @param {(c: T | A) => CountPattern<number>} getValueDestroyed
|
||||||
* @returns {PartialOptionsGroup}
|
* @returns {PartialOptionsGroup}
|
||||||
*/
|
*/
|
||||||
function groupedValueDestroyedFolder(list, all, title, getValueDestroyed) {
|
function groupedValueDestroyedFolder(list, all, title, getValueDestroyed) {
|
||||||
@@ -559,8 +559,8 @@ function groupedValueDestroyedFolder(list, all, title, getValueDestroyed) {
|
|||||||
* @param {readonly T[]} list
|
* @param {readonly T[]} list
|
||||||
* @param {A} all
|
* @param {A} all
|
||||||
* @param {(name: string) => string} title
|
* @param {(name: string) => string} title
|
||||||
* @param {(c: T | A) => CountPattern<any>} getValueDestroyed
|
* @param {(c: T | A) => CountPattern<number>} getValueDestroyed
|
||||||
* @param {(c: T | A) => CountPattern<any>} getAdjustedValueDestroyed
|
* @param {(c: T | A) => CountPattern<number>} getAdjustedValueDestroyed
|
||||||
* @returns {PartialOptionsGroup}
|
* @returns {PartialOptionsGroup}
|
||||||
*/
|
*/
|
||||||
function groupedValueDestroyedFolderWithAdjusted(list, all, title, getValueDestroyed, getAdjustedValueDestroyed) {
|
function groupedValueDestroyedFolderWithAdjusted(list, all, title, getValueDestroyed, getAdjustedValueDestroyed) {
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ function singleDeltaItems(delta, unit, title, name) {
|
|||||||
title,
|
title,
|
||||||
metric: `${name} Change`,
|
metric: `${name} Change`,
|
||||||
unit,
|
unit,
|
||||||
|
legend: "Change",
|
||||||
}),
|
}),
|
||||||
name: "Change",
|
name: "Change",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -525,6 +525,7 @@ function singleBucketFolder({ name, color, pattern }, parentName) {
|
|||||||
title,
|
title,
|
||||||
metric: "Supply Change",
|
metric: "Supply Change",
|
||||||
unit: Unit.sats,
|
unit: Unit.sats,
|
||||||
|
legend: "Change",
|
||||||
}),
|
}),
|
||||||
name: "Change",
|
name: "Change",
|
||||||
},
|
},
|
||||||
@@ -610,63 +611,32 @@ function groupedBucketCharts(list, groupTitle) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Change",
|
name: "Change",
|
||||||
tree: [
|
tree: ROLLING_WINDOWS.map((w) => ({
|
||||||
{
|
name: w.name,
|
||||||
name: "Compare",
|
title: title(`${w.title} Supply Change`),
|
||||||
title: title("Supply Change"),
|
bottom: list.map(({ name, color, pattern }) =>
|
||||||
bottom: ROLLING_WINDOWS.flatMap((w) =>
|
baseline({
|
||||||
list.map(({ name, color, pattern }) =>
|
series: pattern.supply.all.delta.absolute[w.key],
|
||||||
baseline({
|
name,
|
||||||
series: pattern.supply.all.delta.absolute[w.key],
|
color,
|
||||||
name: `${name} ${w.name}`,
|
unit: Unit.sats,
|
||||||
color,
|
}),
|
||||||
unit: Unit.sats,
|
),
|
||||||
}),
|
})),
|
||||||
),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
...ROLLING_WINDOWS.map((w) => ({
|
|
||||||
name: w.name,
|
|
||||||
title: title(`${w.title} Supply Change`),
|
|
||||||
bottom: list.map(({ name, color, pattern }) =>
|
|
||||||
baseline({
|
|
||||||
series: pattern.supply.all.delta.absolute[w.key],
|
|
||||||
name,
|
|
||||||
color,
|
|
||||||
unit: Unit.sats,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
})),
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Growth Rate",
|
name: "Growth Rate",
|
||||||
tree: [
|
tree: ROLLING_WINDOWS.map((w) => ({
|
||||||
{
|
name: w.name,
|
||||||
name: "Compare",
|
title: title(`${w.title} Supply Growth Rate`),
|
||||||
title: title("Supply Growth Rate"),
|
bottom: list.flatMap(({ name, color, pattern }) =>
|
||||||
bottom: ROLLING_WINDOWS.flatMap((w) =>
|
percentRatio({
|
||||||
list.flatMap(({ name, color, pattern }) =>
|
pattern: pattern.supply.all.delta.rate[w.key],
|
||||||
percentRatio({
|
name,
|
||||||
pattern: pattern.supply.all.delta.rate[w.key],
|
color,
|
||||||
name: `${name} ${w.name}`,
|
}),
|
||||||
color,
|
),
|
||||||
}),
|
})),
|
||||||
),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
...ROLLING_WINDOWS.map((w) => ({
|
|
||||||
name: w.name,
|
|
||||||
title: title(`${w.title} Supply Growth Rate`),
|
|
||||||
bottom: list.flatMap(({ name, color, pattern }) =>
|
|
||||||
percentRatio({
|
|
||||||
pattern: pattern.supply.all.delta.rate[w.key],
|
|
||||||
name,
|
|
||||||
color,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
})),
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export function createPricesSectionFull({ cohort, title }) {
|
|||||||
tree: [
|
tree: [
|
||||||
{
|
{
|
||||||
name: "Compare",
|
name: "Compare",
|
||||||
title: title("Prices"),
|
title: title("Realized Prices"),
|
||||||
top: [
|
top: [
|
||||||
price({ series: tree.realized.price, name: "Realized", color: colors.realized }),
|
price({ series: tree.realized.price, name: "Realized", color: colors.realized }),
|
||||||
price({ series: tree.realized.investor.price, name: "Investor", color: colors.investor }),
|
price({ series: tree.realized.investor.price, name: "Investor", color: colors.investor }),
|
||||||
|
|||||||
@@ -399,6 +399,7 @@ function realizedNetFolder({ netPnl, title, extraChange = [] }) {
|
|||||||
title,
|
title,
|
||||||
metric: "Net Realized P&L Change",
|
metric: "Net Realized P&L Change",
|
||||||
unit: Unit.usd,
|
unit: Unit.usd,
|
||||||
|
legend: "Change",
|
||||||
}),
|
}),
|
||||||
name: "Change",
|
name: "Change",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import { ratioBottomSeries, mapCohortsWithAll, flatMapCohortsWithAll } from "../
|
|||||||
*/
|
*/
|
||||||
function singleDeltaItems(tree, title) {
|
function singleDeltaItems(tree, title) {
|
||||||
return [
|
return [
|
||||||
{ ...sumsTreeBaseline({ windows: mapWindows(tree.realized.cap.delta.absolute, (c) => c.usd), title, metric: "Realized Cap Change", unit: Unit.usd }), name: "Change" },
|
{ ...sumsTreeBaseline({ windows: mapWindows(tree.realized.cap.delta.absolute, (c) => c.usd), title, metric: "Realized Cap Change", unit: Unit.usd, legend: "Change" }), name: "Change" },
|
||||||
{ ...rollingPercentRatioTree({ windows: tree.realized.cap.delta.rate, title, metric: "Realized Cap Growth Rate" }), name: "Growth Rate" },
|
{ ...rollingPercentRatioTree({ windows: tree.realized.cap.delta.rate, title, metric: "Realized Cap Growth Rate" }), name: "Growth Rate" },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ function createCompareFolder(context, items) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Accumulated",
|
name: "Accumulated",
|
||||||
title: `Accumulated Value: ${context}`,
|
title: `Accumulated Value ($100/day): ${context}`,
|
||||||
top: topPane,
|
top: topPane,
|
||||||
bottom: items.flatMap(({ name, color, stack }) =>
|
bottom: items.flatMap(({ name, color, stack }) =>
|
||||||
satsBtcUsd({ pattern: stack, name, color }),
|
satsBtcUsd({ pattern: stack, name, color }),
|
||||||
@@ -188,7 +188,7 @@ function createLongCompareFolder(context, items) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Accumulated",
|
name: "Accumulated",
|
||||||
title: `Accumulated Value: ${context}`,
|
title: `Accumulated Value ($100/day): ${context}`,
|
||||||
top: topPane,
|
top: topPane,
|
||||||
bottom: items.flatMap(({ name, color, stack }) =>
|
bottom: items.flatMap(({ name, color, stack }) =>
|
||||||
satsBtcUsd({ pattern: stack, name, color }),
|
satsBtcUsd({ pattern: stack, name, color }),
|
||||||
@@ -218,7 +218,7 @@ function createSingleEntryTree(item, returnsBottom) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Accumulated",
|
name: "Accumulated",
|
||||||
title: `Accumulated Value: ${titlePrefix}`,
|
title: `Accumulated Value ($100/day): ${titlePrefix}`,
|
||||||
top,
|
top,
|
||||||
bottom: satsBtcUsd({ pattern: stack, name: "Value" }),
|
bottom: satsBtcUsd({ pattern: stack, name: "Value" }),
|
||||||
},
|
},
|
||||||
@@ -259,7 +259,7 @@ function createLongSingleEntry(item) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Accumulated",
|
name: "Accumulated",
|
||||||
title: `Accumulated Value: ${titlePrefix}`,
|
title: `Accumulated Value ($100/day): ${titlePrefix}`,
|
||||||
top,
|
top,
|
||||||
bottom: satsBtcUsd({ pattern: stack, name: "Value" }),
|
bottom: satsBtcUsd({ pattern: stack, name: "Value" }),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ function returnsSubSection(name, periods) {
|
|||||||
tree: [
|
tree: [
|
||||||
{
|
{
|
||||||
name: "Compare",
|
name: "Compare",
|
||||||
title: `${name} Returns`,
|
title: `${name} Price Returns`,
|
||||||
bottom: periods.flatMap((p) =>
|
bottom: periods.flatMap((p) =>
|
||||||
percentRatioBaseline({
|
percentRatioBaseline({
|
||||||
pattern: p.returns,
|
pattern: p.returns,
|
||||||
@@ -133,8 +133,8 @@ function returnsSubSection(name, periods) {
|
|||||||
},
|
},
|
||||||
...periods.map((p) => ({
|
...periods.map((p) => ({
|
||||||
name: periodIdToName(p.id, true),
|
name: periodIdToName(p.id, true),
|
||||||
title: `${periodIdToName(p.id, true)} Returns`,
|
title: `${periodIdToName(p.id, true)} Price Returns`,
|
||||||
bottom: percentRatioBaseline({ pattern: p.returns, name: "Total" }),
|
bottom: percentRatioBaseline({ pattern: p.returns, name: "Return" }),
|
||||||
})),
|
})),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
@@ -153,7 +153,7 @@ function returnsSubSectionWithCagr(name, periods) {
|
|||||||
tree: [
|
tree: [
|
||||||
{
|
{
|
||||||
name: "Compare",
|
name: "Compare",
|
||||||
title: `${name} Total Returns`,
|
title: `${name} Total Price Returns`,
|
||||||
bottom: periods.flatMap((p) =>
|
bottom: periods.flatMap((p) =>
|
||||||
percentRatioBaseline({
|
percentRatioBaseline({
|
||||||
pattern: p.returns,
|
pattern: p.returns,
|
||||||
@@ -164,8 +164,8 @@ function returnsSubSectionWithCagr(name, periods) {
|
|||||||
},
|
},
|
||||||
...periods.map((p) => ({
|
...periods.map((p) => ({
|
||||||
name: periodIdToName(p.id, true),
|
name: periodIdToName(p.id, true),
|
||||||
title: `${periodIdToName(p.id, true)} Total Returns`,
|
title: `${periodIdToName(p.id, true)} Total Price Returns`,
|
||||||
bottom: percentRatioBaseline({ pattern: p.returns, name: "Total" }),
|
bottom: percentRatioBaseline({ pattern: p.returns, name: "Return" }),
|
||||||
})),
|
})),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -174,7 +174,7 @@ function returnsSubSectionWithCagr(name, periods) {
|
|||||||
tree: [
|
tree: [
|
||||||
{
|
{
|
||||||
name: "Compare",
|
name: "Compare",
|
||||||
title: `${name} CAGR`,
|
title: `${name} Price CAGR`,
|
||||||
bottom: periods.flatMap((p) =>
|
bottom: periods.flatMap((p) =>
|
||||||
percentRatioBaseline({
|
percentRatioBaseline({
|
||||||
pattern: p.cagr,
|
pattern: p.cagr,
|
||||||
@@ -185,7 +185,7 @@ function returnsSubSectionWithCagr(name, periods) {
|
|||||||
},
|
},
|
||||||
...periods.map((p) => ({
|
...periods.map((p) => ({
|
||||||
name: periodIdToName(p.id, true),
|
name: periodIdToName(p.id, true),
|
||||||
title: `${periodIdToName(p.id, true)} CAGR`,
|
title: `${periodIdToName(p.id, true)} Price CAGR`,
|
||||||
bottom: percentRatioBaseline({ pattern: p.cagr, name: "CAGR" }),
|
bottom: percentRatioBaseline({ pattern: p.cagr, name: "CAGR" }),
|
||||||
})),
|
})),
|
||||||
],
|
],
|
||||||
@@ -469,7 +469,7 @@ export function createMarketSection() {
|
|||||||
tree: [
|
tree: [
|
||||||
{
|
{
|
||||||
name: "Compare",
|
name: "Compare",
|
||||||
title: "Returns Comparison",
|
title: "Price Returns",
|
||||||
bottom: [...shortPeriods, ...longPeriods].flatMap((p) =>
|
bottom: [...shortPeriods, ...longPeriods].flatMap((p) =>
|
||||||
percentRatioBaseline({
|
percentRatioBaseline({
|
||||||
pattern: p.returns,
|
pattern: p.returns,
|
||||||
@@ -490,7 +490,7 @@ export function createMarketSection() {
|
|||||||
tree: [
|
tree: [
|
||||||
{
|
{
|
||||||
name: "Compare",
|
name: "Compare",
|
||||||
title: "Historical Price Comparison",
|
title: "Historical Prices",
|
||||||
top: [...shortPeriods, ...longPeriods].map((p) =>
|
top: [...shortPeriods, ...longPeriods].map((p) =>
|
||||||
price({
|
price({
|
||||||
series: p.lookback,
|
series: p.lookback,
|
||||||
@@ -592,7 +592,7 @@ export function createMarketSection() {
|
|||||||
bottom: [
|
bottom: [
|
||||||
baseline({
|
baseline({
|
||||||
series: supply.marketMinusRealizedCapGrowthRate[w.key],
|
series: supply.marketMinusRealizedCapGrowthRate[w.key],
|
||||||
name: w.name,
|
name: "Spread",
|
||||||
unit: Unit.percentage,
|
unit: Unit.percentage,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
@@ -615,7 +615,7 @@ export function createMarketSection() {
|
|||||||
tree: [
|
tree: [
|
||||||
{
|
{
|
||||||
name: "All Periods",
|
name: "All Periods",
|
||||||
title: "SMA vs EMA Comparison",
|
title: "SMA vs EMA",
|
||||||
top: smaVsEma.flatMap((p) => [
|
top: smaVsEma.flatMap((p) => [
|
||||||
price({
|
price({
|
||||||
series: p.sma,
|
series: p.sma,
|
||||||
@@ -659,7 +659,7 @@ export function createMarketSection() {
|
|||||||
tree: [
|
tree: [
|
||||||
{
|
{
|
||||||
name: "Compare",
|
name: "Compare",
|
||||||
title: "RSI Comparison",
|
title: "RSI",
|
||||||
bottom: [
|
bottom: [
|
||||||
...ROLLING_WINDOWS_TO_1M.flatMap((w) =>
|
...ROLLING_WINDOWS_TO_1M.flatMap((w) =>
|
||||||
indexRatio({
|
indexRatio({
|
||||||
@@ -726,7 +726,7 @@ export function createMarketSection() {
|
|||||||
tree: [
|
tree: [
|
||||||
{
|
{
|
||||||
name: "Compare",
|
name: "Compare",
|
||||||
title: "MACD Comparison",
|
title: "MACD",
|
||||||
bottom: ROLLING_WINDOWS_TO_1M.map((w) =>
|
bottom: ROLLING_WINDOWS_TO_1M.map((w) =>
|
||||||
line({
|
line({
|
||||||
series: technical.macd[w.key].line,
|
series: technical.macd[w.key].line,
|
||||||
@@ -789,7 +789,7 @@ export function createMarketSection() {
|
|||||||
bottom: [
|
bottom: [
|
||||||
line({
|
line({
|
||||||
series: volatility[w.key],
|
series: volatility[w.key],
|
||||||
name: w.name,
|
name: "Volatility",
|
||||||
color: w.color,
|
color: w.color,
|
||||||
unit: Unit.percentage,
|
unit: Unit.percentage,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ export function createMiningSection() {
|
|||||||
/**
|
/**
|
||||||
* @param {(metric: string) => string} title
|
* @param {(metric: string) => string} title
|
||||||
* @param {string} metric
|
* @param {string} metric
|
||||||
* @param {{ _24h: any, _1w: any, _1m: any, _1y: any, percent: any, ratio: any }} dominance
|
* @param {DominancePattern} dominance
|
||||||
*/
|
*/
|
||||||
const dominanceTree = (title, metric, dominance) => ({
|
const dominanceTree = (title, metric, dominance) => ({
|
||||||
name: "Dominance",
|
name: "Dominance",
|
||||||
@@ -98,12 +98,12 @@ export function createMiningSection() {
|
|||||||
...ROLLING_WINDOWS.map((w) => ({
|
...ROLLING_WINDOWS.map((w) => ({
|
||||||
name: w.name,
|
name: w.name,
|
||||||
title: title(`${w.title} ${metric}`),
|
title: title(`${w.title} ${metric}`),
|
||||||
bottom: percentRatio({ pattern: dominance[w.key], name: w.name, color: w.color }),
|
bottom: percentRatio({ pattern: dominance[w.key], name: "Dominance", color: w.color }),
|
||||||
})),
|
})),
|
||||||
{
|
{
|
||||||
name: "All Time",
|
name: "All Time",
|
||||||
title: title(`${metric} All Time`),
|
title: title(`All Time ${metric}`),
|
||||||
bottom: percentRatio({ pattern: dominance, name: "All Time", color: colors.time.all }),
|
bottom: percentRatio({ pattern: dominance, name: "Dominance", color: colors.time.all }),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@@ -251,7 +251,7 @@ export function createMiningSection() {
|
|||||||
}),
|
}),
|
||||||
dotted({
|
dotted({
|
||||||
series: blocks.difficulty.hashrate,
|
series: blocks.difficulty.hashrate,
|
||||||
name: "Difficulty",
|
name: "From Difficulty",
|
||||||
color: colors.default,
|
color: colors.default,
|
||||||
unit: Unit.hashRate,
|
unit: Unit.hashRate,
|
||||||
}),
|
}),
|
||||||
@@ -395,25 +395,25 @@ export function createMiningSection() {
|
|||||||
name: "Hash Price",
|
name: "Hash Price",
|
||||||
title: "Hash Price",
|
title: "Hash Price",
|
||||||
bottom: [
|
bottom: [
|
||||||
line({ series: mining.hashrate.price.ths, name: "TH/s", color: colors.usd, unit: Unit.usdPerThsPerDay }),
|
line({ series: mining.hashrate.price.ths, name: "per TH/s", color: colors.usd, unit: Unit.usdPerThsPerDay }),
|
||||||
line({ series: mining.hashrate.price.phs, name: "PH/s", color: colors.usd, unit: Unit.usdPerPhsPerDay }),
|
line({ series: mining.hashrate.price.phs, name: "per PH/s", color: colors.usd, unit: Unit.usdPerPhsPerDay }),
|
||||||
dotted({ series: mining.hashrate.price.thsMin, name: "TH/s ATL", color: colors.stat.min, unit: Unit.usdPerThsPerDay }),
|
dotted({ series: mining.hashrate.price.thsMin, name: "per TH/s ATL", color: colors.stat.min, unit: Unit.usdPerThsPerDay }),
|
||||||
dotted({ series: mining.hashrate.price.phsMin, name: "PH/s ATL", color: colors.stat.min, unit: Unit.usdPerPhsPerDay }),
|
dotted({ series: mining.hashrate.price.phsMin, name: "per PH/s ATL", color: colors.stat.min, unit: Unit.usdPerPhsPerDay }),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Hash Value",
|
name: "Hash Value",
|
||||||
title: "Hash Value",
|
title: "Hash Value",
|
||||||
bottom: [
|
bottom: [
|
||||||
line({ series: mining.hashrate.value.ths, name: "TH/s", color: colors.bitcoin, unit: Unit.satsPerThsPerDay }),
|
line({ series: mining.hashrate.value.ths, name: "per TH/s", color: colors.bitcoin, unit: Unit.satsPerThsPerDay }),
|
||||||
line({ series: mining.hashrate.value.phs, name: "PH/s", color: colors.bitcoin, unit: Unit.satsPerPhsPerDay }),
|
line({ series: mining.hashrate.value.phs, name: "per PH/s", color: colors.bitcoin, unit: Unit.satsPerPhsPerDay }),
|
||||||
dotted({ series: mining.hashrate.value.thsMin, name: "TH/s ATL", color: colors.stat.min, unit: Unit.satsPerThsPerDay }),
|
dotted({ series: mining.hashrate.value.thsMin, name: "per TH/s ATL", color: colors.stat.min, unit: Unit.satsPerThsPerDay }),
|
||||||
dotted({ series: mining.hashrate.value.phsMin, name: "PH/s ATL", color: colors.stat.min, unit: Unit.satsPerPhsPerDay }),
|
dotted({ series: mining.hashrate.value.phsMin, name: "per PH/s ATL", color: colors.stat.min, unit: Unit.satsPerPhsPerDay }),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Recovery",
|
name: "Recovery",
|
||||||
title: "Mining Recovery",
|
title: "Hash Price & Value Recovery",
|
||||||
bottom: [
|
bottom: [
|
||||||
...percentRatio({ pattern: mining.hashrate.price.rebound, name: "Hash Price", color: colors.usd }),
|
...percentRatio({ pattern: mining.hashrate.price.rebound, name: "Hash Price", color: colors.usd }),
|
||||||
...percentRatio({ pattern: mining.hashrate.value.rebound, name: "Hash Value", color: colors.bitcoin }),
|
...percentRatio({ pattern: mining.hashrate.value.rebound, name: "Hash Value", color: colors.bitcoin }),
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import {
|
|||||||
multiSeriesTree,
|
multiSeriesTree,
|
||||||
percentRatioDots,
|
percentRatioDots,
|
||||||
} from "./series.js";
|
} from "./series.js";
|
||||||
import { satsBtcUsd, satsBtcUsdFrom, satsBtcUsdFullTree } from "./shared.js";
|
import { satsBtcUsd, satsBtcUsdFrom, satsBtcUsdFullTree, formatCohortTitle } from "./shared.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create Network section
|
* Create Network section
|
||||||
@@ -114,15 +114,17 @@ export function createNetworkSection() {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {AddressableType | "all"} key
|
* @param {AddressableType | "all"} key
|
||||||
* @param {string} titlePrefix
|
* @param {string} [typeName]
|
||||||
*/
|
*/
|
||||||
const createAddressSeriesTree = (key, titlePrefix) => [
|
const createAddressSeriesTree = (key, typeName) => {
|
||||||
|
const title = formatCohortTitle(typeName);
|
||||||
|
return [
|
||||||
{
|
{
|
||||||
name: "Count",
|
name: "Count",
|
||||||
tree: [
|
tree: [
|
||||||
{
|
{
|
||||||
name: "Compare",
|
name: "Compare",
|
||||||
title: `${titlePrefix}Address Count`,
|
title: title("Address Count"),
|
||||||
bottom: countMetrics.map((m) =>
|
bottom: countMetrics.map((m) =>
|
||||||
line({
|
line({
|
||||||
series: addrs[m.key][key],
|
series: addrs[m.key][key],
|
||||||
@@ -134,7 +136,7 @@ export function createNetworkSection() {
|
|||||||
},
|
},
|
||||||
...countMetrics.map((m) => ({
|
...countMetrics.map((m) => ({
|
||||||
name: m.name,
|
name: m.name,
|
||||||
title: `${titlePrefix}${m.name} Addresses`,
|
title: title(`${m.name} Addresses`),
|
||||||
bottom: [
|
bottom: [
|
||||||
line({ series: addrs[m.key][key], name: m.name, unit: Unit.count }),
|
line({ series: addrs[m.key][key], name: m.name, unit: Unit.count }),
|
||||||
],
|
],
|
||||||
@@ -143,7 +145,7 @@ export function createNetworkSection() {
|
|||||||
},
|
},
|
||||||
...simpleDeltaTree({
|
...simpleDeltaTree({
|
||||||
delta: addrs.delta[key],
|
delta: addrs.delta[key],
|
||||||
title: (s) => `${titlePrefix}${s}`,
|
title,
|
||||||
metric: "Address Count",
|
metric: "Address Count",
|
||||||
unit: Unit.count,
|
unit: Unit.count,
|
||||||
}),
|
}),
|
||||||
@@ -151,7 +153,7 @@ export function createNetworkSection() {
|
|||||||
name: "New",
|
name: "New",
|
||||||
tree: chartsFromCount({
|
tree: chartsFromCount({
|
||||||
pattern: addrs.new[key],
|
pattern: addrs.new[key],
|
||||||
title: (s) => `${titlePrefix}${s}`,
|
title,
|
||||||
metric: "New Addresses",
|
metric: "New Addresses",
|
||||||
unit: Unit.count,
|
unit: Unit.count,
|
||||||
}),
|
}),
|
||||||
@@ -163,7 +165,7 @@ export function createNetworkSection() {
|
|||||||
name: "Compare",
|
name: "Compare",
|
||||||
tree: ROLLING_WINDOWS.map((w) => ({
|
tree: ROLLING_WINDOWS.map((w) => ({
|
||||||
name: w.name,
|
name: w.name,
|
||||||
title: `${w.title} ${titlePrefix}Active Addresses`,
|
title: title(`${w.title} Active Addresses`),
|
||||||
bottom: activityTypes.map((t, i) =>
|
bottom: activityTypes.map((t, i) =>
|
||||||
line({
|
line({
|
||||||
series: addrs.activity[key][t.key][w.key],
|
series: addrs.activity[key][t.key][w.key],
|
||||||
@@ -178,7 +180,7 @@ export function createNetworkSection() {
|
|||||||
name: t.name,
|
name: t.name,
|
||||||
tree: averagesArray({
|
tree: averagesArray({
|
||||||
windows: addrs.activity[key][t.key],
|
windows: addrs.activity[key][t.key],
|
||||||
title: (s) => `${titlePrefix}${s}`,
|
title,
|
||||||
metric: `${t.name} Addresses`,
|
metric: `${t.name} Addresses`,
|
||||||
unit: Unit.count,
|
unit: Unit.count,
|
||||||
}),
|
}),
|
||||||
@@ -186,6 +188,7 @@ export function createNetworkSection() {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
};
|
||||||
|
|
||||||
/** @type {Record<string, typeof scriptTypes[number]>} */
|
/** @type {Record<string, typeof scriptTypes[number]>} */
|
||||||
const byKey = Object.fromEntries(scriptTypes.map((t) => [t.key, t]));
|
const byKey = Object.fromEntries(scriptTypes.map((t) => [t.key, t]));
|
||||||
@@ -560,7 +563,7 @@ export function createNetworkSection() {
|
|||||||
{
|
{
|
||||||
name: "Addresses",
|
name: "Addresses",
|
||||||
tree: [
|
tree: [
|
||||||
...createAddressSeriesTree("all", ""),
|
...createAddressSeriesTree("all"),
|
||||||
{
|
{
|
||||||
name: "By Type",
|
name: "By Type",
|
||||||
tree: [
|
tree: [
|
||||||
@@ -582,7 +585,7 @@ export function createNetworkSection() {
|
|||||||
},
|
},
|
||||||
...addressTypes.map((t) => ({
|
...addressTypes.map((t) => ({
|
||||||
name: t.name,
|
name: t.name,
|
||||||
tree: createAddressSeriesTree(t.key, `${t.name} `),
|
tree: createAddressSeriesTree(t.key, t.name),
|
||||||
})),
|
})),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -398,8 +398,8 @@ export function histogram({
|
|||||||
/**
|
/**
|
||||||
* Create series from an AverageHeightMaxMedianMinP10P25P75P90Pattern (height + rolling stats)
|
* Create series from an AverageHeightMaxMedianMinP10P25P75P90Pattern (height + rolling stats)
|
||||||
* @param {Object} args
|
* @param {Object} args
|
||||||
* @param {{ height: AnySeriesPattern } & Record<string, any>} args.pattern - Pattern with .height and rolling stats (p10/p25/p75/p90 as _1y24h30d7dPattern)
|
* @param {{ height: AnySeriesPattern } & WindowedStats<AnySeriesPattern>} args.pattern - Pattern with .height and rolling stats
|
||||||
* @param {string} args.window - Rolling window key (e.g., '_24h', '_7d', '_30d', '_1y')
|
* @param {string} args.window - Rolling window key (e.g., '_24h', '_1w', '_1m', '_1y')
|
||||||
* @param {Unit} args.unit
|
* @param {Unit} args.unit
|
||||||
* @param {string} [args.title]
|
* @param {string} [args.title]
|
||||||
* @param {Color} [args.baseColor]
|
* @param {Color} [args.baseColor]
|
||||||
@@ -417,7 +417,7 @@ export function fromBaseStatsPattern({
|
|||||||
return [
|
return [
|
||||||
dots({
|
dots({
|
||||||
series: pattern.height,
|
series: pattern.height,
|
||||||
name: title || "base",
|
name: title || "Base",
|
||||||
color: baseColor,
|
color: baseColor,
|
||||||
unit,
|
unit,
|
||||||
}),
|
}),
|
||||||
@@ -427,8 +427,10 @@ export function fromBaseStatsPattern({
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract stats at a specific rolling window
|
* Extract stats at a specific rolling window
|
||||||
* @param {Record<string, any>} pattern - Pattern with pct10/pct25/pct75/pct90 and median/max/min as _1y24h30d7dPattern
|
* @template T
|
||||||
|
* @param {WindowedStats<T>} pattern - Pattern with pct10/pct25/pct75/pct90 and median/max/min at each rolling window
|
||||||
* @param {string} window
|
* @param {string} window
|
||||||
|
* @returns {{ median: T, max: T, min: T, pct75: T, pct25: T, pct90: T, pct10: T }}
|
||||||
*/
|
*/
|
||||||
export function statsAtWindow(pattern, window) {
|
export function statsAtWindow(pattern, window) {
|
||||||
return {
|
return {
|
||||||
@@ -450,6 +452,7 @@ export function statsAtWindow(pattern, window) {
|
|||||||
* @param {(w: typeof ROLLING_WINDOWS[number]) => string} args.windowTitle
|
* @param {(w: typeof ROLLING_WINDOWS[number]) => string} args.windowTitle
|
||||||
* @param {Unit} args.unit
|
* @param {Unit} args.unit
|
||||||
* @param {string} args.name
|
* @param {string} args.name
|
||||||
|
* @param {string} args.legend
|
||||||
* @returns {PartialOptionsGroup}
|
* @returns {PartialOptionsGroup}
|
||||||
*/
|
*/
|
||||||
function rollingWindowsTreeBaseline({
|
function rollingWindowsTreeBaseline({
|
||||||
@@ -458,6 +461,7 @@ function rollingWindowsTreeBaseline({
|
|||||||
windowTitle,
|
windowTitle,
|
||||||
unit,
|
unit,
|
||||||
name,
|
name,
|
||||||
|
legend,
|
||||||
}) {
|
}) {
|
||||||
return {
|
return {
|
||||||
name,
|
name,
|
||||||
@@ -477,7 +481,7 @@ function rollingWindowsTreeBaseline({
|
|||||||
...ROLLING_WINDOWS.map((w) => ({
|
...ROLLING_WINDOWS.map((w) => ({
|
||||||
name: w.name,
|
name: w.name,
|
||||||
title: windowTitle(w),
|
title: windowTitle(w),
|
||||||
bottom: [baseline({ series: windows[w.key], name: w.name, unit })],
|
bottom: [baseline({ series: windows[w.key], name: legend, unit })],
|
||||||
})),
|
})),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
@@ -605,15 +609,17 @@ export function sumsAndAveragesCumulative({
|
|||||||
* @param {(metric: string) => string} [args.title]
|
* @param {(metric: string) => string} [args.title]
|
||||||
* @param {string} args.metric
|
* @param {string} args.metric
|
||||||
* @param {Unit} args.unit
|
* @param {Unit} args.unit
|
||||||
|
* @param {string} [args.legend]
|
||||||
* @returns {PartialOptionsGroup}
|
* @returns {PartialOptionsGroup}
|
||||||
*/
|
*/
|
||||||
export function sumsTreeBaseline({ windows, title = (s) => s, metric, unit }) {
|
export function sumsTreeBaseline({ windows, title = (s) => s, metric, unit, legend = "Sum" }) {
|
||||||
return rollingWindowsTreeBaseline({
|
return rollingWindowsTreeBaseline({
|
||||||
windows,
|
windows,
|
||||||
title: title(metric),
|
title: title(metric),
|
||||||
windowTitle: (w) => title(`${w.title} ${metric}`),
|
windowTitle: (w) => title(`${w.title} ${metric}`),
|
||||||
unit,
|
unit,
|
||||||
name: "Sums",
|
name: "Sums",
|
||||||
|
legend,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -639,16 +645,16 @@ export function averagesArray({ windows, title = (s) => s, metric, unit }) {
|
|||||||
name: w.name,
|
name: w.name,
|
||||||
title: title(`${w.title} ${metric}`),
|
title: title(`${w.title} ${metric}`),
|
||||||
bottom: [
|
bottom: [
|
||||||
line({ series: windows[w.key], name: w.name, color: w.color, unit }),
|
line({ series: windows[w.key], name: "Average", color: w.color, unit }),
|
||||||
],
|
],
|
||||||
})),
|
})),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a Distribution folder tree with stats at each rolling window (24h/7d/30d/1y)
|
* Create a Distribution folder tree with stats at each rolling window (24h/1w/1m/1y)
|
||||||
* @param {Object} args
|
* @param {Object} args
|
||||||
* @param {Record<string, any>} args.pattern - Pattern with pct10/pct25/... and average/median/... as _1y24h30d7dPattern
|
* @param {WindowedStats<AnySeriesPattern>} args.pattern - Pattern with pct10/pct25/... and average/median/... at each rolling window
|
||||||
* @param {AnySeriesPattern} [args.base] - Optional base series to show as dots on each chart
|
* @param {AnySeriesPattern} [args.base] - Optional base series to show as dots on each chart
|
||||||
* @param {(metric: string) => string} [args.title]
|
* @param {(metric: string) => string} [args.title]
|
||||||
* @param {string} args.metric
|
* @param {string} args.metric
|
||||||
@@ -675,7 +681,7 @@ export function distributionWindowsTree({ pattern, base, title = (s) => s, metri
|
|||||||
name: w.name,
|
name: w.name,
|
||||||
title: title(`${w.title} ${metric} Distribution`),
|
title: title(`${w.title} ${metric} Distribution`),
|
||||||
bottom: [
|
bottom: [
|
||||||
...(base ? [line({ series: base, name: "base", unit })] : []),
|
...(base ? [line({ series: base, name: "Base", unit })] : []),
|
||||||
...percentileSeries({ pattern: statsAtWindow(pattern, w.key), unit }),
|
...percentileSeries({ pattern: statsAtWindow(pattern, w.key), unit }),
|
||||||
],
|
],
|
||||||
})),
|
})),
|
||||||
@@ -871,7 +877,7 @@ export function rollingPercentRatioTree({
|
|||||||
...ROLLING_WINDOWS.map((w) => ({
|
...ROLLING_WINDOWS.map((w) => ({
|
||||||
name: w.name,
|
name: w.name,
|
||||||
title: title(`${w.title} ${metric}`),
|
title: title(`${w.title} ${metric}`),
|
||||||
bottom: percentRatioBaseline({ pattern: windows[w.key], name: w.name }),
|
bottom: percentRatioBaseline({ pattern: windows[w.key], name: "Rate" }),
|
||||||
})),
|
})),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
@@ -911,7 +917,7 @@ export function deltaTree({ delta, title = (s) => s, metric, unit, extract }) {
|
|||||||
bottom: [
|
bottom: [
|
||||||
baseline({
|
baseline({
|
||||||
series: extract(delta.absolute[w.key]),
|
series: extract(delta.absolute[w.key]),
|
||||||
name: w.name,
|
name: "Change",
|
||||||
unit,
|
unit,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
@@ -1068,7 +1074,7 @@ export function chartsFromBlockAnd6b({ pattern, title = (s) => s, metric, unit }
|
|||||||
/**
|
/**
|
||||||
* Averages + Sums + Cumulative charts
|
* Averages + Sums + Cumulative charts
|
||||||
* @param {Object} args
|
* @param {Object} args
|
||||||
* @param {CountPattern<any>} args.pattern
|
* @param {CountPattern<number>} args.pattern
|
||||||
* @param {(metric: string) => string} [args.title]
|
* @param {(metric: string) => string} [args.title]
|
||||||
* @param {string} args.metric
|
* @param {string} args.metric
|
||||||
* @param {Unit} args.unit
|
* @param {Unit} args.unit
|
||||||
@@ -1090,7 +1096,7 @@ export function chartsFromCount({ pattern, title = (s) => s, metric, unit, color
|
|||||||
/**
|
/**
|
||||||
* Windowed sums + cumulative for multiple named entries (e.g. transaction versions)
|
* Windowed sums + cumulative for multiple named entries (e.g. transaction versions)
|
||||||
* @param {Object} args
|
* @param {Object} args
|
||||||
* @param {Array<[string, CountPattern<any>]>} args.entries
|
* @param {Array<[string, CountPattern<number>]>} args.entries
|
||||||
* @param {(metric: string) => string} [args.title]
|
* @param {(metric: string) => string} [args.title]
|
||||||
* @param {string} args.metric
|
* @param {string} args.metric
|
||||||
* @param {Unit} args.unit
|
* @param {Unit} args.unit
|
||||||
|
|||||||
@@ -203,7 +203,7 @@ export function revenueBtcSatsUsd({ coinbase, subsidy, fee, key }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create sats/btc/usd series from a rolling window (24h/7d/30d/1y sum)
|
* Create sats/btc/usd series from a rolling window (24h/1w/1m/1y sum)
|
||||||
* @param {Object} args
|
* @param {Object} args
|
||||||
* @param {AnyValuePattern} args.pattern - A BtcSatsUsdPattern (e.g., source.rolling._24h.sum)
|
* @param {AnyValuePattern} args.pattern - A BtcSatsUsdPattern (e.g., source.rolling._24h.sum)
|
||||||
* @param {string} args.name
|
* @param {string} args.name
|
||||||
@@ -540,14 +540,15 @@ export function ratioBottomSeries(ratio) {
|
|||||||
* @param {AnyRatioPattern} args.ratio
|
* @param {AnyRatioPattern} args.ratio
|
||||||
* @param {Color} args.color
|
* @param {Color} args.color
|
||||||
* @param {string} [args.name]
|
* @param {string} [args.name]
|
||||||
|
* @param {string} [args.legend]
|
||||||
* @returns {PartialChartOption}
|
* @returns {PartialChartOption}
|
||||||
*/
|
*/
|
||||||
export function createRatioChart({ title, pricePattern, ratio, color, name }) {
|
export function createRatioChart({ title, pricePattern, ratio, color, name, legend }) {
|
||||||
return {
|
return {
|
||||||
name: name ?? "Ratio",
|
name: name ?? "Ratio",
|
||||||
title: title(name ?? "Ratio"),
|
title: title(name ?? "Ratio"),
|
||||||
top: [
|
top: [
|
||||||
price({ series: pricePattern, name: "Price", color }),
|
price({ series: pricePattern, name: legend ?? "Price", color }),
|
||||||
...percentileUsdMap(ratio).map(({ name, prop, color }) =>
|
...percentileUsdMap(ratio).map(({ name, prop, color }) =>
|
||||||
price({
|
price({
|
||||||
series: prop,
|
series: prop,
|
||||||
@@ -742,6 +743,7 @@ export function createPriceRatioCharts({
|
|||||||
pricePattern,
|
pricePattern,
|
||||||
ratio,
|
ratio,
|
||||||
color,
|
color,
|
||||||
|
legend,
|
||||||
}),
|
}),
|
||||||
createZScoresFolder({
|
createZScoresFolder({
|
||||||
formatTitle: (name) =>
|
formatTitle: (name) =>
|
||||||
|
|||||||
@@ -100,9 +100,10 @@ export function logUnused(seriesTree, partialOptions) {
|
|||||||
|
|
||||||
if (!all.size) return;
|
if (!all.size) return;
|
||||||
|
|
||||||
/** @type {Record<string, any>} */
|
/** @type {Record<string, unknown>} */
|
||||||
const tree = {};
|
const tree = {};
|
||||||
for (const path of all.values()) {
|
for (const path of all.values()) {
|
||||||
|
/** @type {Record<string, unknown>} */
|
||||||
let current = tree;
|
let current = tree;
|
||||||
for (let i = 0; i < path.length; i++) {
|
for (let i = 0; i < path.length; i++) {
|
||||||
const part = path[i];
|
const part = path[i];
|
||||||
@@ -110,7 +111,7 @@ export function logUnused(seriesTree, partialOptions) {
|
|||||||
current[part] = null;
|
current[part] = null;
|
||||||
} else {
|
} else {
|
||||||
current[part] = current[part] || {};
|
current[part] = current[part] || {};
|
||||||
current = current[part];
|
current = /** @type {Record<string, unknown>} */ (current[part]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import {
|
|||||||
searchLabelElement,
|
searchLabelElement,
|
||||||
searchResultsElement,
|
searchResultsElement,
|
||||||
} from "../utils/elements.js";
|
} from "../utils/elements.js";
|
||||||
import { QuickMatch } from "../modules/quickmatch-js/0.3.1/src/index.js";
|
import { QuickMatch } from "../modules/quickmatch-js/0.4.0/src/index.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Options} options
|
* @param {Options} options
|
||||||
@@ -24,11 +24,7 @@ export function initSearch(options) {
|
|||||||
searchResultsElement.scrollTo({ top: 0 });
|
searchResultsElement.scrollTo({ top: 0 });
|
||||||
searchResultsElement.innerHTML = "";
|
searchResultsElement.innerHTML = "";
|
||||||
|
|
||||||
if (needle.length < 3) {
|
if (!needle.length) {
|
||||||
const li = window.document.createElement("li");
|
|
||||||
li.textContent = 'e.g. "BTC"';
|
|
||||||
li.style.color = "var(--off-color)";
|
|
||||||
searchResultsElement.appendChild(li);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,11 +22,8 @@ export function setQr(url) {
|
|||||||
"";
|
"";
|
||||||
|
|
||||||
imgQrcode.src =
|
imgQrcode.src =
|
||||||
leanQr.generate(/** @type {any} */ (url))?.toDataURL({
|
// @ts-ignore — lean-qr types don't resolve for file path import
|
||||||
// @ts-ignore
|
leanQr.generate(url)?.toDataURL({ padX: 0, padY: 0 }) || "";
|
||||||
padX: 0,
|
|
||||||
padY: 0,
|
|
||||||
}) || "";
|
|
||||||
|
|
||||||
shareDiv.hidden = false;
|
shareDiv.hidden = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -160,6 +160,15 @@
|
|||||||
* Distribution stats: min, max, median, pct10/25/75/90
|
* Distribution stats: min, max, median, pct10/25/75/90
|
||||||
* @typedef {{ min: AnySeriesPattern, max: AnySeriesPattern, median: AnySeriesPattern, pct10: AnySeriesPattern, pct25: AnySeriesPattern, pct75: AnySeriesPattern, pct90: AnySeriesPattern }} DistributionStats
|
* @typedef {{ min: AnySeriesPattern, max: AnySeriesPattern, median: AnySeriesPattern, pct10: AnySeriesPattern, pct25: AnySeriesPattern, pct75: AnySeriesPattern, pct90: AnySeriesPattern }} DistributionStats
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* Windowed distribution stats: each stat property is a rolling window record
|
||||||
|
* @template T
|
||||||
|
* @typedef {{ median: Record<string, T>, max: Record<string, T>, min: Record<string, T>, pct75: Record<string, T>, pct25: Record<string, T>, pct90: Record<string, T>, pct10: Record<string, T> }} WindowedStats
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* Dominance pattern: percent/ratio at top level + per rolling window
|
||||||
|
* @typedef {Brk._1m1w1y24hBpsPercentRatioPattern} DominancePattern
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Typed Object.entries that preserves key types
|
* Typed Object.entries that preserves key types
|
||||||
* @template {Record<string, any>} T
|
* @template {Record<string, unknown>} T
|
||||||
* @param {T} obj
|
* @param {T} obj
|
||||||
* @returns {[keyof T & string, T[keyof T & string]][]}
|
* @returns {[keyof T & string, T[keyof T & string]][]}
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -183,8 +183,8 @@ export function createRadios({
|
|||||||
choices,
|
choices,
|
||||||
initialValue,
|
initialValue,
|
||||||
onChange,
|
onChange,
|
||||||
toKey = /** @type {(choice: T) => string} */ ((/** @type {any} */ c) => c),
|
toKey = /** @type {(choice: T) => string} */ ((c) => String(c)),
|
||||||
toLabel = /** @type {(choice: T) => string} */ ((/** @type {any} */ c) => c),
|
toLabel = /** @type {(choice: T) => string} */ ((c) => String(c)),
|
||||||
toTitle,
|
toTitle,
|
||||||
}) {
|
}) {
|
||||||
const field = window.document.createElement("div");
|
const field = window.document.createElement("div");
|
||||||
@@ -247,8 +247,8 @@ export function createSelect({
|
|||||||
initialValue,
|
initialValue,
|
||||||
onChange,
|
onChange,
|
||||||
sorted,
|
sorted,
|
||||||
toKey = /** @type {(choice: T) => string} */ ((/** @type {any} */ c) => c),
|
toKey = /** @type {(choice: T) => string} */ ((c) => String(c)),
|
||||||
toLabel = /** @type {(choice: T) => string} */ ((/** @type {any} */ c) => c),
|
toLabel = /** @type {(choice: T) => string} */ ((c) => String(c)),
|
||||||
}) {
|
}) {
|
||||||
const choices = sorted
|
const choices = sorted
|
||||||
? unsortedChoices.toSorted((a, b) => toLabel(a).localeCompare(toLabel(b)))
|
? unsortedChoices.toSorted((a, b) => toLabel(a).localeCompare(toLabel(b)))
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export function idle(callback) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @template {(...args: any[]) => any} F
|
* @template {(...args: never[]) => unknown} F
|
||||||
* @param {F} callback
|
* @param {F} callback
|
||||||
* @param {number} [wait]
|
* @param {number} [wait]
|
||||||
*/
|
*/
|
||||||
@@ -51,7 +51,7 @@ export function throttle(callback, wait = 1000) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @template {(...args: any[]) => any} F
|
* @template {(...args: never[]) => unknown} F
|
||||||
* @param {F} callback
|
* @param {F} callback
|
||||||
* @param {number} [wait]
|
* @param {number} [wait]
|
||||||
* @returns {((...args: Parameters<F>) => void) & { cancel: () => void }}
|
* @returns {((...args: Parameters<F>) => void) & { cancel: () => void }}
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ const API = "/api";
|
|||||||
// Match hashed filenames: name.abc12345.js/mjs/css
|
// Match hashed filenames: name.abc12345.js/mjs/css
|
||||||
const HASHED_RE = /\.[0-9a-f]{8}\.(js|mjs|css)$/;
|
const HASHED_RE = /\.[0-9a-f]{8}\.(js|mjs|css)$/;
|
||||||
|
|
||||||
/** @type {ServiceWorkerGlobalScope} */
|
const sw = /** @type {ServiceWorkerGlobalScope} */ (/** @type {unknown} */ (self));
|
||||||
const sw = /** @type {any} */ (self);
|
|
||||||
|
|
||||||
const offline = () =>
|
const offline = () =>
|
||||||
new Response("Offline", {
|
new Response("Offline", {
|
||||||
|
|||||||
Reference in New Issue
Block a user