merge: resolve Cargo.toml conflict with upstream/main

Keep vecdb = 0.6.1 without commented path dependency.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Collins
2026-01-21 14:21:12 -05:00
65 changed files with 5671 additions and 3262 deletions
+3
View File
@@ -13,3 +13,6 @@ rustflags = ["-C", "target-cpu=native", "-C", "target-feature=+bmi1,+bmi2,+avx2"
[target.x86_64-pc-windows-gnu]
rustflags = ["-C", "target-cpu=native", "-C", "target-feature=+bmi1,+bmi2,+avx2"]
[alias]
dev = "run -p brk_cli --features brk_server/bindgen"
+1
View File
@@ -22,6 +22,7 @@ _*
/filter_*
/heatmaps*
/oracle*
/playground
# Logs
*.log*
Generated
+7 -106
View File
@@ -54,7 +54,7 @@ dependencies = [
"serde",
"serde_json",
"serde_qs",
"thiserror 2.0.18",
"thiserror",
"tower-layer",
"tower-service",
"tracing",
@@ -463,7 +463,6 @@ dependencies = [
"color-eyre",
"derive_more",
"pco",
"plotters",
"rayon",
"rustc-hash",
"schemars",
@@ -483,7 +482,7 @@ dependencies = [
"jiff",
"minreq",
"serde_json",
"thiserror 2.0.18",
"thiserror",
"tokio",
"vecdb",
]
@@ -540,10 +539,8 @@ name = "brk_logger"
version = "0.1.0-alpha.6"
dependencies = [
"jiff",
"logroller",
"owo-colors",
"tracing",
"tracing-appender",
"tracing-log",
"tracing-subscriber",
]
@@ -1071,15 +1068,6 @@ dependencies = [
"parking_lot_core",
]
[[package]]
name = "deranged"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587"
dependencies = [
"powerfmt",
]
[[package]]
name = "derive_more"
version = "2.1.1"
@@ -1981,18 +1969,6 @@ version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
name = "logroller"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83db12bbf439ebe64c0b0e4402f435b6f866db498fc1ae17e1b5d1a01625e2be"
dependencies = [
"chrono",
"flate2",
"regex",
"thiserror 1.0.69",
]
[[package]]
name = "lsm-tree"
version = "3.0.1"
@@ -2137,12 +2113,6 @@ dependencies = [
"minimal-lexical",
]
[[package]]
name = "num-conv"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]]
name = "num-traits"
version = "0.2.19"
@@ -2372,12 +2342,6 @@ dependencies = [
"zerovec",
]
[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]]
name = "ppv-lite86"
version = "0.2.21"
@@ -2492,7 +2456,7 @@ dependencies = [
"parking_lot",
"rayon",
"smallvec",
"thiserror 2.0.18",
"thiserror",
]
[[package]]
@@ -2532,7 +2496,7 @@ checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac"
dependencies = [
"getrandom 0.2.17",
"libredox",
"thiserror 2.0.18",
"thiserror",
]
[[package]]
@@ -2845,7 +2809,7 @@ dependencies = [
"futures",
"percent-encoding",
"serde",
"thiserror 2.0.18",
"thiserror",
]
[[package]]
@@ -3000,33 +2964,13 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "thiserror"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [
"thiserror-impl 1.0.69",
]
[[package]]
name = "thiserror"
version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
dependencies = [
"thiserror-impl 2.0.18",
]
[[package]]
name = "thiserror-impl"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
"syn",
"thiserror-impl",
]
[[package]]
@@ -3049,37 +2993,6 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "time"
version = "0.3.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9e442fc33d7fdb45aa9bfeb312c095964abdf596f7567261062b2a7107aaabd"
dependencies = [
"deranged",
"itoa",
"num-conv",
"powerfmt",
"serde_core",
"time-core",
"time-macros",
]
[[package]]
name = "time-core"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b36ee98fd31ec7426d599183e8fe26932a8dc1fb76ddb6214d05493377d34ca"
[[package]]
name = "time-macros"
version = "0.2.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71e552d1249bf61ac2a52db88179fd0673def1e1ad8243a00d9ec9ed71fee3dd"
dependencies = [
"num-conv",
"time-core",
]
[[package]]
name = "tinystr"
version = "0.8.2"
@@ -3227,18 +3140,6 @@ dependencies = [
"tracing-core",
]
[[package]]
name = "tracing-appender"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "786d480bce6247ab75f005b14ae1624ad978d3029d9113f0a22fa1ac773faeaf"
dependencies = [
"crossbeam-channel",
"thiserror 2.0.18",
"time",
"tracing-subscriber",
]
[[package]]
name = "tracing-attributes"
version = "0.1.31"
@@ -3378,7 +3279,7 @@ dependencies = [
"schemars",
"serde",
"serde_json",
"thiserror 2.0.18",
"thiserror",
"vecdb_derive",
"zerocopy",
"zstd",
+1
View File
@@ -82,6 +82,7 @@ tracing = { version = "0.1", default-features = false, features = ["std"] }
tower-http = { version = "0.6.8", features = ["catch-panic", "compression-br", "compression-gzip", "compression-zstd", "cors", "normalize-path", "timeout", "trace"] }
tower-layer = "0.3"
vecdb = { version = "0.6.1", features = ["derive", "serde_json", "pco", "schemars"] }
# vecdb = { path = "../anydb/crates/vecdb", features = ["derive", "serde_json", "pco", "schemars"] }
[workspace.metadata.release]
shared-version = true
@@ -51,9 +51,11 @@ class BrkError extends Error {{
/**
* @template T
* @typedef {{Object}} MetricData
* @property {{number}} version - Version of the metric data
* @property {{number}} total - Total number of data points
* @property {{number}} start - Start index (inclusive)
* @property {{number}} end - End index (exclusive)
* @property {{string}} stamp - ISO 8601 timestamp of when the response was generated
* @property {{T[]}} data - The metric data
*/
/** @typedef {{MetricData<any>}} AnyMetricData */
@@ -133,9 +133,11 @@ pub fn generate_endpoint_class(output: &mut String) {
output,
r#"class MetricData(TypedDict, Generic[T]):
"""Metric data with range information."""
version: int
total: int
start: int
end: int
stamp: str
data: List[T]
+2
View File
@@ -37,6 +37,8 @@ brk
Indexes the blockchain, computes datasets, starts the server on `localhost:3110`, and waits for new blocks.
**Note:** When more than 10,000 blocks behind, indexing completes before the server starts to free up memory from fragmentation that occurs during large syncs. The web interface at `localhost:3110` won't be available until sync finishes.
## Options
```bash
+16
View File
@@ -51,6 +51,22 @@ pub fn run() -> anyhow::Result<()> {
let mut indexer = Indexer::forced_import(&config.brkdir())?;
#[cfg(not(debug_assertions))]
{
// Pre-run indexer if too far behind, then drop and reimport to reduce memory
let chain_height = client.get_last_height()?;
let indexed_height = indexer.vecs.starting_height();
let blocks_behind = chain_height.saturating_sub(*indexed_height);
if blocks_behind > 10_000 {
info!("Indexing {blocks_behind} blocks before starting server...");
sleep(Duration::from_secs(3));
indexer.index(&blocks, &client, &exit)?;
drop(indexer);
Mimalloc::collect();
indexer = Indexer::forced_import(&config.brkdir())?;
}
}
let mut computer = Computer::forced_import(&config.brkdir(), &indexer, config.fetcher())?;
let mempool = Mempool::new(&client);
+3122 -828
View File
File diff suppressed because it is too large Load Diff
-1
View File
@@ -32,6 +32,5 @@ vecdb = { workspace = true }
[dev-dependencies]
brk_alloc = { workspace = true }
plotters = "0.3"
brk_bencher = { workspace = true }
color-eyre = { workspace = true }
+6 -39
View File
@@ -70,7 +70,9 @@ where
}};
}
let index = validate_vec!(first, last, min, max, average, sum, cumulative, median, pct10, pct25, pct75, pct90);
let index = validate_vec!(
first, last, min, max, average, sum, cumulative, median, pct10, pct25, pct75, pct90
);
let needs_first = first.is_some();
let needs_last = last.is_some();
@@ -298,44 +300,9 @@ where
};
}
write_vec!(first, last, min, max, average, sum, cumulative, median, pct10, pct25, pct75, pct90);
Ok(())
}
/// Compute cumulative extension from a source vec.
///
/// Used when only cumulative needs to be extended from an existing source.
pub fn compute_cumulative_extend<I, T>(
max_from: I,
source: &impl IterableVec<I, T>,
cumulative: &mut EagerVec<PcoVec<I, T>>,
exit: &Exit,
) -> Result<()>
where
I: VecIndex,
T: ComputedVecValue + JsonSchema,
{
cumulative.validate_computed_version_or_reset(source.version())?;
let index = max_from.min(I::from(cumulative.len()));
let mut cumulative_val = index
.decremented()
.map_or(T::from(0_usize), |idx| cumulative.iter().get_unwrap(idx));
source
.iter()
.enumerate()
.skip(index.to_usize())
.try_for_each(|(i, v)| -> Result<()> {
cumulative_val += v;
cumulative.truncate_push_at(i, cumulative_val)?;
Ok(())
})?;
let _lock = exit.lock();
cumulative.write()?;
write_vec!(
first, last, min, max, average, sum, cumulative, median, pct10, pct25, pct75, pct90
);
Ok(())
}
@@ -12,16 +12,16 @@ use brk_types::{
};
use derive_more::{Deref, DerefMut};
use vecdb::{
AnyStoredVec, AnyVec, Database, EagerVec, Exit, GenericStoredVec, ImportableVec,
IterableBoxedVec, IterableCloneableVec, IterableVec, LazyVecFrom3,
Database, EagerVec, Exit, ImportableVec, IterableBoxedVec, IterableCloneableVec, LazyVecFrom3,
};
use crate::{
ComputeIndexes, indexes,
indexes,
internal::{
CumulativeVec, Full, LazyBinaryTransformFull, LazyDateDerivedFull, LazyFull,
SatsTimesClosePrice, Stats,
},
ComputeIndexes,
};
/// Lazy dollars at TxIndex: `sats * price[height]`
@@ -137,7 +137,11 @@ impl ValueDollarsFromTxFull {
exit: &Exit,
) -> Result<()> {
// Compute height cumulative by summing lazy height.sum values
self.compute_height_cumulative(starting_indexes.height, exit)?;
self.height_cumulative.0.compute_cumulative(
starting_indexes.height,
&self.height.sum,
exit,
)?;
// Compute dateindex stats by aggregating lazy height stats
self.dateindex.compute(
@@ -150,30 +154,6 @@ impl ValueDollarsFromTxFull {
Ok(())
}
/// Compute cumulative USD by summing `sum_sats[h] * price[h]` for all heights.
fn compute_height_cumulative(&mut self, max_from: Height, exit: &Exit) -> Result<()> {
let starting_height = max_from.min(Height::from(self.height_cumulative.0.len()));
let mut cumulative = starting_height.decremented().map_or(Dollars::ZERO, |h| {
self.height_cumulative.0.iter().get_unwrap(h)
});
let mut sum_iter = self.height.sum.iter();
let start_idx = *starting_height as usize;
let end_idx = sum_iter.len();
for h in start_idx..end_idx {
let sum_usd = sum_iter.get_unwrap(Height::from(h));
cumulative += sum_usd;
self.height_cumulative.0.truncate_push_at(h, cumulative)?;
}
let _lock = exit.lock();
self.height_cumulative.0.write()?;
Ok(())
}
}
fn create_lazy_txindex(
@@ -9,11 +9,11 @@ use schemars::JsonSchema;
use vecdb::{Database, Exit, IterableBoxedVec, IterableCloneableVec, IterableVec};
use crate::{
ComputeIndexes, indexes,
indexes,
internal::{
ComputedVecValue, CumulativeVec, LazyDateDerivedFull, Full, LazyFull, NumericValue,
compute_cumulative_extend,
ComputedVecValue, CumulativeVec, Full, LazyDateDerivedFull, LazyFull, NumericValue,
},
ComputeIndexes,
};
#[derive(Clone, Deref, DerefMut, Traversable)]
@@ -102,6 +102,9 @@ where
height_source: &impl IterableVec<Height, T>,
exit: &Exit,
) -> Result<()> {
compute_cumulative_extend(max_from, height_source, &mut self.height_cumulative.0, exit)
self.height_cumulative
.0
.compute_cumulative(max_from, height_source, exit)?;
Ok(())
}
}
@@ -12,11 +12,11 @@ use vecdb::{
};
use crate::{
ComputeIndexes, indexes,
indexes,
internal::{
ComputedVecValue, CumulativeVec, LazyDateDerivedSumCum, LazySumCum, NumericValue, SumCum,
compute_cumulative_extend,
},
ComputeIndexes,
};
#[derive(Clone, Deref, DerefMut, Traversable)]
@@ -99,7 +99,10 @@ where
source: &impl IterableVec<Height, T>,
exit: &Exit,
) -> Result<()> {
compute_cumulative_extend(max_from, source, &mut self.height_cumulative.0, exit)
self.height_cumulative
.0
.compute_cumulative(max_from, source, exit)?;
Ok(())
}
fn compute_dateindex_sum_cum(
@@ -1,7 +1,10 @@
use brk_error::Result;
use brk_traversable::Traversable;
use schemars::JsonSchema;
use vecdb::{AnyVec, Database, Exit, IterableBoxedVec, IterableCloneableVec, IterableVec, VecIndex, VecValue, Version};
use vecdb::{
AnyVec, Database, Exit, IterableBoxedVec, IterableCloneableVec, IterableVec, VecIndex,
VecValue, Version,
};
use crate::internal::{ComputedVecValue, CumulativeVec, SumVec};
@@ -48,7 +51,7 @@ impl<I: VecIndex, T: ComputedVecValue + JsonSchema> SumCum<I, T> {
first_indexes,
count_indexes,
exit,
0, // min_skip_count
0, // min_skip_count
None, // first
None, // last
None, // min
@@ -64,16 +67,6 @@ impl<I: VecIndex, T: ComputedVecValue + JsonSchema> SumCum<I, T> {
)
}
/// Extend cumulative from an existing source vec.
pub fn extend_cumulative(
&mut self,
max_from: I,
source: &impl IterableVec<I, T>,
exit: &Exit,
) -> Result<()> {
crate::internal::compute_cumulative_extend(max_from, source, &mut self.cumulative.0, exit)
}
pub fn len(&self) -> usize {
self.sum.0.len().min(self.cumulative.0.len())
}
-2
View File
@@ -9,9 +9,7 @@ repository.workspace = true
[dependencies]
jiff = { workspace = true }
logroller = "0.1"
owo-colors = "4.2.3"
tracing = { workspace = true }
tracing-appender = "0.2"
tracing-log = "0.2"
tracing-subscriber = { version = "0.3", default-features = false, features = ["fmt", "env-filter", "std"] }
+168
View File
@@ -0,0 +1,168 @@
use std::fmt::Write;
use jiff::{Timestamp, tz};
use owo_colors::OwoColorize;
use tracing::{Event, Level, Subscriber, field::Field};
use tracing_subscriber::{
fmt::{FmtContext, FormatEvent, FormatFields, format::Writer},
registry::LookupSpan,
};
// Don't remove, used to know the target of unwanted logs
const WITH_TARGET: bool = false;
// const WITH_TARGET: bool = true;
const fn level_str(level: Level) -> &'static str {
match level {
Level::ERROR => "error",
Level::WARN => "warn ",
Level::INFO => "info ",
Level::DEBUG => "debug",
Level::TRACE => "trace",
}
}
pub struct Formatter<const ANSI: bool>;
impl<S, N, const ANSI: bool> FormatEvent<S, N> for Formatter<ANSI>
where
S: Subscriber + for<'a> LookupSpan<'a>,
N: for<'a> FormatFields<'a> + 'static,
{
fn format_event(
&self,
_ctx: &FmtContext<'_, S, N>,
mut writer: Writer<'_>,
event: &Event<'_>,
) -> std::fmt::Result {
let ts = Timestamp::now()
.to_zoned(tz::TimeZone::system())
.strftime("%Y-%m-%d %H:%M:%S")
.to_string();
let level = *event.metadata().level();
let level_str = level_str(level);
if ANSI {
let level_colored = match level {
Level::ERROR => level_str.red().to_string(),
Level::WARN => level_str.yellow().to_string(),
Level::INFO => level_str.green().to_string(),
Level::DEBUG => level_str.blue().to_string(),
Level::TRACE => level_str.cyan().to_string(),
};
if WITH_TARGET {
write!(
writer,
"{} {} {} {level_colored} ",
ts.bright_black(),
event.metadata().target(),
"-".bright_black(),
)?;
} else {
write!(
writer,
"{} {} {level_colored} ",
ts.bright_black(),
"-".bright_black()
)?;
}
} else if WITH_TARGET {
write!(writer, "{ts} {} - {level_str} ", event.metadata().target())?;
} else {
write!(writer, "{ts} - {level_str} ")?;
}
let mut visitor = FieldVisitor::<ANSI>::new();
event.record(&mut visitor);
write!(writer, "{}", visitor.finish())?;
writeln!(writer)
}
}
struct FieldVisitor<const ANSI: bool> {
result: String,
status: Option<u64>,
uri: Option<String>,
latency: Option<String>,
}
impl<const ANSI: bool> FieldVisitor<ANSI> {
fn new() -> Self {
Self {
result: String::new(),
status: None,
uri: None,
latency: None,
}
}
fn finish(self) -> String {
if let Some(status) = self.status {
let status_str = if ANSI {
match status {
200..=299 => status.green().to_string(),
300..=399 => status.bright_black().to_string(),
_ => status.red().to_string(),
}
} else {
status.to_string()
};
let uri = self.uri.as_deref().unwrap_or("");
let latency = self.latency.as_deref().unwrap_or("");
if ANSI {
format!("{status_str} {uri} {}", latency.bright_black())
} else {
format!("{status_str} {uri} {latency}")
}
} else {
self.result
}
}
}
impl<const ANSI: bool> tracing::field::Visit for FieldVisitor<ANSI> {
fn record_u64(&mut self, field: &Field, value: u64) {
let name = field.name();
if name == "status" {
self.status = Some(value);
} else if !name.starts_with("log.") {
let _ = write!(self.result, "{}={} ", name, value);
}
}
fn record_i64(&mut self, field: &Field, value: i64) {
let name = field.name();
if !name.starts_with("log.") {
let _ = write!(self.result, "{}={} ", name, value);
}
}
fn record_str(&mut self, field: &Field, value: &str) {
let name = field.name();
if name == "uri" {
self.uri = Some(value.to_string());
} else if name == "message" {
let _ = write!(self.result, "{value}");
} else if !name.starts_with("log.") {
let _ = write!(self.result, "{}={} ", name, value);
}
}
fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
let name = field.name();
match name {
"uri" => self.uri = Some(format!("{value:?}")),
"latency" => self.latency = Some(format!("{value:?}")),
"message" => {
let _ = write!(self.result, "{value:?}");
}
_ if name.starts_with("log.") => {}
_ => {
let _ = write!(self.result, "{}={:?} ", name, value);
}
}
}
}
+30
View File
@@ -0,0 +1,30 @@
use std::{fmt::Write, sync::OnceLock};
use tracing::{Event, Subscriber, field::Field};
type LogHook = Box<dyn Fn(&str) + Send + Sync>;
pub static LOG_HOOK: OnceLock<LogHook> = OnceLock::new();
pub struct HookLayer;
impl<S: Subscriber> tracing_subscriber::Layer<S> for HookLayer {
fn on_event(&self, event: &Event<'_>, _: tracing_subscriber::layer::Context<'_, S>) {
if let Some(hook) = LOG_HOOK.get() {
let mut msg = String::new();
event.record(&mut MessageVisitor(&mut msg));
hook(&msg);
}
}
}
struct MessageVisitor<'a>(&'a mut String);
impl tracing::field::Visit for MessageVisitor<'_> {
fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
if field.name() == "message" {
self.0.clear();
let _ = write!(self.0, "{value:?}");
}
}
}
+45 -220
View File
@@ -1,215 +1,21 @@
#![doc = include_str!("../README.md")]
use std::{fmt::Write as _, io, path::Path, sync::OnceLock};
mod format;
mod hook;
mod rate_limit;
use jiff::{Timestamp, tz};
use logroller::{LogRollerBuilder, Rotation, RotationSize};
use owo_colors::OwoColorize;
use tracing::{Event, Level, Subscriber, field::Field};
use tracing_appender::non_blocking::WorkerGuard;
use tracing_subscriber::{
EnvFilter,
fmt::{self, FmtContext, FormatEvent, FormatFields, format::Writer},
layer::SubscriberExt,
registry::LookupSpan,
util::SubscriberInitExt,
};
use std::{io, path::Path, time::Duration};
type LogHook = Box<dyn Fn(&str) + Send + Sync>;
use tracing_subscriber::{EnvFilter, fmt, layer::SubscriberExt, util::SubscriberInitExt};
static GUARD: OnceLock<WorkerGuard> = OnceLock::new();
static LOG_HOOK: OnceLock<LogHook> = OnceLock::new();
use format::Formatter;
use hook::{HookLayer, LOG_HOOK};
use rate_limit::RateLimitedFile;
const MAX_LOG_FILES: u64 = 5;
const MAX_FILE_SIZE_MB: u64 = 42;
// Don't remove, used to know the target of unwanted logs
const WITH_TARGET: bool = false;
// const WITH_TARGET: bool = true;
const fn level_str(level: Level) -> &'static str {
match level {
Level::ERROR => "error",
Level::WARN => "warn ",
Level::INFO => "info ",
Level::DEBUG => "debug",
Level::TRACE => "trace",
}
}
struct Formatter<const ANSI: bool>;
/// Visitor that collects structured fields for colored formatting
struct FieldVisitor<const ANSI: bool> {
result: String,
status: Option<u64>,
uri: Option<String>,
latency: Option<String>,
}
impl<const ANSI: bool> FieldVisitor<ANSI> {
fn new() -> Self {
Self {
result: String::new(),
status: None,
uri: None,
latency: None,
}
}
fn finish(self) -> String {
// Format HTTP-style log if we have status
if let Some(status) = self.status {
let status_str = if ANSI {
match status {
200..=299 => status.green().to_string(),
300..=399 => status.bright_black().to_string(),
_ => status.red().to_string(),
}
} else {
status.to_string()
};
let uri = self.uri.as_deref().unwrap_or("");
let latency = self.latency.as_deref().unwrap_or("");
if ANSI {
format!("{status_str} {uri} {}", latency.bright_black())
} else {
format!("{status_str} {uri} {latency}")
}
} else {
self.result
}
}
}
impl<const ANSI: bool> tracing::field::Visit for FieldVisitor<ANSI> {
fn record_u64(&mut self, field: &Field, value: u64) {
let name = field.name();
if name == "status" {
self.status = Some(value);
} else if !name.starts_with("log.") {
let _ = write!(self.result, "{}={} ", name, value);
}
}
fn record_i64(&mut self, field: &Field, value: i64) {
let name = field.name();
if !name.starts_with("log.") {
let _ = write!(self.result, "{}={} ", name, value);
}
}
fn record_str(&mut self, field: &Field, value: &str) {
let name = field.name();
if name == "uri" {
self.uri = Some(value.to_string());
} else if name == "message" {
let _ = write!(self.result, "{value}");
} else if !name.starts_with("log.") {
let _ = write!(self.result, "{}={} ", name, value);
}
}
fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
let name = field.name();
match name {
"uri" => self.uri = Some(format!("{value:?}")),
"latency" => self.latency = Some(format!("{value:?}")),
"message" => {
let _ = write!(self.result, "{value:?}");
}
_ if name.starts_with("log.") => {}
_ => {
let _ = write!(self.result, "{}={:?} ", name, value);
}
}
}
}
impl<S, N, const ANSI: bool> FormatEvent<S, N> for Formatter<ANSI>
where
S: Subscriber + for<'a> LookupSpan<'a>,
N: for<'a> FormatFields<'a> + 'static,
{
fn format_event(
&self,
_ctx: &FmtContext<'_, S, N>,
mut writer: Writer<'_>,
event: &Event<'_>,
) -> std::fmt::Result {
let ts = Timestamp::now()
.to_zoned(tz::TimeZone::system())
.strftime("%Y-%m-%d %H:%M:%S")
.to_string();
let level = *event.metadata().level();
let level_str = level_str(level);
if ANSI {
let level_colored = match level {
Level::ERROR => level_str.red().to_string(),
Level::WARN => level_str.yellow().to_string(),
Level::INFO => level_str.green().to_string(),
Level::DEBUG => level_str.blue().to_string(),
Level::TRACE => level_str.cyan().to_string(),
};
if WITH_TARGET {
write!(
writer,
"{} {} {} {level_colored} ",
ts.bright_black(),
event.metadata().target(),
"-".bright_black(),
)?;
} else {
write!(
writer,
"{} {} {level_colored} ",
ts.bright_black(),
"-".bright_black()
)?;
}
} else if WITH_TARGET {
write!(writer, "{ts} {} - {level_str} ", event.metadata().target())?;
} else {
write!(writer, "{ts} - {level_str} ")?;
}
let mut visitor = FieldVisitor::<ANSI>::new();
event.record(&mut visitor);
write!(writer, "{}", visitor.finish())?;
writeln!(writer)
}
}
struct HookLayer;
impl<S: Subscriber> tracing_subscriber::Layer<S> for HookLayer {
fn on_event(&self, event: &Event<'_>, _: tracing_subscriber::layer::Context<'_, S>) {
if let Some(hook) = LOG_HOOK.get() {
let mut msg = String::new();
event.record(&mut MessageVisitor(&mut msg));
hook(&msg);
}
}
}
struct MessageVisitor<'a>(&'a mut String);
impl tracing::field::Visit for MessageVisitor<'_> {
fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
use std::fmt::Write;
if field.name() == "message" {
self.0.clear();
let _ = write!(self.0, "{value:?}");
}
}
}
/// Days to keep log files before cleanup
const MAX_LOG_AGE_DAYS: u64 = 7;
pub fn init(path: Option<&Path>) -> io::Result<()> {
// Bridge log crate to tracing (for vecdb and other log-based crates)
tracing_log::LogTracer::init().ok();
#[cfg(debug_assertions)]
@@ -217,12 +23,11 @@ pub fn init(path: Option<&Path>) -> io::Result<()> {
#[cfg(not(debug_assertions))]
const DEFAULT_LEVEL: &str = "info";
let default_filter = format!(
"{DEFAULT_LEVEL},bitcoin=off,bitcoincore-rpc=off,fjall=off,brk_fjall=off,lsm_tree=off,brk_rolldown=off,rolldown=off,tracing=off,aide=off,rustls=off,notify=off,oxc_resolver=off,tower_http=off"
);
let filter =
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(default_filter));
let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| {
EnvFilter::new(format!(
"{DEFAULT_LEVEL},bitcoin=off,bitcoincore-rpc=off,fjall=off,brk_fjall=off,lsm_tree=off,brk_rolldown=off,rolldown=off,tracing=off,aide=off,rustls=off,notify=off,oxc_resolver=off,tower_http=off"
))
});
let registry = tracing_subscriber::registry()
.with(filter)
@@ -231,25 +36,20 @@ pub fn init(path: Option<&Path>) -> io::Result<()> {
if let Some(path) = path {
let dir = path.parent().unwrap_or(Path::new("."));
let filename = path
let prefix = path
.file_name()
.and_then(|s| s.to_str())
.unwrap_or("app.log");
let roller = LogRollerBuilder::new(dir, Path::new(filename))
.rotation(Rotation::SizeBased(RotationSize::MB(MAX_FILE_SIZE_MB)))
.max_keep_files(MAX_LOG_FILES)
.build()
.map_err(io::Error::other)?;
cleanup_old_logs(dir, prefix);
let (non_blocking, guard) = tracing_appender::non_blocking(roller);
GUARD.set(guard).ok();
let writer = RateLimitedFile::new(dir, prefix);
registry
.with(
fmt::layer()
.event_format(Formatter::<false>)
.with_writer(non_blocking),
.with_writer(writer),
)
.init();
} else {
@@ -260,7 +60,6 @@ pub fn init(path: Option<&Path>) -> io::Result<()> {
}
/// Register a hook that gets called for every log message.
/// Can only be called once.
pub fn register_hook<F>(hook: F) -> Result<(), &'static str>
where
F: Fn(&str) + Send + Sync + 'static,
@@ -269,3 +68,29 @@ where
.set(Box::new(hook))
.map_err(|_| "Hook already registered")
}
fn cleanup_old_logs(dir: &Path, prefix: &str) {
let max_age = Duration::from_secs(MAX_LOG_AGE_DAYS * 24 * 60 * 60);
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
let Some(name) = path.file_name().and_then(|n| n.to_str()) else {
continue;
};
if !name.starts_with(prefix) || name == prefix {
continue;
}
if let Ok(meta) = path.metadata()
&& let Ok(modified) = meta.modified()
&& let Ok(age) = modified.elapsed()
&& age > max_age
{
let _ = std::fs::remove_file(&path);
}
}
}
+90
View File
@@ -0,0 +1,90 @@
use std::{
fs::OpenOptions,
io::{self, Write},
path::PathBuf,
sync::{
Arc,
atomic::{AtomicU64, Ordering},
},
time::{SystemTime, UNIX_EPOCH},
};
use jiff::{Timestamp, tz};
use tracing_subscriber::fmt::MakeWriter;
const MAX_WRITES_PER_SEC: u64 = 100;
struct Inner {
dir: PathBuf,
prefix: String,
count: AtomicU64,
last_second: AtomicU64,
}
impl Inner {
fn can_write(&self) -> bool {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
let last = self.last_second.load(Ordering::Relaxed);
if now != last {
self.last_second.store(now, Ordering::Relaxed);
self.count.store(1, Ordering::Relaxed);
true
} else {
self.count.fetch_add(1, Ordering::Relaxed) < MAX_WRITES_PER_SEC
}
}
fn path(&self) -> PathBuf {
let date = Timestamp::now()
.to_zoned(tz::TimeZone::system())
.strftime("%Y-%m-%d")
.to_string();
self.dir.join(format!("{}.{}", self.prefix, date))
}
}
#[derive(Clone)]
pub struct RateLimitedFile(Arc<Inner>);
impl RateLimitedFile {
pub fn new(dir: &std::path::Path, prefix: &str) -> Self {
Self(Arc::new(Inner {
dir: dir.to_path_buf(),
prefix: prefix.to_string(),
count: AtomicU64::new(0),
last_second: AtomicU64::new(0),
}))
}
}
pub struct FileWriter(Arc<Inner>);
impl Write for FileWriter {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
if !self.0.can_write() {
return Ok(buf.len());
}
OpenOptions::new()
.create(true)
.append(true)
.open(self.0.path())?
.write(buf)
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
impl<'a> MakeWriter<'a> for RateLimitedFile {
type Writer = FileWriter;
fn make_writer(&'a self) -> Self::Writer {
FileWriter(Arc::clone(&self.0))
}
}
+8 -6
View File
@@ -14,13 +14,15 @@ pub mod oracle;
pub mod render;
pub mod signal;
pub use anchors::{get_anchor_ohlc, get_anchor_range, Ohlc};
pub use conditions::{out_bits, tx_bits, MappedOutputConditions};
pub use constants::{HeatmapFilter, NUM_BINS, ROUND_USD_AMOUNTS};
pub use anchors::{Ohlc, get_anchor_ohlc, get_anchor_range};
pub use conditions::{MappedOutputConditions, out_bits, tx_bits};
pub use constants::{NUM_BINS, OutputFilter, ROUND_USD_AMOUNTS};
pub use filters::FILTERS;
pub use histogram::load_or_compute_output_conditions;
pub use oracle::{
derive_daily_ohlc, derive_daily_ohlc_with_confidence, derive_height_price,
derive_ohlc_from_height_prices, derive_price_from_histogram, OracleConfig, OracleResult,
HeightPriceResult, OracleConfig, OracleResult, derive_daily_ohlc,
derive_daily_ohlc_with_confidence, derive_height_price, derive_height_price_with_confidence,
derive_ohlc_from_height_prices, derive_ohlc_from_height_prices_with_confidence,
derive_price_from_histogram,
};
pub use signal::{compute_expected_bins_per_day, usd_to_bin};
pub use histogram::load_or_compute_output_conditions;
+4 -1
View File
@@ -7,10 +7,13 @@ license.workspace = true
homepage.workspace = true
repository.workspace = true
[features]
bindgen = ["dep:brk_bindgen"]
[dependencies]
aide = { workspace = true }
axum = { workspace = true }
brk_bindgen = { workspace = true }
brk_bindgen = { workspace = true, optional = true }
brk_computer = { workspace = true }
brk_error = { workspace = true, features = ["jiff", "serde_json", "tokio", "vecdb"] }
brk_fetcher = { workspace = true }
+28 -23
View File
@@ -1,7 +1,6 @@
#![doc = include_str!("../README.md")]
use std::{
panic,
path::PathBuf,
sync::Arc,
time::{Duration, Instant},
@@ -62,6 +61,9 @@ impl Server {
pub async fn serve(self, port: Option<Port>) -> brk_error::Result<()> {
let state = self.0;
#[cfg(feature = "bindgen")]
let vecs = state.query.inner().vecs();
let compression_layer = CompressionLayer::new().br(true).gzip(true).zstd(true);
let response_uri_layer = axum::middleware::from_fn(
@@ -96,8 +98,6 @@ impl Server {
)
.on_eos(());
let vecs = state.query.inner().vecs();
let website_router = brk_website::router(state.website.clone());
let mut router = ApiRouter::new().add_api_routes();
if !state.website.is_enabled() {
@@ -141,28 +141,33 @@ impl Server {
let mut openapi = create_openapi();
let router = router.finish_api(&mut openapi);
let workspace_root: PathBuf = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.and_then(|p| p.parent())
.unwrap()
.into();
let output_paths = brk_bindgen::ClientOutputPaths::new()
.rust(workspace_root.join("crates/brk_client/src/lib.rs"))
.javascript(workspace_root.join("modules/brk-client/index.js"))
.python(workspace_root.join("packages/brk_client/brk_client/__init__.py"));
#[cfg(feature = "bindgen")]
{
let workspace_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.and_then(|p| p.parent())
.unwrap()
.to_path_buf();
let output_paths = brk_bindgen::ClientOutputPaths::new()
.rust(workspace_root.join("crates/brk_client/src/lib.rs"))
.javascript(workspace_root.join("modules/brk-client/index.js"))
.python(workspace_root.join("packages/brk_client/brk_client/__init__.py"));
let openapi_json = serde_json::to_string(&openapi).unwrap();
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
brk_bindgen::generate_clients(vecs, &openapi_json, &output_paths)
}));
match result {
Ok(Ok(())) => info!("Generated clients"),
Ok(Err(e)) => error!("Failed to generate clients: {e}"),
Err(_) => error!("Client generation panicked"),
}
}
let api_json = Arc::new(ApiJson::new(&openapi));
let openapi_json = serde_json::to_string(&openapi).unwrap();
let result = panic::catch_unwind(panic::AssertUnwindSafe(|| {
brk_bindgen::generate_clients(vecs, &openapi_json, &output_paths)
}));
match result {
Ok(Ok(())) => info!("Generated clients"),
Ok(Err(e)) => error!("Failed to generate clients: {e}"),
Err(_) => error!("Client generation panicked"),
}
let router = router
.layer(Extension(Arc::new(openapi)))
+214 -212
View File
@@ -839,9 +839,11 @@ class BrkError extends Error {
/**
* @template T
* @typedef {Object} MetricData
* @property {number} version - Version of the metric data
* @property {number} total - Total number of data points
* @property {number} start - Start index (inclusive)
* @property {number} end - End index (exclusive)
* @property {string} stamp - ISO 8601 timestamp of when the response was generated
* @property {T[]} data - The metric data
*/
/** @typedef {MetricData<any>} AnyMetricData */
@@ -2030,17 +2032,17 @@ function createBitcoinPattern(client, acc) {
*/
function createClassAveragePricePattern(client, acc) {
return {
_2015: createMetricPattern4(client, _m(acc, '2015_returns')),
_2016: createMetricPattern4(client, _m(acc, '2016_returns')),
_2017: createMetricPattern4(client, _m(acc, '2017_returns')),
_2018: createMetricPattern4(client, _m(acc, '2018_returns')),
_2019: createMetricPattern4(client, _m(acc, '2019_returns')),
_2020: createMetricPattern4(client, _m(acc, '2020_returns')),
_2021: createMetricPattern4(client, _m(acc, '2021_returns')),
_2022: createMetricPattern4(client, _m(acc, '2022_returns')),
_2023: createMetricPattern4(client, _m(acc, '2023_returns')),
_2024: createMetricPattern4(client, _m(acc, '2024_returns')),
_2025: createMetricPattern4(client, _m(acc, '2025_returns')),
_2015: createMetricPattern4(client, _m(acc, '2015_average_price')),
_2016: createMetricPattern4(client, _m(acc, '2016_average_price')),
_2017: createMetricPattern4(client, _m(acc, '2017_average_price')),
_2018: createMetricPattern4(client, _m(acc, '2018_average_price')),
_2019: createMetricPattern4(client, _m(acc, '2019_average_price')),
_2020: createMetricPattern4(client, _m(acc, '2020_average_price')),
_2021: createMetricPattern4(client, _m(acc, '2021_average_price')),
_2022: createMetricPattern4(client, _m(acc, '2022_average_price')),
_2023: createMetricPattern4(client, _m(acc, '2023_average_price')),
_2024: createMetricPattern4(client, _m(acc, '2024_average_price')),
_2025: createMetricPattern4(client, _m(acc, '2025_average_price')),
};
}
@@ -2083,41 +2085,6 @@ function createDollarsPattern(client, acc) {
};
}
/**
* @typedef {Object} RelativePattern2
* @property {MetricPattern1<StoredF32>} negUnrealizedLossRelToOwnMarketCap
* @property {MetricPattern1<StoredF32>} negUnrealizedLossRelToOwnTotalUnrealizedPnl
* @property {MetricPattern1<StoredF32>} netUnrealizedPnlRelToOwnMarketCap
* @property {MetricPattern1<StoredF32>} netUnrealizedPnlRelToOwnTotalUnrealizedPnl
* @property {MetricPattern1<StoredF64>} supplyInLossRelToOwnSupply
* @property {MetricPattern1<StoredF64>} supplyInProfitRelToOwnSupply
* @property {MetricPattern1<StoredF32>} unrealizedLossRelToOwnMarketCap
* @property {MetricPattern1<StoredF32>} unrealizedLossRelToOwnTotalUnrealizedPnl
* @property {MetricPattern1<StoredF32>} unrealizedProfitRelToOwnMarketCap
* @property {MetricPattern1<StoredF32>} unrealizedProfitRelToOwnTotalUnrealizedPnl
*/
/**
* Create a RelativePattern2 pattern node
* @param {BrkClientBase} client
* @param {string} acc - Accumulated metric name
* @returns {RelativePattern2}
*/
function createRelativePattern2(client, acc) {
return {
negUnrealizedLossRelToOwnMarketCap: createMetricPattern1(client, _m(acc, 'neg_unrealized_loss_rel_to_own_market_cap')),
negUnrealizedLossRelToOwnTotalUnrealizedPnl: createMetricPattern1(client, _m(acc, 'neg_unrealized_loss_rel_to_own_total_unrealized_pnl')),
netUnrealizedPnlRelToOwnMarketCap: createMetricPattern1(client, _m(acc, 'net_unrealized_pnl_rel_to_own_market_cap')),
netUnrealizedPnlRelToOwnTotalUnrealizedPnl: createMetricPattern1(client, _m(acc, 'net_unrealized_pnl_rel_to_own_total_unrealized_pnl')),
supplyInLossRelToOwnSupply: createMetricPattern1(client, _m(acc, 'supply_in_loss_rel_to_own_supply')),
supplyInProfitRelToOwnSupply: createMetricPattern1(client, _m(acc, 'supply_in_profit_rel_to_own_supply')),
unrealizedLossRelToOwnMarketCap: createMetricPattern1(client, _m(acc, 'unrealized_loss_rel_to_own_market_cap')),
unrealizedLossRelToOwnTotalUnrealizedPnl: createMetricPattern1(client, _m(acc, 'unrealized_loss_rel_to_own_total_unrealized_pnl')),
unrealizedProfitRelToOwnMarketCap: createMetricPattern1(client, _m(acc, 'unrealized_profit_rel_to_own_market_cap')),
unrealizedProfitRelToOwnTotalUnrealizedPnl: createMetricPattern1(client, _m(acc, 'unrealized_profit_rel_to_own_total_unrealized_pnl')),
};
}
/**
* @typedef {Object} RelativePattern
* @property {MetricPattern1<StoredF32>} negUnrealizedLossRelToMarketCap
@@ -2153,6 +2120,41 @@ function createRelativePattern(client, acc) {
};
}
/**
* @typedef {Object} RelativePattern2
* @property {MetricPattern1<StoredF32>} negUnrealizedLossRelToOwnMarketCap
* @property {MetricPattern1<StoredF32>} negUnrealizedLossRelToOwnTotalUnrealizedPnl
* @property {MetricPattern1<StoredF32>} netUnrealizedPnlRelToOwnMarketCap
* @property {MetricPattern1<StoredF32>} netUnrealizedPnlRelToOwnTotalUnrealizedPnl
* @property {MetricPattern1<StoredF64>} supplyInLossRelToOwnSupply
* @property {MetricPattern1<StoredF64>} supplyInProfitRelToOwnSupply
* @property {MetricPattern1<StoredF32>} unrealizedLossRelToOwnMarketCap
* @property {MetricPattern1<StoredF32>} unrealizedLossRelToOwnTotalUnrealizedPnl
* @property {MetricPattern1<StoredF32>} unrealizedProfitRelToOwnMarketCap
* @property {MetricPattern1<StoredF32>} unrealizedProfitRelToOwnTotalUnrealizedPnl
*/
/**
* Create a RelativePattern2 pattern node
* @param {BrkClientBase} client
* @param {string} acc - Accumulated metric name
* @returns {RelativePattern2}
*/
function createRelativePattern2(client, acc) {
return {
negUnrealizedLossRelToOwnMarketCap: createMetricPattern1(client, _m(acc, 'neg_unrealized_loss_rel_to_own_market_cap')),
negUnrealizedLossRelToOwnTotalUnrealizedPnl: createMetricPattern1(client, _m(acc, 'neg_unrealized_loss_rel_to_own_total_unrealized_pnl')),
netUnrealizedPnlRelToOwnMarketCap: createMetricPattern1(client, _m(acc, 'net_unrealized_pnl_rel_to_own_market_cap')),
netUnrealizedPnlRelToOwnTotalUnrealizedPnl: createMetricPattern1(client, _m(acc, 'net_unrealized_pnl_rel_to_own_total_unrealized_pnl')),
supplyInLossRelToOwnSupply: createMetricPattern1(client, _m(acc, 'supply_in_loss_rel_to_own_supply')),
supplyInProfitRelToOwnSupply: createMetricPattern1(client, _m(acc, 'supply_in_profit_rel_to_own_supply')),
unrealizedLossRelToOwnMarketCap: createMetricPattern1(client, _m(acc, 'unrealized_loss_rel_to_own_market_cap')),
unrealizedLossRelToOwnTotalUnrealizedPnl: createMetricPattern1(client, _m(acc, 'unrealized_loss_rel_to_own_total_unrealized_pnl')),
unrealizedProfitRelToOwnMarketCap: createMetricPattern1(client, _m(acc, 'unrealized_profit_rel_to_own_market_cap')),
unrealizedProfitRelToOwnTotalUnrealizedPnl: createMetricPattern1(client, _m(acc, 'unrealized_profit_rel_to_own_total_unrealized_pnl')),
};
}
/**
* @template T
* @typedef {Object} CountPattern2
@@ -2386,6 +2388,35 @@ function create_10yTo12yPattern(client, acc) {
};
}
/**
* @typedef {Object} UnrealizedPattern
* @property {MetricPattern1<Dollars>} negUnrealizedLoss
* @property {MetricPattern1<Dollars>} netUnrealizedPnl
* @property {ActiveSupplyPattern} supplyInLoss
* @property {ActiveSupplyPattern} supplyInProfit
* @property {MetricPattern1<Dollars>} totalUnrealizedPnl
* @property {MetricPattern1<Dollars>} unrealizedLoss
* @property {MetricPattern1<Dollars>} unrealizedProfit
*/
/**
* Create a UnrealizedPattern pattern node
* @param {BrkClientBase} client
* @param {string} acc - Accumulated metric name
* @returns {UnrealizedPattern}
*/
function createUnrealizedPattern(client, acc) {
return {
negUnrealizedLoss: createMetricPattern1(client, _m(acc, 'neg_unrealized_loss')),
netUnrealizedPnl: createMetricPattern1(client, _m(acc, 'net_unrealized_pnl')),
supplyInLoss: createActiveSupplyPattern(client, _m(acc, 'supply_in_loss')),
supplyInProfit: createActiveSupplyPattern(client, _m(acc, 'supply_in_profit')),
totalUnrealizedPnl: createMetricPattern1(client, _m(acc, 'total_unrealized_pnl')),
unrealizedLoss: createMetricPattern1(client, _m(acc, 'unrealized_loss')),
unrealizedProfit: createMetricPattern1(client, _m(acc, 'unrealized_profit')),
};
}
/**
* @typedef {Object} _10yPattern
* @property {ActivityPattern2} activity
@@ -2415,35 +2446,6 @@ function create_10yPattern(client, acc) {
};
}
/**
* @typedef {Object} _0satsPattern2
* @property {ActivityPattern2} activity
* @property {CostBasisPattern} costBasis
* @property {OutputsPattern} outputs
* @property {RealizedPattern} realized
* @property {RelativePattern4} relative
* @property {SupplyPattern2} supply
* @property {UnrealizedPattern} unrealized
*/
/**
* Create a _0satsPattern2 pattern node
* @param {BrkClientBase} client
* @param {string} acc - Accumulated metric name
* @returns {_0satsPattern2}
*/
function create_0satsPattern2(client, acc) {
return {
activity: createActivityPattern2(client, acc),
costBasis: createCostBasisPattern(client, acc),
outputs: createOutputsPattern(client, _m(acc, 'utxo_count')),
realized: createRealizedPattern(client, acc),
relative: createRelativePattern4(client, _m(acc, 'supply_in')),
supply: createSupplyPattern2(client, _m(acc, 'supply')),
unrealized: createUnrealizedPattern(client, acc),
};
}
/**
* @typedef {Object} _100btcPattern
* @property {ActivityPattern2} activity
@@ -2473,35 +2475,6 @@ function create_100btcPattern(client, acc) {
};
}
/**
* @typedef {Object} UnrealizedPattern
* @property {MetricPattern1<Dollars>} negUnrealizedLoss
* @property {MetricPattern1<Dollars>} netUnrealizedPnl
* @property {ActiveSupplyPattern} supplyInLoss
* @property {ActiveSupplyPattern} supplyInProfit
* @property {MetricPattern1<Dollars>} totalUnrealizedPnl
* @property {MetricPattern1<Dollars>} unrealizedLoss
* @property {MetricPattern1<Dollars>} unrealizedProfit
*/
/**
* Create a UnrealizedPattern pattern node
* @param {BrkClientBase} client
* @param {string} acc - Accumulated metric name
* @returns {UnrealizedPattern}
*/
function createUnrealizedPattern(client, acc) {
return {
negUnrealizedLoss: createMetricPattern1(client, _m(acc, 'neg_unrealized_loss')),
netUnrealizedPnl: createMetricPattern1(client, _m(acc, 'net_unrealized_pnl')),
supplyInLoss: createActiveSupplyPattern(client, _m(acc, 'supply_in_loss')),
supplyInProfit: createActiveSupplyPattern(client, _m(acc, 'supply_in_profit')),
totalUnrealizedPnl: createMetricPattern1(client, _m(acc, 'total_unrealized_pnl')),
unrealizedLoss: createMetricPattern1(client, _m(acc, 'unrealized_loss')),
unrealizedProfit: createMetricPattern1(client, _m(acc, 'unrealized_profit')),
};
}
/**
* @typedef {Object} PeriodCagrPattern
* @property {MetricPattern4<StoredF32>} _10y
@@ -2531,6 +2504,35 @@ function createPeriodCagrPattern(client, acc) {
};
}
/**
* @typedef {Object} _0satsPattern2
* @property {ActivityPattern2} activity
* @property {CostBasisPattern} costBasis
* @property {OutputsPattern} outputs
* @property {RealizedPattern} realized
* @property {RelativePattern4} relative
* @property {SupplyPattern2} supply
* @property {UnrealizedPattern} unrealized
*/
/**
* Create a _0satsPattern2 pattern node
* @param {BrkClientBase} client
* @param {string} acc - Accumulated metric name
* @returns {_0satsPattern2}
*/
function create_0satsPattern2(client, acc) {
return {
activity: createActivityPattern2(client, acc),
costBasis: createCostBasisPattern(client, acc),
outputs: createOutputsPattern(client, _m(acc, 'utxo_count')),
realized: createRealizedPattern(client, acc),
relative: createRelativePattern4(client, _m(acc, 'supply_in')),
supply: createSupplyPattern2(client, _m(acc, 'supply')),
unrealized: createUnrealizedPattern(client, acc),
};
}
/**
* @typedef {Object} ActivityPattern2
* @property {BlockCountPattern<StoredF64>} coinblocksDestroyed
@@ -2624,44 +2626,23 @@ function createCoinbasePattern2(client, acc) {
}
/**
* @typedef {Object} ActiveSupplyPattern
* @property {MetricPattern1<Bitcoin>} bitcoin
* @property {MetricPattern1<Dollars>} dollars
* @property {MetricPattern1<Sats>} sats
* @typedef {Object} CoinbasePattern
* @property {BitcoinPattern} bitcoin
* @property {DollarsPattern<Dollars>} dollars
* @property {DollarsPattern<Sats>} sats
*/
/**
* Create a ActiveSupplyPattern pattern node
* Create a CoinbasePattern pattern node
* @param {BrkClientBase} client
* @param {string} acc - Accumulated metric name
* @returns {ActiveSupplyPattern}
* @returns {CoinbasePattern}
*/
function createActiveSupplyPattern(client, acc) {
function createCoinbasePattern(client, acc) {
return {
bitcoin: createMetricPattern1(client, _m(acc, 'btc')),
dollars: createMetricPattern1(client, _m(acc, 'usd')),
sats: createMetricPattern1(client, acc),
};
}
/**
* @typedef {Object} SegwitAdoptionPattern
* @property {MetricPattern11<StoredF32>} base
* @property {MetricPattern2<StoredF32>} cumulative
* @property {MetricPattern2<StoredF32>} sum
*/
/**
* Create a SegwitAdoptionPattern pattern node
* @param {BrkClientBase} client
* @param {string} acc - Accumulated metric name
* @returns {SegwitAdoptionPattern}
*/
function createSegwitAdoptionPattern(client, acc) {
return {
base: createMetricPattern11(client, acc),
cumulative: createMetricPattern2(client, _m(acc, 'cumulative')),
sum: createMetricPattern2(client, _m(acc, 'sum')),
bitcoin: createBitcoinPattern(client, _m(acc, 'btc')),
dollars: createDollarsPattern(client, _m(acc, 'usd')),
sats: createDollarsPattern(client, acc),
};
}
@@ -2708,23 +2689,63 @@ function create_2015Pattern(client, acc) {
}
/**
* @typedef {Object} CoinbasePattern
* @property {BitcoinPattern} bitcoin
* @property {DollarsPattern<Dollars>} dollars
* @property {DollarsPattern<Sats>} sats
* @typedef {Object} SegwitAdoptionPattern
* @property {MetricPattern11<StoredF32>} base
* @property {MetricPattern2<StoredF32>} cumulative
* @property {MetricPattern2<StoredF32>} sum
*/
/**
* Create a CoinbasePattern pattern node
* Create a SegwitAdoptionPattern pattern node
* @param {BrkClientBase} client
* @param {string} acc - Accumulated metric name
* @returns {CoinbasePattern}
* @returns {SegwitAdoptionPattern}
*/
function createCoinbasePattern(client, acc) {
function createSegwitAdoptionPattern(client, acc) {
return {
bitcoin: createBitcoinPattern(client, _m(acc, 'btc')),
dollars: createDollarsPattern(client, _m(acc, 'usd')),
sats: createDollarsPattern(client, acc),
base: createMetricPattern11(client, acc),
cumulative: createMetricPattern2(client, _m(acc, 'cumulative')),
sum: createMetricPattern2(client, _m(acc, 'sum')),
};
}
/**
* @typedef {Object} ActiveSupplyPattern
* @property {MetricPattern1<Bitcoin>} bitcoin
* @property {MetricPattern1<Dollars>} dollars
* @property {MetricPattern1<Sats>} sats
*/
/**
* Create a ActiveSupplyPattern pattern node
* @param {BrkClientBase} client
* @param {string} acc - Accumulated metric name
* @returns {ActiveSupplyPattern}
*/
function createActiveSupplyPattern(client, acc) {
return {
bitcoin: createMetricPattern1(client, _m(acc, 'btc')),
dollars: createMetricPattern1(client, _m(acc, 'usd')),
sats: createMetricPattern1(client, acc),
};
}
/**
* @typedef {Object} SupplyPattern2
* @property {ActiveSupplyPattern} halved
* @property {ActiveSupplyPattern} total
*/
/**
* Create a SupplyPattern2 pattern node
* @param {BrkClientBase} client
* @param {string} acc - Accumulated metric name
* @returns {SupplyPattern2}
*/
function createSupplyPattern2(client, acc) {
return {
halved: createActiveSupplyPattern(client, _m(acc, 'halved')),
total: createActiveSupplyPattern(client, acc),
};
}
@@ -2785,25 +2806,6 @@ function createRelativePattern4(client, acc) {
};
}
/**
* @typedef {Object} SupplyPattern2
* @property {ActiveSupplyPattern} halved
* @property {ActiveSupplyPattern} total
*/
/**
* Create a SupplyPattern2 pattern node
* @param {BrkClientBase} client
* @param {string} acc - Accumulated metric name
* @returns {SupplyPattern2}
*/
function createSupplyPattern2(client, acc) {
return {
halved: createActiveSupplyPattern(client, _m(acc, 'halved')),
total: createActiveSupplyPattern(client, acc),
};
}
/**
* @template T
* @typedef {Object} BitcoinPattern2
@@ -2825,27 +2827,6 @@ function createBitcoinPattern2(client, acc) {
};
}
/**
* @template T
* @typedef {Object} BlockCountPattern
* @property {MetricPattern1<T>} cumulative
* @property {MetricPattern1<T>} sum
*/
/**
* Create a BlockCountPattern pattern node
* @template T
* @param {BrkClientBase} client
* @param {string} acc - Accumulated metric name
* @returns {BlockCountPattern<T>}
*/
function createBlockCountPattern(client, acc) {
return {
cumulative: createMetricPattern1(client, _m(acc, 'cumulative')),
sum: createMetricPattern1(client, acc),
};
}
/**
* @template T
* @typedef {Object} SatsPattern
@@ -2867,6 +2848,27 @@ function createSatsPattern(client, acc) {
};
}
/**
* @template T
* @typedef {Object} BlockCountPattern
* @property {MetricPattern1<T>} cumulative
* @property {MetricPattern1<T>} sum
*/
/**
* Create a BlockCountPattern pattern node
* @template T
* @param {BrkClientBase} client
* @param {string} acc - Accumulated metric name
* @returns {BlockCountPattern<T>}
*/
function createBlockCountPattern(client, acc) {
return {
cumulative: createMetricPattern1(client, _m(acc, 'cumulative')),
sum: createMetricPattern1(client, acc),
};
}
/**
* @typedef {Object} RealizedPriceExtraPattern
* @property {MetricPattern4<StoredF32>} ratio
@@ -3696,8 +3698,8 @@ function createOutputsPattern(client, acc) {
/**
* @typedef {Object} MetricsTree_Market_Dca
* @property {MetricsTree_Market_Dca_ClassAveragePrice} classAveragePrice
* @property {ClassAveragePricePattern<StoredF32>} classReturns
* @property {ClassAveragePricePattern<Dollars>} classAveragePrice
* @property {MetricsTree_Market_Dca_ClassReturns} classReturns
* @property {MetricsTree_Market_Dca_ClassStack} classStack
* @property {PeriodAveragePricePattern<Dollars>} periodAveragePrice
* @property {PeriodCagrPattern} periodCagr
@@ -3707,18 +3709,18 @@ function createOutputsPattern(client, acc) {
*/
/**
* @typedef {Object} MetricsTree_Market_Dca_ClassAveragePrice
* @property {MetricPattern4<Dollars>} _2015
* @property {MetricPattern4<Dollars>} _2016
* @property {MetricPattern4<Dollars>} _2017
* @property {MetricPattern4<Dollars>} _2018
* @property {MetricPattern4<Dollars>} _2019
* @property {MetricPattern4<Dollars>} _2020
* @property {MetricPattern4<Dollars>} _2021
* @property {MetricPattern4<Dollars>} _2022
* @property {MetricPattern4<Dollars>} _2023
* @property {MetricPattern4<Dollars>} _2024
* @property {MetricPattern4<Dollars>} _2025
* @typedef {Object} MetricsTree_Market_Dca_ClassReturns
* @property {MetricPattern4<StoredF32>} _2015
* @property {MetricPattern4<StoredF32>} _2016
* @property {MetricPattern4<StoredF32>} _2017
* @property {MetricPattern4<StoredF32>} _2018
* @property {MetricPattern4<StoredF32>} _2019
* @property {MetricPattern4<StoredF32>} _2020
* @property {MetricPattern4<StoredF32>} _2021
* @property {MetricPattern4<StoredF32>} _2022
* @property {MetricPattern4<StoredF32>} _2023
* @property {MetricPattern4<StoredF32>} _2024
* @property {MetricPattern4<StoredF32>} _2025
*/
/**
@@ -5726,20 +5728,20 @@ class BrkClient extends BrkClientBase {
yearsSincePriceAth: createMetricPattern4(this, 'years_since_price_ath'),
},
dca: {
classAveragePrice: {
_2015: createMetricPattern4(this, 'dca_class_2015_average_price'),
_2016: createMetricPattern4(this, 'dca_class_2016_average_price'),
_2017: createMetricPattern4(this, 'dca_class_2017_average_price'),
_2018: createMetricPattern4(this, 'dca_class_2018_average_price'),
_2019: createMetricPattern4(this, 'dca_class_2019_average_price'),
_2020: createMetricPattern4(this, 'dca_class_2020_average_price'),
_2021: createMetricPattern4(this, 'dca_class_2021_average_price'),
_2022: createMetricPattern4(this, 'dca_class_2022_average_price'),
_2023: createMetricPattern4(this, 'dca_class_2023_average_price'),
_2024: createMetricPattern4(this, 'dca_class_2024_average_price'),
_2025: createMetricPattern4(this, 'dca_class_2025_average_price'),
classAveragePrice: createClassAveragePricePattern(this, 'dca_class'),
classReturns: {
_2015: createMetricPattern4(this, 'dca_class_2015_returns'),
_2016: createMetricPattern4(this, 'dca_class_2016_returns'),
_2017: createMetricPattern4(this, 'dca_class_2017_returns'),
_2018: createMetricPattern4(this, 'dca_class_2018_returns'),
_2019: createMetricPattern4(this, 'dca_class_2019_returns'),
_2020: createMetricPattern4(this, 'dca_class_2020_returns'),
_2021: createMetricPattern4(this, 'dca_class_2021_returns'),
_2022: createMetricPattern4(this, 'dca_class_2022_returns'),
_2023: createMetricPattern4(this, 'dca_class_2023_returns'),
_2024: createMetricPattern4(this, 'dca_class_2024_returns'),
_2025: createMetricPattern4(this, 'dca_class_2025_returns'),
},
classReturns: createClassAveragePricePattern(this, 'dca_class'),
classStack: {
_2015: create_2015Pattern(this, 'dca_class_2015_stack'),
_2016: create_2015Pattern(this, 'dca_class_2016_stack'),
+102 -100
View File
@@ -1041,9 +1041,11 @@ def _p(prefix: str, acc: str) -> str:
class MetricData(TypedDict, Generic[T]):
"""Metric data with range information."""
version: int
total: int
start: int
end: int
stamp: str
data: List[T]
@@ -2054,17 +2056,17 @@ class ClassAveragePricePattern(Generic[T]):
def __init__(self, client: BrkClientBase, acc: str):
"""Create pattern node with accumulated metric name."""
self._2015: MetricPattern4[T] = MetricPattern4(client, _m(acc, '2015_returns'))
self._2016: MetricPattern4[T] = MetricPattern4(client, _m(acc, '2016_returns'))
self._2017: MetricPattern4[T] = MetricPattern4(client, _m(acc, '2017_returns'))
self._2018: MetricPattern4[T] = MetricPattern4(client, _m(acc, '2018_returns'))
self._2019: MetricPattern4[T] = MetricPattern4(client, _m(acc, '2019_returns'))
self._2020: MetricPattern4[T] = MetricPattern4(client, _m(acc, '2020_returns'))
self._2021: MetricPattern4[T] = MetricPattern4(client, _m(acc, '2021_returns'))
self._2022: MetricPattern4[T] = MetricPattern4(client, _m(acc, '2022_returns'))
self._2023: MetricPattern4[T] = MetricPattern4(client, _m(acc, '2023_returns'))
self._2024: MetricPattern4[T] = MetricPattern4(client, _m(acc, '2024_returns'))
self._2025: MetricPattern4[T] = MetricPattern4(client, _m(acc, '2025_returns'))
self._2015: MetricPattern4[T] = MetricPattern4(client, _m(acc, '2015_average_price'))
self._2016: MetricPattern4[T] = MetricPattern4(client, _m(acc, '2016_average_price'))
self._2017: MetricPattern4[T] = MetricPattern4(client, _m(acc, '2017_average_price'))
self._2018: MetricPattern4[T] = MetricPattern4(client, _m(acc, '2018_average_price'))
self._2019: MetricPattern4[T] = MetricPattern4(client, _m(acc, '2019_average_price'))
self._2020: MetricPattern4[T] = MetricPattern4(client, _m(acc, '2020_average_price'))
self._2021: MetricPattern4[T] = MetricPattern4(client, _m(acc, '2021_average_price'))
self._2022: MetricPattern4[T] = MetricPattern4(client, _m(acc, '2022_average_price'))
self._2023: MetricPattern4[T] = MetricPattern4(client, _m(acc, '2023_average_price'))
self._2024: MetricPattern4[T] = MetricPattern4(client, _m(acc, '2024_average_price'))
self._2025: MetricPattern4[T] = MetricPattern4(client, _m(acc, '2025_average_price'))
class DollarsPattern(Generic[T]):
"""Pattern struct for repeated tree structure."""
@@ -2083,22 +2085,6 @@ class DollarsPattern(Generic[T]):
self.pct90: MetricPattern6[T] = MetricPattern6(client, _m(acc, 'pct90'))
self.sum: MetricPattern2[T] = MetricPattern2(client, _m(acc, 'sum'))
class RelativePattern2:
"""Pattern struct for repeated tree structure."""
def __init__(self, client: BrkClientBase, acc: str):
"""Create pattern node with accumulated metric name."""
self.neg_unrealized_loss_rel_to_own_market_cap: MetricPattern1[StoredF32] = MetricPattern1(client, _m(acc, 'neg_unrealized_loss_rel_to_own_market_cap'))
self.neg_unrealized_loss_rel_to_own_total_unrealized_pnl: MetricPattern1[StoredF32] = MetricPattern1(client, _m(acc, 'neg_unrealized_loss_rel_to_own_total_unrealized_pnl'))
self.net_unrealized_pnl_rel_to_own_market_cap: MetricPattern1[StoredF32] = MetricPattern1(client, _m(acc, 'net_unrealized_pnl_rel_to_own_market_cap'))
self.net_unrealized_pnl_rel_to_own_total_unrealized_pnl: MetricPattern1[StoredF32] = MetricPattern1(client, _m(acc, 'net_unrealized_pnl_rel_to_own_total_unrealized_pnl'))
self.supply_in_loss_rel_to_own_supply: MetricPattern1[StoredF64] = MetricPattern1(client, _m(acc, 'supply_in_loss_rel_to_own_supply'))
self.supply_in_profit_rel_to_own_supply: MetricPattern1[StoredF64] = MetricPattern1(client, _m(acc, 'supply_in_profit_rel_to_own_supply'))
self.unrealized_loss_rel_to_own_market_cap: MetricPattern1[StoredF32] = MetricPattern1(client, _m(acc, 'unrealized_loss_rel_to_own_market_cap'))
self.unrealized_loss_rel_to_own_total_unrealized_pnl: MetricPattern1[StoredF32] = MetricPattern1(client, _m(acc, 'unrealized_loss_rel_to_own_total_unrealized_pnl'))
self.unrealized_profit_rel_to_own_market_cap: MetricPattern1[StoredF32] = MetricPattern1(client, _m(acc, 'unrealized_profit_rel_to_own_market_cap'))
self.unrealized_profit_rel_to_own_total_unrealized_pnl: MetricPattern1[StoredF32] = MetricPattern1(client, _m(acc, 'unrealized_profit_rel_to_own_total_unrealized_pnl'))
class RelativePattern:
"""Pattern struct for repeated tree structure."""
@@ -2115,6 +2101,22 @@ class RelativePattern:
self.unrealized_loss_rel_to_market_cap: MetricPattern1[StoredF32] = MetricPattern1(client, _m(acc, 'unrealized_loss_rel_to_market_cap'))
self.unrealized_profit_rel_to_market_cap: MetricPattern1[StoredF32] = MetricPattern1(client, _m(acc, 'unrealized_profit_rel_to_market_cap'))
class RelativePattern2:
"""Pattern struct for repeated tree structure."""
def __init__(self, client: BrkClientBase, acc: str):
"""Create pattern node with accumulated metric name."""
self.neg_unrealized_loss_rel_to_own_market_cap: MetricPattern1[StoredF32] = MetricPattern1(client, _m(acc, 'neg_unrealized_loss_rel_to_own_market_cap'))
self.neg_unrealized_loss_rel_to_own_total_unrealized_pnl: MetricPattern1[StoredF32] = MetricPattern1(client, _m(acc, 'neg_unrealized_loss_rel_to_own_total_unrealized_pnl'))
self.net_unrealized_pnl_rel_to_own_market_cap: MetricPattern1[StoredF32] = MetricPattern1(client, _m(acc, 'net_unrealized_pnl_rel_to_own_market_cap'))
self.net_unrealized_pnl_rel_to_own_total_unrealized_pnl: MetricPattern1[StoredF32] = MetricPattern1(client, _m(acc, 'net_unrealized_pnl_rel_to_own_total_unrealized_pnl'))
self.supply_in_loss_rel_to_own_supply: MetricPattern1[StoredF64] = MetricPattern1(client, _m(acc, 'supply_in_loss_rel_to_own_supply'))
self.supply_in_profit_rel_to_own_supply: MetricPattern1[StoredF64] = MetricPattern1(client, _m(acc, 'supply_in_profit_rel_to_own_supply'))
self.unrealized_loss_rel_to_own_market_cap: MetricPattern1[StoredF32] = MetricPattern1(client, _m(acc, 'unrealized_loss_rel_to_own_market_cap'))
self.unrealized_loss_rel_to_own_total_unrealized_pnl: MetricPattern1[StoredF32] = MetricPattern1(client, _m(acc, 'unrealized_loss_rel_to_own_total_unrealized_pnl'))
self.unrealized_profit_rel_to_own_market_cap: MetricPattern1[StoredF32] = MetricPattern1(client, _m(acc, 'unrealized_profit_rel_to_own_market_cap'))
self.unrealized_profit_rel_to_own_total_unrealized_pnl: MetricPattern1[StoredF32] = MetricPattern1(client, _m(acc, 'unrealized_profit_rel_to_own_total_unrealized_pnl'))
class CountPattern2(Generic[T]):
"""Pattern struct for repeated tree structure."""
@@ -2217,6 +2219,19 @@ class _10yTo12yPattern:
self.supply: SupplyPattern2 = SupplyPattern2(client, _m(acc, 'supply'))
self.unrealized: UnrealizedPattern = UnrealizedPattern(client, acc)
class UnrealizedPattern:
"""Pattern struct for repeated tree structure."""
def __init__(self, client: BrkClientBase, acc: str):
"""Create pattern node with accumulated metric name."""
self.neg_unrealized_loss: MetricPattern1[Dollars] = MetricPattern1(client, _m(acc, 'neg_unrealized_loss'))
self.net_unrealized_pnl: MetricPattern1[Dollars] = MetricPattern1(client, _m(acc, 'net_unrealized_pnl'))
self.supply_in_loss: ActiveSupplyPattern = ActiveSupplyPattern(client, _m(acc, 'supply_in_loss'))
self.supply_in_profit: ActiveSupplyPattern = ActiveSupplyPattern(client, _m(acc, 'supply_in_profit'))
self.total_unrealized_pnl: MetricPattern1[Dollars] = MetricPattern1(client, _m(acc, 'total_unrealized_pnl'))
self.unrealized_loss: MetricPattern1[Dollars] = MetricPattern1(client, _m(acc, 'unrealized_loss'))
self.unrealized_profit: MetricPattern1[Dollars] = MetricPattern1(client, _m(acc, 'unrealized_profit'))
class _10yPattern:
"""Pattern struct for repeated tree structure."""
@@ -2230,19 +2245,6 @@ class _10yPattern:
self.supply: SupplyPattern2 = SupplyPattern2(client, _m(acc, 'supply'))
self.unrealized: UnrealizedPattern = UnrealizedPattern(client, acc)
class _0satsPattern2:
"""Pattern struct for repeated tree structure."""
def __init__(self, client: BrkClientBase, acc: str):
"""Create pattern node with accumulated metric name."""
self.activity: ActivityPattern2 = ActivityPattern2(client, acc)
self.cost_basis: CostBasisPattern = CostBasisPattern(client, acc)
self.outputs: OutputsPattern = OutputsPattern(client, _m(acc, 'utxo_count'))
self.realized: RealizedPattern = RealizedPattern(client, acc)
self.relative: RelativePattern4 = RelativePattern4(client, _m(acc, 'supply_in'))
self.supply: SupplyPattern2 = SupplyPattern2(client, _m(acc, 'supply'))
self.unrealized: UnrealizedPattern = UnrealizedPattern(client, acc)
class _100btcPattern:
"""Pattern struct for repeated tree structure."""
@@ -2256,19 +2258,6 @@ class _100btcPattern:
self.supply: SupplyPattern2 = SupplyPattern2(client, _m(acc, 'supply'))
self.unrealized: UnrealizedPattern = UnrealizedPattern(client, acc)
class UnrealizedPattern:
"""Pattern struct for repeated tree structure."""
def __init__(self, client: BrkClientBase, acc: str):
"""Create pattern node with accumulated metric name."""
self.neg_unrealized_loss: MetricPattern1[Dollars] = MetricPattern1(client, _m(acc, 'neg_unrealized_loss'))
self.net_unrealized_pnl: MetricPattern1[Dollars] = MetricPattern1(client, _m(acc, 'net_unrealized_pnl'))
self.supply_in_loss: ActiveSupplyPattern = ActiveSupplyPattern(client, _m(acc, 'supply_in_loss'))
self.supply_in_profit: ActiveSupplyPattern = ActiveSupplyPattern(client, _m(acc, 'supply_in_profit'))
self.total_unrealized_pnl: MetricPattern1[Dollars] = MetricPattern1(client, _m(acc, 'total_unrealized_pnl'))
self.unrealized_loss: MetricPattern1[Dollars] = MetricPattern1(client, _m(acc, 'unrealized_loss'))
self.unrealized_profit: MetricPattern1[Dollars] = MetricPattern1(client, _m(acc, 'unrealized_profit'))
class PeriodCagrPattern:
"""Pattern struct for repeated tree structure."""
@@ -2282,6 +2271,19 @@ class PeriodCagrPattern:
self._6y: MetricPattern4[StoredF32] = MetricPattern4(client, _p('6y', acc))
self._8y: MetricPattern4[StoredF32] = MetricPattern4(client, _p('8y', acc))
class _0satsPattern2:
"""Pattern struct for repeated tree structure."""
def __init__(self, client: BrkClientBase, acc: str):
"""Create pattern node with accumulated metric name."""
self.activity: ActivityPattern2 = ActivityPattern2(client, acc)
self.cost_basis: CostBasisPattern = CostBasisPattern(client, acc)
self.outputs: OutputsPattern = OutputsPattern(client, _m(acc, 'utxo_count'))
self.realized: RealizedPattern = RealizedPattern(client, acc)
self.relative: RelativePattern4 = RelativePattern4(client, _m(acc, 'supply_in'))
self.supply: SupplyPattern2 = SupplyPattern2(client, _m(acc, 'supply'))
self.unrealized: UnrealizedPattern = UnrealizedPattern(client, acc)
class ActivityPattern2:
"""Pattern struct for repeated tree structure."""
@@ -2321,23 +2323,14 @@ class CoinbasePattern2:
self.dollars: BlockCountPattern[Dollars] = BlockCountPattern(client, _m(acc, 'usd'))
self.sats: BlockCountPattern[Sats] = BlockCountPattern(client, acc)
class ActiveSupplyPattern:
class CoinbasePattern:
"""Pattern struct for repeated tree structure."""
def __init__(self, client: BrkClientBase, acc: str):
"""Create pattern node with accumulated metric name."""
self.bitcoin: MetricPattern1[Bitcoin] = MetricPattern1(client, _m(acc, 'btc'))
self.dollars: MetricPattern1[Dollars] = MetricPattern1(client, _m(acc, 'usd'))
self.sats: MetricPattern1[Sats] = MetricPattern1(client, acc)
class SegwitAdoptionPattern:
"""Pattern struct for repeated tree structure."""
def __init__(self, client: BrkClientBase, acc: str):
"""Create pattern node with accumulated metric name."""
self.base: MetricPattern11[StoredF32] = MetricPattern11(client, acc)
self.cumulative: MetricPattern2[StoredF32] = MetricPattern2(client, _m(acc, 'cumulative'))
self.sum: MetricPattern2[StoredF32] = MetricPattern2(client, _m(acc, 'sum'))
self.bitcoin: BitcoinPattern = BitcoinPattern(client, _m(acc, 'btc'))
self.dollars: DollarsPattern[Dollars] = DollarsPattern(client, _m(acc, 'usd'))
self.sats: DollarsPattern[Sats] = DollarsPattern(client, acc)
class UnclaimedRewardsPattern:
"""Pattern struct for repeated tree structure."""
@@ -2357,14 +2350,31 @@ class _2015Pattern:
self.dollars: MetricPattern4[Dollars] = MetricPattern4(client, _m(acc, 'usd'))
self.sats: MetricPattern4[Sats] = MetricPattern4(client, acc)
class CoinbasePattern:
class SegwitAdoptionPattern:
"""Pattern struct for repeated tree structure."""
def __init__(self, client: BrkClientBase, acc: str):
"""Create pattern node with accumulated metric name."""
self.bitcoin: BitcoinPattern = BitcoinPattern(client, _m(acc, 'btc'))
self.dollars: DollarsPattern[Dollars] = DollarsPattern(client, _m(acc, 'usd'))
self.sats: DollarsPattern[Sats] = DollarsPattern(client, acc)
self.base: MetricPattern11[StoredF32] = MetricPattern11(client, acc)
self.cumulative: MetricPattern2[StoredF32] = MetricPattern2(client, _m(acc, 'cumulative'))
self.sum: MetricPattern2[StoredF32] = MetricPattern2(client, _m(acc, 'sum'))
class ActiveSupplyPattern:
"""Pattern struct for repeated tree structure."""
def __init__(self, client: BrkClientBase, acc: str):
"""Create pattern node with accumulated metric name."""
self.bitcoin: MetricPattern1[Bitcoin] = MetricPattern1(client, _m(acc, 'btc'))
self.dollars: MetricPattern1[Dollars] = MetricPattern1(client, _m(acc, 'usd'))
self.sats: MetricPattern1[Sats] = MetricPattern1(client, acc)
class SupplyPattern2:
"""Pattern struct for repeated tree structure."""
def __init__(self, client: BrkClientBase, acc: str):
"""Create pattern node with accumulated metric name."""
self.halved: ActiveSupplyPattern = ActiveSupplyPattern(client, _m(acc, 'halved'))
self.total: ActiveSupplyPattern = ActiveSupplyPattern(client, acc)
class _1dReturns1mSdPattern:
"""Pattern struct for repeated tree structure."""
@@ -2390,14 +2400,6 @@ class RelativePattern4:
self.supply_in_loss_rel_to_own_supply: MetricPattern1[StoredF64] = MetricPattern1(client, _m(acc, 'loss_rel_to_own_supply'))
self.supply_in_profit_rel_to_own_supply: MetricPattern1[StoredF64] = MetricPattern1(client, _m(acc, 'profit_rel_to_own_supply'))
class SupplyPattern2:
"""Pattern struct for repeated tree structure."""
def __init__(self, client: BrkClientBase, acc: str):
"""Create pattern node with accumulated metric name."""
self.halved: ActiveSupplyPattern = ActiveSupplyPattern(client, _m(acc, 'halved'))
self.total: ActiveSupplyPattern = ActiveSupplyPattern(client, acc)
class BitcoinPattern2(Generic[T]):
"""Pattern struct for repeated tree structure."""
@@ -2406,14 +2408,6 @@ class BitcoinPattern2(Generic[T]):
self.cumulative: MetricPattern2[T] = MetricPattern2(client, _m(acc, 'cumulative'))
self.sum: MetricPattern1[T] = MetricPattern1(client, acc)
class BlockCountPattern(Generic[T]):
"""Pattern struct for repeated tree structure."""
def __init__(self, client: BrkClientBase, acc: str):
"""Create pattern node with accumulated metric name."""
self.cumulative: MetricPattern1[T] = MetricPattern1(client, _m(acc, 'cumulative'))
self.sum: MetricPattern1[T] = MetricPattern1(client, acc)
class SatsPattern(Generic[T]):
"""Pattern struct for repeated tree structure."""
@@ -2422,6 +2416,14 @@ class SatsPattern(Generic[T]):
self.ohlc: MetricPattern1[T] = MetricPattern1(client, _m(acc, 'ohlc_sats'))
self.split: SplitPattern2[T] = SplitPattern2(client, _m(acc, 'sats'))
class BlockCountPattern(Generic[T]):
"""Pattern struct for repeated tree structure."""
def __init__(self, client: BrkClientBase, acc: str):
"""Create pattern node with accumulated metric name."""
self.cumulative: MetricPattern1[T] = MetricPattern1(client, _m(acc, 'cumulative'))
self.sum: MetricPattern1[T] = MetricPattern1(client, acc)
class RealizedPriceExtraPattern:
"""Pattern struct for repeated tree structure."""
@@ -3269,21 +3271,21 @@ class MetricsTree_Market_Ath:
self.price_drawdown: MetricPattern3[StoredF32] = MetricPattern3(client, 'price_drawdown')
self.years_since_price_ath: MetricPattern4[StoredF32] = MetricPattern4(client, 'years_since_price_ath')
class MetricsTree_Market_Dca_ClassAveragePrice:
class MetricsTree_Market_Dca_ClassReturns:
"""Metrics tree node."""
def __init__(self, client: BrkClientBase, base_path: str = ''):
self._2015: MetricPattern4[Dollars] = MetricPattern4(client, 'dca_class_2015_average_price')
self._2016: MetricPattern4[Dollars] = MetricPattern4(client, 'dca_class_2016_average_price')
self._2017: MetricPattern4[Dollars] = MetricPattern4(client, 'dca_class_2017_average_price')
self._2018: MetricPattern4[Dollars] = MetricPattern4(client, 'dca_class_2018_average_price')
self._2019: MetricPattern4[Dollars] = MetricPattern4(client, 'dca_class_2019_average_price')
self._2020: MetricPattern4[Dollars] = MetricPattern4(client, 'dca_class_2020_average_price')
self._2021: MetricPattern4[Dollars] = MetricPattern4(client, 'dca_class_2021_average_price')
self._2022: MetricPattern4[Dollars] = MetricPattern4(client, 'dca_class_2022_average_price')
self._2023: MetricPattern4[Dollars] = MetricPattern4(client, 'dca_class_2023_average_price')
self._2024: MetricPattern4[Dollars] = MetricPattern4(client, 'dca_class_2024_average_price')
self._2025: MetricPattern4[Dollars] = MetricPattern4(client, 'dca_class_2025_average_price')
self._2015: MetricPattern4[StoredF32] = MetricPattern4(client, 'dca_class_2015_returns')
self._2016: MetricPattern4[StoredF32] = MetricPattern4(client, 'dca_class_2016_returns')
self._2017: MetricPattern4[StoredF32] = MetricPattern4(client, 'dca_class_2017_returns')
self._2018: MetricPattern4[StoredF32] = MetricPattern4(client, 'dca_class_2018_returns')
self._2019: MetricPattern4[StoredF32] = MetricPattern4(client, 'dca_class_2019_returns')
self._2020: MetricPattern4[StoredF32] = MetricPattern4(client, 'dca_class_2020_returns')
self._2021: MetricPattern4[StoredF32] = MetricPattern4(client, 'dca_class_2021_returns')
self._2022: MetricPattern4[StoredF32] = MetricPattern4(client, 'dca_class_2022_returns')
self._2023: MetricPattern4[StoredF32] = MetricPattern4(client, 'dca_class_2023_returns')
self._2024: MetricPattern4[StoredF32] = MetricPattern4(client, 'dca_class_2024_returns')
self._2025: MetricPattern4[StoredF32] = MetricPattern4(client, 'dca_class_2025_returns')
class MetricsTree_Market_Dca_ClassStack:
"""Metrics tree node."""
@@ -3305,8 +3307,8 @@ class MetricsTree_Market_Dca:
"""Metrics tree node."""
def __init__(self, client: BrkClientBase, base_path: str = ''):
self.class_average_price: MetricsTree_Market_Dca_ClassAveragePrice = MetricsTree_Market_Dca_ClassAveragePrice(client)
self.class_returns: ClassAveragePricePattern[StoredF32] = ClassAveragePricePattern(client, 'dca_class')
self.class_average_price: ClassAveragePricePattern[Dollars] = ClassAveragePricePattern(client, 'dca_class')
self.class_returns: MetricsTree_Market_Dca_ClassReturns = MetricsTree_Market_Dca_ClassReturns(client)
self.class_stack: MetricsTree_Market_Dca_ClassStack = MetricsTree_Market_Dca_ClassStack(client)
self.period_average_price: PeriodAveragePricePattern[Dollars] = PeriodAveragePricePattern(client, 'dca_average_price')
self.period_cagr: PeriodCagrPattern = PeriodCagrPattern(client, 'dca_cagr')
+7 -1
View File
@@ -1533,6 +1533,11 @@
"(prefers-color-scheme: dark)",
);
let storedTheme;
try { storedTheme = localStorage.getItem("theme"); } catch (_) {}
const isDark = storedTheme ? storedTheme === "dark" : preferredColorSchemeMatchMedia.matches;
document.documentElement.style.colorScheme = isDark ? "dark" : "light";
const themeColor = window.document.createElement("meta");
themeColor.name = "theme-color";
window.document.getElementsByTagName("head")[0].appendChild(themeColor);
@@ -1545,7 +1550,7 @@
themeColor.content = theme;
}
updateThemeColor(preferredColorSchemeMatchMedia.matches);
updateThemeColor(isDark);
preferredColorSchemeMatchMedia.addEventListener(
"change",
({ matches }) => {
@@ -1832,6 +1837,7 @@
</label>
<button id="share-button" title="Share">Share</button>
<button id="invert-button" title="Invert">Invert</button>
</fieldset>
</footer>
</main>
@@ -1,13 +1,14 @@
import { ios } from "../../utils/env.js";
import { domToBlob } from "../../modules/modern-screenshot/4.6.7/dist/index.mjs";
import { ios, canShare } from "../utils/env.js";
import { domToBlob } from "../modules/modern-screenshot/4.6.7/dist/index.mjs";
export const canCapture = !ios || canShare;
/**
* @param {Object} args
* @param {Element} args.element
* @param {string} args.name
* @param {string} args.title
*/
export async function screenshot({ element, name, title }) {
export async function capture({ element, name }) {
const blob = await domToBlob(element, {
scale: 2,
});
@@ -16,15 +17,13 @@ export async function screenshot({ element, name, title }) {
const file = new File(
[blob],
`bitview-${name}-${new Date().toJSON().split(".")[0]}.png`,
{
type: "image/png",
},
{ type: "image/png" },
);
try {
await navigator.share({
files: [file],
title: `${title} on ${window.document.location.hostname}`,
title: `${name} on ${window.document.location.hostname}`,
});
return;
} catch (err) {
+98
View File
@@ -0,0 +1,98 @@
import { oklchToRgba } from "./oklch.js";
import { dark } from "../utils/theme.js";
/** @type {Map<string, string>} */
const rgbaCache = new Map();
/**
* Convert oklch to rgba with caching
* @param {string} color - oklch color string
*/
function toRgba(color) {
if (color === "transparent") return color;
const cached = rgbaCache.get(color);
if (cached) return cached;
const rgba = oklchToRgba(color);
rgbaCache.set(color, rgba);
return rgba;
}
/**
* Reduce color opacity to 50% for dimming effect
* @param {string} color - oklch color string
*/
function tameColor(color) {
if (color === "transparent") return color;
return `${color.slice(0, -1)} / 25%)`;
}
/**
* @typedef {Object} ColorMethods
* @property {() => string} tame - Returns tamed (50% opacity) version
* @property {(highlighted: boolean) => string} highlight - Returns normal if highlighted, tamed otherwise
*/
/**
* @typedef {(() => string) & ColorMethods} Color
*/
/**
* Creates a Color object that is callable and has utility methods
* @param {() => string} getter
* @returns {Color}
*/
function createColor(getter) {
const color = /** @type {Color} */ (() => toRgba(getter()));
color.tame = () => toRgba(tameColor(getter()));
color.highlight = (highlighted) =>
highlighted ? toRgba(getter()) : toRgba(tameColor(getter()));
return color;
}
const globalComputedStyle = getComputedStyle(window.document.documentElement);
/**
* @param {string} name
*/
function getColor(name) {
return globalComputedStyle.getPropertyValue(`--${name}`);
}
/**
* @param {string} property
*/
function getLightDarkValue(property) {
const value = globalComputedStyle.getPropertyValue(property);
const [light, _dark] = value.slice(11, -1).split(", ");
return dark ? _dark : light;
}
export const colors = {
default: createColor(() => getLightDarkValue("--color")),
gray: createColor(() => getColor("gray")),
border: createColor(() => getLightDarkValue("--border-color")),
red: createColor(() => getColor("red")),
orange: createColor(() => getColor("orange")),
amber: createColor(() => getColor("amber")),
yellow: createColor(() => getColor("yellow")),
avocado: createColor(() => getColor("avocado")),
lime: createColor(() => getColor("lime")),
green: createColor(() => getColor("green")),
emerald: createColor(() => getColor("emerald")),
teal: createColor(() => getColor("teal")),
cyan: createColor(() => getColor("cyan")),
sky: createColor(() => getColor("sky")),
blue: createColor(() => getColor("blue")),
indigo: createColor(() => getColor("indigo")),
violet: createColor(() => getColor("violet")),
purple: createColor(() => getColor("purple")),
fuchsia: createColor(() => getColor("fuchsia")),
pink: createColor(() => getColor("pink")),
rose: createColor(() => getColor("rose")),
};
/**
* @typedef {typeof colors} Colors
* @typedef {keyof Colors} ColorName
*/
File diff suppressed because it is too large Load Diff
+35 -45
View File
@@ -1,13 +1,26 @@
import { createLabeledInput, createSpanName } from "../utils/dom.js";
import { stringToId } from "../utils/format.js";
/**
* @param {Signals} signals
*/
export function createLegend(signals) {
export function createLegend() {
const element = window.document.createElement("legend");
const hovered = signals.createSignal(/** @type {AnySeries | null} */ (null));
/** @type {AnySeries | null} */
let hoveredSeries = null;
/** @type {Map<AnySeries, { span: HTMLSpanElement, color: Color }[]>} */
const seriesColorSpans = new Map();
/** @param {AnySeries | null} series */
function setHovered(series) {
if (hoveredSeries === series) return;
hoveredSeries = series;
for (const [entrySeries, colorSpans] of seriesColorSpans) {
const shouldHighlight = !hoveredSeries || hoveredSeries === entrySeries;
shouldHighlight ? entrySeries.highlight() : entrySeries.tame();
for (const { span, color } of colorSpans) {
span.style.backgroundColor = color.highlight(shouldHighlight);
}
}
}
/** @type {HTMLElement[]} */
const legends = [];
@@ -44,15 +57,11 @@ export function createLegend(signals) {
title: "Click to toggle",
inputChecked: series.active(),
onClick: () => {
series.active.set(input.checked);
series.setActive(input.checked);
},
type: "checkbox",
});
// Sync checkbox with signal (for shared signals across panes)
signals.createEffect(series.active, (active) => {
input.checked = active;
});
input.dataset.series = series.key;
const spanMain = window.document.createElement("span");
spanMain.classList.add("main");
@@ -62,49 +71,30 @@ export function createLegend(signals) {
spanMain.append(spanName);
div.append(label);
label.addEventListener("mouseover", () => {
const h = hovered();
if (!h || h !== series) {
hovered.set(series);
}
});
label.addEventListener("mouseleave", () => {
hovered.set(null);
});
const shouldHighlight = () => !hovered() || hovered() === series;
// Update series highlighted state
signals.createEffect(shouldHighlight, (shouldHighlight) => {
series.highlighted.set(shouldHighlight);
});
label.addEventListener("mouseover", () => setHovered(series));
label.addEventListener("mouseleave", () => setHovered(null));
const spanColors = window.document.createElement("span");
spanColors.classList.add("colors");
spanMain.prepend(spanColors);
/** @type {{ span: HTMLSpanElement, color: Color }[]} */
const colorSpans = [];
colors.forEach((color) => {
const spanColor = window.document.createElement("span");
spanColor.style.backgroundColor = color.highlight(true);
spanColors.append(spanColor);
signals.createEffect(
() => color.highlight(shouldHighlight()),
(c) => {
spanColor.style.backgroundColor = c;
},
);
colorSpans.push({ span: spanColor, color });
});
seriesColorSpans.set(series, colorSpans);
const anchor = window.document.createElement("a");
signals.createEffect(series.url, (url) => {
if (url) {
anchor.href = url;
anchor.target = "_blank";
anchor.rel = "noopener noreferrer";
anchor.title = "Click to view data";
div.append(anchor);
}
});
if (series.url) {
const anchor = window.document.createElement("a");
anchor.href = series.url;
anchor.target = "_blank";
anchor.rel = "noopener noreferrer";
anchor.title = "Click to view data";
div.append(anchor);
}
},
/**
* @param {number} start
-129
View File
@@ -1,129 +0,0 @@
import { throttle } from "../utils/timing.js";
/**
* @param {Object} args
* @param {IChartApi} args.chart
* @param {Accessor<Set<AnySeries>>} args.seriesList
* @param {Colors} args.colors
* @param {(value: number) => string} args.formatValue
*/
export function createMinMaxMarkers({ chart, seriesList, colors, formatValue }) {
/** @type {Set<AnySeries>} */
const prevMarkerSeries = new Set();
function update() {
const timeScale = chart.timeScale();
const width = timeScale.width();
const range = timeScale.getVisibleRange();
if (!range) return;
const tLeft = timeScale.coordinateToTime(30);
const tRight = timeScale.coordinateToTime(width - 30);
const t0 = /** @type {number} */ (tLeft ?? range.from);
const t1 = /** @type {number} */ (tRight ?? range.to);
const color = colors.gray();
/** @type {Map<number, { minV: number, minT: Time, minS: AnySeries, maxV: number, maxT: Time, maxS: AnySeries }>} */
const byPane = new Map();
for (const series of seriesList()) {
if (!series.active() || !series.hasData()) continue;
const data = series.getData();
const len = data.length;
if (!len) continue;
// Binary search for start
let lo = 0, hi = len;
while (lo < hi) {
const mid = (lo + hi) >>> 1;
if (/** @type {number} */ (data[mid].time) < t0) lo = mid + 1;
else hi = mid;
}
if (lo >= len) continue;
const paneIndex = series.paneIndex;
let pane = byPane.get(paneIndex);
if (!pane) {
pane = {
minV: Infinity,
minT: /** @type {Time} */ (0),
minS: series,
maxV: -Infinity,
maxT: /** @type {Time} */ (0),
maxS: series,
};
byPane.set(paneIndex, pane);
}
for (let i = lo; i < len; i++) {
const pt = data[i];
if (/** @type {number} */ (pt.time) > t1) break;
const v = pt.low ?? pt.value;
const h = pt.high ?? pt.value;
if (v && v < pane.minV) {
pane.minV = v;
pane.minT = pt.time;
pane.minS = series;
}
if (h && h > pane.maxV) {
pane.maxV = h;
pane.maxT = pt.time;
pane.maxS = series;
}
}
}
// Set new markers
/** @type {Set<AnySeries>} */
const used = new Set();
for (const { minV, minT, minS, maxV, maxT, maxS } of byPane.values()) {
if (!Number.isFinite(minV) || !Number.isFinite(maxV) || minT === maxT)
continue;
const minM = /** @type {TimeSeriesMarker} */ ({
time: minT,
position: "belowBar",
shape: "arrowUp",
color,
size: 0,
text: formatValue(minV),
});
const maxM = /** @type {TimeSeriesMarker} */ ({
time: maxT,
position: "aboveBar",
shape: "arrowDown",
color,
size: 0,
text: formatValue(maxV),
});
used.add(minS);
used.add(maxS);
if (minS === maxS) {
minS.setMarkers([minM, maxM]);
} else {
minS.setMarkers([minM]);
maxS.setMarkers([maxM]);
}
}
// Clear stale
for (const s of prevMarkerSeries) {
if (!used.has(s)) s.clearMarkers();
}
prevMarkerSeries.clear();
for (const s of used) prevMarkerSeries.add(s);
}
function clear() {
for (const s of prevMarkerSeries) s.clearMarkers();
prevMarkerSeries.clear();
}
return {
update,
scheduleUpdate: throttle(update, 100),
clear,
};
}
+106 -99
View File
@@ -1,100 +1,107 @@
export function createOklchToRGBA() {
{
/**
*
* @param {readonly [number, number, number, number, number, number, number, number, number]} A
* @param {readonly [number, number, number]} B
* @returns
*/
function multiplyMatrices(A, B) {
return /** @type {const} */ ([
A[0] * B[0] + A[1] * B[1] + A[2] * B[2],
A[3] * B[0] + A[4] * B[1] + A[5] * B[2],
A[6] * B[0] + A[7] * B[1] + A[8] * B[2],
]);
}
/**
* @param {readonly [number, number, number]} param0
*/
function oklch2oklab([l, c, h]) {
return /** @type {const} */ ([
l,
isNaN(h) ? 0 : c * Math.cos((h * Math.PI) / 180),
isNaN(h) ? 0 : c * Math.sin((h * Math.PI) / 180),
]);
}
/**
* @param {readonly [number, number, number]} rgb
*/
function srgbLinear2rgb(rgb) {
return rgb.map((c) =>
Math.abs(c) > 0.0031308
? (c < 0 ? -1 : 1) * (1.055 * Math.abs(c) ** (1 / 2.4) - 0.055)
: 12.92 * c,
);
}
/**
* @param {readonly [number, number, number]} lab
*/
function oklab2xyz(lab) {
const LMSg = multiplyMatrices(
/** @type {const} */ ([
1, 0.3963377773761749, 0.2158037573099136, 1, -0.1055613458156586,
-0.0638541728258133, 1, -0.0894841775298119, -1.2914855480194092,
]),
lab,
);
const LMS = /** @type {[number, number, number]} */ (
LMSg.map((val) => val ** 3)
);
return multiplyMatrices(
/** @type {const} */ ([
1.2268798758459243, -0.5578149944602171, 0.2813910456659647,
-0.0405757452148008, 1.112286803280317, -0.0717110580655164,
-0.0763729366746601, -0.4214933324022432, 1.5869240198367816,
]),
LMS,
);
}
/**
* @param {readonly [number, number, number]} xyz
*/
function xyz2rgbLinear(xyz) {
return multiplyMatrices(
[
3.2409699419045226, -1.537383177570094, -0.4986107602930034,
-0.9692436362808796, 1.8759675015077202, 0.04155505740717559,
0.05563007969699366, -0.20397695888897652, 1.0569715142428786,
],
xyz,
);
}
/** @param {string} oklch */
return function (oklch) {
oklch = oklch.replace("oklch(", "");
oklch = oklch.replace(")", "");
let splitOklch = oklch.split(" / ");
let alpha = 1;
if (splitOklch.length === 2) {
alpha = Number(splitOklch.pop()?.replace("%", "")) / 100;
}
splitOklch = oklch.split(" ");
const lch = splitOklch.map((v, i) => {
if (!i && v.includes("%")) {
return Number(v.replace("%", "")) / 100;
} else {
return Number(v);
}
});
const rgb = srgbLinear2rgb(
xyz2rgbLinear(
oklab2xyz(oklch2oklab(/** @type {[number, number, number]} */ (lch))),
),
).map((v) => {
return Math.max(Math.min(Math.round(v * 255), 255), 0);
});
return [...rgb, alpha];
};
}
/**
* @param {readonly [number, number, number, number, number, number, number, number, number]} A
* @param {readonly [number, number, number]} B
*/
function multiplyMatrices(A, B) {
return /** @type {const} */ ([
A[0] * B[0] + A[1] * B[1] + A[2] * B[2],
A[3] * B[0] + A[4] * B[1] + A[5] * B[2],
A[6] * B[0] + A[7] * B[1] + A[8] * B[2],
]);
}
/** @param {readonly [number, number, number]} param0 */
function oklch2oklab([l, c, h]) {
return /** @type {const} */ ([
l,
isNaN(h) ? 0 : c * Math.cos((h * Math.PI) / 180),
isNaN(h) ? 0 : c * Math.sin((h * Math.PI) / 180),
]);
}
/** @param {readonly [number, number, number]} rgb */
function srgbLinear2rgb(rgb) {
return rgb.map((c) =>
Math.abs(c) > 0.0031308
? (c < 0 ? -1 : 1) * (1.055 * Math.abs(c) ** (1 / 2.4) - 0.055)
: 12.92 * c,
);
}
/** @param {readonly [number, number, number]} lab */
function oklab2xyz(lab) {
const LMSg = multiplyMatrices(
[1, 0.3963377773761749, 0.2158037573099136, 1, -0.1055613458156586,
-0.0638541728258133, 1, -0.0894841775298119, -1.2914855480194092],
lab,
);
const LMS = /** @type {[number, number, number]} */ (LMSg.map((val) => val ** 3));
return multiplyMatrices(
[1.2268798758459243, -0.5578149944602171, 0.2813910456659647,
-0.0405757452148008, 1.112286803280317, -0.0717110580655164,
-0.0763729366746601, -0.4214933324022432, 1.5869240198367816],
LMS,
);
}
/** @param {readonly [number, number, number]} xyz */
function xyz2rgbLinear(xyz) {
return multiplyMatrices(
[3.2409699419045226, -1.537383177570094, -0.4986107602930034,
-0.9692436362808796, 1.8759675015077202, 0.04155505740717559,
0.05563007969699366, -0.20397695888897652, 1.0569715142428786],
xyz,
);
}
/** @type {Map<string, [number, number, number, number]>} */
const conversionCache = new Map();
/**
* Parse oklch string and return rgba tuple
* @param {string} oklch
* @returns {[number, number, number, number] | null}
*/
function parseOklch(oklch) {
if (!oklch.startsWith("oklch(")) return null;
const cached = conversionCache.get(oklch);
if (cached) return cached;
let str = oklch.slice(6, -1); // remove "oklch(" and ")"
let alpha = 1;
const slashIdx = str.indexOf(" / ");
if (slashIdx !== -1) {
const alphaPart = str.slice(slashIdx + 3);
alpha = alphaPart.includes("%")
? Number(alphaPart.replace("%", "")) / 100
: Number(alphaPart);
str = str.slice(0, slashIdx);
}
const parts = str.split(" ");
const l = parts[0].includes("%") ? Number(parts[0].replace("%", "")) / 100 : Number(parts[0]);
const c = Number(parts[1]);
const h = Number(parts[2]);
const rgb = srgbLinear2rgb(xyz2rgbLinear(oklab2xyz(oklch2oklab([l, c, h]))))
.map((v) => Math.max(Math.min(Math.round(v * 255), 255), 0));
const result = /** @type {[number, number, number, number]} */ ([...rgb, alpha]);
conversionCache.set(oklch, result);
return result;
}
/**
* Convert oklch string to rgba string
* @param {string} oklch
* @returns {string}
*/
export function oklchToRgba(oklch) {
const result = parseOklch(oklch);
if (!result) return oklch;
const [r, g, b, a] = result;
return a === 1 ? `rgb(${r}, ${g}, ${b})` : `rgba(${r}, ${g}, ${b}, ${a})`;
}
-59
View File
@@ -1,59 +0,0 @@
import { readParam, writeParam } from "../utils/url.js";
import { readStored, writeToStorage } from "../utils/storage.js";
/**
* @typedef {{ from: number | null, to: number | null }} Range
*/
const RANGES_KEY = "chart-ranges";
const RANGE_SEP = "_";
/**
* @param {Signals} signals
*/
export function createChartState(signals) {
const index = signals.createPersistedSignal({
storageKey: "chart-index",
urlKey: "index",
defaultValue: /** @type {ChartableIndexName} */ ("date"),
serialize: (v) => v,
deserialize: (s) => /** @type {ChartableIndexName} */ (s),
});
// Ranges stored per-index in localStorage only
/** @type {Record<string, Range>} */
let ranges = {};
try {
const stored = readStored(RANGES_KEY);
if (stored) ranges = JSON.parse(stored);
} catch {}
// Initialize from URL if present
const urlRange = readParam("range");
if (urlRange) {
const [from, to] = urlRange.split(RANGE_SEP).map(Number);
if (!isNaN(from) && !isNaN(to)) {
ranges[index()] = { from, to };
writeToStorage(RANGES_KEY, JSON.stringify(ranges));
}
}
return {
index,
/** @returns {Range} */
range: () => ranges[index()] ?? { from: null, to: null },
/** @param {Range} value */
setRange(value) {
ranges[index()] = value;
writeToStorage(RANGES_KEY, JSON.stringify(ranges));
if (value.from !== null && value.to !== null) {
// Round to 2 decimals for cleaner URLs
const f = Math.floor(value.from * 100) / 100;
const t = Math.floor(value.to * 100) / 100;
writeParam("range", `${f}${RANGE_SEP}${t}`);
} else {
writeParam("range", null);
}
},
};
}
+4 -5
View File
@@ -1,7 +1,7 @@
/**
* @import * as _ from "./modules/leeoniya-ufuzzy/1.0.19/dist/uFuzzy.d.ts"
*
* @import { IChartApi, ISeriesApi as _ISeriesApi, SeriesDefinition, SingleValueData as _SingleValueData, CandlestickData as _CandlestickData, BaselineData as _BaselineData, HistogramData as _HistogramData, SeriesType as LCSeriesType, IPaneApi, LineSeriesPartialOptions as _LineSeriesPartialOptions, HistogramSeriesPartialOptions as _HistogramSeriesPartialOptions, BaselineSeriesPartialOptions as _BaselineSeriesPartialOptions, CandlestickSeriesPartialOptions as _CandlestickSeriesPartialOptions, WhitespaceData, DeepPartial, ChartOptions, Time, LineData as _LineData, createChart as CreateChart, LineStyle, createSeriesMarkers as CreateSeriesMarkers, SeriesMarker, ISeriesMarkersPluginApi } from './modules/lightweight-charts/5.1.0/dist/typings.js'
* @import { IChartApi, ISeriesApi as _ISeriesApi, SeriesDefinition, SingleValueData as _SingleValueData, CandlestickData as _CandlestickData, BaselineData as _BaselineData, HistogramData as _HistogramData, SeriesType as LCSeriesType, IPaneApi, LineSeriesPartialOptions as _LineSeriesPartialOptions, HistogramSeriesPartialOptions as _HistogramSeriesPartialOptions, BaselineSeriesPartialOptions as _BaselineSeriesPartialOptions, CandlestickSeriesPartialOptions as _CandlestickSeriesPartialOptions, WhitespaceData, DeepPartial, ChartOptions, Time, LineData as _LineData, createChart as CreateLCChart, LineStyle, createSeriesMarkers as CreateSeriesMarkers, SeriesMarker, ISeriesMarkersPluginApi } from './modules/lightweight-charts/5.1.0/dist/typings.js'
*
* @import { Signal, Signals, Accessor } from "./signals.js";
*
@@ -10,19 +10,18 @@
*
* @import { Resources, MetricResource } from './resources.js'
*
* @import { SingleValueData, CandlestickData, Series, AnySeries, ISeries, HistogramData, LineData, BaselineData, LineSeriesPartialOptions, BaselineSeriesPartialOptions, HistogramSeriesPartialOptions, CandlestickSeriesPartialOptions, CreateChartElement, Chart, Legend } from "./chart/index.js"
* @import { SingleValueData, CandlestickData, Series, AnySeries, ISeries, HistogramData, LineData, BaselineData, LineSeriesPartialOptions, BaselineSeriesPartialOptions, HistogramSeriesPartialOptions, CandlestickSeriesPartialOptions, Chart, Legend } from "./chart/index.js"
*
* @import { Color, ColorName, Colors } from "./utils/colors.js"
* @import { Color, ColorName, Colors } from "./chart/colors.js"
*
* @import { WebSockets } from "./utils/ws.js"
*
* @import { Option, PartialChartOption, ChartOption, AnyPartialOption, ProcessedOptionAddons, OptionsTree, SimulationOption, AnySeriesBlueprint, SeriesType, AnyFetchedSeriesBlueprint, TableOption, ExplorerOption, UrlOption, PartialOptionsGroup, OptionsGroup, PartialOptionsTree, UtxoCohortObject, AddressCohortObject, CohortObject, CohortGroupObject, FetchedLineSeriesBlueprint, FetchedBaselineSeriesBlueprint, FetchedHistogramSeriesBlueprint, PartialContext, PatternAll, PatternFull, PatternWithAdjusted, PatternWithPercentiles, PatternBasic, CohortAll, CohortFull, CohortWithAdjusted, CohortWithPercentiles, CohortBasic, CohortGroupFull, CohortGroupWithAdjusted, CohortGroupWithPercentiles, CohortGroupBasic, UtxoCohortGroupObject, AddressCohortGroupObject, FetchedDotsSeriesBlueprint, FetchedCandlestickSeriesBlueprint } from "./options/partial.js"
*
* @import { line as LineSeriesFn, dots as DotsSeriesFn, candlestick as CandlestickSeriesFn, baseline as BaselineSeriesFn, histogram as HistogramSeriesFn } from "./options/series.js"
*
* @import { UnitObject as Unit } from "./utils/units.js"
*
* @import { ChartableIndexName } from "./panes/chart/index.js";
* @import { ChartableIndexName } from "./utils/serde.js";
*/
// import uFuzzy = require("./modules/leeoniya-ufuzzy/1.0.19/dist/uFuzzy.d.ts");
+3 -23
View File
@@ -1,4 +1,3 @@
import { createColors } from "./utils/colors.js";
import { webSockets } from "./utils/ws.js";
import * as formatters from "./utils/format.js";
import { onFirstIntersection, getElementById, isHidden } from "./utils/dom.js";
@@ -8,7 +7,7 @@ import { initOptions } from "./options/full.js";
import ufuzzy from "./modules/leeoniya-ufuzzy/1.0.19/dist/uFuzzy.mjs";
import * as leanQr from "./modules/lean-qr/2.7.1/index.mjs";
import { init as initExplorer } from "./panes/_explorer.js";
import { init as initChart } from "./panes/chart/index.js";
import { init as initChart } from "./panes/chart.js";
import { init as initTable } from "./panes/table.js";
import { init as initSimulation } from "./panes/_simulation.js";
import { next } from "./utils/timing.js";
@@ -121,18 +120,6 @@ signals.createRoot(() => {
console.log(`VERSION = ${brk.VERSION}`);
function initDark() {
const preferredColorSchemeMatchMedia = window.matchMedia(
"(prefers-color-scheme: dark)",
);
const dark = signals.createSignal(preferredColorSchemeMatchMedia.matches);
preferredColorSchemeMatchMedia.addEventListener("change", ({ matches }) => {
dark.set(matches);
});
return dark;
}
const dark = initDark();
const qrcode = signals.createSignal(/** @type {string | null} */ (null));
signals.createEffect(webSockets.kraken1dCandle.latest, (latest) => {
@@ -159,10 +146,7 @@ signals.createRoot(() => {
// }
// const lastHeight = createLastHeightResource();
const colors = createColors(dark);
const options = initOptions({
colors,
signals,
brk,
qrcode,
@@ -234,7 +218,6 @@ signals.createRoot(() => {
if (firstTimeLoadingChart) {
signals.runWithOwner(owner, () =>
initChart({
colors,
option: /** @type {Accessor<ChartOption>} */ (chartOption),
brk,
}),
@@ -260,11 +243,7 @@ signals.createRoot(() => {
simOption.set(option);
if (firstTimeLoadingSimulation) {
signals.runWithOwner(owner, () =>
initSimulation({
colors,
}),
);
signals.runWithOwner(owner, () => initSimulation());
}
firstTimeLoadingSimulation = false;
@@ -552,6 +531,7 @@ signals.createRoot(() => {
qrcode.set(window.location.href);
});
shareDiv.addEventListener("click", () => {
qrcode.set(null);
});
+2 -4
View File
@@ -1,6 +1,7 @@
/** Chain section builder - typed tree-based patterns */
import { Unit } from "../utils/units.js";
import { line, baseline, dots } from "./series.js";
import { satsBtcUsd } from "./shared.js";
/**
@@ -12,9 +13,6 @@ export function createChainSection(ctx) {
const {
colors,
brk,
line,
baseline,
dots,
createPriceLine,
fromSizePattern,
fromFullnessPattern,
@@ -240,7 +238,7 @@ export function createChainSection(ctx) {
name: "Volume",
title: "Transaction Volume",
bottom: [
...satsBtcUsd(ctx, transactions.volume.sentSum, "Sent"),
...satsBtcUsd( transactions.volume.sentSum, "Sent"),
line({
metric: transactions.volume.annualizedVolume.sats,
name: "annualized",
+14 -22
View File
@@ -5,6 +5,7 @@
*/
import { Unit } from "../../utils/units.js";
import { line, baseline } from "../series.js";
import {
createSingleSupplySeries,
createGroupedSupplyTotalSeries,
@@ -54,12 +55,12 @@ export function createAddressCohortFolder(ctx, args) {
{
name: "in profit",
title: `Supply In Profit ${title}`,
bottom: createGroupedSupplyInProfitSeries(ctx, list),
bottom: createGroupedSupplyInProfitSeries(list),
},
{
name: "in loss",
title: `Supply In Loss ${title}`,
bottom: createGroupedSupplyInLossSeries(ctx, list),
bottom: createGroupedSupplyInLossSeries(list),
},
],
},
@@ -68,7 +69,7 @@ export function createAddressCohortFolder(ctx, args) {
{
name: "utxo count",
title: `UTXO Count ${title}`,
bottom: createUtxoCountSeries(ctx, list, useGroupName),
bottom: createUtxoCountSeries(list, useGroupName),
},
// Address count (ADDRESS COHORTS ONLY - fully type safe!)
@@ -87,7 +88,7 @@ export function createAddressCohortFolder(ctx, args) {
{
name: "Price",
title: `Realized Price ${title}`,
top: createRealizedPriceSeries(ctx, list),
top: createRealizedPriceSeries(list),
},
{
name: "Ratio",
@@ -96,7 +97,6 @@ export function createAddressCohortFolder(ctx, args) {
},
]
: createRealizedPriceOptions(
ctx,
/** @type {AddressCohortObject} */ (args),
title,
)),
@@ -119,24 +119,22 @@ export function createAddressCohortFolder(ctx, args) {
...createUnrealizedSection(ctx, list, useGroupName, title),
// Cost basis section (no percentiles for address cohorts)
...createCostBasisSection(ctx, list, useGroupName, title),
...createCostBasisSection(list, useGroupName, title),
// Activity section
...createActivitySection(ctx, list, useGroupName, title),
...createActivitySection(list, useGroupName, title),
],
};
}
/**
* Create realized price options for single cohort
* @param {PartialContext} ctx
* @param {AddressCohortObject} args
* @param {string} title
* @returns {PartialOptionsTree}
*/
function createRealizedPriceOptions(ctx, args, title) {
const { line } = ctx;
const { tree, color } = args;
function createRealizedPriceOptions(args, title) {
const { tree, color } = args;
return [
{
@@ -163,7 +161,7 @@ function createRealizedPriceOptions(ctx, args, title) {
* @returns {AnyFetchedSeriesBlueprint[]}
*/
function createRealizedCapWithExtras(ctx, list, args, useGroupName) {
const { line, baseline, createPriceLine } = ctx;
const { createPriceLine } = ctx;
const isSingle = !("list" in args);
return list.flatMap(({ color, name, tree }) => [
@@ -196,7 +194,7 @@ function createRealizedCapWithExtras(ctx, list, args, useGroupName) {
* @returns {PartialOptionsTree}
*/
function createRealizedPnlSection(ctx, args, title) {
const { colors, line } = ctx;
const { colors } = ctx;
const { realized } = args.tree;
return [
@@ -251,7 +249,7 @@ function createRealizedPnlSection(ctx, args, title) {
* @returns {PartialOptionsTree}
*/
function createUnrealizedSection(ctx, list, useGroupName, title) {
const { colors, line, baseline } = ctx;
const { colors } = ctx;
return [
{
@@ -301,15 +299,12 @@ function createUnrealizedSection(ctx, list, useGroupName, title) {
/**
* Create cost basis section (no percentiles for address cohorts)
* @param {PartialContext} ctx
* @param {readonly AddressCohortObject[]} list
* @param {boolean} useGroupName
* @param {string} title
* @returns {PartialOptionsTree}
*/
function createCostBasisSection(ctx, list, useGroupName, title) {
const { line } = ctx;
function createCostBasisSection(list, useGroupName, title) {
return [
{
name: "Cost Basis",
@@ -345,15 +340,12 @@ function createCostBasisSection(ctx, list, useGroupName, title) {
/**
* Create activity section
* @param {PartialContext} ctx
* @param {readonly AddressCohortObject[]} list
* @param {boolean} useGroupName
* @param {string} title
* @returns {PartialOptionsTree}
*/
function createActivitySection(ctx, list, useGroupName, title) {
const { line } = ctx;
function createActivitySection(list, useGroupName, title) {
return [
{
name: "Activity",
+21 -39
View File
@@ -1,6 +1,7 @@
/** Shared cohort chart section builders */
import { Unit } from "../../utils/units.js";
import { line } from "../series.js";
import { satsBtcUsd } from "../shared.js";
/**
@@ -10,11 +11,11 @@ import { satsBtcUsd } from "../shared.js";
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createSingleSupplySeries(ctx, cohort) {
const { colors, line, createPriceLine } = ctx;
const { colors, createPriceLine } = ctx;
const { tree } = cohort;
return [
...satsBtcUsd(ctx, tree.supply.total, "Supply", colors.default),
...satsBtcUsd( tree.supply.total, "Supply", colors.default),
...("supplyRelToCirculatingSupply" in tree.relative
? [
line({
@@ -25,9 +26,9 @@ export function createSingleSupplySeries(ctx, cohort) {
}),
]
: []),
...satsBtcUsd(ctx, tree.unrealized.supplyInProfit, "In Profit", colors.green),
...satsBtcUsd(ctx, tree.unrealized.supplyInLoss, "In Loss", colors.red),
...satsBtcUsd(ctx, tree.supply.halved, "half", colors.gray).map((s) => ({
...satsBtcUsd( tree.unrealized.supplyInProfit, "In Profit", colors.green),
...satsBtcUsd( tree.unrealized.supplyInLoss, "In Loss", colors.red),
...satsBtcUsd( tree.supply.halved, "half", colors.gray).map((s) => ({
...s,
options: { lineStyle: 4 },
})),
@@ -76,11 +77,11 @@ export function createSingleSupplySeries(ctx, cohort) {
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createGroupedSupplyTotalSeries(ctx, list) {
const { line, brk } = ctx;
const { brk } = ctx;
const constant100 = brk.metrics.constants.constant100;
return list.flatMap(({ color, name, tree }) => [
...satsBtcUsd(ctx, tree.supply.total, name, color),
...satsBtcUsd( tree.supply.total, name, color),
line({
metric:
"supplyRelToCirculatingSupply" in tree.relative
@@ -95,15 +96,13 @@ export function createGroupedSupplyTotalSeries(ctx, list) {
/**
* Create supply in profit series for grouped cohorts
* @param {PartialContext} ctx
* @param {readonly CohortObject[]} list
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createGroupedSupplyInProfitSeries(ctx, list) {
const { line } = ctx;
export function createGroupedSupplyInProfitSeries(list) {
return list.flatMap(({ color, name, tree }) => [
...satsBtcUsd(ctx, tree.unrealized.supplyInProfit, name, color),
...satsBtcUsd( tree.unrealized.supplyInProfit, name, color),
...("supplyInProfitRelToCirculatingSupply" in tree.relative
? [
line({
@@ -119,15 +118,13 @@ export function createGroupedSupplyInProfitSeries(ctx, list) {
/**
* Create supply in loss series for grouped cohorts
* @param {PartialContext} ctx
* @param {readonly CohortObject[]} list
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createGroupedSupplyInLossSeries(ctx, list) {
const { line } = ctx;
export function createGroupedSupplyInLossSeries(list) {
return list.flatMap(({ color, name, tree }) => [
...satsBtcUsd(ctx, tree.unrealized.supplyInLoss, name, color),
...satsBtcUsd( tree.unrealized.supplyInLoss, name, color),
...("supplyInLossRelToCirculatingSupply" in tree.relative
? [
line({
@@ -143,14 +140,11 @@ export function createGroupedSupplyInLossSeries(ctx, list) {
/**
* Create UTXO count series
* @param {PartialContext} ctx
* @param {readonly CohortObject[]} list
* @param {boolean} useGroupName
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createUtxoCountSeries(ctx, list, useGroupName) {
const { line } = ctx;
export function createUtxoCountSeries(list, useGroupName) {
return list.flatMap(({ color, name, tree }) => [
line({
metric: tree.outputs.utxoCount,
@@ -169,7 +163,7 @@ export function createUtxoCountSeries(ctx, list, useGroupName) {
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createAddressCountSeries(ctx, list, useGroupName) {
const { line, colors } = ctx;
const { colors } = ctx;
return list.flatMap(({ color, name, tree }) => [
line({
@@ -183,13 +177,10 @@ export function createAddressCountSeries(ctx, list, useGroupName) {
/**
* Create realized price series for grouped cohorts
* @param {PartialContext} ctx
* @param {readonly CohortObject[]} list
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createRealizedPriceSeries(ctx, list) {
const { line } = ctx;
export function createRealizedPriceSeries(list) {
return list.map(({ color, name, tree }) =>
line({ metric: tree.realized.realizedPrice, name, color, unit: Unit.usd }),
);
@@ -202,7 +193,7 @@ export function createRealizedPriceSeries(ctx, list) {
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createRealizedPriceRatioSeries(ctx, list) {
const { line, createPriceLine } = ctx;
const { createPriceLine } = ctx;
return [
...list.map(({ color, name, tree }) =>
@@ -219,14 +210,11 @@ export function createRealizedPriceRatioSeries(ctx, list) {
/**
* Create realized capitalization series
* @param {PartialContext} ctx
* @param {readonly CohortObject[]} list
* @param {boolean} useGroupName
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createRealizedCapSeries(ctx, list, useGroupName) {
const { line } = ctx;
export function createRealizedCapSeries(list, useGroupName) {
return list.flatMap(({ color, name, tree }) => [
line({
metric: tree.realized.realizedCap,
@@ -239,14 +227,11 @@ export function createRealizedCapSeries(ctx, list, useGroupName) {
/**
* Create cost basis min/max series (available on all cohorts)
* @param {PartialContext} ctx
* @param {readonly CohortObject[]} list
* @param {boolean} useGroupName
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createCostBasisMinMaxSeries(ctx, list, useGroupName) {
const { line } = ctx;
export function createCostBasisMinMaxSeries(list, useGroupName) {
return list.flatMap(({ color, name, tree }) => [
line({
metric: tree.costBasis.min,
@@ -265,14 +250,11 @@ export function createCostBasisMinMaxSeries(ctx, list, useGroupName) {
/**
* Create cost basis percentile series (only for cohorts with CostBasisPattern2)
* @param {PartialContext} ctx
* @param {readonly CohortWithCostBasisPercentiles[]} list
* @param {boolean} useGroupName
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createCostBasisPercentilesSeries(ctx, list, useGroupName) {
const { line } = ctx;
export function createCostBasisPercentilesSeries(list, useGroupName) {
return list.flatMap(({ color, name, tree }) => {
const percentiles = tree.costBasis.percentiles;
return [
+82 -112
View File
@@ -34,6 +34,7 @@ import {
createCostBasisPercentilesSeries,
} from "./shared.js";
import { Unit } from "../../utils/units.js";
import { line, baseline } from "../series.js";
// ============================================================================
// Folder Builders (4 variants based on pattern capabilities)
@@ -51,10 +52,10 @@ export function createCohortFolderAll(ctx, args) {
name: args.name || "all",
tree: [
createSingleSupplyChart(ctx, args, title),
createSingleUtxoCountChart(ctx, args, title),
createSingleUtxoCountChart(args, title),
createSingleRealizedSectionWithAdjusted(ctx, args, title),
createSingleUnrealizedSectionAll(ctx, args, title),
createSingleCostBasisSectionWithPercentiles(ctx, args, title),
createSingleCostBasisSectionWithPercentiles(args, title),
...createSingleActivitySectionWithAdjusted(ctx, args, title),
],
};
@@ -74,11 +75,11 @@ export function createCohortFolderFull(ctx, args) {
name: args.name || "all",
tree: [
createGroupedSupplySection(ctx, list, title),
createGroupedUtxoCountChart(ctx, list, title),
createGroupedUtxoCountChart(list, title),
createGroupedRealizedSectionWithAdjusted(ctx, list, title),
createGroupedUnrealizedSectionFull(ctx, list, title),
createGroupedCostBasisSectionWithPercentiles(ctx, list, title),
...createGroupedActivitySectionWithAdjusted(ctx, list, title),
createGroupedCostBasisSectionWithPercentiles(list, title),
...createGroupedActivitySectionWithAdjusted(list, title),
],
};
}
@@ -87,10 +88,10 @@ export function createCohortFolderFull(ctx, args) {
name: args.name || "all",
tree: [
createSingleSupplyChart(ctx, args, title),
createSingleUtxoCountChart(ctx, args, title),
createSingleUtxoCountChart(args, title),
createSingleRealizedSectionWithAdjusted(ctx, args, title),
createSingleUnrealizedSectionFull(ctx, args, title),
createSingleCostBasisSectionWithPercentiles(ctx, args, title),
createSingleCostBasisSectionWithPercentiles(args, title),
...createSingleActivitySectionWithAdjusted(ctx, args, title),
],
};
@@ -110,11 +111,11 @@ export function createCohortFolderWithAdjusted(ctx, args) {
name: args.name || "all",
tree: [
createGroupedSupplySection(ctx, list, title),
createGroupedUtxoCountChart(ctx, list, title),
createGroupedUtxoCountChart(list, title),
createGroupedRealizedSectionWithAdjusted(ctx, list, title),
createGroupedUnrealizedSectionWithMarketCap(ctx, list, title),
createGroupedCostBasisSection(ctx, list, title),
...createGroupedActivitySectionWithAdjusted(ctx, list, title),
createGroupedCostBasisSection(list, title),
...createGroupedActivitySectionWithAdjusted(list, title),
],
};
}
@@ -123,10 +124,10 @@ export function createCohortFolderWithAdjusted(ctx, args) {
name: args.name || "all",
tree: [
createSingleSupplyChart(ctx, args, title),
createSingleUtxoCountChart(ctx, args, title),
createSingleUtxoCountChart(args, title),
createSingleRealizedSectionWithAdjusted(ctx, args, title),
createSingleUnrealizedSectionWithMarketCap(ctx, args, title),
createSingleCostBasisSection(ctx, args, title),
createSingleCostBasisSection(args, title),
...createSingleActivitySectionWithAdjusted(ctx, args, title),
],
};
@@ -146,11 +147,11 @@ export function createCohortFolderWithPercentiles(ctx, args) {
name: args.name || "all",
tree: [
createGroupedSupplySection(ctx, list, title),
createGroupedUtxoCountChart(ctx, list, title),
createGroupedUtxoCountChart(list, title),
createGroupedRealizedSectionBasic(ctx, list, title),
createGroupedUnrealizedSectionWithOwnCaps(ctx, list, title),
createGroupedCostBasisSectionWithPercentiles(ctx, list, title),
...createGroupedActivitySectionBasic(ctx, list, title),
createGroupedCostBasisSectionWithPercentiles(list, title),
...createGroupedActivitySectionBasic(list, title),
],
};
}
@@ -159,10 +160,10 @@ export function createCohortFolderWithPercentiles(ctx, args) {
name: args.name || "all",
tree: [
createSingleSupplyChart(ctx, args, title),
createSingleUtxoCountChart(ctx, args, title),
createSingleUtxoCountChart(args, title),
createSingleRealizedSectionBasic(ctx, args, title),
createSingleUnrealizedSectionWithOwnCaps(ctx, args, title),
createSingleCostBasisSectionWithPercentiles(ctx, args, title),
createSingleCostBasisSectionWithPercentiles(args, title),
...createSingleActivitySectionBasic(ctx, args, title),
],
};
@@ -182,11 +183,11 @@ export function createCohortFolderBasic(ctx, args) {
name: args.name || "all",
tree: [
createGroupedSupplySection(ctx, list, title),
createGroupedUtxoCountChart(ctx, list, title),
createGroupedUtxoCountChart(list, title),
createGroupedRealizedSectionBasic(ctx, list, title),
createGroupedUnrealizedSectionBase(ctx, list, title),
createGroupedCostBasisSection(ctx, list, title),
...createGroupedActivitySectionBasic(ctx, list, title),
createGroupedCostBasisSection(list, title),
...createGroupedActivitySectionBasic(list, title),
],
};
}
@@ -195,10 +196,10 @@ export function createCohortFolderBasic(ctx, args) {
name: args.name || "all",
tree: [
createSingleSupplyChart(ctx, args, title),
createSingleUtxoCountChart(ctx, args, title),
createSingleUtxoCountChart(args, title),
createSingleRealizedSectionBasic(ctx, args, title),
createSingleUnrealizedSectionBase(ctx, args, title),
createSingleCostBasisSection(ctx, args, title),
createSingleCostBasisSection(args, title),
...createSingleActivitySectionBasic(ctx, args, title),
],
};
@@ -238,12 +239,12 @@ function createGroupedSupplySection(ctx, list, title) {
{
name: "in profit",
title: `Supply In Profit ${title}`,
bottom: createGroupedSupplyInProfitSeries(ctx, list),
bottom: createGroupedSupplyInProfitSeries(list),
},
{
name: "in loss",
title: `Supply In Loss ${title}`,
bottom: createGroupedSupplyInLossSeries(ctx, list),
bottom: createGroupedSupplyInLossSeries(list),
},
],
};
@@ -251,31 +252,29 @@ function createGroupedSupplySection(ctx, list, title) {
/**
* Create UTXO count chart for single cohort
* @param {PartialContext} ctx
* @param {UtxoCohortObject} cohort
* @param {string} title
* @returns {PartialChartOption}
*/
function createSingleUtxoCountChart(ctx, cohort, title) {
function createSingleUtxoCountChart(cohort, title) {
return {
name: "utxo count",
title: `UTXO Count ${title}`,
bottom: createUtxoCountSeries(ctx, [cohort], false),
bottom: createUtxoCountSeries( [cohort], false),
};
}
/**
* Create UTXO count chart for grouped cohorts
* @param {PartialContext} ctx
* @param {readonly UtxoCohortObject[]} list
* @param {string} title
* @returns {PartialChartOption}
*/
function createGroupedUtxoCountChart(ctx, list, title) {
function createGroupedUtxoCountChart(list, title) {
return {
name: "utxo count",
title: `UTXO Count ${title}`,
bottom: createUtxoCountSeries(ctx, list, true),
bottom: createUtxoCountSeries( list, true),
};
}
@@ -290,7 +289,7 @@ function createSingleRealizedSectionWithAdjusted(ctx, cohort, title) {
return {
name: "Realized",
tree: [
createSingleRealizedPriceChart(ctx, cohort, title),
createSingleRealizedPriceChart(cohort, title),
{
name: "capitalization",
title: `Realized Capitalization ${title}`,
@@ -316,7 +315,7 @@ function createGroupedRealizedSectionWithAdjusted(ctx, list, title) {
{
name: "Price",
title: `Realized Price ${title}`,
top: createRealizedPriceSeries(ctx, list),
top: createRealizedPriceSeries(list),
},
{
name: "Ratio",
@@ -326,7 +325,7 @@ function createGroupedRealizedSectionWithAdjusted(ctx, list, title) {
{
name: "capitalization",
title: `Realized Capitalization ${title}`,
bottom: createGroupedRealizedCapSeries(ctx, list),
bottom: createGroupedRealizedCapSeries(list),
},
...createGroupedRealizedPnlSections(ctx, list, title),
createGroupedSoprSectionWithAdjusted(ctx, list, title),
@@ -345,7 +344,7 @@ function createSingleRealizedSectionBasic(ctx, cohort, title) {
return {
name: "Realized",
tree: [
createSingleRealizedPriceChart(ctx, cohort, title),
createSingleRealizedPriceChart(cohort, title),
{
name: "capitalization",
title: `Realized Capitalization ${title}`,
@@ -371,7 +370,7 @@ function createGroupedRealizedSectionBasic(ctx, list, title) {
{
name: "Price",
title: `Realized Price ${title}`,
top: createRealizedPriceSeries(ctx, list),
top: createRealizedPriceSeries(list),
},
{
name: "Ratio",
@@ -381,7 +380,7 @@ function createGroupedRealizedSectionBasic(ctx, list, title) {
{
name: "capitalization",
title: `Realized Capitalization ${title}`,
bottom: createGroupedRealizedCapSeries(ctx, list),
bottom: createGroupedRealizedCapSeries(list),
},
...createGroupedRealizedPnlSections(ctx, list, title),
createGroupedSoprSectionBasic(ctx, list, title),
@@ -391,14 +390,12 @@ function createGroupedRealizedSectionBasic(ctx, list, title) {
/**
* Create realized price chart for single cohort
* @param {PartialContext} ctx
* @param {UtxoCohortObject} cohort
* @param {string} title
* @returns {PartialChartOption}
*/
function createSingleRealizedPriceChart(ctx, cohort, title) {
const { line } = ctx;
const { tree, color } = cohort;
function createSingleRealizedPriceChart(cohort, title) {
const { tree, color } = cohort;
return {
name: "price",
@@ -421,7 +418,7 @@ function createSingleRealizedPriceChart(ctx, cohort, title) {
* @returns {AnyFetchedSeriesBlueprint[]}
*/
function createSingleRealizedCapSeries(ctx, cohort) {
const { colors, line, baseline, createPriceLine } = ctx;
const { colors, createPriceLine } = ctx;
const { color, tree } = cohort;
return [
@@ -459,13 +456,10 @@ function createSingleRealizedCapSeries(ctx, cohort) {
/**
* Create realized cap series for grouped cohorts
* @param {PartialContext} ctx
* @param {readonly UtxoCohortObject[]} list
* @returns {AnyFetchedSeriesBlueprint[]}
*/
function createGroupedRealizedCapSeries(ctx, list) {
const { line } = ctx;
function createGroupedRealizedCapSeries(list) {
return list.map(({ color, name, tree }) =>
line({
metric: tree.realized.realizedCap,
@@ -486,8 +480,6 @@ function createGroupedRealizedCapSeries(ctx, list) {
function createSingleRealizedPnlSection(ctx, cohort, title) {
const {
colors,
line,
baseline,
createPriceLine,
fromBlockCountWithUnit,
fromBitcoinPatternWithUnit,
@@ -598,7 +590,7 @@ function createSingleRealizedPnlSection(ctx, cohort, title) {
* @returns {PartialOptionsTree}
*/
function createGroupedRealizedPnlSections(ctx, list, title) {
const { line, baseline, createPriceLine } = ctx;
const { createPriceLine } = ctx;
return [
{
@@ -795,7 +787,7 @@ function createGroupedRealizedPnlSections(ctx, list, title) {
* @returns {PartialChartOption}
*/
function createSingleBaseSoprChart(ctx, cohort, title) {
const { colors, baseline, createPriceLine } = ctx;
const { colors, createPriceLine } = ctx;
const { tree } = cohort;
return {
@@ -837,7 +829,7 @@ function createSingleBaseSoprChart(ctx, cohort, title) {
* @returns {PartialChartOption}
*/
function createSingleAdjustedSoprChart(ctx, cohort, title) {
const { colors, baseline, createPriceLine } = ctx;
const { colors, createPriceLine } = ctx;
const { tree } = cohort;
return {
@@ -880,7 +872,7 @@ function createSingleAdjustedSoprChart(ctx, cohort, title) {
* @returns {PartialChartOption}
*/
function createGroupedBaseSoprChart(ctx, list, title) {
const { baseline, createPriceLine } = ctx;
const { createPriceLine } = ctx;
return {
name: "Normal",
@@ -924,7 +916,7 @@ function createGroupedBaseSoprChart(ctx, list, title) {
* @returns {PartialChartOption}
*/
function createGroupedAdjustedSoprChart(ctx, list, title) {
const { baseline, createPriceLine } = ctx;
const { createPriceLine } = ctx;
return {
name: "Adjusted",
@@ -1035,7 +1027,7 @@ function createGroupedSoprSectionBasic(ctx, list, title) {
* @param {RelativeWithMarketCap} rel
*/
function createUnrealizedPnlRelToMarketCapMetrics(ctx, rel) {
const { colors, line } = ctx;
const { colors } = ctx;
return [
line({
metric: rel.unrealizedProfitRelToMarketCap,
@@ -1064,7 +1056,7 @@ function createUnrealizedPnlRelToMarketCapMetrics(ctx, rel) {
* @param {RelativeWithOwnMarketCap} rel
*/
function createUnrealizedPnlRelToOwnMarketCapMetrics(ctx, rel) {
const { colors, line, createPriceLine } = ctx;
const { colors, createPriceLine } = ctx;
return [
line({
metric: rel.unrealizedProfitRelToOwnMarketCap,
@@ -1095,7 +1087,7 @@ function createUnrealizedPnlRelToOwnMarketCapMetrics(ctx, rel) {
* @param {RelativeWithOwnPnl} rel
*/
function createUnrealizedPnlRelToOwnPnlMetrics(ctx, rel) {
const { colors, line, createPriceLine } = ctx;
const { colors, createPriceLine } = ctx;
return [
line({
metric: rel.unrealizedProfitRelToOwnTotalUnrealizedPnl,
@@ -1122,12 +1114,10 @@ function createUnrealizedPnlRelToOwnPnlMetrics(ctx, rel) {
}
/**
* @param {PartialContext} ctx
* @param {RelativeWithMarketCap} rel
*/
function createNetUnrealizedPnlRelToMarketCapMetrics(ctx, rel) {
const { baseline } = ctx;
return [
function createNetUnrealizedPnlRelToMarketCapMetrics(rel) {
return [
baseline({
metric: rel.netUnrealizedPnlRelToMarketCap,
name: "Net",
@@ -1141,7 +1131,7 @@ function createNetUnrealizedPnlRelToMarketCapMetrics(ctx, rel) {
* @param {RelativeWithOwnMarketCap} rel
*/
function createNetUnrealizedPnlRelToOwnMarketCapMetrics(ctx, rel) {
const { baseline, createPriceLine } = ctx;
const { createPriceLine } = ctx;
return [
baseline({
metric: rel.netUnrealizedPnlRelToOwnMarketCap,
@@ -1157,7 +1147,7 @@ function createNetUnrealizedPnlRelToOwnMarketCapMetrics(ctx, rel) {
* @param {RelativeWithOwnPnl} rel
*/
function createNetUnrealizedPnlRelToOwnPnlMetrics(ctx, rel) {
const { baseline, createPriceLine } = ctx;
const { createPriceLine } = ctx;
return [
baseline({
metric: rel.netUnrealizedPnlRelToOwnTotalUnrealizedPnl,
@@ -1174,7 +1164,7 @@ function createNetUnrealizedPnlRelToOwnPnlMetrics(ctx, rel) {
* @param {{ unrealized: { totalUnrealizedPnl: AnyMetricPattern, unrealizedProfit: AnyMetricPattern, unrealizedLoss: AnyMetricPattern, negUnrealizedLoss: AnyMetricPattern } }} tree
*/
function createUnrealizedPnlBaseMetrics(ctx, tree) {
const { colors, line } = ctx;
const { colors } = ctx;
return [
line({
metric: tree.unrealized.totalUnrealizedPnl,
@@ -1206,12 +1196,10 @@ function createUnrealizedPnlBaseMetrics(ctx, tree) {
/**
* Base net unrealized metric (always present)
* @param {PartialContext} ctx
* @param {{ unrealized: { netUnrealizedPnl: AnyMetricPattern } }} tree
*/
function createNetUnrealizedPnlBaseMetric(ctx, tree) {
const { baseline } = ctx;
return baseline({
function createNetUnrealizedPnlBaseMetric(tree) {
return baseline({
metric: tree.unrealized.netUnrealizedPnl,
name: "Net",
unit: Unit.ratio,
@@ -1249,7 +1237,7 @@ function createSingleUnrealizedSectionAll(ctx, cohort, title) {
name: "Net pnl",
title: `Net Unrealized Profit And Loss ${title}`,
bottom: [
createNetUnrealizedPnlBaseMetric(ctx, tree),
createNetUnrealizedPnlBaseMetric(tree),
...createNetUnrealizedPnlRelToOwnPnlMetrics(ctx, tree.relative),
createPriceLine({ unit: Unit.usd }),
],
@@ -1287,8 +1275,8 @@ function createSingleUnrealizedSectionFull(ctx, cohort, title) {
name: "Net pnl",
title: `Net Unrealized Profit And Loss ${title}`,
bottom: [
createNetUnrealizedPnlBaseMetric(ctx, tree),
...createNetUnrealizedPnlRelToMarketCapMetrics(ctx, tree.relative),
createNetUnrealizedPnlBaseMetric(tree),
...createNetUnrealizedPnlRelToMarketCapMetrics(tree.relative),
...createNetUnrealizedPnlRelToOwnMarketCapMetrics(ctx, tree.relative),
...createNetUnrealizedPnlRelToOwnPnlMetrics(ctx, tree.relative),
createPriceLine({ unit: Unit.usd }),
@@ -1326,8 +1314,8 @@ function createSingleUnrealizedSectionWithMarketCap(ctx, cohort, title) {
name: "Net pnl",
title: `Net Unrealized Profit And Loss ${title}`,
bottom: [
createNetUnrealizedPnlBaseMetric(ctx, tree),
...createNetUnrealizedPnlRelToMarketCapMetrics(ctx, tree.relative),
createNetUnrealizedPnlBaseMetric(tree),
...createNetUnrealizedPnlRelToMarketCapMetrics(tree.relative),
createPriceLine({ unit: Unit.usd }),
createPriceLine({ unit: Unit.pctMcap }),
],
@@ -1364,7 +1352,7 @@ function createSingleUnrealizedSectionWithOwnCaps(ctx, cohort, title) {
name: "Net pnl",
title: `Net Unrealized Profit And Loss ${title}`,
bottom: [
createNetUnrealizedPnlBaseMetric(ctx, tree),
createNetUnrealizedPnlBaseMetric(tree),
...createNetUnrealizedPnlRelToOwnMarketCapMetrics(ctx, tree.relative),
...createNetUnrealizedPnlRelToOwnPnlMetrics(ctx, tree.relative),
createPriceLine({ unit: Unit.usd }),
@@ -1400,7 +1388,7 @@ function createSingleUnrealizedSectionBase(ctx, cohort, title) {
name: "Net pnl",
title: `Net Unrealized Profit And Loss ${title}`,
bottom: [
createNetUnrealizedPnlBaseMetric(ctx, tree),
createNetUnrealizedPnlBaseMetric(tree),
createPriceLine({ unit: Unit.usd }),
],
},
@@ -1410,13 +1398,11 @@ function createSingleUnrealizedSectionBase(ctx, cohort, title) {
/**
* Grouped unrealized base charts (profit, loss, total pnl)
* @param {PartialContext} ctx
* @param {readonly { color: Color, name: string, tree: { unrealized: PatternAll["unrealized"] } }[]} list
* @param {string} title
*/
function createGroupedUnrealizedBaseCharts(ctx, list, title) {
const { line } = ctx;
return [
function createGroupedUnrealizedBaseCharts(list, title) {
return [
{
name: "profit",
title: `Unrealized Profit ${title}`,
@@ -1464,11 +1450,11 @@ function createGroupedUnrealizedBaseCharts(ctx, list, title) {
* @returns {PartialOptionsGroup}
*/
function createGroupedUnrealizedSectionFull(ctx, list, title) {
const { baseline, createPriceLine } = ctx;
const { createPriceLine } = ctx;
return {
name: "Unrealized",
tree: [
...createGroupedUnrealizedBaseCharts(ctx, list, title),
...createGroupedUnrealizedBaseCharts(list, title),
{
name: "Net pnl",
title: `Net Unrealized Profit And Loss ${title}`,
@@ -1517,11 +1503,11 @@ function createGroupedUnrealizedSectionFull(ctx, list, title) {
* @returns {PartialOptionsGroup}
*/
function createGroupedUnrealizedSectionWithMarketCap(ctx, list, title) {
const { baseline, createPriceLine } = ctx;
const { createPriceLine } = ctx;
return {
name: "Unrealized",
tree: [
...createGroupedUnrealizedBaseCharts(ctx, list, title),
...createGroupedUnrealizedBaseCharts(list, title),
{
name: "Net pnl",
title: `Net Unrealized Profit And Loss ${title}`,
@@ -1556,11 +1542,11 @@ function createGroupedUnrealizedSectionWithMarketCap(ctx, list, title) {
* @returns {PartialOptionsGroup}
*/
function createGroupedUnrealizedSectionWithOwnCaps(ctx, list, title) {
const { baseline, createPriceLine } = ctx;
const { createPriceLine } = ctx;
return {
name: "Unrealized",
tree: [
...createGroupedUnrealizedBaseCharts(ctx, list, title),
...createGroupedUnrealizedBaseCharts(list, title),
{
name: "Net pnl",
title: `Net Unrealized Profit And Loss ${title}`,
@@ -1602,11 +1588,11 @@ function createGroupedUnrealizedSectionWithOwnCaps(ctx, list, title) {
* @returns {PartialOptionsGroup}
*/
function createGroupedUnrealizedSectionBase(ctx, list, title) {
const { baseline, createPriceLine } = ctx;
const { createPriceLine } = ctx;
return {
name: "Unrealized",
tree: [
...createGroupedUnrealizedBaseCharts(ctx, list, title),
...createGroupedUnrealizedBaseCharts(list, title),
{
name: "Net pnl",
title: `Net Unrealized Profit And Loss ${title}`,
@@ -1628,14 +1614,12 @@ function createGroupedUnrealizedSectionBase(ctx, list, title) {
/**
* Create cost basis section for single cohort WITH percentiles
* @param {PartialContext} ctx
* @param {CohortAll | CohortFull | CohortWithPercentiles} cohort
* @param {string} title
* @returns {PartialOptionsGroup}
*/
function createSingleCostBasisSectionWithPercentiles(ctx, cohort, title) {
const { line } = ctx;
const { color, tree } = cohort;
function createSingleCostBasisSectionWithPercentiles(cohort, title) {
const { color, tree } = cohort;
return {
name: "Cost Basis",
@@ -1668,7 +1652,7 @@ function createSingleCostBasisSectionWithPercentiles(ctx, cohort, title) {
{
name: "percentiles",
title: `Cost Basis Percentiles ${title}`,
top: createCostBasisPercentilesSeries(ctx, [cohort], false),
top: createCostBasisPercentilesSeries( [cohort], false),
},
],
};
@@ -1676,14 +1660,11 @@ function createSingleCostBasisSectionWithPercentiles(ctx, cohort, title) {
/**
* Create cost basis section for grouped cohorts WITH percentiles
* @param {PartialContext} ctx
* @param {readonly (CohortFull | CohortWithPercentiles)[]} list
* @param {string} title
* @returns {PartialOptionsGroup}
*/
function createGroupedCostBasisSectionWithPercentiles(ctx, list, title) {
const { line } = ctx;
function createGroupedCostBasisSectionWithPercentiles(list, title) {
return {
name: "Cost Basis",
tree: [
@@ -1719,14 +1700,12 @@ function createGroupedCostBasisSectionWithPercentiles(ctx, list, title) {
/**
* Create cost basis section for single cohort (no percentiles)
* @param {PartialContext} ctx
* @param {CohortWithAdjusted | CohortBasic} cohort
* @param {string} title
* @returns {PartialOptionsGroup}
*/
function createSingleCostBasisSection(ctx, cohort, title) {
const { line } = ctx;
const { color, tree } = cohort;
function createSingleCostBasisSection(cohort, title) {
const { color, tree } = cohort;
return {
name: "Cost Basis",
@@ -1762,14 +1741,11 @@ function createSingleCostBasisSection(ctx, cohort, title) {
/**
* Create cost basis section for grouped cohorts (no percentiles)
* @param {PartialContext} ctx
* @param {readonly (CohortWithAdjusted | CohortBasic)[]} list
* @param {string} title
* @returns {PartialOptionsGroup}
*/
function createGroupedCostBasisSection(ctx, list, title) {
const { line } = ctx;
function createGroupedCostBasisSection(list, title) {
return {
name: "Cost Basis",
tree: [
@@ -1811,7 +1787,7 @@ function createGroupedCostBasisSection(ctx, list, title) {
* @returns {PartialOptionsTree}
*/
function createSingleActivitySectionWithAdjusted(ctx, cohort, title) {
const { colors, line } = ctx;
const { colors } = ctx;
const { tree, color } = cohort;
return [
@@ -1925,7 +1901,7 @@ function createSingleActivitySectionWithAdjusted(ctx, cohort, title) {
* @returns {PartialOptionsTree}
*/
function createSingleActivitySectionBasic(ctx, cohort, title) {
const { colors, line } = ctx;
const { colors } = ctx;
const { tree, color } = cohort;
return [
@@ -2021,14 +1997,11 @@ function createSingleActivitySectionBasic(ctx, cohort, title) {
/**
* Create activity section for grouped cohorts with adjusted values (for cohorts with RealizedPattern3/4)
* @param {PartialContext} ctx
* @param {readonly (CohortFull | CohortWithAdjusted)[]} list
* @param {string} title
* @returns {PartialOptionsTree}
*/
function createGroupedActivitySectionWithAdjusted(ctx, list, title) {
const { line } = ctx;
function createGroupedActivitySectionWithAdjusted(list, title) {
return [
{
name: "Sell Side Risk",
@@ -2151,14 +2124,11 @@ function createGroupedActivitySectionWithAdjusted(ctx, list, title) {
/**
* Create activity section for grouped cohorts without adjusted values (for cohorts with RealizedPattern/2)
* @param {PartialContext} ctx
* @param {readonly (CohortWithPercentiles | CohortBasic)[]} list
* @param {string} title
* @returns {PartialOptionsTree}
*/
function createGroupedActivitySectionBasic(ctx, list, title) {
const { line } = ctx;
function createGroupedActivitySectionBasic(list, title) {
return [
{
name: "Sell Side Risk",
+34 -13
View File
@@ -1,6 +1,7 @@
/** Cointime section builder - typed tree-based patterns */
import { Unit } from "../utils/units.js";
import { line, baseline } from "./series.js";
import {
satsBtcUsd,
priceLines,
@@ -25,7 +26,7 @@ function createCointimePriceWithRatioOptions(
ctx,
{ title, legend, price, ratio, color },
) {
const { line, colors, createPriceLine } = ctx;
const { colors, createPriceLine } = ctx;
const pctUsdMap = percentileUsdMap(colors, ratio);
const pctMap = percentileMap(colors, ratio);
@@ -54,42 +55,53 @@ function createCointimePriceWithRatioOptions(
),
],
bottom: [
line({ metric: ratio.ratio, name: "Ratio", color, unit: Unit.ratio }),
baseline({
metric: ratio.ratio,
name: "Ratio",
color,
unit: Unit.ratio,
}),
line({
metric: ratio.ratio1wSma,
name: "1w SMA",
color: colors.lime,
unit: Unit.ratio,
defaultActive: false,
}),
line({
metric: ratio.ratio1mSma,
name: "1m SMA",
color: colors.teal,
unit: Unit.ratio,
defaultActive: false,
}),
line({
metric: ratio.ratio1ySd.sma,
name: "1y SMA",
color: colors.sky,
unit: Unit.ratio,
defaultActive: false,
}),
line({
metric: ratio.ratio2ySd.sma,
name: "2y SMA",
color: colors.indigo,
unit: Unit.ratio,
defaultActive: false,
}),
line({
metric: ratio.ratio4ySd.sma,
name: "4y SMA",
color: colors.purple,
unit: Unit.ratio,
defaultActive: false,
}),
line({
metric: ratio.ratioSd.sma,
name: "All SMA",
color: colors.rose,
unit: Unit.ratio,
defaultActive: false,
}),
...pctMap.map(({ name: pctName, prop, color: pctColor }) =>
line({
@@ -174,13 +186,14 @@ function createCointimePriceWithRatioOptions(
...sdPats.map(({ nameAddon, titleAddon, sd }) => ({
name: nameAddon,
title: `${title} ${titleAddon} Z-Score`,
top: sdBands(colors, sd).map(({ name: bandName, prop, color: bandColor }) =>
line({
metric: prop,
name: bandName,
color: bandColor,
unit: Unit.usd,
}),
top: sdBands(colors, sd).map(
({ name: bandName, prop, color: bandColor }) =>
line({
metric: prop,
name: bandName,
color: bandColor,
unit: Unit.usd,
}),
),
bottom: [
line({ metric: sd.zscore, name: "Z-Score", color, unit: Unit.sd }),
@@ -198,7 +211,7 @@ function createCointimePriceWithRatioOptions(
* @returns {PartialOptionsGroup}
*/
export function createCointimeSection(ctx) {
const { colors, brk, line } = ctx;
const { colors, brk } = ctx;
const { cointime, distribution, supply } = brk.metrics;
const { pricing, cap, activity, supply: cointimeSupply, adjusted } = cointime;
const { all } = distribution.utxoCohorts;
@@ -348,9 +361,17 @@ export function createCointimeSection(ctx) {
name: "Supply",
title: "Cointime Supply",
bottom: [
...satsBtcUsd(ctx, all.supply.total, "All", colors.orange),
...satsBtcUsd(ctx, cointimeSupply.vaultedSupply, "Vaulted", colors.lime),
...satsBtcUsd(ctx, cointimeSupply.activeSupply, "Active", colors.rose),
...satsBtcUsd( all.supply.total, "All", colors.orange),
...satsBtcUsd(
cointimeSupply.vaultedSupply,
"Vaulted",
colors.lime,
),
...satsBtcUsd(
cointimeSupply.activeSupply,
"Active",
colors.rose,
),
],
},
+2 -13
View File
@@ -1,9 +1,4 @@
import {
line,
dots,
candlestick,
baseline,
histogram,
fromBlockCount,
fromBitcoin,
fromBlockSize,
@@ -22,27 +17,21 @@ import {
createPriceLines,
constantLine,
} from "./constants.js";
import { colors } from "../chart/colors.js";
/**
* Create a context object with all dependencies for building partial options
* @param {Object} args
* @param {Colors} args.colors
* @param {BrkClient} args.brk
* @returns {PartialContext}
*/
export function createContext({ colors, brk }) {
export function createContext({ brk }) {
const constants = brk.metrics.constants;
return {
colors,
brk,
// Series helpers
line,
dots,
candlestick,
baseline,
histogram,
fromBlockCount: (pattern, title, color) =>
fromBlockCount(colors, pattern, title, color),
fromBitcoin: (pattern, title, color) =>
+1 -3
View File
@@ -10,12 +10,11 @@ import { collect, markUsed, logUnused } from "./unused.js";
/**
* @param {Object} args
* @param {Colors} args.colors
* @param {Signals} args.signals
* @param {BrkClient} args.brk
* @param {Signal<string | null>} args.qrcode
*/
export function initOptions({ colors, signals, brk, qrcode }) {
export function initOptions({ signals, brk, qrcode }) {
collect(brk.metrics);
const LS_SELECTED_KEY = `selected_path`;
@@ -33,7 +32,6 @@ export function initOptions({ colors, signals, brk, qrcode }) {
const selected = signals.createSignal(/** @type {any} */ (undefined));
const partialOptions = createPartialOptions({
colors,
brk,
});
+24 -11
View File
@@ -1,6 +1,7 @@
/** Moving averages section */
import { Unit } from "../../utils/units.js";
import { line, baseline } from "../series.js";
import {
priceLines,
percentileUsdMap,
@@ -55,7 +56,7 @@ export function createPriceWithRatioOptions(
ctx,
{ title, legend, ratio, color },
) {
const { line, colors, createPriceLine } = ctx;
const { colors, createPriceLine } = ctx;
const priceMetric = ratio.price;
const pctUsdMap = percentileUsdMap(colors, ratio);
@@ -85,42 +86,54 @@ export function createPriceWithRatioOptions(
),
],
bottom: [
line({ metric: ratio.ratio, name: "Ratio", color, unit: Unit.ratio }),
baseline({
metric: ratio.ratio,
name: "Ratio",
base: 1,
color,
unit: Unit.ratio,
}),
line({
metric: ratio.ratio1wSma,
name: "1w SMA",
color: colors.lime,
unit: Unit.ratio,
defaultActive: false,
}),
line({
metric: ratio.ratio1mSma,
name: "1m SMA",
color: colors.teal,
unit: Unit.ratio,
defaultActive: false,
}),
line({
metric: ratio.ratio1ySd.sma,
name: "1y SMA",
color: colors.sky,
unit: Unit.ratio,
defaultActive: false,
}),
line({
metric: ratio.ratio2ySd.sma,
name: "2y SMA",
color: colors.indigo,
unit: Unit.ratio,
defaultActive: false,
}),
line({
metric: ratio.ratio4ySd.sma,
name: "4y SMA",
color: colors.purple,
unit: Unit.ratio,
defaultActive: false,
}),
line({
metric: ratio.ratioSd.sma,
name: "All SMA",
color: colors.rose,
unit: Unit.ratio,
defaultActive: false,
}),
...pctMap.map(({ name: pctName, prop, color: pctColor }) =>
line({
@@ -140,13 +153,14 @@ export function createPriceWithRatioOptions(
tree: sdPats.map(({ nameAddon, titleAddon, sd }) => ({
name: nameAddon,
title: `${title} ${titleAddon} Z-Score`,
top: sdBands(colors, sd).map(({ name: bandName, prop, color: bandColor }) =>
line({
metric: prop,
name: bandName,
color: bandColor,
unit: Unit.usd,
}),
top: sdBands(colors, sd).map(
({ name: bandName, prop, color: bandColor }) =>
line({
metric: prop,
name: bandName,
color: bandColor,
unit: Unit.usd,
}),
),
bottom: [
line({ metric: sd.zscore, name: "Z-Score", color, unit: Unit.sd }),
@@ -163,8 +177,7 @@ export function createPriceWithRatioOptions(
* @param {ReturnType<typeof buildAverages>} averages
*/
export function createAveragesSection(ctx, averages) {
const { line } = ctx;
return {
name: "Averages",
tree: [
+2 -1
View File
@@ -2,6 +2,7 @@
import { localhost } from "../../utils/env.js";
import { Unit } from "../../utils/units.js";
import { line } from "../series.js";
import { buildAverages, createAveragesSection } from "./averages.js";
import { createPerformanceSection } from "./performance.js";
import { createIndicatorsSection } from "./indicators/index.js";
@@ -13,7 +14,7 @@ import { createInvestingSection } from "./investing.js";
* @returns {PartialOptionsGroup}
*/
export function createMarketSection(ctx) {
const { colors, brk, line } = ctx;
const { colors, brk } = ctx;
const { market, supply, price } = brk.metrics;
const {
movingAverage,
@@ -1,6 +1,7 @@
/** Bands indicators (MinMax, Mayer Multiple) */
import { Unit } from "../../../utils/units.js";
import { line } from "../../series.js";
/**
* Create Bands section
@@ -10,7 +11,7 @@ import { Unit } from "../../../utils/units.js";
* @param {Market["movingAverage"]} args.movingAverage
*/
export function createBandsSection(ctx, { range, movingAverage }) {
const { line, colors } = ctx;
const { colors } = ctx;
return {
name: "Bands",
@@ -1,6 +1,7 @@
/** Momentum indicators (RSI, StochRSI, Stochastic, MACD) */
import { Unit } from "../../../utils/units.js";
import { line, histogram } from "../../series.js";
/**
* Create Momentum section
@@ -8,7 +9,7 @@ import { Unit } from "../../../utils/units.js";
* @param {Market["indicators"]} indicators
*/
export function createMomentumSection(ctx, indicators) {
const { line, histogram, colors, createPriceLine } = ctx;
const { colors, createPriceLine } = ctx;
return {
name: "Momentum",
@@ -1,6 +1,7 @@
/** On-chain indicators (Pi Cycle, Puell, NVT, Gini) */
import { Unit } from "../../../utils/units.js";
import { line } from "../../series.js";
/**
* Create On-chain section
@@ -10,7 +11,7 @@ import { Unit } from "../../../utils/units.js";
* @param {Market["movingAverage"]} args.movingAverage
*/
export function createOnchainSection(ctx, { indicators, movingAverage }) {
const { line, colors, createPriceLine } = ctx;
const { colors, createPriceLine } = ctx;
return {
name: "On-chain",
@@ -1,6 +1,7 @@
/** Volatility indicators (Index, True Range, Choppiness, Sharpe, Sortino) */
import { Unit } from "../../../utils/units.js";
import { line } from "../../series.js";
/**
* Create Volatility section
@@ -10,7 +11,7 @@ import { Unit } from "../../../utils/units.js";
* @param {Market["range"]} args.range
*/
export function createVolatilitySection(ctx, { volatility, range }) {
const { line, colors, createPriceLine } = ctx;
const { colors, createPriceLine } = ctx;
return {
name: "Volatility",
+6 -5
View File
@@ -1,6 +1,7 @@
/** Investing section (DCA) */
import { Unit } from "../../utils/units.js";
import { line, baseline } from "../series.js";
import { satsBtcUsd } from "../shared.js";
import { periodIdToName } from "./utils.js";
@@ -41,7 +42,7 @@ export function buildDcaClasses(colors, dca) {
* @param {Market["returns"]} args.returns
*/
export function createInvestingSection(ctx, { dca, lookback, returns }) {
const { line, baseline, colors, createPriceLine } = ctx;
const { colors, createPriceLine } = ctx;
const dcaClasses = buildDcaClasses(colors, dca);
return {
@@ -114,8 +115,8 @@ export function createInvestingSection(ctx, { dca, lookback, returns }) {
name: "Stack",
title: `${name} DCA vs Lump Sum Stack ($100/day)`,
bottom: [
...satsBtcUsd(ctx, dcaStack, "DCA", colors.green),
...satsBtcUsd(ctx, lumpSumStack, "Lump sum", colors.orange),
...satsBtcUsd( dcaStack, "DCA", colors.green),
...satsBtcUsd( lumpSumStack, "Lump sum", colors.orange),
],
},
],
@@ -164,7 +165,7 @@ export function createInvestingSection(ctx, { dca, lookback, returns }) {
title: "DCA Stack by Year ($100/day)",
bottom: dcaClasses.flatMap(
({ year, color, defaultActive, stack }) =>
satsBtcUsd(ctx, stack, `${year}`, color, { defaultActive }),
satsBtcUsd( stack, `${year}`, color, { defaultActive }),
),
},
],
@@ -200,7 +201,7 @@ export function createInvestingSection(ctx, { dca, lookback, returns }) {
{
name: "Stack",
title: `DCA Class ${year} Stack ($100/day)`,
bottom: satsBtcUsd(ctx, stack, "Stack", color),
bottom: satsBtcUsd( stack, "Stack", color),
},
],
})),
@@ -1,6 +1,7 @@
/** Performance section */
import { Unit } from "../../utils/units.js";
import { baseline } from "../series.js";
import { periodIdToName } from "./utils.js";
/**
@@ -9,7 +10,7 @@ import { periodIdToName } from "./utils.js";
* @param {Market["returns"]} returns
*/
export function createPerformanceSection(ctx, returns) {
const { colors, baseline, createPriceLine } = ctx;
const { colors, createPriceLine } = ctx;
return {
name: "Performance",
+3 -3
View File
@@ -13,6 +13,7 @@ import {
import { createMarketSection } from "./market/index.js";
import { createChainSection } from "./chain.js";
import { createCointimeSection } from "./cointime.js";
import { colors } from "../chart/colors.js";
// Re-export types for external consumers
export * from "./types.js";
@@ -20,13 +21,12 @@ export * from "./types.js";
/**
* Create partial options tree
* @param {Object} args
* @param {Colors} args.colors
* @param {BrkClient} args.brk
* @returns {PartialOptionsTree}
*/
export function createPartialOptions({ colors, brk }) {
export function createPartialOptions({ brk }) {
// Create context with all helpers
const ctx = createContext({ colors, brk });
const ctx = createContext({ brk });
// Build cohort data
const {
+9 -1
View File
@@ -85,6 +85,8 @@ export function candlestick({
* @param {Unit} args.unit
* @param {Color | [Color, Color]} [args.color]
* @param {boolean} [args.defaultActive]
* @param {boolean} [args.defaultActive]
* @param {number | undefined} [args.base]
* @param {BaselineSeriesPartialOptions} [args.options]
* @returns {FetchedBaselineSeriesBlueprint}
*/
@@ -94,6 +96,7 @@ export function baseline({
color,
defaultActive,
unit,
base,
options,
}) {
const isTuple = Array.isArray(color);
@@ -105,7 +108,12 @@ export function baseline({
colors: isTuple ? color : undefined,
unit,
defaultActive,
options,
options: {
baseValue: {
price: base,
},
...options,
},
};
}
+5 -5
View File
@@ -1,22 +1,22 @@
/** Shared helpers for options */
import { Unit } from "../utils/units.js";
import { line } from "./series.js";
/**
* Create sats/btc/usd line series from a pattern with .sats/.bitcoin/.dollars
* @param {PartialContext} ctx
* @param {{ sats: AnyMetricPattern, bitcoin: AnyMetricPattern, dollars: AnyMetricPattern }} pattern
* @param {string} name
* @param {Color} [color]
* @param {{ defaultActive?: boolean }} [options]
* @returns {FetchedLineSeriesBlueprint[]}
*/
export function satsBtcUsd(ctx, pattern, name, color, options) {
export function satsBtcUsd(pattern, name, color, options) {
const { defaultActive } = options || {};
return [
ctx.line({ metric: pattern.sats, name, color, unit: Unit.sats, defaultActive }),
ctx.line({ metric: pattern.bitcoin, name, color, unit: Unit.btc, defaultActive }),
ctx.line({ metric: pattern.dollars, name, color, unit: Unit.usd, defaultActive }),
line({ metric: pattern.sats, name, color, unit: Unit.sats, defaultActive }),
line({ metric: pattern.bitcoin, name, color, unit: Unit.btc, defaultActive }),
line({ metric: pattern.dollars, name, color, unit: Unit.usd, defaultActive }),
];
}
-5
View File
@@ -241,11 +241,6 @@
* @typedef {Object} PartialContext
* @property {Colors} colors
* @property {BrkClient} brk
* @property {LineSeriesFn} line
* @property {DotsSeriesFn} dots
* @property {CandlestickSeriesFn} candlestick
* @property {BaselineSeriesFn} baseline
* @property {HistogramSeriesFn} histogram
* @property {(pattern: BlockCountPattern<any>, title: string, color?: Color) => AnyFetchedSeriesBlueprint[]} fromBlockCount
* @property {(pattern: FullnessPattern<any>, title: string, color?: Color) => AnyFetchedSeriesBlueprint[]} fromBitcoin
* @property {(pattern: AnyStatsPattern, title: string, color?: Color) => AnyFetchedSeriesBlueprint[]} fromBlockSize
+8 -11
View File
@@ -19,14 +19,11 @@ import {
} from "../utils/format.js";
import { serdeDate, serdeOptDate, serdeOptNumber } from "../utils/serde.js";
import signals from "../signals.js";
import { createChartElement } from "../chart/index.js";
import { createChart } from "../chart/index.js";
import { resources } from "../resources.js";
import { colors } from "../chart/colors.js";
/**
* @param {Object} args
* @param {Colors} args.colors
*/
export function init({ colors }) {
export function init() {
/**
* @typedef {Object} Frequency
* @property {string} name
@@ -684,7 +681,7 @@ export function init({ colors }) {
/** @type {() => IndexName} */
const index = () => "dateindex";
createChartElement({
createChart({
index,
parent: resultsElement,
signals,
@@ -727,7 +724,7 @@ export function init({ colors }) {
],
});
createChartElement({
createChart({
index,
parent: resultsElement,
signals,
@@ -750,7 +747,7 @@ export function init({ colors }) {
],
});
createChartElement({
createChart({
index,
parent: resultsElement,
signals,
@@ -779,7 +776,7 @@ export function init({ colors }) {
],
});
createChartElement({
createChart({
index,
parent: resultsElement,
signals,
@@ -801,7 +798,7 @@ export function init({ colors }) {
],
});
createChartElement({
createChart({
index,
parent: resultsElement,
signals,
+454
View File
@@ -0,0 +1,454 @@
import {
createShadow,
createReactiveChoiceField,
createHeader,
} from "../utils/dom.js";
import { chartElement } from "../utils/elements.js";
import { serdeChartableIndex } from "../utils/serde.js";
import { Unit } from "../utils/units.js";
import signals from "../signals.js";
import { createChart } from "../chart/index.js";
import { webSockets } from "../utils/ws.js";
const keyPrefix = "chart";
const ONE_BTC_IN_SATS = 100_000_000;
/**
* @param {Object} args
* @param {Accessor<ChartOption>} args.option
* @param {BrkClient} args.brk
*/
export function init({ option, brk }) {
chartElement.append(createShadow("left"));
chartElement.append(createShadow("right"));
const { headerElement, headingElement } = createHeader();
chartElement.append(headerElement);
const chart = createChart({
parent: chartElement,
signals,
id: "charts",
brk,
captureElement: chartElement,
});
// Create index selector using chart's index state
const fieldset = createIndexSelector(option, chart);
chartElement.append(fieldset);
const unitChoices = /** @type {const} */ ([Unit.usd, Unit.sats]);
/** @type {Signal<Unit>} */
const topUnit = signals.createPersistedSignal({
defaultValue: /** @type {Unit} */ (Unit.usd),
storageKey: `${keyPrefix}-price`,
urlKey: "price",
serialize: (u) => u.id,
deserialize: (s) =>
/** @type {Unit} */ (unitChoices.find((u) => u.id === s) ?? Unit.usd),
});
const topUnitField = createReactiveChoiceField({
defaultValue: Unit.usd,
choices: unitChoices,
toKey: (u) => u.id,
toLabel: (u) => u.name,
selected: topUnit,
signals,
sorted: true,
type: "select",
});
chart.addFieldsetIfNeeded({
id: "charts-unit-0",
paneIndex: 0,
position: "nw",
createChild() {
return topUnitField;
},
});
const seriesListTop = /** @type {AnySeries[]} */ ([]);
const seriesListBottom = /** @type {AnySeries[]} */ ([]);
/**
* @param {Object} params
* @param {AnySeries} params.series
* @param {Unit} params.unit
* @param {IndexName} params.index
*/
function printLatest({ series, unit, index }) {
const _latest = webSockets.kraken1dCandle.latest();
if (!_latest) return;
const latest = { ..._latest };
if (unit === Unit.sats) {
latest.open = Math.floor(ONE_BTC_IN_SATS / latest.open);
latest.high = Math.floor(ONE_BTC_IN_SATS / latest.high);
latest.low = Math.floor(ONE_BTC_IN_SATS / latest.low);
latest.close = Math.floor(ONE_BTC_IN_SATS / latest.close);
}
const last_ = series.getData().at(-1);
if (!last_) return;
const last = { ...last_ };
if ("close" in last) {
last.close = latest.close;
}
if ("value" in last) {
last.value = latest.close;
}
const date = new Date(/** @type {number} */ (latest.time) * 1000);
switch (index) {
case "height":
case "difficultyepoch":
case "halvingepoch": {
if ("close" in last) {
last.low = Math.min(last.low, latest.close);
last.high = Math.max(last.high, latest.close);
}
series.update(last);
break;
}
default: {
if (index === "weekindex") {
date.setUTCDate(date.getUTCDate() - ((date.getUTCDay() + 6) % 7));
} else if (index === "monthindex") {
date.setUTCDate(1);
} else if (index === "quarterindex") {
const month = date.getUTCMonth();
date.setUTCMonth(month - (month % 3), 1);
} else if (index === "semesterindex") {
const month = date.getUTCMonth();
date.setUTCMonth(month - (month % 6), 1);
} else if (index === "yearindex") {
date.setUTCMonth(0, 1);
} else if (index === "decadeindex") {
date.setUTCFullYear(
Math.floor(date.getUTCFullYear() / 10) * 10,
0,
1,
);
} else if (index !== "dateindex") {
throw Error("Unsupported");
}
const time = date.valueOf() / 1000;
if (time === last.time) {
if ("close" in last) {
last.low = Math.min(last.low, latest.low);
last.high = Math.max(last.high, latest.high);
}
series.update(last);
} else {
last.time = time;
series.update(last);
}
}
}
}
signals.createScopedEffect(option, (option) => {
headingElement.innerHTML = option.title;
const bottomUnits = Array.from(option.bottom.keys());
/** @type {Signal<Unit> | undefined} */
let bottomUnit;
if (bottomUnits.length) {
// Storage key based on unit group (sorted unit IDs) so each group remembers its selection
const unitGroupKey = bottomUnits
.map((u) => u.id)
.sort()
.join("-");
bottomUnit = signals.createPersistedSignal({
defaultValue: bottomUnits[0],
storageKey: `${keyPrefix}-unit-${unitGroupKey}`,
urlKey: "unit",
serialize: (u) => u.id,
deserialize: (s) =>
bottomUnits.find((u) => u.id === s) ?? bottomUnits[0],
});
const field = createReactiveChoiceField({
defaultValue: bottomUnits[0],
choices: bottomUnits,
toKey: (u) => u.id,
toLabel: (u) => u.name,
selected: bottomUnit,
signals,
sorted: true,
type: "select",
});
chart.addFieldsetIfNeeded({
id: "charts-unit-1",
paneIndex: 1,
position: "nw",
createChild() {
return field;
},
});
} else {
// Clean up bottom pane when new option has no bottom series
seriesListBottom.forEach((series) => series.remove());
seriesListBottom.length = 0;
chart.legendBottom.removeFrom(0);
}
/**
* @param {Object} args
* @param {Map<Unit, AnyFetchedSeriesBlueprint[]>} args.blueprints
* @param {number} args.paneIndex
* @param {Unit} args.unit
* @param {IndexName} args.idx
* @param {AnySeries[]} args.seriesList
* @param {number} args.orderStart
* @param {Legend} args.legend
*/
function createSeriesFromBlueprints({
blueprints,
paneIndex,
unit,
idx,
seriesList,
orderStart,
legend,
}) {
legend.removeFrom(orderStart);
seriesList.splice(orderStart).forEach((series) => series.remove());
blueprints.get(unit)?.forEach((blueprint, order) => {
order += orderStart;
const options = blueprint.options;
const indexes = Object.keys(blueprint.metric.by);
if (indexes.includes(idx)) {
switch (blueprint.type) {
case "Baseline": {
seriesList.push(
chart.addBaselineSeries({
metric: blueprint.metric,
name: blueprint.title,
unit,
defaultActive: blueprint.defaultActive,
paneIndex,
options: {
...options,
topLineColor:
blueprint.color?.() ?? blueprint.colors?.[0](),
bottomLineColor:
blueprint.color?.() ?? blueprint.colors?.[1](),
},
order,
}),
);
break;
}
case "Histogram": {
seriesList.push(
chart.addHistogramSeries({
metric: blueprint.metric,
name: blueprint.title,
unit,
color: blueprint.color,
defaultActive: blueprint.defaultActive,
paneIndex,
options,
order,
}),
);
break;
}
case "Candlestick": {
seriesList.push(
chart.addCandlestickSeries({
metric: blueprint.metric,
name: blueprint.title,
unit,
colors: blueprint.colors,
defaultActive: blueprint.defaultActive,
paneIndex,
options,
order,
}),
);
break;
}
case "Dots": {
seriesList.push(
chart.addDotsSeries({
metric: blueprint.metric,
color: blueprint.color,
name: blueprint.title,
unit,
defaultActive: blueprint.defaultActive,
paneIndex,
options,
order,
}),
);
break;
}
case "Line":
case undefined:
seriesList.push(
chart.addLineSeries({
metric: blueprint.metric,
color: blueprint.color,
name: blueprint.title,
unit,
defaultActive: blueprint.defaultActive,
paneIndex,
options,
order,
}),
);
}
}
});
}
// Price series + top pane blueprints: combined effect on index + topUnit
signals.createScopedEffect(
() => ({ idx: chart.index(), unit: topUnit() }),
({ idx, unit }) => {
// Create price series
/** @type {AnySeries | undefined} */
let series;
switch (unit) {
case Unit.usd: {
series = chart.addCandlestickSeries({
metric: brk.metrics.price.usd.ohlc,
name: "Price",
unit,
order: 0,
});
break;
}
case Unit.sats: {
series = chart.addCandlestickSeries({
metric: brk.metrics.price.sats.ohlc,
name: "Price",
unit,
inverse: true,
order: 0,
});
break;
}
}
if (!series) throw Error("Unreachable");
seriesListTop[0]?.remove();
seriesListTop[0] = series;
// Live price update effect
signals.createEffect(
() => ({
latest: webSockets.kraken1dCandle.latest(),
hasData: series.hasData(),
}),
({ latest, hasData }) => {
if (!series || !latest || !hasData) return;
printLatest({ series, unit, index: idx });
},
);
// Top pane blueprint series
createSeriesFromBlueprints({
blueprints: option.top,
paneIndex: 0,
unit,
idx,
seriesList: seriesListTop,
orderStart: 1,
legend: chart.legendTop,
});
},
);
// Bottom pane blueprints: combined effect on index + bottomUnit
if (bottomUnit) {
signals.createScopedEffect(
() => ({ idx: chart.index(), unit: bottomUnit() }),
({ idx, unit }) => {
createSeriesFromBlueprints({
blueprints: option.bottom,
paneIndex: 1,
unit,
idx,
seriesList: seriesListBottom,
orderStart: 0,
legend: chart.legendBottom,
});
},
);
}
});
}
/**
* @param {Accessor<ChartOption>} option
* @param {Chart} chart
*/
function createIndexSelector(option, chart) {
const choices_ = /** @satisfies {ChartableIndexName[]} */ ([
"timestamp",
"date",
"week",
"month",
"quarter",
"semester",
"year",
"decade",
]);
/** @type {Accessor<typeof choices_>} */
const choices = signals.createMemo(() => {
const o = option();
if (!o.top.size && !o.bottom.size) {
return [...choices_];
}
const rawIndexes = new Set(
[Array.from(o.top.values()), Array.from(o.bottom.values())]
.flat(2)
.filter((blueprint) => {
const path = Object.values(blueprint.metric.by)[0]?.path ?? "";
return !path.includes("constant_");
})
.flatMap((blueprint) => blueprint.metric.indexes()),
);
return /** @type {any} */ (
choices_.filter((choice) =>
rawIndexes.has(serdeChartableIndex.deserialize(choice)),
)
);
});
/** @type {ChartableIndexName} */
const defaultIndex = "date";
const field = createReactiveChoiceField({
defaultValue: defaultIndex,
selected: chart.indexName,
choices,
id: "index",
signals,
});
const fieldset = window.document.createElement("fieldset");
fieldset.id = "interval";
const screenshotSpan = window.document.createElement("span");
screenshotSpan.innerText = "interval:";
fieldset.append(screenshotSpan);
fieldset.append(field);
fieldset.dataset.size = "sm";
return fieldset;
}
-518
View File
@@ -1,518 +0,0 @@
import {
createShadow,
createChoiceField,
createHeader,
} from "../../utils/dom.js";
import { chartElement } from "../../utils/elements.js";
import { ios, canShare } from "../../utils/env.js";
import { serdeChartableIndex } from "../../utils/serde.js";
import { Unit } from "../../utils/units.js";
import signals from "../../signals.js";
import { createChartElement } from "../../chart/index.js";
import { createChartState } from "../../chart/state.js";
import { webSockets } from "../../utils/ws.js";
import { screenshot } from "./screenshot.js";
import { debounce } from "../../utils/timing.js";
const keyPrefix = "chart";
const ONE_BTC_IN_SATS = 100_000_000;
/**
* @typedef {"timestamp" | "date" | "week" | "month" | "quarter" | "semester" | "year" | "decade" } ChartableIndexName
*/
/**
* @param {Object} args
* @param {Colors} args.colors
* @param {Accessor<ChartOption>} args.option
* @param {BrkClient} args.brk
*/
export function init({ colors, option, brk }) {
chartElement.append(createShadow("left"));
chartElement.append(createShadow("right"));
const { headerElement, headingElement } = createHeader();
chartElement.append(headerElement);
const state = createChartState(signals);
const { fieldset, index } = createIndexSelector(option, state);
const { from, to } = state.range();
const chart = createChartElement({
parent: chartElement,
signals,
colors,
id: "charts",
brk,
index,
initialVisibleBarsCount:
from !== null && to !== null ? to - from : null,
timeScaleSetCallback: (unknownTimeScaleCallback) => {
const { from, to } = state.range();
if (from !== null && to !== null) {
chart.setVisibleLogicalRange({ from, to });
} else {
unknownTimeScaleCallback();
}
},
});
if (!(ios && !canShare)) {
const domain = window.document.createElement("p");
domain.innerText = `${window.location.host}`;
domain.id = "domain";
chart.addFieldsetIfNeeded({
id: "capture",
paneIndex: 0,
position: "ne",
createChild() {
const button = window.document.createElement("button");
button.id = "capture";
button.innerText = "capture";
button.title = "Capture chart as image";
button.addEventListener("click", async () => {
chartElement.dataset.screenshot = "true";
chartElement.append(domain);
try {
await screenshot({
element: chartElement,
name: option().path.join("-"),
title: option().title,
});
} catch {}
chartElement.removeChild(domain);
chartElement.dataset.screenshot = "false";
});
return button;
},
});
}
// Sync chart → state.range on user pan/zoom
// Debounce to avoid rapid URL updates while panning
const debouncedSetRange = debounce(
(/** @type {{ from: number, to: number }} */ range) => state.setRange(range),
500,
);
chart.onVisibleLogicalRangeChange((t) => {
if (!t || t.from >= t.to) return;
debouncedSetRange({ from: t.from, to: t.to });
});
chartElement.append(fieldset);
const unitChoices = /** @type {const} */ ([Unit.usd, Unit.sats]);
/** @type {Signal<Unit>} */
const topUnit = signals.createPersistedSignal({
defaultValue: /** @type {Unit} */ (Unit.usd),
storageKey: `${keyPrefix}-price`,
urlKey: "price",
serialize: (u) => u.id,
deserialize: (s) => /** @type {Unit} */ (unitChoices.find((u) => u.id === s) ?? Unit.usd),
});
const topUnitField = createChoiceField({
defaultValue: Unit.usd,
choices: unitChoices,
toKey: (u) => u.id,
toLabel: (u) => u.name,
selected: topUnit,
signals,
sorted: true,
type: "select",
});
chart.addFieldsetIfNeeded({
id: "charts-unit-0",
paneIndex: 0,
position: "nw",
createChild() {
return topUnitField;
},
});
const seriesListTop = /** @type {AnySeries[]} */ ([]);
const seriesListBottom = /** @type {AnySeries[]} */ ([]);
/**
* @param {Object} params
* @param {AnySeries} params.series
* @param {Unit} params.unit
* @param {IndexName} params.index
*/
function printLatest({ series, unit, index }) {
const _latest = webSockets.kraken1dCandle.latest();
if (!_latest) return;
const latest = { ..._latest };
if (unit === Unit.sats) {
latest.open = Math.floor(ONE_BTC_IN_SATS / latest.open);
latest.high = Math.floor(ONE_BTC_IN_SATS / latest.high);
latest.low = Math.floor(ONE_BTC_IN_SATS / latest.low);
latest.close = Math.floor(ONE_BTC_IN_SATS / latest.close);
}
const last_ = series.getData().at(-1);
if (!last_) return;
const last = { ...last_ };
if ("close" in last) {
last.close = latest.close;
}
if ("value" in last) {
last.value = latest.close;
}
const date = new Date(/** @type {number} */ (latest.time) * 1000);
switch (index) {
case "height":
case "difficultyepoch":
case "halvingepoch": {
if ("close" in last) {
last.low = Math.min(last.low, latest.close);
last.high = Math.max(last.high, latest.close);
}
series.update(last);
break;
}
default: {
if (index === "weekindex") {
date.setUTCDate(date.getUTCDate() - ((date.getUTCDay() + 6) % 7));
} else if (index === "monthindex") {
date.setUTCDate(1);
} else if (index === "quarterindex") {
const month = date.getUTCMonth();
date.setUTCMonth(month - (month % 3), 1);
} else if (index === "semesterindex") {
const month = date.getUTCMonth();
date.setUTCMonth(month - (month % 6), 1);
} else if (index === "yearindex") {
date.setUTCMonth(0, 1);
} else if (index === "decadeindex") {
date.setUTCFullYear(
Math.floor(date.getUTCFullYear() / 10) * 10,
0,
1,
);
} else if (index !== "dateindex") {
throw Error("Unsupported");
}
const time = date.valueOf() / 1000;
if (time === last.time) {
if ("close" in last) {
last.low = Math.min(last.low, latest.low);
last.high = Math.max(last.high, latest.high);
}
series.update(last);
} else {
last.time = time;
series.update(last);
}
}
}
}
signals.createScopedEffect(option, (option) => {
headingElement.innerHTML = option.title;
const bottomUnits = Array.from(option.bottom.keys());
/** @type {{ field: HTMLDivElement, selected: Signal<Unit> } | undefined} */
let bottomUnitSelector;
if (bottomUnits.length) {
const selected = signals.createPersistedSignal({
defaultValue: bottomUnits[0],
storageKey: `${keyPrefix}-unit`,
urlKey: "unit",
serialize: (u) => u.id,
deserialize: (s) => bottomUnits.find((u) => u.id === s) ?? bottomUnits[0],
});
const field = createChoiceField({
defaultValue: bottomUnits[0],
choices: bottomUnits,
toKey: (u) => u.id,
toLabel: (u) => u.name,
selected,
signals,
sorted: true,
type: "select",
});
bottomUnitSelector = { field, selected };
chart.addFieldsetIfNeeded({
id: "charts-unit-1",
paneIndex: 1,
position: "nw",
createChild() {
return field;
},
});
} else {
// Clean up bottom pane when new option has no bottom series
seriesListBottom.forEach((series) => series.remove());
seriesListBottom.length = 0;
chart.legendBottom.removeFrom(0);
}
signals.createScopedEffect(index, (index) => {
signals.createScopedEffect(topUnit, (topUnit) => {
/** @type {AnySeries | undefined} */
let series;
switch (topUnit) {
case Unit.usd: {
series = chart.addCandlestickSeries({
metric: brk.metrics.price.usd.ohlc,
name: "Price",
unit: topUnit,
order: 0,
});
break;
}
case Unit.sats: {
series = chart.addCandlestickSeries({
metric: brk.metrics.price.sats.ohlc,
name: "Price",
unit: topUnit,
inverse: true,
order: 0,
});
break;
}
}
if (!series) throw Error("Unreachable");
seriesListTop[0]?.remove();
seriesListTop[0] = series;
signals.createEffect(
() => ({
latest: webSockets.kraken1dCandle.latest(),
hasData: series.hasData(),
}),
({ latest, hasData }) => {
if (!series || !latest || !hasData) return;
printLatest({ series, unit: topUnit, index });
},
);
});
/**
* @param {Object} args
* @param {Map<Unit, AnyFetchedSeriesBlueprint[]>} args.blueprints
* @param {number} args.paneIndex
* @param {Accessor<Unit>} args.unit
* @param {AnySeries[]} args.seriesList
* @param {number} args.orderStart
* @param {Legend} args.legend
*/
function processPane({
blueprints,
paneIndex,
unit,
seriesList,
orderStart,
legend,
}) {
signals.createScopedEffect(unit, (unit) => {
legend.removeFrom(orderStart);
seriesList.splice(orderStart).forEach((series) => {
series.remove();
});
blueprints.get(unit)?.forEach((blueprint, order) => {
order += orderStart;
const options = blueprint.options;
// Tree-first: metric is now an accessor with .by property
const indexes = Object.keys(blueprint.metric.by);
if (indexes.includes(index)) {
switch (blueprint.type) {
case "Baseline": {
seriesList.push(
chart.addBaselineSeries({
metric: blueprint.metric,
name: blueprint.title,
unit,
defaultActive: blueprint.defaultActive,
paneIndex,
options: {
...options,
topLineColor:
blueprint.color?.() ?? blueprint.colors?.[0](),
bottomLineColor:
blueprint.color?.() ?? blueprint.colors?.[1](),
},
order,
}),
);
break;
}
case "Histogram": {
seriesList.push(
chart.addHistogramSeries({
metric: blueprint.metric,
name: blueprint.title,
unit,
color: blueprint.color,
defaultActive: blueprint.defaultActive,
paneIndex,
options,
order,
}),
);
break;
}
case "Candlestick": {
seriesList.push(
chart.addCandlestickSeries({
metric: blueprint.metric,
name: blueprint.title,
unit,
colors: blueprint.colors,
defaultActive: blueprint.defaultActive,
paneIndex,
options,
order,
}),
);
break;
}
case "Dots": {
seriesList.push(
chart.addDotsSeries({
metric: blueprint.metric,
color: blueprint.color,
name: blueprint.title,
unit,
defaultActive: blueprint.defaultActive,
paneIndex,
options,
order,
}),
);
break;
}
case "Line":
case undefined:
seriesList.push(
chart.addLineSeries({
metric: blueprint.metric,
color: blueprint.color,
name: blueprint.title,
unit,
defaultActive: blueprint.defaultActive,
paneIndex,
options,
order,
}),
);
}
}
});
});
}
processPane({
blueprints: option.top,
paneIndex: 0,
unit: topUnit,
seriesList: seriesListTop,
orderStart: 1,
legend: chart.legendTop,
});
if (bottomUnitSelector) {
processPane({
blueprints: option.bottom,
paneIndex: 1,
unit: bottomUnitSelector.selected,
seriesList: seriesListBottom,
orderStart: 0,
legend: chart.legendBottom,
});
}
});
});
}
/**
* @param {Accessor<ChartOption>} option
* @param {ReturnType<typeof createChartState>} state
*/
function createIndexSelector(option, state) {
const choices_ = /** @satisfies {ChartableIndexName[]} */ ([
"timestamp",
"date",
"week",
"month",
"quarter",
"semester",
"year",
"decade",
]);
/** @type {Accessor<typeof choices_>} */
const choices = signals.createMemo(() => {
const o = option();
if (!o.top.size && !o.bottom.size) {
return [...choices_];
}
const rawIndexes = new Set(
[Array.from(o.top.values()), Array.from(o.bottom.values())]
.flat(2)
.filter((blueprint) => {
const path = Object.values(blueprint.metric.by)[0]?.path ?? "";
return !path.includes("constant_");
})
.flatMap((blueprint) => blueprint.metric.indexes()),
);
const serializedIndexes = [...rawIndexes].flatMap((index) => {
const c = serdeChartableIndex.serialize(index);
return c ? [c] : [];
});
return /** @type {any} */ (
choices_.filter((choice) => serializedIndexes.includes(choice))
);
});
/** @type {ChartableIndexName} */
const defaultIndex = "date";
const field = createChoiceField({
defaultValue: defaultIndex,
selected: state.index,
choices,
id: "index",
signals,
});
const fieldset = window.document.createElement("fieldset");
fieldset.id = "interval";
const screenshotSpan = window.document.createElement("span");
screenshotSpan.innerText = "interval:";
fieldset.append(screenshotSpan);
fieldset.append(field);
fieldset.dataset.size = "sm";
// Convert short name to internal name
const index = signals.createMemo(() =>
serdeChartableIndex.deserialize(state.index()),
);
return { fieldset, index };
}
+2 -2
View File
@@ -87,7 +87,7 @@ function useMetricEndpoint(endpoint) {
* @param {number} [to]
* @returns {RangeState<T>}
*/
function range(from, to) {
function range(from = -10000, to) {
const key = `${from}-${to ?? ""}`;
const existing = ranges.get(key);
if (existing) return existing;
@@ -111,7 +111,7 @@ function useMetricEndpoint(endpoint) {
* @param {number} [start=-10000]
* @param {number} [end]
*/
async fetch(start, end) {
async fetch(start = -10000, end) {
const r = range(start, end);
r.loading.set(true);
try {
+19 -36
View File
@@ -23,11 +23,9 @@ import {
runWithOwner,
onCleanup,
} from "./modules/solidjs-signals/0.6.3/dist/prod.js";
import { debounce } from "./utils/timing.js";
import { writeParam, readParam } from "./utils/url.js";
import { readStored, writeToStorage } from "./utils/storage.js";
import { createPersistedValue } from "./utils/persisted.js";
let effectCount = 0;
// let effectCount = 0;
const signals = {
createSolidSignal: /** @type {typeof CreateSignal} */ (createSignal),
@@ -45,13 +43,13 @@ const signals = {
if (dispose) {
dispose();
dispose = null;
console.log("effectCount = ", --effectCount);
// console.log("effectCount = ", --effectCount);
}
}
// @ts-ignore
createEffect(compute, (v, oldV) => {
console.log("effectCount = ", ++effectCount);
// console.log("effectCount = ", ++effectCount);
cleanup();
signals.createRoot((_dispose) => {
dispose = _dispose;
@@ -74,7 +72,10 @@ const signals = {
* @returns {Signal<T>}
*/
createSignal(initialValue, options) {
const [get, set] = this.createSolidSignal(/** @type {any} */ (initialValue), options);
const [get, set] = this.createSolidSignal(
/** @type {any} */ (initialValue),
options,
);
// @ts-ignore
get.set = set;
@@ -104,42 +105,24 @@ const signals = {
deserialize,
saveDefaultValue = false,
}) {
const defaultSerialized = serialize(defaultValue);
const persisted = createPersistedValue({
defaultValue,
storageKey,
urlKey,
serialize,
deserialize,
saveDefaultValue,
});
// Read: URL > localStorage > default
let serialized = urlKey ? readParam(urlKey) : null;
if (serialized === null) {
serialized = readStored(storageKey);
}
const initialValue = serialized !== null ? deserialize(serialized) : defaultValue;
const signal = this.createSignal(initialValue);
/** @param {T} value */
const write = (value) => {
const s = serialize(value);
const isDefault = s === defaultSerialized;
if (!isDefault || saveDefaultValue) {
writeToStorage(storageKey, s);
} else {
writeToStorage(storageKey, null);
}
if (urlKey) {
writeParam(urlKey, !isDefault || saveDefaultValue ? s : null);
}
};
const debouncedWrite = debounce(write, 250);
const signal = this.createSignal(persisted.value);
// Sync signal changes to persisted storage
let firstRun = true;
this.createEffect(signal, (value) => {
if (firstRun) {
write(value);
firstRun = false;
} else {
debouncedWrite(value);
persisted.set(value);
}
});
-83
View File
@@ -1,83 +0,0 @@
/**
* Reduce color opacity to 50% for dimming effect
* @param {string} color - oklch color string
*/
export function tameColor(color) {
if (color === "transparent") return color;
return `${color.slice(0, -1)} / 50%)`;
}
/**
* @typedef {Object} ColorMethods
* @property {() => string} tame - Returns tamed (50% opacity) version
* @property {(highlighted: boolean) => string} highlight - Returns normal if highlighted, tamed otherwise
*/
/**
* @typedef {(() => string) & ColorMethods} Color
*/
/**
* Creates a Color object that is callable and has utility methods
* @param {() => string} getter
* @returns {Color}
*/
function createColor(getter) {
const color = /** @type {Color} */ (() => getter());
color.tame = () => tameColor(getter());
color.highlight = (highlighted) => highlighted ? getter() : tameColor(getter());
return color;
}
/**
* @param {Accessor<boolean>} dark
*/
export function createColors(dark) {
const globalComputedStyle = getComputedStyle(window.document.documentElement);
/**
* @param {string} name
*/
function getColor(name) {
return globalComputedStyle.getPropertyValue(`--${name}`);
}
/**
* @param {string} property
*/
function getLightDarkValue(property) {
const value = globalComputedStyle.getPropertyValue(property);
const [light, _dark] = value.slice(11, -1).split(", ");
return dark() ? _dark : light;
}
return {
default: createColor(() => getLightDarkValue("--color")),
gray: createColor(() => getColor("gray")),
border: createColor(() => getLightDarkValue("--border-color")),
red: createColor(() => getColor("red")),
orange: createColor(() => getColor("orange")),
amber: createColor(() => getColor("amber")),
yellow: createColor(() => getColor("yellow")),
avocado: createColor(() => getColor("avocado")),
lime: createColor(() => getColor("lime")),
green: createColor(() => getColor("green")),
emerald: createColor(() => getColor("emerald")),
teal: createColor(() => getColor("teal")),
cyan: createColor(() => getColor("cyan")),
sky: createColor(() => getColor("sky")),
blue: createColor(() => getColor("blue")),
indigo: createColor(() => getColor("indigo")),
violet: createColor(() => getColor("violet")),
purple: createColor(() => getColor("purple")),
fuchsia: createColor(() => getColor("fuchsia")),
pink: createColor(() => getColor("pink")),
rose: createColor(() => getColor("rose")),
};
}
/**
* @typedef {ReturnType<typeof createColors>} Colors
* @typedef {keyof Colors} ColorName
*/
+82 -2
View File
@@ -225,7 +225,7 @@ export function importStyle(href) {
* @param {(choice: T) => string} [args.toLabel] - Extract display label (defaults to identity for strings)
* @param {"radio" | "select"} [args.type] - Render as radio buttons or select dropdown
*/
export function createChoiceField({
export function createReactiveChoiceField({
id,
choices: unsortedChoices,
defaultValue,
@@ -257,7 +257,8 @@ export function createChoiceField({
});
/** @param {string} key */
const fromKey = (key) => choices().find((c) => toKey(c) === key) ?? defaultValue;
const fromKey = (key) =>
choices().find((c) => toKey(c) === key) ?? defaultValue;
const field = window.document.createElement("div");
field.classList.add("field");
@@ -354,6 +355,85 @@ export function createChoiceField({
return field;
}
/**
* @template T
* @param {Object} args
* @param {T} args.initialValue
* @param {string} [args.id]
* @param {readonly T[]} args.choices
* @param {(value: T) => void} [args.onChange]
* @param {(choice: T) => string} [args.toKey]
* @param {(choice: T) => string} [args.toLabel]
* @param {"radio" | "select"} [args.type]
*/
export function createChoiceField({
id,
choices,
initialValue,
onChange,
toKey = /** @type {(choice: T) => string} */ ((/** @type {any} */ c) => c),
toLabel = /** @type {(choice: T) => string} */ ((/** @type {any} */ c) => c),
type = "radio",
}) {
const field = window.document.createElement("div");
field.classList.add("field");
const div = window.document.createElement("div");
field.append(div);
const initialKey = toKey(initialValue);
/** @param {string} key */
const fromKey = (key) =>
choices.find((c) => toKey(c) === key) ?? initialValue;
if (type === "select") {
const select = window.document.createElement("select");
select.id = id ?? "";
select.name = id ?? "";
choices.forEach((choice) => {
const option = window.document.createElement("option");
option.value = toKey(choice);
option.textContent = toLabel(choice);
if (toKey(choice) === initialKey) {
option.selected = true;
}
select.append(option);
});
select.addEventListener("change", () => {
onChange?.(fromKey(select.value));
});
div.append(select);
} else {
const fieldId = id ?? "";
choices.forEach((choice) => {
const choiceKey = toKey(choice);
const choiceLabel = toLabel(choice);
const { label } = createLabeledInput({
inputId: `${fieldId}-${choiceKey.toLowerCase()}`,
inputName: fieldId,
inputValue: choiceKey,
inputChecked: choiceKey === initialKey,
type: "radio",
});
const text = window.document.createTextNode(choiceLabel);
label.append(text);
div.append(label);
});
field.addEventListener("change", (event) => {
// @ts-ignore
onChange?.(fromKey(event.target.value));
});
}
return field;
}
/**
* @param {string} [title]
* @param {1 | 2 | 3} [level]
+72
View File
@@ -0,0 +1,72 @@
import { readParam, writeParam } from "./url.js";
import { readStored, writeToStorage } from "./storage.js";
import { debounce } from "./timing.js";
/**
* @template T
* @param {Object} args
* @param {T} args.defaultValue
* @param {string} [args.storageKey]
* @param {string} [args.urlKey]
* @param {(v: T) => string} args.serialize
* @param {(s: string) => T} args.deserialize
* @param {boolean} [args.saveDefaultValue]
*/
export function createPersistedValue({
defaultValue,
storageKey,
urlKey,
serialize,
deserialize,
saveDefaultValue = false,
}) {
const defaultSerialized = serialize(defaultValue);
// Read: URL > localStorage > default
let serialized = urlKey ? readParam(urlKey) : null;
if (serialized === null && storageKey) {
serialized = readStored(storageKey);
}
let value = serialized !== null ? deserialize(serialized) : defaultValue;
/** @param {T} v */
const write = (v) => {
const s = serialize(v);
const isDefault = s === defaultSerialized;
if (storageKey) {
if (!isDefault || saveDefaultValue) {
writeToStorage(storageKey, s);
} else {
writeToStorage(storageKey, null);
}
}
if (urlKey) {
writeParam(urlKey, !isDefault || saveDefaultValue ? s : null);
}
};
const debouncedWrite = debounce(write, 250);
// Write initial value
write(value);
return {
get value() {
return value;
},
/** @param {T} v */
set(v) {
value = v;
debouncedWrite(v);
},
/** @param {T} v */
setImmediate(v) {
value = v;
write(v);
},
};
}
/** @typedef {ReturnType<typeof createPersistedValue>} PersistedValue */
+4
View File
@@ -110,6 +110,10 @@ export const serdeBool = {
},
};
/**
* @typedef {"timestamp" | "date" | "week" | "month" | "quarter" | "semester" | "year" | "decade"} ChartableIndexName
*/
export const serdeChartableIndex = {
/**
* @param {IndexName} v
+50
View File
@@ -0,0 +1,50 @@
import { readStored, removeStored, writeToStorage } from "./storage.js";
const preferredColorSchemeMatchMedia = window.matchMedia(
"(prefers-color-scheme: dark)",
);
const stored = readStored("theme");
const initial = stored ? stored === "dark" : preferredColorSchemeMatchMedia.matches;
export let dark = initial;
/** @type {Set<() => void>} */
const callbacks = new Set();
/** @param {() => void} callback */
export function onChange(callback) {
callbacks.add(callback);
return () => callbacks.delete(callback);
}
/** @param {boolean} value */
export function setDark(value) {
if (dark === value) return;
dark = value;
apply(value);
callbacks.forEach((cb) => cb());
}
/** @param {boolean} isDark */
function apply(isDark) {
document.documentElement.style.colorScheme = isDark ? "dark" : "light";
}
apply(initial);
preferredColorSchemeMatchMedia.addEventListener("change", ({ matches }) => {
if (!readStored("theme")) {
setDark(matches);
}
});
function invert() {
const newValue = !dark;
setDark(newValue);
if (newValue === preferredColorSchemeMatchMedia.matches) {
removeStored("theme");
} else {
writeToStorage("theme", newValue ? "dark" : "light");
}
}
document.getElementById("invert-button")?.addEventListener("click", invert);
+12 -8
View File
@@ -22,18 +22,22 @@ export function throttle(callback, wait = 1000) {
let timeoutId = null;
/** @type {Parameters<F>} */
let latestArgs;
let hasTrailing = false;
return (/** @type {Parameters<F>} */ ...args) => {
latestArgs = args;
if (!timeoutId) {
// Otherwise it optimizes away timeoutId in Chrome and FF
timeoutId = timeoutId;
timeoutId = setTimeout(() => {
callback(...latestArgs); // Execute with latest args
timeoutId = null;
}, wait);
if (timeoutId) {
hasTrailing = true;
return;
}
callback(...latestArgs);
timeoutId = setTimeout(() => {
timeoutId = null;
if (hasTrailing) {
hasTrailing = false;
callback(...latestArgs);
}
}, wait);
};
}