mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-06-08 14:11:56 -07:00
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:
@@ -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"
|
||||
|
||||
@@ -22,6 +22,7 @@ _*
|
||||
/filter_*
|
||||
/heatmaps*
|
||||
/oracle*
|
||||
/playground
|
||||
|
||||
# Logs
|
||||
*.log*
|
||||
|
||||
Generated
+7
-106
@@ -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",
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -32,6 +32,5 @@ vecdb = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
brk_alloc = { workspace = true }
|
||||
plotters = "0.3"
|
||||
brk_bencher = { workspace = true }
|
||||
color-eyre = { workspace = true }
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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
@@ -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'),
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
@@ -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
|
||||
*/
|
||||
+616
-406
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
@@ -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
@@ -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})`;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 [
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
},
|
||||
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -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,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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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 }),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
@@ -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]
|
||||
|
||||
@@ -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 */
|
||||
@@ -110,6 +110,10 @@ export const serdeBool = {
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @typedef {"timestamp" | "date" | "week" | "month" | "quarter" | "semester" | "year" | "decade"} ChartableIndexName
|
||||
*/
|
||||
|
||||
export const serdeChartableIndex = {
|
||||
/**
|
||||
* @param {IndexName} v
|
||||
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user