website: snapshot

This commit is contained in:
nym21
2026-02-03 23:43:52 +01:00
parent 277a0eb6a7
commit 0d5d7da70f
44 changed files with 2999 additions and 1591 deletions

View File

@@ -2,7 +2,7 @@ name: Check outdated dependencies
on:
schedule:
- cron: '0 9 * * *'
- cron: "0 9 * * 1"
workflow_dispatch:
jobs:

32
Cargo.lock generated
View File

@@ -758,9 +758,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
[[package]]
name = "bytes"
version = "1.11.0"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
[[package]]
name = "byteview"
@@ -1241,9 +1241,9 @@ dependencies = [
[[package]]
name = "flate2"
version = "1.1.8"
version = "1.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b375d6465b98090a5f25b1c7703f3859783755aa9a80433b36e0379a3ec2f369"
checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
dependencies = [
"crc32fast",
"miniz_oxide",
@@ -2434,9 +2434,9 @@ dependencies = [
[[package]]
name = "rawdb"
version = "0.6.5"
version = "0.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89f7fffcea393bd56abe3f7f691b88d5cb321e0b46b9caf1064bbcf2ec22180b"
checksum = "be3e770fd2d36882cf78a62f211de8342fe8619801e8d39f4273ee87837e4bae"
dependencies = [
"libc",
"log",
@@ -2509,9 +2509,9 @@ dependencies = [
[[package]]
name = "regex"
version = "1.12.2"
version = "1.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4"
checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
dependencies = [
"aho-corasick",
"memchr",
@@ -2521,9 +2521,9 @@ dependencies = [
[[package]]
name = "regex-automata"
version = "0.4.13"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c"
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
dependencies = [
"aho-corasick",
"memchr",
@@ -2532,9 +2532,9 @@ dependencies = [
[[package]]
name = "regex-syntax"
version = "0.8.8"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c"
[[package]]
name = "ring"
@@ -3254,9 +3254,9 @@ checksum = "8f54a172d0620933a27a4360d3db3e2ae0dd6cceae9730751a036bbf182c4b23"
[[package]]
name = "vecdb"
version = "0.6.5"
version = "0.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92a5c013b67f59b479557f9ee47b365e3802b33348b5000c9f21381b5249669d"
checksum = "e212dbc82f5651d33e30b677df3ff789f2d903b917af482078487872cac76d89"
dependencies = [
"ctrlc",
"log",
@@ -3275,9 +3275,9 @@ dependencies = [
[[package]]
name = "vecdb_derive"
version = "0.6.5"
version = "0.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d90acf14f49129f0f205bc45331e2608dcb09c55d218e7fdbfdbf4b7c31b9433"
checksum = "704e0810d01c394ff35112f861c4889ec155279a7f0a55cb37e2a33682343dd4"
dependencies = [
"quote",
"syn",

View File

@@ -83,7 +83,7 @@ tokio = { version = "1.49.0", features = ["rt-multi-thread"] }
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.5", features = ["derive", "serde_json", "pco", "schemars"] }
vecdb = { version = "0.6.7", features = ["derive", "serde_json", "pco", "schemars"] }
# vecdb = { path = "../anydb/crates/vecdb", features = ["derive", "serde_json", "pco", "schemars"] }
[workspace.metadata.release]

View File

@@ -2025,32 +2025,62 @@ impl<T: DeserializeOwned> AverageCumulativeMaxMedianMinPct10Pct25Pct75Pct90SumPa
}
}
/// Pattern struct for repeated tree structure.
pub struct ActivityAddrCostOutputsRealizedRelativeSupplyUnrealizedPattern {
pub activity: CoinblocksCoindaysSatblocksSatdaysSentPattern,
pub addr_count: MetricPattern1<StoredU64>,
pub addr_count_30d_change: MetricPattern4<StoredF64>,
pub cost_basis: MaxMinPattern,
pub outputs: UtxoPattern,
pub realized: CapCapitulationInvestorLossMvrvNegNetPeakProfitRealizedSellSentSoprTotalValuePattern,
pub relative: InvestedNegNetNuplSupplyUnrealizedPattern,
pub supply: _30dHalvedTotalPattern,
pub unrealized: GreedInvestedInvestorNegNetPainSupplyTotalUnrealizedPattern,
}
impl ActivityAddrCostOutputsRealizedRelativeSupplyUnrealizedPattern {
/// Create a new pattern node with accumulated metric name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
activity: CoinblocksCoindaysSatblocksSatdaysSentPattern::new(client.clone(), acc.clone()),
addr_count: MetricPattern1::new(client.clone(), _m(&acc, "addr_count")),
addr_count_30d_change: MetricPattern4::new(client.clone(), _m(&acc, "addr_count_30d_change")),
cost_basis: MaxMinPattern::new(client.clone(), acc.clone()),
outputs: UtxoPattern::new(client.clone(), _m(&acc, "utxo_count")),
realized: CapCapitulationInvestorLossMvrvNegNetPeakProfitRealizedSellSentSoprTotalValuePattern::new(client.clone(), acc.clone()),
relative: InvestedNegNetNuplSupplyUnrealizedPattern::new(client.clone(), acc.clone()),
supply: _30dHalvedTotalPattern::new(client.clone(), acc.clone()),
unrealized: GreedInvestedInvestorNegNetPainSupplyTotalUnrealizedPattern::new(client.clone(), acc.clone()),
}
}
}
/// Pattern struct for repeated tree structure.
pub struct AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern {
pub all: MetricPattern1<StoredU64>,
pub p2a: MetricPattern1<StoredU64>,
pub p2pk33: MetricPattern1<StoredU64>,
pub p2pk65: MetricPattern1<StoredU64>,
pub p2pkh: MetricPattern1<StoredU64>,
pub p2sh: MetricPattern1<StoredU64>,
pub p2tr: MetricPattern1<StoredU64>,
pub p2wpkh: MetricPattern1<StoredU64>,
pub p2wsh: MetricPattern1<StoredU64>,
pub all: _30dCountPattern,
pub p2a: _30dCountPattern,
pub p2pk33: _30dCountPattern,
pub p2pk65: _30dCountPattern,
pub p2pkh: _30dCountPattern,
pub p2sh: _30dCountPattern,
pub p2tr: _30dCountPattern,
pub p2wpkh: _30dCountPattern,
pub p2wsh: _30dCountPattern,
}
impl AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern {
/// Create a new pattern node with accumulated metric name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
all: MetricPattern1::new(client.clone(), acc.clone()),
p2a: MetricPattern1::new(client.clone(), _p("p2a", &acc)),
p2pk33: MetricPattern1::new(client.clone(), _p("p2pk33", &acc)),
p2pk65: MetricPattern1::new(client.clone(), _p("p2pk65", &acc)),
p2pkh: MetricPattern1::new(client.clone(), _p("p2pkh", &acc)),
p2sh: MetricPattern1::new(client.clone(), _p("p2sh", &acc)),
p2tr: MetricPattern1::new(client.clone(), _p("p2tr", &acc)),
p2wpkh: MetricPattern1::new(client.clone(), _p("p2wpkh", &acc)),
p2wsh: MetricPattern1::new(client.clone(), _p("p2wsh", &acc)),
all: _30dCountPattern::new(client.clone(), acc.clone()),
p2a: _30dCountPattern::new(client.clone(), _p("p2a", &acc)),
p2pk33: _30dCountPattern::new(client.clone(), _p("p2pk33", &acc)),
p2pk65: _30dCountPattern::new(client.clone(), _p("p2pk65", &acc)),
p2pkh: _30dCountPattern::new(client.clone(), _p("p2pkh", &acc)),
p2sh: _30dCountPattern::new(client.clone(), _p("p2sh", &acc)),
p2tr: _30dCountPattern::new(client.clone(), _p("p2tr", &acc)),
p2wpkh: _30dCountPattern::new(client.clone(), _p("p2wpkh", &acc)),
p2wsh: _30dCountPattern::new(client.clone(), _p("p2wsh", &acc)),
}
}
}
@@ -2115,34 +2145,6 @@ impl<T: DeserializeOwned> AverageBaseMaxMedianMinPct10Pct25Pct75Pct90Pattern<T>
}
}
/// Pattern struct for repeated tree structure.
pub struct ActivityAddrCostOutputsRealizedRelativeSupplyUnrealizedPattern {
pub activity: CoinblocksCoindaysSatblocksSatdaysSentPattern,
pub addr_count: MetricPattern1<StoredU64>,
pub cost_basis: MaxMinPattern,
pub outputs: UtxoPattern,
pub realized: CapCapitulationInvestorLossMvrvNegNetPeakProfitRealizedSellSentSoprTotalValuePattern,
pub relative: InvestedNegNetNuplSupplyUnrealizedPattern,
pub supply: _30dHalvedTotalPattern,
pub unrealized: GreedInvestedInvestorNegNetPainSupplyTotalUnrealizedPattern,
}
impl ActivityAddrCostOutputsRealizedRelativeSupplyUnrealizedPattern {
/// Create a new pattern node with accumulated metric name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
activity: CoinblocksCoindaysSatblocksSatdaysSentPattern::new(client.clone(), acc.clone()),
addr_count: MetricPattern1::new(client.clone(), _m(&acc, "addr_count")),
cost_basis: MaxMinPattern::new(client.clone(), acc.clone()),
outputs: UtxoPattern::new(client.clone(), _m(&acc, "utxo_count")),
realized: CapCapitulationInvestorLossMvrvNegNetPeakProfitRealizedSellSentSoprTotalValuePattern::new(client.clone(), acc.clone()),
relative: InvestedNegNetNuplSupplyUnrealizedPattern::new(client.clone(), acc.clone()),
supply: _30dHalvedTotalPattern::new(client.clone(), acc.clone()),
unrealized: GreedInvestedInvestorNegNetPainSupplyTotalUnrealizedPattern::new(client.clone(), acc.clone()),
}
}
}
/// Pattern struct for repeated tree structure.
pub struct _10y2y3y4y5y6y8yPattern {
pub _10y: MetricPattern4<StoredF32>,
@@ -2561,6 +2563,22 @@ impl BitcoinDollarsSatsPattern3 {
}
}
/// Pattern struct for repeated tree structure.
pub struct _30dCountPattern {
pub _30d_change: MetricPattern4<StoredF64>,
pub count: MetricPattern1<StoredU64>,
}
impl _30dCountPattern {
/// Create a new pattern node with accumulated metric name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
_30d_change: MetricPattern4::new(client.clone(), _m(&acc, "30d_change")),
count: MetricPattern1::new(client.clone(), acc.clone()),
}
}
}
/// Pattern struct for repeated tree structure.
pub struct DollarsSatsPattern {
pub dollars: MetricPattern1<Dollars>,
@@ -2625,6 +2643,22 @@ impl SdSmaPattern {
}
}
/// Pattern struct for repeated tree structure.
pub struct UtxoPattern {
pub utxo_count: MetricPattern1<StoredU64>,
pub utxo_count_30d_change: MetricPattern4<StoredF64>,
}
impl UtxoPattern {
/// Create a new pattern node with accumulated metric name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
utxo_count: MetricPattern1::new(client.clone(), acc.clone()),
utxo_count_30d_change: MetricPattern4::new(client.clone(), _m(&acc, "30d_change")),
}
}
}
/// Pattern struct for repeated tree structure.
pub struct CumulativeSumPattern<T> {
pub cumulative: MetricPattern1<T>,
@@ -2687,20 +2721,6 @@ impl RatioPattern2 {
}
}
/// Pattern struct for repeated tree structure.
pub struct UtxoPattern {
pub utxo_count: MetricPattern1<StoredU64>,
}
impl UtxoPattern {
/// Create a new pattern node with accumulated metric name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
utxo_count: MetricPattern1::new(client.clone(), acc.clone()),
}
}
}
// Metrics tree
/// Metrics tree node.
@@ -2859,6 +2879,8 @@ pub struct MetricsTree_Blocks_Mining {
pub hash_rate_1m_sma: MetricPattern4<StoredF32>,
pub hash_rate_2m_sma: MetricPattern4<StoredF32>,
pub hash_rate_1y_sma: MetricPattern4<StoredF32>,
pub hash_rate_ath: MetricPattern1<StoredF64>,
pub hash_rate_drawdown: MetricPattern1<StoredF32>,
pub hash_price_ths: MetricPattern1<StoredF32>,
pub hash_price_ths_min: MetricPattern1<StoredF32>,
pub hash_price_phs: MetricPattern1<StoredF32>,
@@ -2879,6 +2901,8 @@ impl MetricsTree_Blocks_Mining {
hash_rate_1m_sma: MetricPattern4::new(client.clone(), "hash_rate_1m_sma".to_string()),
hash_rate_2m_sma: MetricPattern4::new(client.clone(), "hash_rate_2m_sma".to_string()),
hash_rate_1y_sma: MetricPattern4::new(client.clone(), "hash_rate_1y_sma".to_string()),
hash_rate_ath: MetricPattern1::new(client.clone(), "hash_rate_ath".to_string()),
hash_rate_drawdown: MetricPattern1::new(client.clone(), "hash_rate_drawdown".to_string()),
hash_price_ths: MetricPattern1::new(client.clone(), "hash_price_ths".to_string()),
hash_price_ths_min: MetricPattern1::new(client.clone(), "hash_price_ths_min".to_string()),
hash_price_phs: MetricPattern1::new(client.clone(), "hash_price_phs".to_string()),
@@ -3515,7 +3539,7 @@ impl MetricsTree_Cointime_Adjusted {
/// Metrics tree node.
pub struct MetricsTree_Cointime_ReserveRisk {
pub vocdd_365d_sma: MetricPattern6<StoredF64>,
pub vocdd_365d_median: MetricPattern6<StoredF64>,
pub hodl_bank: MetricPattern6<StoredF64>,
pub reserve_risk: MetricPattern4<StoredF64>,
}
@@ -3523,7 +3547,7 @@ pub struct MetricsTree_Cointime_ReserveRisk {
impl MetricsTree_Cointime_ReserveRisk {
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
Self {
vocdd_365d_sma: MetricPattern6::new(client.clone(), "vocdd_365d_sma".to_string()),
vocdd_365d_median: MetricPattern6::new(client.clone(), "vocdd_365d_median".to_string()),
hodl_bank: MetricPattern6::new(client.clone(), "hodl_bank".to_string()),
reserve_risk: MetricPattern4::new(client.clone(), "reserve_risk".to_string()),
}
@@ -5109,7 +5133,7 @@ pub struct MetricsTree_Distribution {
pub addr_count: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern,
pub empty_addr_count: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern,
pub address_activity: MetricsTree_Distribution_AddressActivity,
pub total_addr_count: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern,
pub total_addr_count: MetricsTree_Distribution_TotalAddrCount,
pub new_addr_count: MetricsTree_Distribution_NewAddrCount,
pub growth_rate: MetricsTree_Distribution_GrowthRate,
pub fundedaddressindex: MetricPattern31<FundedAddressIndex>,
@@ -5127,7 +5151,7 @@ impl MetricsTree_Distribution {
addr_count: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern::new(client.clone(), "addr_count".to_string()),
empty_addr_count: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern::new(client.clone(), "empty_addr_count".to_string()),
address_activity: MetricsTree_Distribution_AddressActivity::new(client.clone(), format!("{base_path}_address_activity")),
total_addr_count: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern::new(client.clone(), "total_addr_count".to_string()),
total_addr_count: MetricsTree_Distribution_TotalAddrCount::new(client.clone(), format!("{base_path}_total_addr_count")),
new_addr_count: MetricsTree_Distribution_NewAddrCount::new(client.clone(), format!("{base_path}_new_addr_count")),
growth_rate: MetricsTree_Distribution_GrowthRate::new(client.clone(), format!("{base_path}_growth_rate")),
fundedaddressindex: MetricPattern31::new(client.clone(), "fundedaddressindex".to_string()),
@@ -5864,6 +5888,35 @@ impl MetricsTree_Distribution_AddressActivity {
}
}
/// Metrics tree node.
pub struct MetricsTree_Distribution_TotalAddrCount {
pub all: MetricPattern1<StoredU64>,
pub p2pk65: MetricPattern1<StoredU64>,
pub p2pk33: MetricPattern1<StoredU64>,
pub p2pkh: MetricPattern1<StoredU64>,
pub p2sh: MetricPattern1<StoredU64>,
pub p2wpkh: MetricPattern1<StoredU64>,
pub p2wsh: MetricPattern1<StoredU64>,
pub p2tr: MetricPattern1<StoredU64>,
pub p2a: MetricPattern1<StoredU64>,
}
impl MetricsTree_Distribution_TotalAddrCount {
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
Self {
all: MetricPattern1::new(client.clone(), "total_addr_count".to_string()),
p2pk65: MetricPattern1::new(client.clone(), "p2pk65_total_addr_count".to_string()),
p2pk33: MetricPattern1::new(client.clone(), "p2pk33_total_addr_count".to_string()),
p2pkh: MetricPattern1::new(client.clone(), "p2pkh_total_addr_count".to_string()),
p2sh: MetricPattern1::new(client.clone(), "p2sh_total_addr_count".to_string()),
p2wpkh: MetricPattern1::new(client.clone(), "p2wpkh_total_addr_count".to_string()),
p2wsh: MetricPattern1::new(client.clone(), "p2wsh_total_addr_count".to_string()),
p2tr: MetricPattern1::new(client.clone(), "p2tr_total_addr_count".to_string()),
p2a: MetricPattern1::new(client.clone(), "p2a_total_addr_count".to_string()),
}
}
}
/// Metrics tree node.
pub struct MetricsTree_Distribution_NewAddrCount {
pub all: AverageBaseCumulativeMaxMedianMinPct10Pct25Pct75Pct90SumPattern2<StoredU64>,
@@ -5998,7 +6051,7 @@ pub struct BrkClient {
impl BrkClient {
/// Client version.
pub const VERSION: &'static str = "v0.1.2";
pub const VERSION: &'static str = "v0.1.3";
/// Create a new client with the given base URL.
pub fn new(base_url: impl Into<String>) -> Self {

View File

@@ -4,7 +4,7 @@ use vecdb::Exit;
use super::super::{ONE_TERA_HASH, TARGET_BLOCKS_PER_DAY_F64, count, difficulty, rewards};
use super::Vecs;
use crate::{ComputeIndexes, indexes};
use crate::{ComputeIndexes, indexes, traits::ComputeDrawdown};
impl Vecs {
pub fn compute(
@@ -80,6 +80,27 @@ impl Vecs {
Ok(())
})?;
self.hash_rate_ath
.compute_all(indexes, starting_indexes, exit, |v| {
v.compute_all_time_high(
starting_indexes.height,
&self.hash_rate.height,
exit,
)?;
Ok(())
})?;
self.hash_rate_drawdown
.compute_all(indexes, starting_indexes, exit, |v| {
v.compute_drawdown(
starting_indexes.height,
&self.hash_rate.height,
&self.hash_rate_ath.height,
exit,
)?;
Ok(())
})?;
self.hash_price_ths
.compute_all(indexes, starting_indexes, exit, |v| {
v.compute_transform2(

View File

@@ -43,6 +43,18 @@ impl Vecs {
version,
indexes,
)?,
hash_rate_ath: ComputedFromHeightLast::forced_import(
db,
"hash_rate_ath",
version,
indexes,
)?,
hash_rate_drawdown: ComputedFromHeightLast::forced_import(
db,
"hash_rate_drawdown",
version,
indexes,
)?,
hash_price_ths: ComputedFromHeightLast::forced_import(
db,
"hash_price_ths",

View File

@@ -11,6 +11,8 @@ pub struct Vecs {
pub hash_rate_1m_sma: ComputedFromDateLast<StoredF32>,
pub hash_rate_2m_sma: ComputedFromDateLast<StoredF32>,
pub hash_rate_1y_sma: ComputedFromDateLast<StoredF32>,
pub hash_rate_ath: ComputedFromHeightLast<StoredF64>,
pub hash_rate_drawdown: ComputedFromHeightLast<StoredF32>,
pub hash_price_ths: ComputedFromHeightLast<StoredF32>,
pub hash_price_ths_min: ComputedFromHeightLast<StoredF32>,
pub hash_price_phs: ComputedFromHeightLast<StoredF32>,

View File

@@ -15,7 +15,7 @@ impl Vecs {
) -> Result<()> {
let vocdd_dateindex_sum = &value.vocdd.dateindex.sum.0;
self.vocdd_365d_sma.compute_sma(
self.vocdd_365d_median.compute_rolling_median(
starting_indexes.dateindex,
vocdd_dateindex_sum,
365,
@@ -27,8 +27,8 @@ impl Vecs {
self.hodl_bank.compute_cumulative_transformed_binary(
starting_indexes.dateindex,
price_close,
&self.vocdd_365d_sma,
|price: Close<Dollars>, sma: StoredF64| StoredF64::from(f64::from(price) - f64::from(sma)),
&self.vocdd_365d_median,
|price: Close<Dollars>, median: StoredF64| StoredF64::from(f64::from(price) - f64::from(median)),
exit,
)?;
@@ -47,54 +47,3 @@ impl Vecs {
Ok(())
}
}
#[cfg(test)]
mod tests {
#[test]
fn test_hodl_bank_formula() {
let prices = [100.0, 110.0, 105.0, 120.0, 115.0];
let vocdd_sma = [50.0, 55.0, 52.0, 60.0, 58.0];
let mut hodl_bank = 0.0_f64;
let mut expected = Vec::new();
for i in 0..prices.len() {
hodl_bank += prices[i] - vocdd_sma[i];
expected.push(hodl_bank);
}
assert!((expected[0] - 50.0).abs() < 0.001);
assert!((expected[1] - 105.0).abs() < 0.001);
assert!((expected[2] - 158.0).abs() < 0.001);
assert!((expected[3] - 218.0).abs() < 0.001);
assert!((expected[4] - 275.0).abs() < 0.001);
}
#[test]
fn test_reserve_risk_formula() {
let price = 100.0_f64;
let hodl_bank = 1000.0_f64;
let reserve_risk = price / hodl_bank;
assert!((reserve_risk - 0.1).abs() < 0.0001);
}
#[test]
fn test_reserve_risk_interpretation() {
let high_confidence = 100.0 / 10000.0;
let low_confidence = 100.0 / 100.0;
assert!(high_confidence < low_confidence);
}
#[test]
fn test_hodl_bank_negative_contribution() {
let prices = [100.0, 80.0, 90.0];
let vocdd_sma = [50.0, 100.0, 85.0];
let mut hodl_bank = 0.0_f64;
for i in 0..prices.len() {
hodl_bank += prices[i] - vocdd_sma[i];
}
assert!((hodl_bank - 35.0).abs() < 0.001);
}
}

View File

@@ -12,11 +12,12 @@ impl Vecs {
indexes: &indexes::Vecs,
compute_dollars: bool,
) -> Result<Self> {
let v1 = version + Version::ONE;
Ok(Self {
vocdd_365d_sma: EagerVec::forced_import(db, "vocdd_365d_sma", version)?,
hodl_bank: EagerVec::forced_import(db, "hodl_bank", version)?,
vocdd_365d_median: EagerVec::forced_import(db, "vocdd_365d_median", v1)?,
hodl_bank: EagerVec::forced_import(db, "hodl_bank", v1)?,
reserve_risk: compute_dollars
.then(|| ComputedFromDateLast::forced_import(db, "reserve_risk", version, indexes))
.then(|| ComputedFromDateLast::forced_import(db, "reserve_risk", v1, indexes))
.transpose()?,
})
}

View File

@@ -6,7 +6,7 @@ use crate::internal::ComputedFromDateLast;
#[derive(Clone, Traversable)]
pub struct Vecs {
pub vocdd_365d_sma: EagerVec<PcoVec<DateIndex, StoredF64>>,
pub vocdd_365d_median: EagerVec<PcoVec<DateIndex, StoredF64>>,
pub hodl_bank: EagerVec<PcoVec<DateIndex, StoredF64>>,
pub reserve_risk: Option<ComputedFromDateLast<StoredF64>>,
}

View File

@@ -1,5 +1,6 @@
use brk_error::Result;
use vecdb::Exit;
use brk_types::{Bitcoin, Close, Dollars, StoredF64};
use vecdb::{Exit, TypedVecIterator};
use super::super::activity;
use super::Vecs;
@@ -29,6 +30,15 @@ impl Vecs {
.activity
.coindays_destroyed;
let circulating_supply = &distribution
.utxo_cohorts
.all
.metrics
.supply
.total
.bitcoin
.height;
self.cointime_value_destroyed
.compute_all(indexes, starting_indexes, exit, |vec| {
vec.compute_multiply(
@@ -62,14 +72,27 @@ impl Vecs {
Ok(())
})?;
// VOCDD: Value-weighted Coin Days Destroyed = CDD × price
// This is a key input for Reserve Risk calculation
// VOCDD: Value of Coin Days Destroyed = price × (CDD / circulating_supply)
// Supply-adjusted to account for growing supply over time
// This is a key input for Reserve Risk / HODL Bank calculation
self.vocdd
.compute_all(indexes, starting_indexes, exit, |vec| {
vec.compute_multiply(
let mut supply_iter = circulating_supply.into_iter();
vec.compute_transform2(
starting_indexes.height,
&price.usd.split.close.height,
&coindays_destroyed.height,
|(i, price, cdd, _): (_, Close<Dollars>, StoredF64, _)| {
let supply: Bitcoin = supply_iter.get_unwrap(i);
let supply_f64 = f64::from(supply);
if supply_f64 == 0.0 {
(i, StoredF64::from(0.0))
} else {
// VOCDD = price × (CDD / supply)
let vocdd = f64::from(price) * f64::from(cdd) / supply_f64;
(i, StoredF64::from(vocdd))
}
},
exit,
)?;
Ok(())

View File

@@ -29,7 +29,7 @@ impl Vecs {
vocdd: ComputedFromHeightSumCum::forced_import(
db,
"vocdd",
version,
version + Version::ONE,
indexes,
)?,
})

View File

@@ -1,14 +1,63 @@
use brk_cohort::ByAddressType;
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Height, StoredU64, Version};
use brk_types::{Height, StoredF64, StoredU64, Version};
use derive_more::{Deref, DerefMut};
use rayon::prelude::*;
use vecdb::{
AnyStoredVec, AnyVec, Database, EagerVec, Exit, GenericStoredVec, PcoVec, TypedVecIterator,
};
use crate::{ComputeIndexes, indexes, internal::ComputedFromHeightLast};
use crate::{ComputeIndexes, indexes, internal::{ComputedFromDateLast, ComputedFromHeightLast}};
/// Address count with 30d change metric for a single type.
#[derive(Clone, Traversable)]
pub struct AddrCountVecs {
#[traversable(flatten)]
pub count: ComputedFromHeightLast<StoredU64>,
pub _30d_change: ComputedFromDateLast<StoredF64>,
}
impl AddrCountVecs {
pub fn forced_import(
db: &Database,
name: &str,
version: Version,
indexes: &indexes::Vecs,
) -> Result<Self> {
Ok(Self {
count: ComputedFromHeightLast::forced_import(db, name, version, indexes)?,
_30d_change: ComputedFromDateLast::forced_import(
db,
&format!("{name}_30d_change"),
version,
indexes,
)?,
})
}
pub fn compute_rest(
&mut self,
indexes: &indexes::Vecs,
starting_indexes: &ComputeIndexes,
exit: &Exit,
) -> Result<()> {
self.count.compute_rest(indexes, starting_indexes, exit)?;
self._30d_change
.compute_all(starting_indexes, exit, |v| {
v.compute_change(
starting_indexes.dateindex,
&*self.count.dateindex,
30,
exit,
)?;
Ok(())
})?;
Ok(())
}
}
/// Address count per address type (runtime state).
#[derive(Debug, Default, Deref, DerefMut)]
@@ -28,47 +77,54 @@ impl From<(&AddressTypeToAddrCountVecs, Height)> for AddressTypeToAddressCount {
Self(ByAddressType {
p2pk65: groups
.p2pk65
.count
.height
.into_iter()
.get_unwrap(prev_height)
.into(),
p2pk33: groups
.p2pk33
.count
.height
.into_iter()
.get_unwrap(prev_height)
.into(),
p2pkh: groups
.p2pkh
.count
.height
.into_iter()
.get_unwrap(prev_height)
.into(),
p2sh: groups
.p2sh
.count
.height
.into_iter()
.get_unwrap(prev_height)
.into(),
p2wpkh: groups
.p2wpkh
.count
.height
.into_iter()
.get_unwrap(prev_height)
.into(),
p2wsh: groups
.p2wsh
.count
.height
.into_iter()
.get_unwrap(prev_height)
.into(),
p2tr: groups
.p2tr
.count
.height
.into_iter()
.get_unwrap(prev_height)
.into(),
p2a: groups.p2a.height.into_iter().get_unwrap(prev_height).into(),
p2a: groups.p2a.count.height.into_iter().get_unwrap(prev_height).into(),
})
} else {
Default::default()
@@ -76,13 +132,13 @@ impl From<(&AddressTypeToAddrCountVecs, Height)> for AddressTypeToAddressCount {
}
}
/// Address count per address type, with height + derived indexes.
/// Address count per address type, with height + derived indexes + 30d change.
#[derive(Clone, Deref, DerefMut, Traversable)]
pub struct AddressTypeToAddrCountVecs(ByAddressType<ComputedFromHeightLast<StoredU64>>);
pub struct AddressTypeToAddrCountVecs(ByAddressType<AddrCountVecs>);
impl From<ByAddressType<ComputedFromHeightLast<StoredU64>>> for AddressTypeToAddrCountVecs {
impl From<ByAddressType<AddrCountVecs>> for AddressTypeToAddrCountVecs {
#[inline]
fn from(value: ByAddressType<ComputedFromHeightLast<StoredU64>>) -> Self {
fn from(value: ByAddressType<AddrCountVecs>) -> Self {
Self(value)
}
}
@@ -95,8 +151,8 @@ impl AddressTypeToAddrCountVecs {
indexes: &indexes::Vecs,
) -> Result<Self> {
Ok(Self::from(
ByAddressType::<ComputedFromHeightLast<StoredU64>>::new_with_name(|type_name| {
ComputedFromHeightLast::forced_import(
ByAddressType::<AddrCountVecs>::new_with_name(|type_name| {
AddrCountVecs::forced_import(
db,
&format!("{type_name}_{name}"),
version,
@@ -108,28 +164,29 @@ impl AddressTypeToAddrCountVecs {
pub fn min_stateful_height(&self) -> usize {
self.p2pk65
.count
.height
.len()
.min(self.p2pk33.height.len())
.min(self.p2pkh.height.len())
.min(self.p2sh.height.len())
.min(self.p2wpkh.height.len())
.min(self.p2wsh.height.len())
.min(self.p2tr.height.len())
.min(self.p2a.height.len())
.min(self.p2pk33.count.height.len())
.min(self.p2pkh.count.height.len())
.min(self.p2sh.count.height.len())
.min(self.p2wpkh.count.height.len())
.min(self.p2wsh.count.height.len())
.min(self.p2tr.count.height.len())
.min(self.p2a.count.height.len())
}
pub fn par_iter_height_mut(&mut self) -> impl ParallelIterator<Item = &mut dyn AnyStoredVec> {
let inner = &mut self.0;
[
&mut inner.p2pk65.height as &mut dyn AnyStoredVec,
&mut inner.p2pk33.height as &mut dyn AnyStoredVec,
&mut inner.p2pkh.height as &mut dyn AnyStoredVec,
&mut inner.p2sh.height as &mut dyn AnyStoredVec,
&mut inner.p2wpkh.height as &mut dyn AnyStoredVec,
&mut inner.p2wsh.height as &mut dyn AnyStoredVec,
&mut inner.p2tr.height as &mut dyn AnyStoredVec,
&mut inner.p2a.height as &mut dyn AnyStoredVec,
&mut inner.p2pk65.count.height as &mut dyn AnyStoredVec,
&mut inner.p2pk33.count.height as &mut dyn AnyStoredVec,
&mut inner.p2pkh.count.height as &mut dyn AnyStoredVec,
&mut inner.p2sh.count.height as &mut dyn AnyStoredVec,
&mut inner.p2wpkh.count.height as &mut dyn AnyStoredVec,
&mut inner.p2wsh.count.height as &mut dyn AnyStoredVec,
&mut inner.p2tr.count.height as &mut dyn AnyStoredVec,
&mut inner.p2a.count.height as &mut dyn AnyStoredVec,
]
.into_par_iter()
}
@@ -140,27 +197,35 @@ impl AddressTypeToAddrCountVecs {
addr_counts: &AddressTypeToAddressCount,
) -> Result<()> {
self.p2pk65
.count
.height
.truncate_push(height, addr_counts.p2pk65.into())?;
self.p2pk33
.count
.height
.truncate_push(height, addr_counts.p2pk33.into())?;
self.p2pkh
.count
.height
.truncate_push(height, addr_counts.p2pkh.into())?;
self.p2sh
.count
.height
.truncate_push(height, addr_counts.p2sh.into())?;
self.p2wpkh
.count
.height
.truncate_push(height, addr_counts.p2wpkh.into())?;
self.p2wsh
.count
.height
.truncate_push(height, addr_counts.p2wsh.into())?;
self.p2tr
.count
.height
.truncate_push(height, addr_counts.p2tr.into())?;
self.p2a
.count
.height
.truncate_push(height, addr_counts.p2a.into())?;
Ok(())
@@ -168,14 +233,14 @@ impl AddressTypeToAddrCountVecs {
pub fn reset_height(&mut self) -> Result<()> {
use vecdb::GenericStoredVec;
self.p2pk65.height.reset()?;
self.p2pk33.height.reset()?;
self.p2pkh.height.reset()?;
self.p2sh.height.reset()?;
self.p2wpkh.height.reset()?;
self.p2wsh.height.reset()?;
self.p2tr.height.reset()?;
self.p2a.height.reset()?;
self.p2pk65.count.height.reset()?;
self.p2pk33.count.height.reset()?;
self.p2pkh.count.height.reset()?;
self.p2sh.count.height.reset()?;
self.p2wpkh.count.height.reset()?;
self.p2wsh.count.height.reset()?;
self.p2tr.count.height.reset()?;
self.p2a.count.height.reset()?;
Ok(())
}
@@ -198,26 +263,26 @@ impl AddressTypeToAddrCountVecs {
pub fn by_height(&self) -> Vec<&EagerVec<PcoVec<Height, StoredU64>>> {
vec![
&self.p2pk65.height,
&self.p2pk33.height,
&self.p2pkh.height,
&self.p2sh.height,
&self.p2wpkh.height,
&self.p2wsh.height,
&self.p2tr.height,
&self.p2a.height,
&self.p2pk65.count.height,
&self.p2pk33.count.height,
&self.p2pkh.count.height,
&self.p2sh.count.height,
&self.p2wpkh.count.height,
&self.p2wsh.count.height,
&self.p2tr.count.height,
&self.p2a.count.height,
]
}
}
#[derive(Clone, Traversable)]
pub struct AddrCountVecs {
pub all: ComputedFromHeightLast<StoredU64>,
pub struct AddrCountsVecs {
pub all: AddrCountVecs,
#[traversable(flatten)]
pub by_addresstype: AddressTypeToAddrCountVecs,
}
impl AddrCountVecs {
impl AddrCountsVecs {
pub fn forced_import(
db: &Database,
name: &str,
@@ -225,22 +290,22 @@ impl AddrCountVecs {
indexes: &indexes::Vecs,
) -> Result<Self> {
Ok(Self {
all: ComputedFromHeightLast::forced_import(db, name, version, indexes)?,
all: AddrCountVecs::forced_import(db, name, version, indexes)?,
by_addresstype: AddressTypeToAddrCountVecs::forced_import(db, name, version, indexes)?,
})
}
pub fn min_stateful_height(&self) -> usize {
self.all.height.len().min(self.by_addresstype.min_stateful_height())
self.all.count.height.len().min(self.by_addresstype.min_stateful_height())
}
pub fn par_iter_height_mut(&mut self) -> impl ParallelIterator<Item = &mut dyn AnyStoredVec> {
rayon::iter::once(&mut self.all.height as &mut dyn AnyStoredVec)
rayon::iter::once(&mut self.all.count.height as &mut dyn AnyStoredVec)
.chain(self.by_addresstype.par_iter_height_mut())
}
pub fn reset_height(&mut self) -> Result<()> {
self.all.height.reset()?;
self.all.count.height.reset()?;
self.by_addresstype.reset_height()?;
Ok(())
}
@@ -251,7 +316,7 @@ impl AddrCountVecs {
total: u64,
addr_counts: &AddressTypeToAddressCount,
) -> Result<()> {
self.all.height.truncate_push(height, total.into())?;
self.all.count.height.truncate_push(height, total.into())?;
self.by_addresstype
.truncate_push_height(height, addr_counts)?;
Ok(())
@@ -268,10 +333,22 @@ impl AddrCountVecs {
let sources = self.by_addresstype.by_height();
self.all
.count
.compute_all(indexes, starting_indexes, exit, |height_vec| {
Ok(height_vec.compute_sum_of_others(starting_indexes.height, &sources, exit)?)
})?;
self.all._30d_change
.compute_all(starting_indexes, exit, |v| {
v.compute_change(
starting_indexes.dateindex,
&*self.all.count.dateindex,
30,
exit,
)?;
Ok(())
})?;
Ok(())
}
}

View File

@@ -11,7 +11,7 @@ use crate::{
internal::{LazyBinaryComputedFromHeightDistribution, RatioU64F32},
};
use super::{AddrCountVecs, NewAddrCountVecs};
use super::{AddrCountsVecs, NewAddrCountVecs};
/// Growth rate by type - lazy ratio with distribution stats
pub type GrowthRateByType =
@@ -31,7 +31,7 @@ impl GrowthRateVecs {
version: Version,
indexes: &indexes::Vecs,
new_addr_count: &NewAddrCountVecs,
addr_count: &AddrCountVecs,
addr_count: &AddrCountsVecs,
) -> Result<Self> {
let all = make_growth_rate(
db,
@@ -39,7 +39,7 @@ impl GrowthRateVecs {
version,
indexes,
&new_addr_count.all.height,
&addr_count.all.height,
&addr_count.all.count.height,
)?;
let by_addresstype: GrowthRateByType = zip2_by_addresstype(
@@ -52,7 +52,7 @@ impl GrowthRateVecs {
version,
indexes,
&new.height,
&addr.height,
&addr.count.height,
)
},
)?;

View File

@@ -8,7 +8,7 @@ mod total_addr_count;
mod type_map;
pub use activity::{AddressActivityVecs, AddressTypeToActivityCounts};
pub use address_count::{AddrCountVecs, AddressTypeToAddressCount};
pub use address_count::{AddrCountVecs, AddrCountsVecs, AddressTypeToAddressCount};
pub use data::AddressesDataVecs;
pub use growth_rate::GrowthRateVecs;
pub use indexes::AnyAddressIndexesVecs;

View File

@@ -8,7 +8,7 @@ use vecdb::{Database, Exit, IterableCloneableVec};
use crate::{ComputeIndexes, indexes, internal::{LazyBinaryComputedFromHeightLast, U64Plus}};
use super::AddrCountVecs;
use super::AddrCountsVecs;
/// Total addresses by type - lazy sum with all derived indexes
pub type TotalAddrCountByType =
@@ -27,15 +27,15 @@ impl TotalAddrCountVecs {
db: &Database,
version: Version,
indexes: &indexes::Vecs,
addr_count: &AddrCountVecs,
empty_addr_count: &AddrCountVecs,
addr_count: &AddrCountsVecs,
empty_addr_count: &AddrCountsVecs,
) -> Result<Self> {
let all = LazyBinaryComputedFromHeightLast::forced_import::<U64Plus>(
db,
"total_addr_count",
version,
addr_count.all.height.boxed_clone(),
empty_addr_count.all.height.boxed_clone(),
addr_count.all.count.height.boxed_clone(),
empty_addr_count.all.count.height.boxed_clone(),
indexes,
)?;
@@ -47,8 +47,8 @@ impl TotalAddrCountVecs {
db,
&format!("{name}_total_addr_count"),
version,
addr.height.boxed_clone(),
empty.height.boxed_clone(),
addr.count.height.boxed_clone(),
empty.count.height.boxed_clone(),
indexes,
)
},

View File

@@ -3,13 +3,15 @@ use std::path::Path;
use brk_cohort::{CohortContext, Filter, Filtered};
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{CentsUnsigned, DateIndex, Dollars, Height, StoredU64, Version};
use brk_types::{CentsUnsigned, DateIndex, Dollars, Height, StoredF64, StoredU64, Version};
use rayon::prelude::*;
use vecdb::{AnyStoredVec, AnyVec, Database, Exit, GenericStoredVec, IterableVec};
use crate::{
ComputeIndexes, distribution::state::AddressCohortState, indexes, internal::ComputedFromHeightLast,
price,
distribution::state::AddressCohortState,
indexes,
internal::{ComputedFromDateLast, ComputedFromHeightLast},
price, ComputeIndexes,
};
use crate::distribution::metrics::{CohortMetrics, ImportConfig, SupplyMetrics};
@@ -33,6 +35,7 @@ pub struct AddressCohortVecs {
pub metrics: CohortMetrics,
pub addr_count: ComputedFromHeightLast<StoredU64>,
pub addr_count_30d_change: ComputedFromDateLast<StoredF64>,
}
impl AddressCohortVecs {
@@ -79,6 +82,12 @@ impl AddressCohortVecs {
version + VERSION,
indexes,
)?,
addr_count_30d_change: ComputedFromDateLast::forced_import(
db,
&cfg.name("addr_count_30d_change"),
version + VERSION,
indexes,
)?,
})
}
@@ -234,6 +243,18 @@ impl DynCohortVecs for AddressCohortVecs {
) -> Result<()> {
self.addr_count
.compute_rest(indexes, starting_indexes, exit)?;
self.addr_count_30d_change
.compute_all(starting_indexes, exit, |v| {
v.compute_change(
starting_indexes.dateindex,
&*self.addr_count.dateindex,
30,
exit,
)?;
Ok(())
})?;
self.metrics
.compute_rest_part1(indexes, price, starting_indexes, exit)?;
Ok(())

View File

@@ -1,10 +1,10 @@
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Height, StoredU64};
use brk_types::{Height, StoredF64, StoredU64};
use rayon::prelude::*;
use vecdb::{AnyStoredVec, AnyVec, Exit, GenericStoredVec};
use crate::{ComputeIndexes, indexes, internal::ComputedFromHeightLast};
use crate::{ComputeIndexes, indexes, internal::{ComputedFromDateLast, ComputedFromHeightLast}};
use super::ImportConfig;
@@ -12,6 +12,7 @@ use super::ImportConfig;
#[derive(Clone, Traversable)]
pub struct OutputsMetrics {
pub utxo_count: ComputedFromHeightLast<StoredU64>,
pub utxo_count_30d_change: ComputedFromDateLast<StoredF64>,
}
impl OutputsMetrics {
@@ -24,6 +25,12 @@ impl OutputsMetrics {
cfg.version,
cfg.indexes,
)?,
utxo_count_30d_change: ComputedFromDateLast::forced_import(
cfg.db,
&cfg.name("utxo_count_30d_change"),
cfg.version,
cfg.indexes,
)?,
})
}
@@ -42,7 +49,11 @@ impl OutputsMetrics {
/// Returns a parallel iterator over all vecs for parallel writing.
pub fn par_iter_mut(&mut self) -> impl ParallelIterator<Item = &mut dyn AnyStoredVec> {
vec![&mut self.utxo_count.height as &mut dyn AnyStoredVec].into_par_iter()
vec![
&mut self.utxo_count.height as &mut dyn AnyStoredVec,
&mut self.utxo_count_30d_change.dateindex as &mut dyn AnyStoredVec,
]
.into_par_iter()
}
/// Compute aggregate values from separate cohorts.
@@ -70,6 +81,20 @@ impl OutputsMetrics {
starting_indexes: &ComputeIndexes,
exit: &Exit,
) -> Result<()> {
self.utxo_count.compute_rest(indexes, starting_indexes, exit)
self.utxo_count
.compute_rest(indexes, starting_indexes, exit)?;
self.utxo_count_30d_change
.compute_all(starting_indexes, exit, |v| {
v.compute_change(
starting_indexes.dateindex,
&*self.utxo_count.dateindex,
30,
exit,
)?;
Ok(())
})?;
Ok(())
}
}

View File

@@ -25,7 +25,7 @@ use crate::{
use super::{
AddressCohorts, AddressesDataVecs, AnyAddressIndexesVecs, UTXOCohorts,
address::{
AddrCountVecs, AddressActivityVecs, GrowthRateVecs, NewAddrCountVecs, TotalAddrCountVecs,
AddrCountsVecs, AddressActivityVecs, GrowthRateVecs, NewAddrCountVecs, TotalAddrCountVecs,
},
compute::aggregates,
};
@@ -44,8 +44,8 @@ pub struct Vecs {
pub utxo_cohorts: UTXOCohorts,
pub address_cohorts: AddressCohorts,
pub addr_count: AddrCountVecs,
pub empty_addr_count: AddrCountVecs,
pub addr_count: AddrCountsVecs,
pub empty_addr_count: AddrCountsVecs,
pub address_activity: AddressActivityVecs,
/// Total addresses ever seen (addr_count + empty_addr_count) - lazy, global + per-type
@@ -115,9 +115,9 @@ impl Vecs {
|index, _| Some(index),
);
let addr_count = AddrCountVecs::forced_import(&db, "addr_count", version, indexes)?;
let addr_count = AddrCountsVecs::forced_import(&db, "addr_count", version, indexes)?;
let empty_addr_count =
AddrCountVecs::forced_import(&db, "empty_addr_count", version, indexes)?;
AddrCountsVecs::forced_import(&db, "empty_addr_count", version, indexes)?;
let address_activity =
AddressActivityVecs::forced_import(&db, "address_activity", version, indexes)?;

View File

@@ -1,7 +1,8 @@
use brk_error::Result;
use brk_types::{Bitcoin, CheckedSub, Close, Date, DateIndex, Dollars, Sats, StoredF32};
use vecdb::{
AnyStoredVec, AnyVec, EagerVec, Exit, GenericStoredVec, IterableVec, PcoVec, VecIndex, Version,
AnyStoredVec, AnyVec, EagerVec, Exit, GenericStoredVec, IterableVec, PcoVec, VecIndex, VecValue,
Version,
};
mod pricing;
@@ -295,37 +296,47 @@ where
}
pub trait ComputeDrawdown<I> {
fn compute_drawdown(
fn compute_drawdown<C, A>(
&mut self,
max_from: I,
close: &impl IterableVec<I, Close<Dollars>>,
ath: &impl IterableVec<I, Dollars>,
current: &impl IterableVec<I, C>,
ath: &impl IterableVec<I, A>,
exit: &Exit,
) -> Result<()>;
) -> Result<()>
where
C: VecValue,
A: VecValue,
f64: From<C> + From<A>;
}
impl<I> ComputeDrawdown<I> for EagerVec<PcoVec<I, StoredF32>>
where
I: VecIndex,
{
fn compute_drawdown(
fn compute_drawdown<C, A>(
&mut self,
max_from: I,
close: &impl IterableVec<I, Close<Dollars>>,
ath: &impl IterableVec<I, Dollars>,
current: &impl IterableVec<I, C>,
ath: &impl IterableVec<I, A>,
exit: &Exit,
) -> Result<()> {
) -> Result<()>
where
C: VecValue,
A: VecValue,
f64: From<C> + From<A>,
{
self.compute_transform2(
max_from,
current,
ath,
close,
|(i, ath, close, _)| {
if ath == Dollars::ZERO {
(i, StoredF32::default())
|(i, current, ath, _)| {
let ath_f64 = f64::from(ath);
let drawdown = if ath_f64 == 0.0 {
StoredF32::default()
} else {
let drawdown = StoredF32::from((*ath - **close) / *ath * -100.0);
(i, drawdown)
}
StoredF32::from((f64::from(current) - ath_f64) / ath_f64 * 100.0)
};
(i, drawdown)
},
exit,
)?;

View File

@@ -10,7 +10,7 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use vecdb::{CheckedSub, Formattable, Pco, PrintableIndex};
use crate::{Bitcoin, Dollars};
use crate::{Bitcoin, Dollars, StoredU64};
/// Fixed-size 64-bit floating point value optimized for on-disk storage
#[derive(Debug, Deref, Default, Clone, Copy, Serialize, Deserialize, Pco, JsonSchema)]
@@ -48,6 +48,13 @@ impl From<usize> for StoredF64 {
}
}
impl From<StoredU64> for StoredF64 {
#[inline]
fn from(value: StoredU64) -> Self {
Self(*value as f64)
}
}
impl CheckedSub<StoredF64> for StoredF64 {
fn checked_sub(self, rhs: Self) -> Option<Self> {
Some(Self(self.0 - rhs.0))

View File

@@ -2613,17 +2613,50 @@ function createAverageCumulativeMaxMedianMinPct10Pct25Pct75Pct90SumPattern2(clie
};
}
/**
* @typedef {Object} ActivityAddrCostOutputsRealizedRelativeSupplyUnrealizedPattern
* @property {CoinblocksCoindaysSatblocksSatdaysSentPattern} activity
* @property {MetricPattern1<StoredU64>} addrCount
* @property {MetricPattern4<StoredF64>} addrCount30dChange
* @property {MaxMinPattern} costBasis
* @property {UtxoPattern} outputs
* @property {CapCapitulationInvestorLossMvrvNegNetPeakProfitRealizedSellSentSoprTotalValuePattern} realized
* @property {InvestedNegNetNuplSupplyUnrealizedPattern} relative
* @property {_30dHalvedTotalPattern} supply
* @property {GreedInvestedInvestorNegNetPainSupplyTotalUnrealizedPattern} unrealized
*/
/**
* Create a ActivityAddrCostOutputsRealizedRelativeSupplyUnrealizedPattern pattern node
* @param {BrkClientBase} client
* @param {string} acc - Accumulated metric name
* @returns {ActivityAddrCostOutputsRealizedRelativeSupplyUnrealizedPattern}
*/
function createActivityAddrCostOutputsRealizedRelativeSupplyUnrealizedPattern(client, acc) {
return {
activity: createCoinblocksCoindaysSatblocksSatdaysSentPattern(client, acc),
addrCount: createMetricPattern1(client, _m(acc, 'addr_count')),
addrCount30dChange: createMetricPattern4(client, _m(acc, 'addr_count_30d_change')),
costBasis: createMaxMinPattern(client, acc),
outputs: createUtxoPattern(client, _m(acc, 'utxo_count')),
realized: createCapCapitulationInvestorLossMvrvNegNetPeakProfitRealizedSellSentSoprTotalValuePattern(client, acc),
relative: createInvestedNegNetNuplSupplyUnrealizedPattern(client, acc),
supply: create_30dHalvedTotalPattern(client, acc),
unrealized: createGreedInvestedInvestorNegNetPainSupplyTotalUnrealizedPattern(client, acc),
};
}
/**
* @typedef {Object} AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern
* @property {MetricPattern1<StoredU64>} all
* @property {MetricPattern1<StoredU64>} p2a
* @property {MetricPattern1<StoredU64>} p2pk33
* @property {MetricPattern1<StoredU64>} p2pk65
* @property {MetricPattern1<StoredU64>} p2pkh
* @property {MetricPattern1<StoredU64>} p2sh
* @property {MetricPattern1<StoredU64>} p2tr
* @property {MetricPattern1<StoredU64>} p2wpkh
* @property {MetricPattern1<StoredU64>} p2wsh
* @property {_30dCountPattern} all
* @property {_30dCountPattern} p2a
* @property {_30dCountPattern} p2pk33
* @property {_30dCountPattern} p2pk65
* @property {_30dCountPattern} p2pkh
* @property {_30dCountPattern} p2sh
* @property {_30dCountPattern} p2tr
* @property {_30dCountPattern} p2wpkh
* @property {_30dCountPattern} p2wsh
*/
/**
@@ -2634,15 +2667,15 @@ function createAverageCumulativeMaxMedianMinPct10Pct25Pct75Pct90SumPattern2(clie
*/
function createAllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern(client, acc) {
return {
all: createMetricPattern1(client, acc),
p2a: createMetricPattern1(client, _p('p2a', acc)),
p2pk33: createMetricPattern1(client, _p('p2pk33', acc)),
p2pk65: createMetricPattern1(client, _p('p2pk65', acc)),
p2pkh: createMetricPattern1(client, _p('p2pkh', acc)),
p2sh: createMetricPattern1(client, _p('p2sh', acc)),
p2tr: createMetricPattern1(client, _p('p2tr', acc)),
p2wpkh: createMetricPattern1(client, _p('p2wpkh', acc)),
p2wsh: createMetricPattern1(client, _p('p2wsh', acc)),
all: create_30dCountPattern(client, acc),
p2a: create_30dCountPattern(client, _p('p2a', acc)),
p2pk33: create_30dCountPattern(client, _p('p2pk33', acc)),
p2pk65: create_30dCountPattern(client, _p('p2pk65', acc)),
p2pkh: create_30dCountPattern(client, _p('p2pkh', acc)),
p2sh: create_30dCountPattern(client, _p('p2sh', acc)),
p2tr: create_30dCountPattern(client, _p('p2tr', acc)),
p2wpkh: create_30dCountPattern(client, _p('p2wpkh', acc)),
p2wsh: create_30dCountPattern(client, _p('p2wsh', acc)),
};
}
@@ -2716,37 +2749,6 @@ function createAverageBaseMaxMedianMinPct10Pct25Pct75Pct90Pattern(client, acc) {
};
}
/**
* @typedef {Object} ActivityAddrCostOutputsRealizedRelativeSupplyUnrealizedPattern
* @property {CoinblocksCoindaysSatblocksSatdaysSentPattern} activity
* @property {MetricPattern1<StoredU64>} addrCount
* @property {MaxMinPattern} costBasis
* @property {UtxoPattern} outputs
* @property {CapCapitulationInvestorLossMvrvNegNetPeakProfitRealizedSellSentSoprTotalValuePattern} realized
* @property {InvestedNegNetNuplSupplyUnrealizedPattern} relative
* @property {_30dHalvedTotalPattern} supply
* @property {GreedInvestedInvestorNegNetPainSupplyTotalUnrealizedPattern} unrealized
*/
/**
* Create a ActivityAddrCostOutputsRealizedRelativeSupplyUnrealizedPattern pattern node
* @param {BrkClientBase} client
* @param {string} acc - Accumulated metric name
* @returns {ActivityAddrCostOutputsRealizedRelativeSupplyUnrealizedPattern}
*/
function createActivityAddrCostOutputsRealizedRelativeSupplyUnrealizedPattern(client, acc) {
return {
activity: createCoinblocksCoindaysSatblocksSatdaysSentPattern(client, acc),
addrCount: createMetricPattern1(client, _m(acc, 'addr_count')),
costBasis: createMaxMinPattern(client, acc),
outputs: createUtxoPattern(client, _m(acc, 'utxo_count')),
realized: createCapCapitulationInvestorLossMvrvNegNetPeakProfitRealizedSellSentSoprTotalValuePattern(client, acc),
relative: createInvestedNegNetNuplSupplyUnrealizedPattern(client, acc),
supply: create_30dHalvedTotalPattern(client, acc),
unrealized: createGreedInvestedInvestorNegNetPainSupplyTotalUnrealizedPattern(client, acc),
};
}
/**
* @typedef {Object} _10y2y3y4y5y6y8yPattern
* @property {MetricPattern4<StoredF32>} _10y
@@ -3224,6 +3226,25 @@ function createBitcoinDollarsSatsPattern3(client, acc) {
};
}
/**
* @typedef {Object} _30dCountPattern
* @property {MetricPattern4<StoredF64>} _30dChange
* @property {MetricPattern1<StoredU64>} count
*/
/**
* Create a _30dCountPattern pattern node
* @param {BrkClientBase} client
* @param {string} acc - Accumulated metric name
* @returns {_30dCountPattern}
*/
function create_30dCountPattern(client, acc) {
return {
_30dChange: createMetricPattern4(client, _m(acc, '30d_change')),
count: createMetricPattern1(client, acc),
};
}
/**
* @typedef {Object} DollarsSatsPattern
* @property {MetricPattern1<Dollars>} dollars
@@ -3300,6 +3321,25 @@ function createSdSmaPattern(client, acc) {
};
}
/**
* @typedef {Object} UtxoPattern
* @property {MetricPattern1<StoredU64>} utxoCount
* @property {MetricPattern4<StoredF64>} utxoCount30dChange
*/
/**
* Create a UtxoPattern pattern node
* @param {BrkClientBase} client
* @param {string} acc - Accumulated metric name
* @returns {UtxoPattern}
*/
function createUtxoPattern(client, acc) {
return {
utxoCount: createMetricPattern1(client, acc),
utxoCount30dChange: createMetricPattern4(client, _m(acc, '30d_change')),
};
}
/**
* @template T
* @typedef {Object} CumulativeSumPattern
@@ -3380,23 +3420,6 @@ function createRatioPattern2(client, acc) {
};
}
/**
* @typedef {Object} UtxoPattern
* @property {MetricPattern1<StoredU64>} utxoCount
*/
/**
* Create a UtxoPattern pattern node
* @param {BrkClientBase} client
* @param {string} acc - Accumulated metric name
* @returns {UtxoPattern}
*/
function createUtxoPattern(client, acc) {
return {
utxoCount: createMetricPattern1(client, acc),
};
}
// Catalog tree typedefs
/**
@@ -3473,6 +3496,8 @@ function createUtxoPattern(client, acc) {
* @property {MetricPattern4<StoredF32>} hashRate1mSma
* @property {MetricPattern4<StoredF32>} hashRate2mSma
* @property {MetricPattern4<StoredF32>} hashRate1ySma
* @property {MetricPattern1<StoredF64>} hashRateAth
* @property {MetricPattern1<StoredF32>} hashRateDrawdown
* @property {MetricPattern1<StoredF32>} hashPriceThs
* @property {MetricPattern1<StoredF32>} hashPriceThsMin
* @property {MetricPattern1<StoredF32>} hashPricePhs
@@ -3755,7 +3780,7 @@ function createUtxoPattern(client, acc) {
/**
* @typedef {Object} MetricsTree_Cointime_ReserveRisk
* @property {MetricPattern6<StoredF64>} vocdd365dSma
* @property {MetricPattern6<StoredF64>} vocdd365dMedian
* @property {MetricPattern6<StoredF64>} hodlBank
* @property {MetricPattern4<StoredF64>} reserveRisk
*/
@@ -4478,7 +4503,7 @@ function createUtxoPattern(client, acc) {
* @property {AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern} addrCount
* @property {AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern} emptyAddrCount
* @property {MetricsTree_Distribution_AddressActivity} addressActivity
* @property {AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern} totalAddrCount
* @property {MetricsTree_Distribution_TotalAddrCount} totalAddrCount
* @property {MetricsTree_Distribution_NewAddrCount} newAddrCount
* @property {MetricsTree_Distribution_GrowthRate} growthRate
* @property {MetricPattern31<FundedAddressIndex>} fundedaddressindex
@@ -4816,6 +4841,19 @@ function createUtxoPattern(client, acc) {
* @property {BalanceBothReactivatedReceivingSendingPattern} p2a
*/
/**
* @typedef {Object} MetricsTree_Distribution_TotalAddrCount
* @property {MetricPattern1<StoredU64>} all
* @property {MetricPattern1<StoredU64>} p2pk65
* @property {MetricPattern1<StoredU64>} p2pk33
* @property {MetricPattern1<StoredU64>} p2pkh
* @property {MetricPattern1<StoredU64>} p2sh
* @property {MetricPattern1<StoredU64>} p2wpkh
* @property {MetricPattern1<StoredU64>} p2wsh
* @property {MetricPattern1<StoredU64>} p2tr
* @property {MetricPattern1<StoredU64>} p2a
*/
/**
* @typedef {Object} MetricsTree_Distribution_NewAddrCount
* @property {AverageBaseCumulativeMaxMedianMinPct10Pct25Pct75Pct90SumPattern2<StoredU64>} all
@@ -4875,7 +4913,7 @@ function createUtxoPattern(client, acc) {
* @extends BrkClientBase
*/
class BrkClient extends BrkClientBase {
VERSION = "v0.1.2";
VERSION = "v0.1.3";
INDEXES = /** @type {const} */ ([
"dateindex",
@@ -5840,6 +5878,8 @@ class BrkClient extends BrkClientBase {
hashRate1mSma: createMetricPattern4(this, 'hash_rate_1m_sma'),
hashRate2mSma: createMetricPattern4(this, 'hash_rate_2m_sma'),
hashRate1ySma: createMetricPattern4(this, 'hash_rate_1y_sma'),
hashRateAth: createMetricPattern1(this, 'hash_rate_ath'),
hashRateDrawdown: createMetricPattern1(this, 'hash_rate_drawdown'),
hashPriceThs: createMetricPattern1(this, 'hash_price_ths'),
hashPriceThsMin: createMetricPattern1(this, 'hash_price_ths_min'),
hashPricePhs: createMetricPattern1(this, 'hash_price_phs'),
@@ -6047,7 +6087,7 @@ class BrkClient extends BrkClientBase {
cointimeAdjTxUsdVelocity: createMetricPattern4(this, 'cointime_adj_tx_usd_velocity'),
},
reserveRisk: {
vocdd365dSma: createMetricPattern6(this, 'vocdd_365d_sma'),
vocdd365dMedian: createMetricPattern6(this, 'vocdd_365d_median'),
hodlBank: createMetricPattern6(this, 'hodl_bank'),
reserveRisk: createMetricPattern4(this, 'reserve_risk'),
},
@@ -6886,7 +6926,17 @@ class BrkClient extends BrkClientBase {
p2tr: createBalanceBothReactivatedReceivingSendingPattern(this, 'p2tr_address_activity'),
p2a: createBalanceBothReactivatedReceivingSendingPattern(this, 'p2a_address_activity'),
},
totalAddrCount: createAllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern(this, 'total_addr_count'),
totalAddrCount: {
all: createMetricPattern1(this, 'total_addr_count'),
p2pk65: createMetricPattern1(this, 'p2pk65_total_addr_count'),
p2pk33: createMetricPattern1(this, 'p2pk33_total_addr_count'),
p2pkh: createMetricPattern1(this, 'p2pkh_total_addr_count'),
p2sh: createMetricPattern1(this, 'p2sh_total_addr_count'),
p2wpkh: createMetricPattern1(this, 'p2wpkh_total_addr_count'),
p2wsh: createMetricPattern1(this, 'p2wsh_total_addr_count'),
p2tr: createMetricPattern1(this, 'p2tr_total_addr_count'),
p2a: createMetricPattern1(this, 'p2a_total_addr_count'),
},
newAddrCount: {
all: createAverageBaseCumulativeMaxMedianMinPct10Pct25Pct75Pct90SumPattern2(this, 'new_addr_count'),
p2pk65: createAverageBaseCumulativeMaxMedianMinPct10Pct25Pct75Pct90SumPattern2(this, 'p2pk65_new_addr_count'),

View File

@@ -2413,20 +2413,35 @@ class AverageCumulativeMaxMedianMinPct10Pct25Pct75Pct90SumPattern2(Generic[T]):
self.pct90: MetricPattern11[T] = MetricPattern11(client, _m(acc, 'pct90'))
self.sum: MetricPattern1[T] = MetricPattern1(client, _m(acc, 'sum'))
class ActivityAddrCostOutputsRealizedRelativeSupplyUnrealizedPattern:
"""Pattern struct for repeated tree structure."""
def __init__(self, client: BrkClientBase, acc: str):
"""Create pattern node with accumulated metric name."""
self.activity: CoinblocksCoindaysSatblocksSatdaysSentPattern = CoinblocksCoindaysSatblocksSatdaysSentPattern(client, acc)
self.addr_count: MetricPattern1[StoredU64] = MetricPattern1(client, _m(acc, 'addr_count'))
self.addr_count_30d_change: MetricPattern4[StoredF64] = MetricPattern4(client, _m(acc, 'addr_count_30d_change'))
self.cost_basis: MaxMinPattern = MaxMinPattern(client, acc)
self.outputs: UtxoPattern = UtxoPattern(client, _m(acc, 'utxo_count'))
self.realized: CapCapitulationInvestorLossMvrvNegNetPeakProfitRealizedSellSentSoprTotalValuePattern = CapCapitulationInvestorLossMvrvNegNetPeakProfitRealizedSellSentSoprTotalValuePattern(client, acc)
self.relative: InvestedNegNetNuplSupplyUnrealizedPattern = InvestedNegNetNuplSupplyUnrealizedPattern(client, acc)
self.supply: _30dHalvedTotalPattern = _30dHalvedTotalPattern(client, acc)
self.unrealized: GreedInvestedInvestorNegNetPainSupplyTotalUnrealizedPattern = GreedInvestedInvestorNegNetPainSupplyTotalUnrealizedPattern(client, acc)
class AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern:
"""Pattern struct for repeated tree structure."""
def __init__(self, client: BrkClientBase, acc: str):
"""Create pattern node with accumulated metric name."""
self.all: MetricPattern1[StoredU64] = MetricPattern1(client, acc)
self.p2a: MetricPattern1[StoredU64] = MetricPattern1(client, _p('p2a', acc))
self.p2pk33: MetricPattern1[StoredU64] = MetricPattern1(client, _p('p2pk33', acc))
self.p2pk65: MetricPattern1[StoredU64] = MetricPattern1(client, _p('p2pk65', acc))
self.p2pkh: MetricPattern1[StoredU64] = MetricPattern1(client, _p('p2pkh', acc))
self.p2sh: MetricPattern1[StoredU64] = MetricPattern1(client, _p('p2sh', acc))
self.p2tr: MetricPattern1[StoredU64] = MetricPattern1(client, _p('p2tr', acc))
self.p2wpkh: MetricPattern1[StoredU64] = MetricPattern1(client, _p('p2wpkh', acc))
self.p2wsh: MetricPattern1[StoredU64] = MetricPattern1(client, _p('p2wsh', acc))
self.all: _30dCountPattern = _30dCountPattern(client, acc)
self.p2a: _30dCountPattern = _30dCountPattern(client, _p('p2a', acc))
self.p2pk33: _30dCountPattern = _30dCountPattern(client, _p('p2pk33', acc))
self.p2pk65: _30dCountPattern = _30dCountPattern(client, _p('p2pk65', acc))
self.p2pkh: _30dCountPattern = _30dCountPattern(client, _p('p2pkh', acc))
self.p2sh: _30dCountPattern = _30dCountPattern(client, _p('p2sh', acc))
self.p2tr: _30dCountPattern = _30dCountPattern(client, _p('p2tr', acc))
self.p2wpkh: _30dCountPattern = _30dCountPattern(client, _p('p2wpkh', acc))
self.p2wsh: _30dCountPattern = _30dCountPattern(client, _p('p2wsh', acc))
class AverageMaxMedianMinPct10Pct25Pct75Pct90TxindexPattern(Generic[T]):
"""Pattern struct for repeated tree structure."""
@@ -2458,20 +2473,6 @@ class AverageBaseMaxMedianMinPct10Pct25Pct75Pct90Pattern(Generic[T]):
self.pct75: MetricPattern6[T] = MetricPattern6(client, _m(acc, 'pct75'))
self.pct90: MetricPattern6[T] = MetricPattern6(client, _m(acc, 'pct90'))
class ActivityAddrCostOutputsRealizedRelativeSupplyUnrealizedPattern:
"""Pattern struct for repeated tree structure."""
def __init__(self, client: BrkClientBase, acc: str):
"""Create pattern node with accumulated metric name."""
self.activity: CoinblocksCoindaysSatblocksSatdaysSentPattern = CoinblocksCoindaysSatblocksSatdaysSentPattern(client, acc)
self.addr_count: MetricPattern1[StoredU64] = MetricPattern1(client, _m(acc, 'addr_count'))
self.cost_basis: MaxMinPattern = MaxMinPattern(client, acc)
self.outputs: UtxoPattern = UtxoPattern(client, _m(acc, 'utxo_count'))
self.realized: CapCapitulationInvestorLossMvrvNegNetPeakProfitRealizedSellSentSoprTotalValuePattern = CapCapitulationInvestorLossMvrvNegNetPeakProfitRealizedSellSentSoprTotalValuePattern(client, acc)
self.relative: InvestedNegNetNuplSupplyUnrealizedPattern = InvestedNegNetNuplSupplyUnrealizedPattern(client, acc)
self.supply: _30dHalvedTotalPattern = _30dHalvedTotalPattern(client, acc)
self.unrealized: GreedInvestedInvestorNegNetPainSupplyTotalUnrealizedPattern = GreedInvestedInvestorNegNetPainSupplyTotalUnrealizedPattern(client, acc)
class _10y2y3y4y5y6y8yPattern:
"""Pattern struct for repeated tree structure."""
@@ -2681,6 +2682,14 @@ class BitcoinDollarsSatsPattern3:
self.dollars: CumulativeSumPattern[Dollars] = CumulativeSumPattern(client, _m(acc, 'usd'))
self.sats: CumulativeSumPattern[Sats] = CumulativeSumPattern(client, acc)
class _30dCountPattern:
"""Pattern struct for repeated tree structure."""
def __init__(self, client: BrkClientBase, acc: str):
"""Create pattern node with accumulated metric name."""
self._30d_change: MetricPattern4[StoredF64] = MetricPattern4(client, _m(acc, '30d_change'))
self.count: MetricPattern1[StoredU64] = MetricPattern1(client, acc)
class DollarsSatsPattern:
"""Pattern struct for repeated tree structure."""
@@ -2713,6 +2722,14 @@ class SdSmaPattern:
self.sd: MetricPattern4[StoredF32] = MetricPattern4(client, _m(acc, 'sd'))
self.sma: MetricPattern4[StoredF32] = MetricPattern4(client, _m(acc, 'sma'))
class UtxoPattern:
"""Pattern struct for repeated tree structure."""
def __init__(self, client: BrkClientBase, acc: str):
"""Create pattern node with accumulated metric name."""
self.utxo_count: MetricPattern1[StoredU64] = MetricPattern1(client, acc)
self.utxo_count_30d_change: MetricPattern4[StoredF64] = MetricPattern4(client, _m(acc, '30d_change'))
class CumulativeSumPattern(Generic[T]):
"""Pattern struct for repeated tree structure."""
@@ -2744,13 +2761,6 @@ class RatioPattern2:
"""Create pattern node with accumulated metric name."""
self.ratio: MetricPattern4[StoredF32] = MetricPattern4(client, acc)
class UtxoPattern:
"""Pattern struct for repeated tree structure."""
def __init__(self, client: BrkClientBase, acc: str):
"""Create pattern node with accumulated metric name."""
self.utxo_count: MetricPattern1[StoredU64] = MetricPattern1(client, acc)
# Metrics tree classes
class MetricsTree_Blocks_Difficulty:
@@ -2796,6 +2806,8 @@ class MetricsTree_Blocks_Mining:
self.hash_rate_1m_sma: MetricPattern4[StoredF32] = MetricPattern4(client, 'hash_rate_1m_sma')
self.hash_rate_2m_sma: MetricPattern4[StoredF32] = MetricPattern4(client, 'hash_rate_2m_sma')
self.hash_rate_1y_sma: MetricPattern4[StoredF32] = MetricPattern4(client, 'hash_rate_1y_sma')
self.hash_rate_ath: MetricPattern1[StoredF64] = MetricPattern1(client, 'hash_rate_ath')
self.hash_rate_drawdown: MetricPattern1[StoredF32] = MetricPattern1(client, 'hash_rate_drawdown')
self.hash_price_ths: MetricPattern1[StoredF32] = MetricPattern1(client, 'hash_price_ths')
self.hash_price_ths_min: MetricPattern1[StoredF32] = MetricPattern1(client, 'hash_price_ths_min')
self.hash_price_phs: MetricPattern1[StoredF32] = MetricPattern1(client, 'hash_price_phs')
@@ -3113,7 +3125,7 @@ class MetricsTree_Cointime_ReserveRisk:
"""Metrics tree node."""
def __init__(self, client: BrkClientBase, base_path: str = ''):
self.vocdd_365d_sma: MetricPattern6[StoredF64] = MetricPattern6(client, 'vocdd_365d_sma')
self.vocdd_365d_median: MetricPattern6[StoredF64] = MetricPattern6(client, 'vocdd_365d_median')
self.hodl_bank: MetricPattern6[StoredF64] = MetricPattern6(client, 'hodl_bank')
self.reserve_risk: MetricPattern4[StoredF64] = MetricPattern4(client, 'reserve_risk')
@@ -4241,6 +4253,20 @@ class MetricsTree_Distribution_AddressActivity:
self.p2tr: BalanceBothReactivatedReceivingSendingPattern = BalanceBothReactivatedReceivingSendingPattern(client, 'p2tr_address_activity')
self.p2a: BalanceBothReactivatedReceivingSendingPattern = BalanceBothReactivatedReceivingSendingPattern(client, 'p2a_address_activity')
class MetricsTree_Distribution_TotalAddrCount:
"""Metrics tree node."""
def __init__(self, client: BrkClientBase, base_path: str = ''):
self.all: MetricPattern1[StoredU64] = MetricPattern1(client, 'total_addr_count')
self.p2pk65: MetricPattern1[StoredU64] = MetricPattern1(client, 'p2pk65_total_addr_count')
self.p2pk33: MetricPattern1[StoredU64] = MetricPattern1(client, 'p2pk33_total_addr_count')
self.p2pkh: MetricPattern1[StoredU64] = MetricPattern1(client, 'p2pkh_total_addr_count')
self.p2sh: MetricPattern1[StoredU64] = MetricPattern1(client, 'p2sh_total_addr_count')
self.p2wpkh: MetricPattern1[StoredU64] = MetricPattern1(client, 'p2wpkh_total_addr_count')
self.p2wsh: MetricPattern1[StoredU64] = MetricPattern1(client, 'p2wsh_total_addr_count')
self.p2tr: MetricPattern1[StoredU64] = MetricPattern1(client, 'p2tr_total_addr_count')
self.p2a: MetricPattern1[StoredU64] = MetricPattern1(client, 'p2a_total_addr_count')
class MetricsTree_Distribution_NewAddrCount:
"""Metrics tree node."""
@@ -4281,7 +4307,7 @@ class MetricsTree_Distribution:
self.addr_count: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern = AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern(client, 'addr_count')
self.empty_addr_count: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern = AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern(client, 'empty_addr_count')
self.address_activity: MetricsTree_Distribution_AddressActivity = MetricsTree_Distribution_AddressActivity(client)
self.total_addr_count: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern = AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern(client, 'total_addr_count')
self.total_addr_count: MetricsTree_Distribution_TotalAddrCount = MetricsTree_Distribution_TotalAddrCount(client)
self.new_addr_count: MetricsTree_Distribution_NewAddrCount = MetricsTree_Distribution_NewAddrCount(client)
self.growth_rate: MetricsTree_Distribution_GrowthRate = MetricsTree_Distribution_GrowthRate(client)
self.fundedaddressindex: MetricPattern31[FundedAddressIndex] = MetricPattern31(client, 'fundedaddressindex')
@@ -4342,7 +4368,7 @@ class MetricsTree:
class BrkClient(BrkClientBase):
"""Main BRK client with metrics tree and API methods."""
VERSION = "v0.1.2"
VERSION = "v0.1.3"
INDEXES = [
"dateindex",

View File

@@ -241,13 +241,14 @@ export function createChart({ parent, id: chartId, brk, fitContent }) {
);
// Debounced range persistence
ichart.timeScale().subscribeVisibleLogicalRangeChange(
debounce((range) => {
if (range && range.from < range.to) {
setRange({ from: range.from, to: range.to });
}
}, 100),
);
const debouncedSetRange = debounce((/** @type {Range | null} */ range) => {
if (range && range.from < range.to) {
setRange({ from: range.from, to: range.to });
}
}, 100);
// Cancel pending range saves on index change to prevent saving stale ranges to wrong index
index.onChange.add(() => debouncedSetRange.cancel());
ichart.timeScale().subscribeVisibleLogicalRangeChange(debouncedSetRange);
function applyColors() {
const defaultColor = colors.default();
@@ -734,8 +735,8 @@ export function createChart({ parent, id: chartId, brk, fitContent }) {
defaultActive,
options,
}) {
const upColor = customColors?.[0] ?? colors.green;
const downColor = customColors?.[1] ?? colors.red;
const upColor = customColors?.[0] ?? colors.bi.profitLoss[0];
const downColor = customColors?.[1] ?? colors.bi.profitLoss[1];
/** @type {CandlestickISeries} */
const candlestickISeries = /** @type {any} */ (
@@ -875,7 +876,7 @@ export function createChart({ parent, id: chartId, brk, fitContent }) {
metric,
name,
key,
color = [colors.green, colors.red],
color = colors.bi.profitLoss,
order,
unit,
paneIndex = 0,
@@ -1188,8 +1189,8 @@ export function createChart({ parent, id: chartId, brk, fitContent }) {
unit,
paneIndex: _paneIndex,
defaultActive,
topColor = colors.green,
bottomColor = colors.red,
topColor = colors.bi.profitLoss[0],
bottomColor = colors.bi.profitLoss[1],
options,
}) {
const paneIndex = _paneIndex ?? 0;
@@ -1351,7 +1352,7 @@ export function createChart({ parent, id: chartId, brk, fitContent }) {
const options = blueprint.options;
const indexes = Object.keys(blueprint.metric.by);
const defaultColor = unit === Unit.usd ? colors.green : colors.orange;
const defaultColor = unit === Unit.usd ? colors.usd : colors.bitcoin;
if (indexes.includes(idx)) {
switch (blueprint.type) {

View File

@@ -27,7 +27,7 @@ export function createCointimeSection() {
{
metric: all.realized.realizedCap,
name: "Realized",
color: colors.orange,
color: colors.realized,
},
]);
@@ -36,47 +36,47 @@ export function createCointimeSection() {
pricePattern: pricing.trueMarketMean,
ratio: pricing.trueMarketMeanRatio,
name: "True Market Mean",
color: colors.blue,
color: colors.trueMarketMean,
},
{
pricePattern: pricing.vaultedPrice,
ratio: pricing.vaultedPriceRatio,
name: "Vaulted",
color: colors.lime,
color: colors.vaulted,
},
{
pricePattern: pricing.activePrice,
ratio: pricing.activePriceRatio,
name: "Active",
color: colors.rose,
color: colors.active,
},
{
pricePattern: pricing.cointimePrice,
ratio: pricing.cointimePriceRatio,
name: "Cointime",
color: colors.yellow,
color: colors.cointime,
},
]);
const caps = /** @type {const} */ ([
{ metric: cap.vaultedCap, name: "Vaulted", color: colors.lime },
{ metric: cap.activeCap, name: "Active", color: colors.rose },
{ metric: cap.cointimeCap, name: "Cointime", color: colors.yellow },
{ metric: cap.investorCap, name: "Investor", color: colors.fuchsia },
{ metric: cap.thermoCap, name: "Thermo", color: colors.emerald },
{ metric: cap.vaultedCap, name: "Vaulted", color: colors.vaulted },
{ metric: cap.activeCap, name: "Active", color: colors.active },
{ metric: cap.cointimeCap, name: "Cointime", color: colors.cointime },
{ metric: cap.investorCap, name: "Investor", color: colors.investor },
{ metric: cap.thermoCap, name: "Thermo", color: colors.thermo },
]);
const supplyBreakdown = /** @type {const} */ ([
{ pattern: all.supply.total, name: "Total", color: colors.orange },
{ pattern: all.supply.total, name: "Total", color: colors.bitcoin },
{
pattern: cointimeSupply.vaultedSupply,
name: "Vaulted",
color: colors.lime,
color: colors.vaulted,
},
{
pattern: cointimeSupply.activeSupply,
name: "Active",
color: colors.rose,
color: colors.active,
},
]);
@@ -85,19 +85,19 @@ export function createCointimeSection() {
pattern: all.activity.coinblocksDestroyed,
name: "Destroyed",
title: "Coinblocks Destroyed",
color: colors.red,
color: colors.destroyed,
},
{
pattern: activity.coinblocksCreated,
name: "Created",
title: "Coinblocks Created",
color: colors.orange,
color: colors.created,
},
{
pattern: activity.coinblocksStored,
name: "Stored",
title: "Coinblocks Stored",
color: colors.green,
color: colors.stored,
},
]);
@@ -107,19 +107,19 @@ export function createCointimeSection() {
pattern: value.cointimeValueCreated,
name: "Created",
title: "Cointime Value Created",
color: colors.orange,
color: colors.created,
},
{
pattern: value.cointimeValueDestroyed,
name: "Destroyed",
title: "Cointime Value Destroyed",
color: colors.red,
color: colors.destroyed,
},
{
pattern: value.cointimeValueStored,
name: "Stored",
title: "Cointime Value Stored",
color: colors.green,
color: colors.stored,
},
]);
@@ -127,7 +127,7 @@ export function createCointimeSection() {
pattern: value.vocdd,
name: "VOCDD",
title: "Value of Coin Days Destroyed",
color: colors.purple,
color: colors.vocdd,
});
return {
@@ -144,12 +144,12 @@ export function createCointimeSection() {
price({
metric: all.realized.realizedPrice,
name: "Realized",
color: colors.orange,
color: colors.realized,
}),
price({
metric: all.realized.investorPrice,
name: "Investor",
color: colors.fuchsia,
color: colors.investor,
}),
...prices.map(({ pricePattern, name, color }) =>
price({ metric: pricePattern, name, color }),
@@ -168,7 +168,7 @@ export function createCointimeSection() {
price({
metric: all.realized.realizedPrice,
name: "Realized",
color: colors.orange,
color: colors.realized,
defaultActive: false,
}),
],
@@ -228,19 +228,19 @@ export function createCointimeSection() {
line({
metric: activity.liveliness,
name: "Liveliness",
color: colors.rose,
color: colors.liveliness,
unit: Unit.ratio,
}),
line({
metric: activity.vaultedness,
name: "Vaultedness",
color: colors.lime,
color: colors.vaulted,
unit: Unit.ratio,
}),
line({
metric: activity.activityToVaultednessRatio,
name: "L/V Ratio",
color: colors.purple,
color: colors.activity,
unit: Unit.ratio,
defaultActive: false,
}),
@@ -394,9 +394,9 @@ export function createCointimeSection() {
unit: Unit.usd,
}),
line({
metric: reserveRisk.vocdd365dSma,
name: "365d SMA",
color: colors.cyan,
metric: reserveRisk.vocdd365dMedian,
name: "365d Median",
color: colors.ma._1y,
unit: Unit.usd,
}),
],
@@ -429,7 +429,7 @@ export function createCointimeSection() {
line({
metric: reserveRisk.reserveRisk,
name: "Ratio",
color: colors.orange,
color: colors.reserveRisk,
unit: Unit.ratio,
}),
],
@@ -441,7 +441,7 @@ export function createCointimeSection() {
line({
metric: reserveRisk.hodlBank,
name: "Value",
color: colors.blue,
color: colors.hodlBank,
unit: Unit.usd,
}),
],
@@ -460,13 +460,13 @@ export function createCointimeSection() {
dots({
metric: supply.inflation,
name: "Base",
color: colors.orange,
color: colors.base,
unit: Unit.percentage,
}),
dots({
metric: adjusted.cointimeAdjInflationRate,
name: "Cointime-Adjusted",
color: colors.purple,
color: colors.adjusted,
unit: Unit.percentage,
}),
],
@@ -481,13 +481,13 @@ export function createCointimeSection() {
line({
metric: supply.velocity.btc,
name: "Base",
color: colors.orange,
color: colors.base,
unit: Unit.ratio,
}),
line({
metric: adjusted.cointimeAdjTxBtcVelocity,
name: "Cointime-Adjusted",
color: colors.red,
color: colors.adjusted,
unit: Unit.ratio,
}),
],
@@ -499,13 +499,13 @@ export function createCointimeSection() {
line({
metric: supply.velocity.usd,
name: "Base",
color: colors.emerald,
color: colors.thermo,
unit: Unit.ratio,
}),
line({
metric: adjusted.cointimeAdjTxUsdVelocity,
name: "Cointime-Adjusted",
color: colors.lime,
color: colors.vaulted,
unit: Unit.ratio,
}),
],

View File

@@ -242,52 +242,52 @@ function createRealizedPnlSection(args, title) {
line({
metric: realized.realizedProfit.sum,
name: "Profit",
color: colors.green,
color: colors.profit,
unit: Unit.usd,
}),
line({
metric: realized.realizedProfit7dEma,
name: "Profit 7d EMA",
color: colors.green,
color: colors.profit,
unit: Unit.usd,
}),
line({
metric: realized.realizedProfit.cumulative,
name: "Profit Cumulative",
color: colors.green,
color: colors.profit,
unit: Unit.usd,
defaultActive: false,
}),
line({
metric: realized.realizedLoss.sum,
name: "Loss",
color: colors.red,
color: colors.loss,
unit: Unit.usd,
}),
line({
metric: realized.realizedLoss7dEma,
name: "Loss 7d EMA",
color: colors.red,
color: colors.loss,
unit: Unit.usd,
}),
line({
metric: realized.realizedLoss.cumulative,
name: "Loss Cumulative",
color: colors.red,
color: colors.loss,
unit: Unit.usd,
defaultActive: false,
}),
line({
metric: realized.negRealizedLoss.sum,
name: "Negative Loss",
color: colors.red,
color: colors.loss,
unit: Unit.usd,
defaultActive: false,
}),
line({
metric: realized.negRealizedLoss.cumulative,
name: "Negative Loss Cumulative",
color: colors.red,
color: colors.loss,
unit: Unit.usd,
defaultActive: false,
}),
@@ -301,26 +301,26 @@ function createRealizedPnlSection(args, title) {
baseline({
metric: realized.realizedProfitRelToRealizedCap.sum,
name: "Profit",
color: colors.green,
color: colors.profit,
unit: Unit.pctRcap,
}),
baseline({
metric: realized.realizedProfitRelToRealizedCap.cumulative,
name: "Profit Cumulative",
color: colors.green,
color: colors.profit,
unit: Unit.pctRcap,
defaultActive: false,
}),
baseline({
metric: realized.realizedLossRelToRealizedCap.sum,
name: "Loss",
color: colors.red,
color: colors.loss,
unit: Unit.pctRcap,
}),
baseline({
metric: realized.realizedLossRelToRealizedCap.cumulative,
name: "Loss Cumulative",
color: colors.red,
color: colors.loss,
unit: Unit.pctRcap,
defaultActive: false,
}),
@@ -390,7 +390,7 @@ function createRealizedPnlSection(args, title) {
name: "SOPR",
title: title("SOPR"),
bottom: [
...createSingleSoprSeries(colors, args.tree),
...createSingleSoprSeries(args.tree),
priceLine({
unit: Unit.ratio,
number: 1,
@@ -400,7 +400,7 @@ function createRealizedPnlSection(args, title) {
{
name: "Sell Side Risk",
title: title("Sell Side Risk Ratio"),
bottom: createSingleSellSideRiskSeries(colors, args.tree),
bottom: createSingleSellSideRiskSeries(args.tree),
},
{
name: "Value",
@@ -408,17 +408,17 @@ function createRealizedPnlSection(args, title) {
{
name: "Created & Destroyed",
title: title("Value Created & Destroyed"),
bottom: createSingleValueCreatedDestroyedSeries(colors, args.tree),
bottom: createSingleValueCreatedDestroyedSeries(args.tree),
},
{
name: "Breakdown",
title: title("Value Flow Breakdown"),
bottom: createSingleValueFlowBreakdownSeries(colors, args.tree),
bottom: createSingleValueFlowBreakdownSeries(args.tree),
},
{
name: "Flow",
title: title("Capitulation & Profit Flow"),
bottom: createSingleCapitulationProfitFlowSeries(colors, args.tree),
bottom: createSingleCapitulationProfitFlowSeries(args.tree),
},
],
},
@@ -429,20 +429,20 @@ function createRealizedPnlSection(args, title) {
line({
metric: realized.peakRegret.sum,
name: "Sum",
color: colors.red,
color: colors.loss,
unit: Unit.usd,
}),
line({
metric: realized.peakRegret.cumulative,
name: "Cumulative",
color: colors.red,
color: colors.loss,
unit: Unit.usd,
defaultActive: false,
}),
baseline({
metric: realized.peakRegretRelToRealizedCap,
name: "Rel. to Realized Cap",
color: colors.orange,
color: colors.realized,
unit: Unit.pctRcap,
}),
],
@@ -457,39 +457,39 @@ function createRealizedPnlSection(args, title) {
line({
metric: realized.sentInProfit.bitcoin.sum,
name: "Sum",
color: colors.green,
color: colors.profit,
unit: Unit.btc,
}),
line({
metric: realized.sentInProfit.bitcoin.cumulative,
name: "Cumulative",
color: colors.green,
color: colors.profit,
unit: Unit.btc,
defaultActive: false,
}),
line({
metric: realized.sentInProfit.sats.sum,
name: "Sum",
color: colors.green,
color: colors.profit,
unit: Unit.sats,
}),
line({
metric: realized.sentInProfit.sats.cumulative,
name: "Cumulative",
color: colors.green,
color: colors.profit,
unit: Unit.sats,
defaultActive: false,
}),
line({
metric: realized.sentInProfit.dollars.sum,
name: "Sum",
color: colors.green,
color: colors.profit,
unit: Unit.usd,
}),
line({
metric: realized.sentInProfit.dollars.cumulative,
name: "Cumulative",
color: colors.green,
color: colors.profit,
unit: Unit.usd,
defaultActive: false,
}),
@@ -502,39 +502,39 @@ function createRealizedPnlSection(args, title) {
line({
metric: realized.sentInLoss.bitcoin.sum,
name: "Sum",
color: colors.red,
color: colors.loss,
unit: Unit.btc,
}),
line({
metric: realized.sentInLoss.bitcoin.cumulative,
name: "Cumulative",
color: colors.red,
color: colors.loss,
unit: Unit.btc,
defaultActive: false,
}),
line({
metric: realized.sentInLoss.sats.sum,
name: "Sum",
color: colors.red,
color: colors.loss,
unit: Unit.sats,
}),
line({
metric: realized.sentInLoss.sats.cumulative,
name: "Cumulative",
color: colors.red,
color: colors.loss,
unit: Unit.sats,
defaultActive: false,
}),
line({
metric: realized.sentInLoss.dollars.sum,
name: "Sum",
color: colors.red,
color: colors.loss,
unit: Unit.usd,
}),
line({
metric: realized.sentInLoss.dollars.cumulative,
name: "Cumulative",
color: colors.red,
color: colors.loss,
unit: Unit.usd,
defaultActive: false,
}),
@@ -546,7 +546,7 @@ function createRealizedPnlSection(args, title) {
bottom: satsBtcUsd({
pattern: realized.sentInProfit14dEma,
name: "14d EMA",
color: colors.green,
color: colors.profit,
}),
},
{
@@ -555,7 +555,7 @@ function createRealizedPnlSection(args, title) {
bottom: satsBtcUsd({
pattern: realized.sentInLoss14dEma,
name: "14d EMA",
color: colors.red,
color: colors.loss,
}),
},
],
@@ -820,7 +820,7 @@ function createUnrealizedSection(list, useGroupName, title) {
line({
metric: tree.unrealized.unrealizedProfit,
name: useGroupName ? name : "Profit",
color: useGroupName ? color : colors.green,
color: useGroupName ? color : colors.profit,
unit: Unit.usd,
}),
]),
@@ -832,7 +832,7 @@ function createUnrealizedSection(list, useGroupName, title) {
line({
metric: tree.unrealized.unrealizedLoss,
name: useGroupName ? name : "Loss",
color: useGroupName ? color : colors.red,
color: useGroupName ? color : colors.loss,
unit: Unit.usd,
}),
]),
@@ -856,7 +856,7 @@ function createUnrealizedSection(list, useGroupName, title) {
line({
metric: tree.unrealized.negUnrealizedLoss,
name: useGroupName ? name : "Negative Loss",
color: useGroupName ? color : colors.red,
color: useGroupName ? color : colors.loss,
unit: Unit.usd,
}),
]),
@@ -871,7 +871,7 @@ function createUnrealizedSection(list, useGroupName, title) {
line({
metric: tree.unrealized.investedCapitalInProfit,
name: useGroupName ? name : "In Profit",
color: useGroupName ? color : colors.green,
color: useGroupName ? color : colors.profit,
unit: Unit.usd,
}),
]),
@@ -883,7 +883,7 @@ function createUnrealizedSection(list, useGroupName, title) {
line({
metric: tree.unrealized.investedCapitalInLoss,
name: useGroupName ? name : "In Loss",
color: useGroupName ? color : colors.red,
color: useGroupName ? color : colors.loss,
unit: Unit.usd,
}),
]),
@@ -913,7 +913,7 @@ function createUnrealizedSection(list, useGroupName, title) {
line({
metric: tree.relative.unrealizedProfitRelToMarketCap,
name: useGroupName ? name : "Profit",
color: useGroupName ? color : colors.green,
color: useGroupName ? color : colors.profit,
unit: Unit.pctMcap,
}),
]),
@@ -925,7 +925,7 @@ function createUnrealizedSection(list, useGroupName, title) {
line({
metric: tree.relative.unrealizedLossRelToMarketCap,
name: useGroupName ? name : "Loss",
color: useGroupName ? color : colors.red,
color: useGroupName ? color : colors.loss,
unit: Unit.pctMcap,
}),
]),
@@ -949,7 +949,7 @@ function createUnrealizedSection(list, useGroupName, title) {
line({
metric: tree.relative.negUnrealizedLossRelToMarketCap,
name: useGroupName ? name : "Negative Loss",
color: useGroupName ? color : colors.red,
color: useGroupName ? color : colors.loss,
unit: Unit.pctMcap,
}),
]),
@@ -961,7 +961,7 @@ function createUnrealizedSection(list, useGroupName, title) {
baseline({
metric: tree.relative.investedCapitalInProfitPct,
name: useGroupName ? name : "In Profit",
color: useGroupName ? color : colors.green,
color: useGroupName ? color : colors.profit,
unit: Unit.pctRcap,
}),
]),
@@ -973,7 +973,7 @@ function createUnrealizedSection(list, useGroupName, title) {
baseline({
metric: tree.relative.investedCapitalInLossPct,
name: useGroupName ? name : "In Loss",
color: useGroupName ? color : colors.red,
color: useGroupName ? color : colors.loss,
unit: Unit.pctRcap,
}),
]),

View File

@@ -0,0 +1,453 @@
/**
* Cost Basis section builders
*
* Structure:
* - Summary: Key stats (avg + median active, quartiles/extremes available)
* - By Coin: BTC-weighted percentiles (IQR active: p25, p50, p75)
* - By Capital: USD-weighted percentiles (IQR active: p25, p50, p75)
* - Price Position: Spot percentile (both perspectives active)
*
* For cohorts WITHOUT percentiles: Summary only
*/
import { colors } from "../../utils/colors.js";
import { Unit } from "../../utils/units.js";
import { priceLines } from "../constants.js";
import { line, price } from "../series.js";
/**
* @param {PercentilesPattern} p
* @param {(name: string) => string} [n]
* @returns {FetchedPriceSeriesBlueprint[]}
*/
function createCorePercentileSeries(p, n = (x) => x) {
return [
price({
metric: p.pct95,
name: n("p95"),
color: colors.pct._95,
defaultActive: false,
}),
price({
metric: p.pct90,
name: n("p90"),
color: colors.pct._90,
defaultActive: false,
}),
price({
metric: p.pct85,
name: n("p85"),
color: colors.pct._85,
defaultActive: false,
}),
price({
metric: p.pct80,
name: n("p80"),
color: colors.pct._80,
defaultActive: false,
}),
price({ metric: p.pct75, name: n("p75"), color: colors.pct._75 }),
price({
metric: p.pct70,
name: n("p70"),
color: colors.pct._70,
defaultActive: false,
}),
price({
metric: p.pct65,
name: n("p65"),
color: colors.pct._65,
defaultActive: false,
}),
price({
metric: p.pct60,
name: n("p60"),
color: colors.pct._60,
defaultActive: false,
}),
price({
metric: p.pct55,
name: n("p55"),
color: colors.pct._55,
defaultActive: false,
}),
price({ metric: p.pct50, name: n("p50"), color: colors.pct._50 }),
price({
metric: p.pct45,
name: n("p45"),
color: colors.pct._45,
defaultActive: false,
}),
price({
metric: p.pct40,
name: n("p40"),
color: colors.pct._40,
defaultActive: false,
}),
price({
metric: p.pct35,
name: n("p35"),
color: colors.pct._35,
defaultActive: false,
}),
price({
metric: p.pct30,
name: n("p30"),
color: colors.pct._30,
defaultActive: false,
}),
price({ metric: p.pct25, name: n("p25"), color: colors.pct._25 }),
price({
metric: p.pct20,
name: n("p20"),
color: colors.pct._20,
defaultActive: false,
}),
price({
metric: p.pct15,
name: n("p15"),
color: colors.pct._15,
defaultActive: false,
}),
price({
metric: p.pct10,
name: n("p10"),
color: colors.pct._10,
defaultActive: false,
}),
price({
metric: p.pct05,
name: n("p05"),
color: colors.pct._05,
defaultActive: false,
}),
];
}
/**
* @param {UtxoCohortObject | CohortWithoutRelative} cohort
* @returns {FetchedPriceSeriesBlueprint[]}
*/
function createSingleSummarySeriesBasic(cohort) {
const { color, tree } = cohort;
return [
price({ metric: tree.realized.realizedPrice, name: "Average", color }),
price({
metric: tree.costBasis.max,
name: "Max",
color: colors.pct._100,
defaultActive: false,
}),
price({
metric: tree.costBasis.min,
name: "Min",
color: colors.pct._0,
defaultActive: false,
}),
];
}
/**
* @param {CohortAll | CohortFull | CohortWithPercentiles} cohort
* @returns {FetchedPriceSeriesBlueprint[]}
*/
function createSingleSummarySeriesWithPercentiles(cohort) {
const { color, tree } = cohort;
const p = tree.costBasis.percentiles;
return [
price({ metric: tree.realized.realizedPrice, name: "Average", color }),
price({
metric: tree.costBasis.max,
name: "Max (p100)",
color: colors.pct._100,
defaultActive: false,
}),
price({
metric: p.pct75,
name: "Q3 (p75)",
color: colors.pct._75,
defaultActive: false,
}),
price({ metric: p.pct50, name: "Median (p50)", color: colors.pct._50 }),
price({
metric: p.pct25,
name: "Q1 (p25)",
color: colors.pct._25,
defaultActive: false,
}),
price({
metric: tree.costBasis.min,
name: "Min (p0)",
color: colors.pct._0,
defaultActive: false,
}),
];
}
/**
* @template {readonly { name: string, color: Color, tree: { realized: { realizedPrice: ActivePricePattern } } }[]} T
* @param {T} list
* @returns {FetchedPriceSeriesBlueprint[]}
*/
function createGroupedSummarySeries(list) {
return list.map(({ name, color, tree }) =>
price({ metric: tree.realized.realizedPrice, name, color }),
);
}
/**
* @param {CohortAll | CohortFull | CohortWithPercentiles} cohort
* @returns {FetchedPriceSeriesBlueprint[]}
*/
function createSingleByCoinSeries(cohort) {
const { color, tree } = cohort;
const cb = tree.costBasis;
return [
price({ metric: tree.realized.realizedPrice, name: "Average", color }),
price({
metric: cb.max,
name: "p100",
color: colors.pct._100,
defaultActive: false,
}),
...createCorePercentileSeries(cb.percentiles),
price({
metric: cb.min,
name: "p0",
color: colors.pct._0,
defaultActive: false,
}),
];
}
/**
* @param {CohortAll | CohortFull | CohortWithPercentiles} cohort
* @returns {FetchedPriceSeriesBlueprint[]}
*/
function createSingleByCapitalSeries(cohort) {
const { color, tree } = cohort;
return [
price({ metric: tree.realized.investorPrice, name: "Average", color }),
...createCorePercentileSeries(tree.costBasis.investedCapital),
];
}
/**
* @param {CohortAll | CohortFull | CohortWithPercentiles} cohort
* @returns {AnyFetchedSeriesBlueprint[]}
*/
function createSinglePricePositionSeries(cohort) {
const { tree } = cohort;
return [
line({
metric: tree.costBasis.spotCostBasisPercentile,
name: "By Coin",
color: colors.bitcoin,
unit: Unit.percentage,
}),
line({
metric: tree.costBasis.spotInvestedCapitalPercentile,
name: "By Capital",
color: colors.usd,
unit: Unit.percentage,
}),
...priceLines({ numbers: [100, 50, 0], unit: Unit.percentage }),
];
}
/**
* @param {{ cohort: UtxoCohortObject | CohortWithoutRelative, title: (metric: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createCostBasisSection({ cohort, title }) {
return {
name: "Cost Basis",
tree: [
{
name: "Summary",
title: title("Cost Basis Summary"),
top: createSingleSummarySeriesBasic(cohort),
},
],
};
}
/**
* @param {{ cohort: CohortAll | CohortFull | CohortWithPercentiles, title: (metric: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createCostBasisSectionWithPercentiles({ cohort, title }) {
return {
name: "Cost Basis",
tree: [
{
name: "Summary",
title: title("Cost Basis Summary"),
top: createSingleSummarySeriesWithPercentiles(cohort),
},
{
name: "By Coin",
title: title("Cost Basis Distribution (BTC-weighted)"),
top: createSingleByCoinSeries(cohort),
},
{
name: "By Capital",
title: title("Cost Basis Distribution (USD-weighted)"),
top: createSingleByCapitalSeries(cohort),
},
{
name: "Price Position",
title: title("Current Price Position"),
bottom: createSinglePricePositionSeries(cohort),
},
],
};
}
/**
* @template {readonly (UtxoCohortObject | CohortWithoutRelative)[]} T
* @param {{ list: T, title: (metric: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createGroupedCostBasisSection({ list, title }) {
return {
name: "Cost Basis",
tree: [
{
name: "Summary",
title: title("Cost Basis Summary"),
top: createGroupedSummarySeries(list),
},
],
};
}
/**
* @param {{ list: readonly (CohortAll | CohortFull | CohortWithPercentiles)[], title: (metric: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createGroupedCostBasisSectionWithPercentiles({ list, title }) {
return {
name: "Cost Basis",
tree: [
{
name: "Summary",
title: title("Cost Basis Summary"),
top: createGroupedSummarySeries(list),
},
{
name: "By Coin",
tree: [
{
name: "Average",
title: title("Realized Price Comparison"),
top: list.map(({ name, color, tree }) =>
price({ metric: tree.realized.realizedPrice, name, color }),
),
},
{
name: "Median",
title: title("Cost Basis Median (BTC-weighted)"),
top: list.map(({ name, color, tree }) =>
price({ metric: tree.costBasis.percentiles.pct50, name, color }),
),
},
{
name: "Q3",
title: title("Cost Basis Q3 (BTC-weighted)"),
top: list.map(({ name, color, tree }) =>
price({ metric: tree.costBasis.percentiles.pct75, name, color }),
),
},
{
name: "Q1",
title: title("Cost Basis Q1 (BTC-weighted)"),
top: list.map(({ name, color, tree }) =>
price({ metric: tree.costBasis.percentiles.pct25, name, color }),
),
},
],
},
{
name: "By Capital",
tree: [
{
name: "Average",
title: title("Investor Price Comparison"),
top: list.map(({ name, color, tree }) =>
price({ metric: tree.realized.investorPrice, name, color }),
),
},
{
name: "Median",
title: title("Cost Basis Median (USD-weighted)"),
top: list.map(({ name, color, tree }) =>
price({
metric: tree.costBasis.investedCapital.pct50,
name,
color,
}),
),
},
{
name: "Q3",
title: title("Cost Basis Q3 (USD-weighted)"),
top: list.map(({ name, color, tree }) =>
price({
metric: tree.costBasis.investedCapital.pct75,
name,
color,
}),
),
},
{
name: "Q1",
title: title("Cost Basis Q1 (USD-weighted)"),
top: list.map(({ name, color, tree }) =>
price({
metric: tree.costBasis.investedCapital.pct25,
name,
color,
}),
),
},
],
},
{
name: "Price Position",
tree: [
{
name: "By Coin",
title: title("Price Position (BTC-weighted)"),
bottom: [
...list.map(({ name, color, tree }) =>
line({
metric: tree.costBasis.spotCostBasisPercentile,
name,
color,
unit: Unit.percentage,
}),
),
...priceLines({ numbers: [100, 50, 0], unit: Unit.percentage }),
],
},
{
name: "By Capital",
title: title("Price Position (USD-weighted)"),
bottom: [
...list.map(({ name, color, tree }) =>
line({
metric: tree.costBasis.spotInvestedCapitalPercentile,
name,
color,
unit: Unit.percentage,
}),
),
...priceLines({ numbers: [100, 50, 0], unit: Unit.percentage }),
],
},
],
},
],
};
}

View File

@@ -44,7 +44,7 @@ export function buildCohortData() {
const cohortAll = {
name: "",
title: "",
color: colors.orange,
color: colors.bitcoin,
tree: utxoCohorts.all,
addrCount: addrCount.all,
};

View File

@@ -0,0 +1,479 @@
/**
* Holdings section builders
*
* Structure (Option C - optimized for UX):
* - Supply: Total BTC held (flat, one click)
* - UTXO Count: Number of UTXOs (flat, one click)
* - Address Count: Number of addresses (when available, flat)
* - 30d Changes/: Folder for change metrics
* - Supply: 30d supply change
* - Relative: % of circulating supply (when available)
*
* Rationale: Most-used metrics (Supply, UTXO Count) are immediately accessible.
* 30d changes are grouped together for consistency and cleaner navigation.
*/
import { Unit } from "../../utils/units.js";
import { line, baseline } from "../series.js";
import { satsBtcUsd, satsBtcUsdBaseline } from "../shared.js";
/**
* @param {UtxoCohortObject | CohortWithoutRelative} cohort
* @returns {AnyFetchedSeriesBlueprint[]}
*/
function createSingleSupplySeries(cohort) {
const { color, tree } = cohort;
return [...satsBtcUsd({ pattern: tree.supply.total, name: "Supply", color })];
}
/**
* @param {UtxoCohortObject | CohortWithoutRelative} cohort
* @returns {AnyFetchedSeriesBlueprint[]}
*/
function createSingle30dChangeSeries(cohort) {
return satsBtcUsdBaseline({ pattern: cohort.tree.supply._30dChange, name: "30d Change" });
}
/**
* @param {UtxoCohortObject | CohortWithoutRelative} cohort
* @returns {AnyFetchedSeriesBlueprint[]}
*/
function createSingleUtxoCountSeries(cohort) {
const { color, tree } = cohort;
return [
line({
metric: tree.outputs.utxoCount,
name: "UTXO Count",
color,
unit: Unit.count,
}),
];
}
/**
* @param {UtxoCohortObject | CohortWithoutRelative} cohort
* @returns {AnyFetchedSeriesBlueprint[]}
*/
function createSingleUtxoCount30dChangeSeries(cohort) {
return [
baseline({
metric: cohort.tree.outputs.utxoCount30dChange,
name: "30d Change",
unit: Unit.count,
}),
];
}
/**
* @param {CohortAll | CohortAddress} cohort
* @returns {AnyFetchedSeriesBlueprint[]}
*/
function createSingleAddrCount30dChangeSeries(cohort) {
return [
baseline({
metric: cohort.addrCount._30dChange,
name: "30d Change",
unit: Unit.count,
}),
];
}
/**
* @param {CohortFull | CohortWithAdjusted | CohortBasicWithMarketCap | CohortMinAge} cohort
* @returns {AnyFetchedSeriesBlueprint[]}
*/
function createSingleRelativeSeries(cohort) {
const { color, tree } = cohort;
return [
line({
metric: tree.relative.supplyRelToCirculatingSupply,
name: "% of Circulating",
color,
unit: Unit.pctSupply,
}),
];
}
/**
* @param {{ cohort: UtxoCohortObject | CohortWithoutRelative, title: (metric: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createHoldingsSection({ cohort, title }) {
return {
name: "Holdings",
tree: [
{
name: "Supply",
title: title("Supply"),
bottom: createSingleSupplySeries(cohort),
},
{
name: "UTXO Count",
title: title("UTXO Count"),
bottom: createSingleUtxoCountSeries(cohort),
},
{
name: "30d Changes",
tree: [
{
name: "Supply",
title: title("Supply 30d Change"),
bottom: createSingle30dChangeSeries(cohort),
},
{
name: "UTXO Count",
title: title("UTXO Count 30d Change"),
bottom: createSingleUtxoCount30dChangeSeries(cohort),
},
],
},
],
};
}
/**
* @param {{ cohort: CohortFull | CohortWithAdjusted | CohortBasicWithMarketCap | CohortMinAge, title: (metric: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createHoldingsSectionWithRelative({ cohort, title }) {
return {
name: "Holdings",
tree: [
{
name: "Supply",
title: title("Supply"),
bottom: createSingleSupplySeries(cohort),
},
{
name: "UTXO Count",
title: title("UTXO Count"),
bottom: createSingleUtxoCountSeries(cohort),
},
{
name: "30d Changes",
tree: [
{
name: "Supply",
title: title("Supply 30d Change"),
bottom: createSingle30dChangeSeries(cohort),
},
{
name: "UTXO Count",
title: title("UTXO Count 30d Change"),
bottom: createSingleUtxoCount30dChangeSeries(cohort),
},
],
},
{
name: "Relative",
title: title("Relative to Circulating Supply"),
bottom: createSingleRelativeSeries(cohort),
},
],
};
}
/**
* @param {{ cohort: CohortAll, title: (metric: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createHoldingsSectionAll({ cohort, title }) {
return {
name: "Holdings",
tree: [
{
name: "Supply",
title: title("Supply"),
bottom: createSingleSupplySeries(cohort),
},
{
name: "UTXO Count",
title: title("UTXO Count"),
bottom: createSingleUtxoCountSeries(cohort),
},
{
name: "Address Count",
title: title("Address Count"),
bottom: [
line({
metric: cohort.addrCount.count,
name: "Address Count",
color: cohort.color,
unit: Unit.count,
}),
],
},
{
name: "30d Changes",
tree: [
{
name: "Supply",
title: title("Supply 30d Change"),
bottom: createSingle30dChangeSeries(cohort),
},
{
name: "UTXO Count",
title: title("UTXO Count 30d Change"),
bottom: createSingleUtxoCount30dChangeSeries(cohort),
},
{
name: "Address Count",
title: title("Address Count 30d Change"),
bottom: createSingleAddrCount30dChangeSeries(cohort),
},
],
},
],
};
}
/**
* @param {{ cohort: CohortAddress, title: (metric: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createHoldingsSectionAddress({ cohort, title }) {
return {
name: "Holdings",
tree: [
{
name: "Supply",
title: title("Supply"),
bottom: createSingleSupplySeries(cohort),
},
{
name: "UTXO Count",
title: title("UTXO Count"),
bottom: createSingleUtxoCountSeries(cohort),
},
{
name: "Address Count",
title: title("Address Count"),
bottom: [
line({
metric: cohort.addrCount.count,
name: "Address Count",
color: cohort.color,
unit: Unit.count,
}),
],
},
{
name: "30d Changes",
tree: [
{
name: "Supply",
title: title("Supply 30d Change"),
bottom: createSingle30dChangeSeries(cohort),
},
{
name: "UTXO Count",
title: title("UTXO Count 30d Change"),
bottom: createSingleUtxoCount30dChangeSeries(cohort),
},
{
name: "Address Count",
title: title("Address Count 30d Change"),
bottom: createSingleAddrCount30dChangeSeries(cohort),
},
],
},
],
};
}
/**
* @param {{ list: readonly CohortAddress[], title: (metric: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createGroupedHoldingsSectionAddress({ list, title }) {
return {
name: "Holdings",
tree: [
{
name: "Supply",
title: title("Supply"),
bottom: list.flatMap(({ name, color, tree }) =>
satsBtcUsd({ pattern: tree.supply.total, name, color }),
),
},
{
name: "UTXO Count",
title: title("UTXO Count"),
bottom: list.map(({ name, color, tree }) =>
line({
metric: tree.outputs.utxoCount,
name,
color,
unit: Unit.count,
}),
),
},
{
name: "Address Count",
title: title("Address Count"),
bottom: list.map(({ name, color, addrCount }) =>
line({ metric: addrCount.count, name, color, unit: Unit.count }),
),
},
{
name: "30d Changes",
tree: [
{
name: "Supply",
title: title("Supply 30d Change"),
bottom: list.flatMap(({ name, color, tree }) =>
satsBtcUsdBaseline({ pattern: tree.supply._30dChange, name, color }),
),
},
{
name: "UTXO Count",
title: title("UTXO Count 30d Change"),
bottom: list.map(({ name, color, tree }) =>
baseline({
metric: tree.outputs.utxoCount30dChange,
name,
unit: Unit.count,
color,
}),
),
},
{
name: "Address Count",
title: title("Address Count 30d Change"),
bottom: list.map(({ name, color, addrCount }) =>
baseline({
metric: addrCount._30dChange,
name,
unit: Unit.count,
color,
}),
),
},
],
},
],
};
}
/**
* @template {readonly (UtxoCohortObject | CohortWithoutRelative)[]} T
* @param {{ list: T, title: (metric: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createGroupedHoldingsSection({ list, title }) {
return {
name: "Holdings",
tree: [
{
name: "Supply",
title: title("Supply"),
bottom: list.flatMap(({ name, color, tree }) =>
satsBtcUsd({ pattern: tree.supply.total, name, color }),
),
},
{
name: "UTXO Count",
title: title("UTXO Count"),
bottom: list.map(({ name, color, tree }) =>
line({
metric: tree.outputs.utxoCount,
name,
color,
unit: Unit.count,
}),
),
},
{
name: "30d Changes",
tree: [
{
name: "Supply",
title: title("Supply 30d Change"),
bottom: list.flatMap(({ name, color, tree }) =>
satsBtcUsdBaseline({ pattern: tree.supply._30dChange, name, color }),
),
},
{
name: "UTXO Count",
title: title("UTXO Count 30d Change"),
bottom: list.map(({ name, color, tree }) =>
baseline({
metric: tree.outputs.utxoCount30dChange,
name,
unit: Unit.count,
color,
}),
),
},
],
},
],
};
}
/**
* @param {{ list: readonly (CohortFull | CohortWithAdjusted | CohortBasicWithMarketCap | CohortMinAge)[], title: (metric: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createGroupedHoldingsSectionWithRelative({ list, title }) {
return {
name: "Holdings",
tree: [
{
name: "Supply",
title: title("Supply"),
bottom: list.flatMap(({ name, color, tree }) =>
satsBtcUsd({ pattern: tree.supply.total, name, color }),
),
},
{
name: "UTXO Count",
title: title("UTXO Count"),
bottom: list.map(({ name, color, tree }) =>
line({
metric: tree.outputs.utxoCount,
name,
color,
unit: Unit.count,
}),
),
},
{
name: "30d Changes",
tree: [
{
name: "Supply",
title: title("Supply 30d Change"),
bottom: list.flatMap(({ name, color, tree }) =>
satsBtcUsdBaseline({ pattern: tree.supply._30dChange, name, color }),
),
},
{
name: "UTXO Count",
title: title("UTXO Count 30d Change"),
bottom: list.map(({ name, color, tree }) =>
baseline({
metric: tree.outputs.utxoCount30dChange,
name,
unit: Unit.count,
color,
}),
),
},
],
},
{
name: "Relative",
title: title("Relative to Circulating Supply"),
bottom: list.map(({ name, color, tree }) =>
line({
metric: tree.relative.supplyRelToCirculatingSupply,
name,
color,
unit: Unit.pctSupply,
}),
),
},
],
};
}

View File

@@ -0,0 +1,225 @@
/**
* Prices section builders
*
* Structure (single cohort):
* - Compare: Both prices on one chart
* - Realized: Price + Ratio (MVRV) + Z-Scores (for full cohorts)
* - Investor: Price + Ratio + Z-Scores (for full cohorts)
*
* Structure (grouped cohorts):
* - Realized: Price + Ratio comparison across cohorts
* - Investor: Price + Ratio comparison across cohorts
*
* For cohorts WITHOUT full ratio patterns: basic Price/Ratio charts only (no Z-Scores)
*/
import { colors } from "../../utils/colors.js";
import { createPriceRatioCharts } from "../shared.js";
import { baseline, price } from "../series.js";
import { Unit } from "../../utils/units.js";
/**
* Create prices section for cohorts with full ActivePriceRatioPattern
* (CohortAll, CohortFull, CohortWithPercentiles)
* @param {{ cohort: CohortAll | CohortFull | CohortWithPercentiles, title: (metric: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createPricesSectionFull({ cohort, title }) {
const { tree, color } = cohort;
return {
name: "Prices",
tree: [
{
name: "Compare",
title: title("Prices"),
top: [
price({
metric: tree.realized.realizedPrice,
name: "Realized",
color: colors.realized,
}),
price({
metric: tree.realized.investorPrice,
name: "Investor",
color: colors.investor,
}),
],
},
{
name: "Realized",
tree: createPriceRatioCharts({
context: cohort.name,
legend: "Realized",
pricePattern: tree.realized.realizedPrice,
ratio: tree.realized.realizedPriceExtra,
color,
priceTitle: title("Realized Price"),
titlePrefix: "Realized Price",
}),
},
{
name: "Investor",
tree: createPriceRatioCharts({
context: cohort.name,
legend: "Investor",
pricePattern: tree.realized.investorPrice,
ratio: tree.realized.investorPriceExtra,
color,
priceTitle: title("Investor Price"),
titlePrefix: "Investor Price",
}),
},
],
};
}
/**
* Create prices section for cohorts with basic ratio patterns only
* (CohortWithAdjusted, CohortBasic, CohortAddress, CohortWithoutRelative)
* @param {{ cohort: CohortWithAdjusted | CohortBasic | CohortAddress | CohortWithoutRelative, title: (metric: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createPricesSectionBasic({ cohort, title }) {
const { tree, color } = cohort;
return {
name: "Prices",
tree: [
{
name: "Compare",
title: title("Prices"),
top: [
price({
metric: tree.realized.realizedPrice,
name: "Realized",
color: colors.realized,
}),
price({
metric: tree.realized.investorPrice,
name: "Investor",
color: colors.investor,
}),
],
},
{
name: "Realized",
tree: [
{
name: "Price",
title: title("Realized Price"),
top: [
price({
metric: tree.realized.realizedPrice,
name: "Realized",
color,
}),
],
},
{
name: "Ratio",
title: title("Realized Price Ratio"),
bottom: [
baseline({
metric: tree.realized.realizedPriceExtra.ratio,
name: "Ratio",
unit: Unit.ratio,
base: 1,
}),
],
},
],
},
{
name: "Investor",
tree: [
{
name: "Price",
title: title("Investor Price"),
top: [
price({
metric: tree.realized.investorPrice,
name: "Investor",
color,
}),
],
},
{
name: "Ratio",
title: title("Investor Price Ratio"),
bottom: [
baseline({
metric: tree.realized.investorPriceExtra.ratio,
name: "Ratio",
unit: Unit.ratio,
base: 1,
}),
],
},
],
},
],
};
}
/**
* Create prices section for grouped cohorts
* @template {readonly (CohortAll | CohortFull | CohortWithPercentiles | CohortWithAdjusted | CohortBasic | CohortAddress | CohortWithoutRelative)[]} T
* @param {{ list: T, title: (metric: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createGroupedPricesSection({ list, title }) {
return {
name: "Prices",
tree: [
{
name: "Realized",
tree: [
{
name: "Price",
title: title("Realized Price"),
top: list.map(({ name, color, tree }) =>
price({ metric: tree.realized.realizedPrice, name, color }),
),
},
{
name: "Ratio",
title: title("Realized Price Ratio"),
bottom: list.map(({ name, color, tree }) =>
baseline({
metric: tree.realized.realizedPriceExtra.ratio,
name,
color,
unit: Unit.ratio,
base: 1,
}),
),
},
],
},
{
name: "Investor",
tree: [
{
name: "Price",
title: title("Investor Price"),
top: list.map(({ name, color, tree }) =>
price({ metric: tree.realized.investorPrice, name, color }),
),
},
{
name: "Ratio",
title: title("Investor Price Ratio"),
bottom: list.map(({ name, color, tree }) =>
baseline({
metric: tree.realized.investorPriceExtra.ratio,
name,
color,
unit: Unit.ratio,
base: 1,
}),
),
},
],
},
],
};
}

View File

@@ -176,17 +176,17 @@ function createSingleSupplySeriesBase(cohort) {
...satsBtcUsd({
pattern: tree.supply._30dChange,
name: "30d Change",
color: colors.orange,
color: colors.bitcoin,
}),
...satsBtcUsd({
pattern: tree.unrealized.supplyInProfit,
name: "In Profit",
color: colors.green,
color: colors.profit,
}),
...satsBtcUsd({
pattern: tree.unrealized.supplyInLoss,
name: "In Loss",
color: colors.red,
color: colors.loss,
}),
...satsBtcUsd({
pattern: tree.supply.halved,
@@ -211,13 +211,13 @@ function createSingleSupplyRelativeToOwnMetrics(cohort) {
line({
metric: tree.relative.supplyInProfitRelToOwnSupply,
name: "In Profit",
color: colors.green,
color: colors.profit,
unit: Unit.pctOwn,
}),
line({
metric: tree.relative.supplyInLossRelToOwnSupply,
name: "In Loss",
color: colors.red,
color: colors.loss,
unit: Unit.pctOwn,
}),
priceLine({
@@ -403,13 +403,13 @@ export function createSupplyPnlRelativeToCirculatingSeries(cohort) {
line({
metric: cohort.tree.relative.supplyInProfitRelToCirculatingSupply,
name: "In Profit",
color: colors.green,
color: colors.profit,
unit: Unit.pctSupply,
}),
line({
metric: cohort.tree.relative.supplyInLossRelToCirculatingSupply,
name: "In Loss",
color: colors.red,
color: colors.loss,
unit: Unit.pctSupply,
}),
];
@@ -513,7 +513,7 @@ export function createAddressCountSeries(list, useGroupName) {
line({
metric: tree.addrCount,
name: useGroupName ? name : "Count",
color: useGroupName ? color : colors.orange,
color: useGroupName ? color : colors.bitcoin,
unit: Unit.count,
}),
]);
@@ -566,12 +566,11 @@ export function createRealizedCapSeries(list, useGroupName) {
/**
* Create cost basis percentile series (only for cohorts with CostBasisPattern2)
* Includes min (p0) and max (p100) with full rainbow coloring
* @param {Colors} colors
* @param {readonly CohortWithCostBasisPercentiles[]} list
* @param {boolean} useGroupName
* @returns {FetchedPriceSeriesBlueprint[]}
*/
export function createCostBasisPercentilesSeries(colors, list, useGroupName) {
export function createCostBasisPercentilesSeries(list, useGroupName) {
return list.flatMap(({ name, tree }) => {
const cb = tree.costBasis;
const p = cb.percentiles;
@@ -581,122 +580,122 @@ export function createCostBasisPercentilesSeries(colors, list, useGroupName) {
price({
metric: cb.max,
name: n(100),
color: colors.purple,
color: colors.pct._100,
defaultActive: false,
}),
price({
metric: p.pct95,
name: n(95),
color: colors.fuchsia,
color: colors.pct._95,
defaultActive: false,
}),
price({
metric: p.pct90,
name: n(90),
color: colors.pink,
color: colors.pct._90,
defaultActive: false,
}),
price({
metric: p.pct85,
name: n(85),
color: colors.pink,
color: colors.pct._85,
defaultActive: false,
}),
price({
metric: p.pct80,
name: n(80),
color: colors.rose,
color: colors.pct._80,
defaultActive: false,
}),
price({
metric: p.pct75,
name: n(75),
color: colors.red,
color: colors.pct._75,
defaultActive: false,
}),
price({
metric: p.pct70,
name: n(70),
color: colors.orange,
color: colors.pct._70,
defaultActive: false,
}),
price({
metric: p.pct65,
name: n(65),
color: colors.amber,
color: colors.pct._65,
defaultActive: false,
}),
price({
metric: p.pct60,
name: n(60),
color: colors.yellow,
color: colors.pct._60,
defaultActive: false,
}),
price({
metric: p.pct55,
name: n(55),
color: colors.yellow,
color: colors.pct._55,
defaultActive: false,
}),
price({ metric: p.pct50, name: n(50), color: colors.avocado }),
price({ metric: p.pct50, name: n(50), color: colors.pct._50 }),
price({
metric: p.pct45,
name: n(45),
color: colors.lime,
color: colors.pct._45,
defaultActive: false,
}),
price({
metric: p.pct40,
name: n(40),
color: colors.green,
color: colors.pct._40,
defaultActive: false,
}),
price({
metric: p.pct35,
name: n(35),
color: colors.emerald,
color: colors.pct._35,
defaultActive: false,
}),
price({
metric: p.pct30,
name: n(30),
color: colors.teal,
color: colors.pct._30,
defaultActive: false,
}),
price({
metric: p.pct25,
name: n(25),
color: colors.teal,
color: colors.pct._25,
defaultActive: false,
}),
price({
metric: p.pct20,
name: n(20),
color: colors.cyan,
color: colors.pct._20,
defaultActive: false,
}),
price({
metric: p.pct15,
name: n(15),
color: colors.sky,
color: colors.pct._15,
defaultActive: false,
}),
price({
metric: p.pct10,
name: n(10),
color: colors.blue,
color: colors.pct._10,
defaultActive: false,
}),
price({
metric: p.pct05,
name: n(5),
color: colors.indigo,
color: colors.pct._05,
defaultActive: false,
}),
price({
metric: cb.min,
name: n(0),
color: colors.violet,
color: colors.pct._0,
defaultActive: false,
}),
];
@@ -706,16 +705,11 @@ export function createCostBasisPercentilesSeries(colors, list, useGroupName) {
/**
* Create invested capital percentile series (only for cohorts with CostBasisPattern2)
* Shows invested capital at each percentile level
* @param {Colors} colors
* @param {readonly CohortWithCostBasisPercentiles[]} list
* @param {boolean} useGroupName
* @returns {FetchedPriceSeriesBlueprint[]}
*/
export function createInvestedCapitalPercentilesSeries(
colors,
list,
useGroupName,
) {
export function createInvestedCapitalPercentilesSeries(list, useGroupName) {
return list.flatMap(({ name, tree }) => {
const ic = tree.costBasis.investedCapital;
const n = (/** @type {number} */ pct) =>
@@ -724,110 +718,110 @@ export function createInvestedCapitalPercentilesSeries(
price({
metric: ic.pct95,
name: n(95),
color: colors.fuchsia,
color: colors.pct._95,
defaultActive: false,
}),
price({
metric: ic.pct90,
name: n(90),
color: colors.pink,
color: colors.pct._90,
defaultActive: false,
}),
price({
metric: ic.pct85,
name: n(85),
color: colors.pink,
color: colors.pct._85,
defaultActive: false,
}),
price({
metric: ic.pct80,
name: n(80),
color: colors.rose,
color: colors.pct._80,
defaultActive: false,
}),
price({
metric: ic.pct75,
name: n(75),
color: colors.red,
color: colors.pct._75,
defaultActive: false,
}),
price({
metric: ic.pct70,
name: n(70),
color: colors.orange,
color: colors.pct._70,
defaultActive: false,
}),
price({
metric: ic.pct65,
name: n(65),
color: colors.amber,
color: colors.pct._65,
defaultActive: false,
}),
price({
metric: ic.pct60,
name: n(60),
color: colors.yellow,
color: colors.pct._60,
defaultActive: false,
}),
price({
metric: ic.pct55,
name: n(55),
color: colors.yellow,
color: colors.pct._55,
defaultActive: false,
}),
price({ metric: ic.pct50, name: n(50), color: colors.avocado }),
price({ metric: ic.pct50, name: n(50), color: colors.pct._50 }),
price({
metric: ic.pct45,
name: n(45),
color: colors.lime,
color: colors.pct._45,
defaultActive: false,
}),
price({
metric: ic.pct40,
name: n(40),
color: colors.green,
color: colors.pct._40,
defaultActive: false,
}),
price({
metric: ic.pct35,
name: n(35),
color: colors.emerald,
color: colors.pct._35,
defaultActive: false,
}),
price({
metric: ic.pct30,
name: n(30),
color: colors.teal,
color: colors.pct._30,
defaultActive: false,
}),
price({
metric: ic.pct25,
name: n(25),
color: colors.teal,
color: colors.pct._25,
defaultActive: false,
}),
price({
metric: ic.pct20,
name: n(20),
color: colors.cyan,
color: colors.pct._20,
defaultActive: false,
}),
price({
metric: ic.pct15,
name: n(15),
color: colors.sky,
color: colors.pct._15,
defaultActive: false,
}),
price({
metric: ic.pct10,
name: n(10),
color: colors.blue,
color: colors.pct._10,
defaultActive: false,
}),
price({
metric: ic.pct05,
name: n(5),
color: colors.indigo,
color: colors.pct._05,
defaultActive: false,
}),
];
@@ -836,12 +830,11 @@ export function createInvestedCapitalPercentilesSeries(
/**
* Create spot percentile series (shows current percentile of price relative to cost basis/invested capital)
* @param {Colors} colors
* @param {readonly CohortWithCostBasisPercentiles[]} list
* @param {boolean} useGroupName
* @returns {FetchedBaselineSeriesBlueprint[]}
*/
export function createSpotPercentileSeries(colors, list, useGroupName) {
export function createSpotPercentileSeries(list, useGroupName) {
return list.flatMap(({ name, color, tree }) => [
baseline({
metric: tree.costBasis.spotCostBasisPercentile,
@@ -852,7 +845,7 @@ export function createSpotPercentileSeries(colors, list, useGroupName) {
baseline({
metric: tree.costBasis.spotInvestedCapitalPercentile,
name: useGroupName ? `${name} Invested Capital` : "Invested Capital",
color: useGroupName ? color : colors.orange,
color: useGroupName ? color : colors.bitcoin,
unit: Unit.ratio,
defaultActive: false,
}),
@@ -990,28 +983,27 @@ export function createSingleSentSeries(cohort) {
/**
* Create sell side risk ratio series for single cohort
* @param {Colors} colors
* @param {{ realized: AnyRealizedPattern }} tree
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createSingleSellSideRiskSeries(colors, tree) {
export function createSingleSellSideRiskSeries(tree) {
return [
dots({
metric: tree.realized.sellSideRiskRatio,
name: "Raw",
color: colors.orange,
color: colors.bitcoin,
unit: Unit.ratio,
}),
line({
metric: tree.realized.sellSideRiskRatio7dEma,
name: "7d EMA",
color: colors.red,
color: colors.ma._1w,
unit: Unit.ratio,
}),
line({
metric: tree.realized.sellSideRiskRatio30dEma,
name: "30d EMA",
color: colors.pink,
color: colors.ma._1m,
unit: Unit.ratio,
}),
];
@@ -1039,22 +1031,21 @@ export function createGroupedSellSideRiskSeries(list) {
/**
* Create value created & destroyed series for single cohort
* @param {Colors} colors
* @param {{ realized: AnyRealizedPattern }} tree
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createSingleValueCreatedDestroyedSeries(colors, tree) {
export function createSingleValueCreatedDestroyedSeries(tree) {
return [
line({
metric: tree.realized.valueCreated,
name: "Created",
color: colors.emerald,
color: colors.usd,
unit: Unit.usd,
}),
line({
metric: tree.realized.valueDestroyed,
name: "Destroyed",
color: colors.red,
color: colors.loss,
unit: Unit.usd,
}),
];
@@ -1063,36 +1054,35 @@ export function createSingleValueCreatedDestroyedSeries(colors, tree) {
/**
* Create profit/loss value breakdown series for single cohort
* Shows profit value created/destroyed and loss value created/destroyed
* @param {Colors} colors
* @param {{ realized: AnyRealizedPattern }} tree
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createSingleValueFlowBreakdownSeries(colors, tree) {
export function createSingleValueFlowBreakdownSeries(tree) {
return [
line({
metric: tree.realized.profitValueCreated,
name: "Profit Created",
color: colors.green,
color: colors.profit,
unit: Unit.usd,
}),
line({
metric: tree.realized.profitValueDestroyed,
name: "Profit Destroyed",
color: colors.lime,
color: colors.loss,
unit: Unit.usd,
defaultActive: false,
}),
line({
metric: tree.realized.lossValueCreated,
name: "Loss Created",
color: colors.orange,
color: colors.bitcoin,
unit: Unit.usd,
defaultActive: false,
}),
line({
metric: tree.realized.lossValueDestroyed,
name: "Loss Destroyed",
color: colors.red,
color: colors.loss,
unit: Unit.usd,
}),
];
@@ -1100,22 +1090,21 @@ export function createSingleValueFlowBreakdownSeries(colors, tree) {
/**
* Create capitulation & profit flow series for single cohort
* @param {Colors} colors
* @param {{ realized: AnyRealizedPattern }} tree
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createSingleCapitulationProfitFlowSeries(colors, tree) {
export function createSingleCapitulationProfitFlowSeries(tree) {
return [
line({
metric: tree.realized.profitFlow,
name: "Profit Flow",
color: colors.green,
color: colors.profit,
unit: Unit.usd,
}),
line({
metric: tree.realized.capitulationFlow,
name: "Capitulation Flow",
color: colors.red,
color: colors.loss,
unit: Unit.usd,
}),
];
@@ -1127,11 +1116,10 @@ export function createSingleCapitulationProfitFlowSeries(colors, tree) {
/**
* Create base SOPR series for single cohort (all cohorts have base SOPR)
* @param {Colors} colors
* @param {{ realized: AnyRealizedPattern }} tree
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createSingleSoprSeries(colors, tree) {
export function createSingleSoprSeries(tree) {
return [
baseline({
metric: tree.realized.sopr,
@@ -1142,7 +1130,7 @@ export function createSingleSoprSeries(colors, tree) {
baseline({
metric: tree.realized.sopr7dEma,
name: "7d EMA",
color: [colors.lime, colors.rose],
color: colors.bi.sopr7d,
unit: Unit.ratio,
defaultActive: false,
base: 1,
@@ -1150,7 +1138,7 @@ export function createSingleSoprSeries(colors, tree) {
baseline({
metric: tree.realized.sopr30dEma,
name: "30d EMA",
color: [colors.avocado, colors.pink],
color: colors.bi.sopr30d,
unit: Unit.ratio,
defaultActive: false,
base: 1,
@@ -1337,11 +1325,10 @@ export function createGroupedRealizedAthRegretSeries(list) {
/**
* Create sentiment series for single cohort
* @param {Colors} colors
* @param {{ unrealized: UnrealizedPattern }} tree
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createSingleSentimentSeries(colors, tree) {
export function createSingleSentimentSeries(tree) {
return [
baseline({
metric: tree.unrealized.netSentiment,
@@ -1351,13 +1338,13 @@ export function createSingleSentimentSeries(colors, tree) {
line({
metric: tree.unrealized.greedIndex,
name: "Greed Index",
color: colors.green,
color: colors.profit,
unit: Unit.usd,
}),
line({
metric: tree.unrealized.painIndex,
name: "Pain Index",
color: colors.red,
color: colors.loss,
unit: Unit.usd,
}),
];

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,165 @@
/**
* Valuation section builders
*
* Structure:
* - Realized Cap: Total value at cost basis (USD)
* - 30d Change: Recent realized cap changes
* - MVRV: Market Value to Realized Value ratio
*
* For cohorts WITH full ratio patterns: MVRV uses createRatioChart (price + percentiles)
* For cohorts WITHOUT full ratio patterns: MVRV is simple baseline
*/
import { Unit } from "../../utils/units.js";
import { line, baseline } from "../series.js";
import { createRatioChart } from "../shared.js";
/**
* @param {UtxoCohortObject | CohortWithoutRelative} cohort
* @returns {AnyFetchedSeriesBlueprint[]}
*/
function createSingleRealizedCapSeries(cohort) {
const { color, tree } = cohort;
return [
line({
metric: tree.realized.realizedCap,
name: "Realized Cap",
color,
unit: Unit.usd,
}),
];
}
/**
* @param {UtxoCohortObject | CohortWithoutRelative} cohort
* @returns {AnyFetchedSeriesBlueprint[]}
*/
function createSingle30dChangeSeries(cohort) {
return [
baseline({
metric: cohort.tree.realized.realizedCap30dDelta,
name: "30d Change",
unit: Unit.usd,
}),
];
}
/**
* Create valuation section for cohorts with full ratio patterns
* (CohortAll, CohortFull, CohortWithPercentiles)
* @param {{ cohort: CohortAll | CohortFull | CohortWithPercentiles, title: (metric: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createValuationSectionFull({ cohort, title }) {
const { tree, color } = cohort;
return {
name: "Valuation",
tree: [
{
name: "Realized Cap",
title: title("Realized Cap"),
bottom: createSingleRealizedCapSeries(cohort),
},
{
name: "30d Change",
title: title("Realized Cap 30d Change"),
bottom: createSingle30dChangeSeries(cohort),
},
createRatioChart({
title,
pricePattern: tree.realized.realizedPrice,
ratio: tree.realized.realizedPriceExtra,
color,
name: "MVRV",
}),
],
};
}
/**
* Create valuation section for cohorts with basic ratio patterns
* (CohortWithAdjusted, CohortBasic, CohortAddress, CohortWithoutRelative)
* @param {{ cohort: CohortWithAdjusted | CohortBasic | CohortAddress | CohortWithoutRelative, title: (metric: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createValuationSection({ cohort, title }) {
const { tree, color } = cohort;
return {
name: "Valuation",
tree: [
{
name: "Realized Cap",
title: title("Realized Cap"),
bottom: createSingleRealizedCapSeries(cohort),
},
{
name: "30d Change",
title: title("Realized Cap 30d Change"),
bottom: createSingle30dChangeSeries(cohort),
},
{
name: "MVRV",
title: title("MVRV"),
bottom: [
baseline({
metric: tree.realized.realizedPriceExtra.ratio,
name: "MVRV",
color,
unit: Unit.ratio,
base: 1,
}),
],
},
],
};
}
/**
* @template {readonly (UtxoCohortObject | CohortWithoutRelative)[]} T
* @param {{ list: T, title: (metric: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createGroupedValuationSection({ list, title }) {
return {
name: "Valuation",
tree: [
{
name: "Realized Cap",
title: title("Realized Cap"),
bottom: list.map(({ name, color, tree }) =>
line({
metric: tree.realized.realizedCap,
name,
color,
unit: Unit.usd,
}),
),
},
{
name: "30d Change",
title: title("Realized Cap 30d Change"),
bottom: list.map(({ name, color, tree }) =>
baseline({
metric: tree.realized.realizedCap30dDelta,
name,
color,
unit: Unit.usd,
}),
),
},
{
name: "MVRV",
title: title("MVRV"),
bottom: list.map(({ name, color, tree }) =>
baseline({
metric: tree.realized.realizedPriceExtra.ratio,
name,
color,
unit: Unit.ratio,
base: 1,
}),
),
},
],
};
}

View File

@@ -68,12 +68,11 @@ const periodName = (key) => periodIdToName(key.slice(1), true);
/**
* Build DCA class entry from year
* @param {Colors} colors
* @param {MarketDca} dca
* @param {DcaYear} year
* @returns {BaseEntryItem}
*/
function buildYearEntry(colors, dca, year) {
function buildYearEntry(dca, year) {
const key = /** @type {DcaYearKey} */ (`_${year}`);
return {
name: `${year}`,
@@ -184,11 +183,10 @@ function createCompareFolder(context, items) {
/**
* Create single entry tree structure
* @param {Colors} colors
* @param {BaseEntryItem & { titlePrefix?: string }} item
* @param {object[]} returnsBottom - Bottom pane items for returns chart
*/
function createSingleEntryTree(colors, item, returnsBottom) {
function createSingleEntryTree(item, returnsBottom) {
const {
name,
titlePrefix = name,
@@ -217,13 +215,13 @@ function createSingleEntryTree(colors, item, returnsBottom) {
line({
metric: daysInProfit,
name: "Days in Profit",
color: colors.green,
color: colors.profit,
unit: Unit.days,
}),
line({
metric: daysInLoss,
name: "Days in Loss",
color: colors.red,
color: colors.loss,
unit: Unit.days,
}),
],
@@ -240,24 +238,23 @@ function createSingleEntryTree(colors, item, returnsBottom) {
/**
* Create a single entry from a base item (no CAGR)
* @param {Colors} colors
* @param {BaseEntryItem & { titlePrefix?: string }} item
*/
function createShortSingleEntry(colors, item) {
function createShortSingleEntry(item) {
const { returns, minReturn, maxReturn } = item;
return createSingleEntryTree(colors, item, [
return createSingleEntryTree(item, [
baseline({ metric: returns, name: "Current", unit: Unit.percentage }),
dotted({
metric: maxReturn,
name: "Max",
color: colors.green,
color: colors.profit,
unit: Unit.percentage,
defaultActive: false,
}),
dotted({
metric: minReturn,
name: "Min",
color: colors.red,
color: colors.loss,
unit: Unit.percentage,
defaultActive: false,
}),
@@ -266,25 +263,24 @@ function createShortSingleEntry(colors, item) {
/**
* Create a single entry from a long item (with CAGR)
* @param {Colors} colors
* @param {LongEntryItem & { titlePrefix?: string }} item
*/
function createLongSingleEntry(colors, item) {
function createLongSingleEntry(item) {
const { returns, minReturn, maxReturn, cagr } = item;
return createSingleEntryTree(colors, item, [
return createSingleEntryTree(item, [
baseline({ metric: returns, name: "Current", unit: Unit.percentage }),
baseline({ metric: cagr, name: "CAGR", unit: Unit.cagr }),
dotted({
metric: maxReturn,
name: "Max",
color: colors.green,
color: colors.profit,
unit: Unit.percentage,
defaultActive: false,
}),
dotted({
metric: minReturn,
name: "Min",
color: colors.red,
color: colors.loss,
unit: Unit.percentage,
defaultActive: false,
}),
@@ -304,9 +300,9 @@ export function createDcaVsLumpSumSection({ dca, lookback, returns }) {
price({
metric: dca.periodAveragePrice[key],
name: "DCA",
color: colors.green,
color: colors.profit,
}),
price({ metric: lookback[key], name: "Lump Sum", color: colors.orange }),
price({ metric: lookback[key], name: "Lump Sum", color: colors.bitcoin }),
];
/** @param {string} name @param {AllPeriodKey} key */
@@ -331,7 +327,7 @@ export function createDcaVsLumpSumSection({ dca, lookback, returns }) {
baseline({
metric: dca.periodLumpSumMaxReturn[key],
name: "Lump Sum",
color: [colors.cyan, colors.orange],
color: colors.bi.lumpSum,
unit: Unit.percentage,
}),
],
@@ -349,7 +345,7 @@ export function createDcaVsLumpSumSection({ dca, lookback, returns }) {
baseline({
metric: dca.periodLumpSumMinReturn[key],
name: "Lump Sum",
color: [colors.cyan, colors.orange],
color: colors.bi.lumpSum,
unit: Unit.percentage,
}),
],
@@ -373,7 +369,7 @@ export function createDcaVsLumpSumSection({ dca, lookback, returns }) {
baseline({
metric: dca.periodLumpSumReturns[key],
name: "Lump Sum",
color: [colors.cyan, colors.orange],
color: colors.bi.lumpSum,
unit: Unit.percentage,
}),
],
@@ -399,7 +395,7 @@ export function createDcaVsLumpSumSection({ dca, lookback, returns }) {
baseline({
metric: dca.periodLumpSumReturns[key],
name: "Lump Sum",
color: [colors.cyan, colors.orange],
color: colors.bi.lumpSum,
unit: Unit.percentage,
}),
baseline({
@@ -410,7 +406,7 @@ export function createDcaVsLumpSumSection({ dca, lookback, returns }) {
baseline({
metric: returns.cagr[key],
name: "Lump Sum",
color: [colors.cyan, colors.orange],
color: colors.bi.lumpSum,
unit: Unit.cagr,
}),
],
@@ -431,13 +427,13 @@ export function createDcaVsLumpSumSection({ dca, lookback, returns }) {
line({
metric: dca.periodDaysInProfit[key],
name: "DCA",
color: colors.green,
color: colors.profit,
unit: Unit.days,
}),
line({
metric: dca.periodLumpSumDaysInProfit[key],
name: "Lump Sum",
color: colors.orange,
color: colors.bitcoin,
unit: Unit.days,
}),
],
@@ -450,13 +446,13 @@ export function createDcaVsLumpSumSection({ dca, lookback, returns }) {
line({
metric: dca.periodDaysInLoss[key],
name: "DCA",
color: colors.green,
color: colors.profit,
unit: Unit.days,
}),
line({
metric: dca.periodLumpSumDaysInLoss[key],
name: "Lump Sum",
color: colors.orange,
color: colors.bitcoin,
unit: Unit.days,
}),
],
@@ -473,12 +469,12 @@ export function createDcaVsLumpSumSection({ dca, lookback, returns }) {
...satsBtcUsd({
pattern: dca.periodStack[key],
name: "DCA",
color: colors.green,
color: colors.profit,
}),
...satsBtcUsd({
pattern: dca.periodLumpSumStack[key],
name: "Lump Sum",
color: colors.orange,
color: colors.bitcoin,
}),
],
});
@@ -570,14 +566,14 @@ function createPeriodSection({ dca, lookback, returns }) {
/** @param {BaseEntryItem} entry */
const createShortEntry = (entry) =>
createShortSingleEntry(colors, {
createShortSingleEntry({
...entry,
titlePrefix: `${entry.name} ${suffix}`,
});
/** @param {LongEntryItem} entry */
const createLongEntry = (entry) =>
createLongSingleEntry(colors, {
createLongSingleEntry({
...entry,
titlePrefix: `${entry.name} ${suffix}`,
});
@@ -647,7 +643,7 @@ export function createDcaByStartYearSection({ dca }) {
tree: [
createCompareFolder(`${name} DCA`, entries),
...entries.map((entry) =>
createShortSingleEntry(colors, {
createShortSingleEntry({
...entry,
titlePrefix: `${entry.name} DCA`,
}),
@@ -655,12 +651,8 @@ export function createDcaByStartYearSection({ dca }) {
],
});
const entries2020s = YEARS_2020S.map((year) =>
buildYearEntry(colors, dca, year),
);
const entries2010s = YEARS_2010S.map((year) =>
buildYearEntry(colors, dca, year),
);
const entries2020s = YEARS_2020S.map((year) => buildYearEntry(dca, year));
const entries2010s = YEARS_2010S.map((year) => buildYearEntry(dca, year));
return {
name: "DCA by Start Year",

View File

@@ -75,20 +75,19 @@ function createMaSubSection(label, averages) {
}
/**
* @param {Colors} colors
* @param {string} name
* @param {string} title
* @param {Unit} unit
* @param {{ _1w: AnyMetricPattern, _1m: AnyMetricPattern, _1y: AnyMetricPattern }} metrics
*/
function volatilityChart(colors, name, title, unit, metrics) {
function volatilityChart(name, title, unit, metrics) {
return {
name,
title,
bottom: [
line({ metric: metrics._1w, name: "1w", color: colors.red, unit }),
line({ metric: metrics._1m, name: "1m", color: colors.orange, unit }),
line({ metric: metrics._1y, name: "1y", color: colors.lime, unit }),
line({ metric: metrics._1w, name: "1w", color: colors.time._1w, unit }),
line({ metric: metrics._1m, name: "1m", color: colors.time._1m, unit }),
line({ metric: metrics._1y, name: "1y", color: colors.time._1y, unit }),
],
};
}
@@ -423,7 +422,7 @@ export function createMarketSection() {
line({
metric: ath.priceDrawdown,
name: "Drawdown",
color: colors.red,
color: colors.loss,
unit: Unit.percentage,
}),
],
@@ -446,13 +445,13 @@ export function createMarketSection() {
line({
metric: ath.maxDaysBetweenPriceAths,
name: "Max",
color: colors.red,
color: colors.loss,
unit: Unit.days,
}),
line({
metric: ath.maxYearsBetweenPriceAths,
name: "Max",
color: colors.red,
color: colors.loss,
unit: Unit.years,
}),
],
@@ -484,17 +483,11 @@ export function createMarketSection() {
{
name: "Volatility",
tree: [
volatilityChart(
colors,
"Index",
"Volatility Index",
Unit.percentage,
{
_1w: volatility.price1wVolatility,
_1m: volatility.price1mVolatility,
_1y: volatility.price1yVolatility,
},
),
volatilityChart("Index", "Volatility Index", Unit.percentage, {
_1w: volatility.price1wVolatility,
_1m: volatility.price1mVolatility,
_1y: volatility.price1yVolatility,
}),
{
name: "True Range",
title: "True Range",
@@ -502,13 +495,13 @@ export function createMarketSection() {
line({
metric: range.priceTrueRange,
name: "Daily",
color: colors.yellow,
color: colors.time._24h,
unit: Unit.usd,
}),
line({
metric: range.priceTrueRange2wSum,
name: "2w Sum",
color: colors.orange,
color: colors.time._1w,
unit: Unit.usd,
defaultActive: false,
}),
@@ -521,28 +514,22 @@ export function createMarketSection() {
line({
metric: range.price2wChoppinessIndex,
name: "2w",
color: colors.red,
color: colors.indicator.main,
unit: Unit.index,
}),
...priceLines({ unit: Unit.index, numbers: [61.8, 38.2] }),
],
},
volatilityChart(colors, "Sharpe Ratio", "Sharpe Ratio", Unit.ratio, {
volatilityChart("Sharpe Ratio", "Sharpe Ratio", Unit.ratio, {
_1w: volatility.sharpe1w,
_1m: volatility.sharpe1m,
_1y: volatility.sharpe1y,
}),
volatilityChart(
colors,
"Sortino Ratio",
"Sortino Ratio",
Unit.ratio,
{
_1w: volatility.sortino1w,
_1m: volatility.sortino1m,
_1y: volatility.sortino1y,
},
),
volatilityChart("Sortino Ratio", "Sortino Ratio", Unit.ratio, {
_1w: volatility.sortino1w,
_1m: volatility.sortino1m,
_1y: volatility.sortino1y,
}),
],
},
@@ -623,17 +610,17 @@ export function createMarketSection() {
name: p.id,
title: `${p.name} MinMax`,
top: [
price({
metric: p.min,
name: "Min",
key: "price-min",
color: colors.red,
}),
price({
metric: p.max,
name: "Max",
key: "price-max",
color: colors.green,
color: colors.stat.max,
}),
price({
metric: p.min,
name: "Min",
key: "price-min",
color: colors.stat.min,
}),
],
})),
@@ -645,17 +632,17 @@ export function createMarketSection() {
price({
metric: ma.price200dSma.price,
name: "200d SMA",
color: colors.yellow,
color: colors.ma._200d,
}),
price({
metric: ma.price200dSmaX24,
name: "200d SMA x2.4",
color: colors.green,
color: colors.indicator.upper,
}),
price({
metric: ma.price200dSmaX08,
name: "200d SMA x0.8",
color: colors.red,
color: colors.indicator.lower,
}),
],
},
@@ -672,20 +659,20 @@ export function createMarketSection() {
line({
metric: indicators.rsi14d,
name: "RSI",
color: colors.indigo,
unit: Unit.index,
}),
line({
metric: indicators.rsi14dMin,
name: "Min",
color: colors.red,
defaultActive: false,
color: colors.indicator.main,
unit: Unit.index,
}),
line({
metric: indicators.rsi14dMax,
name: "Max",
color: colors.green,
color: colors.stat.max,
defaultActive: false,
unit: Unit.index,
}),
line({
metric: indicators.rsi14dMin,
name: "Min",
color: colors.stat.min,
defaultActive: false,
unit: Unit.index,
}),
@@ -701,13 +688,13 @@ export function createMarketSection() {
line({
metric: indicators.stochRsiK,
name: "K",
color: colors.blue,
color: colors.indicator.fast,
unit: Unit.index,
}),
line({
metric: indicators.stochRsiD,
name: "D",
color: colors.orange,
color: colors.indicator.slow,
unit: Unit.index,
}),
...priceLines({ unit: Unit.index, numbers: [80, 20] }),
@@ -720,13 +707,13 @@ export function createMarketSection() {
line({
metric: indicators.macdLine,
name: "MACD",
color: colors.blue,
color: colors.indicator.fast,
unit: Unit.usd,
}),
line({
metric: indicators.macdSignal,
name: "Signal",
color: colors.orange,
color: colors.indicator.slow,
unit: Unit.usd,
}),
histogram({
@@ -769,12 +756,12 @@ export function createMarketSection() {
price({
metric: ma.price111dSma.price,
name: "111d SMA",
color: colors.green,
color: colors.indicator.upper,
}),
price({
metric: ma.price350dSmaX2,
name: "350d SMA x2",
color: colors.red,
color: colors.indicator.lower,
}),
],
bottom: [
@@ -793,7 +780,7 @@ export function createMarketSection() {
line({
metric: indicators.puellMultiple,
name: "Puell",
color: colors.green,
color: colors.usd,
unit: Unit.ratio,
}),
],
@@ -805,7 +792,7 @@ export function createMarketSection() {
line({
metric: indicators.nvt,
name: "NVT",
color: colors.orange,
color: colors.bitcoin,
unit: Unit.ratio,
}),
],
@@ -817,7 +804,7 @@ export function createMarketSection() {
line({
metric: indicators.gini,
name: "Gini",
color: colors.red,
color: colors.loss,
unit: Unit.ratio,
}),
],

View File

@@ -2,7 +2,7 @@
import { Unit } from "../utils/units.js";
import { entries, includes } from "../utils/array.js";
import { colorAt, colors } from "../utils/colors.js";
import { colors } from "../utils/colors.js";
import {
line,
baseline,
@@ -217,47 +217,89 @@ export function createMiningSection() {
// Hashrate
{
name: "Hashrate",
title: "Network Hashrate",
bottom: [
dots({
metric: blocks.mining.hashRate,
name: "Hashrate",
unit: Unit.hashRate,
}),
line({
metric: blocks.mining.hashRate1wSma,
name: "1w SMA",
color: colors.time._1w,
unit: Unit.hashRate,
defaultActive: false,
}),
line({
metric: blocks.mining.hashRate1mSma,
name: "1m SMA",
color: colors.time._1m,
unit: Unit.hashRate,
defaultActive: false,
}),
line({
metric: blocks.mining.hashRate2mSma,
name: "2m SMA",
color: colors.orange,
unit: Unit.hashRate,
defaultActive: false,
}),
line({
metric: blocks.mining.hashRate1ySma,
name: "1y SMA",
color: colors.time._1y,
unit: Unit.hashRate,
defaultActive: false,
}),
dotted({
metric: blocks.difficulty.asHash,
name: "Difficulty",
color: colors.default,
unit: Unit.hashRate,
}),
tree: [
{
name: "Current",
title: "Network Hashrate",
bottom: [
dots({
metric: blocks.mining.hashRate,
name: "Hashrate",
unit: Unit.hashRate,
}),
line({
metric: blocks.mining.hashRate1wSma,
name: "1w SMA",
color: colors.time._1w,
unit: Unit.hashRate,
defaultActive: false,
}),
line({
metric: blocks.mining.hashRate1mSma,
name: "1m SMA",
color: colors.time._1m,
unit: Unit.hashRate,
defaultActive: false,
}),
line({
metric: blocks.mining.hashRate2mSma,
name: "2m SMA",
color: colors.ma._2m,
unit: Unit.hashRate,
defaultActive: false,
}),
line({
metric: blocks.mining.hashRate1ySma,
name: "1y SMA",
color: colors.time._1y,
unit: Unit.hashRate,
defaultActive: false,
}),
dotted({
metric: blocks.difficulty.asHash,
name: "Difficulty",
color: colors.default,
unit: Unit.hashRate,
}),
line({
metric: blocks.mining.hashRateAth,
name: "ATH",
color: colors.loss,
unit: Unit.hashRate,
defaultActive: false,
}),
],
},
{
name: "ATH",
title: "Network Hashrate ATH",
bottom: [
line({
metric: blocks.mining.hashRateAth,
name: "ATH",
color: colors.loss,
unit: Unit.hashRate,
}),
dots({
metric: blocks.mining.hashRate,
name: "Hashrate",
color: colors.bitcoin,
unit: Unit.hashRate,
}),
],
},
{
name: "Drawdown",
title: "Network Hashrate Drawdown",
bottom: [
line({
metric: blocks.mining.hashRateDrawdown,
name: "Drawdown",
unit: Unit.percentage,
color: colors.loss,
}),
],
},
],
},
@@ -305,13 +347,11 @@ export function createMiningSection() {
line({
metric: blocks.difficulty.blocksBeforeNextAdjustment,
name: "Remaining",
color: colors.indigo,
unit: Unit.blocks,
}),
line({
metric: blocks.difficulty.daysBeforeNextAdjustment,
name: "Remaining",
color: colors.purple,
unit: Unit.days,
}),
],
@@ -466,13 +506,13 @@ export function createMiningSection() {
line({
metric: blocks.rewards.subsidyDominance,
name: "Subsidy",
color: colors.lime,
color: colors.mining.subsidy,
unit: Unit.percentage,
}),
line({
metric: blocks.rewards.feeDominance,
name: "Fees",
color: colors.cyan,
color: colors.mining.fee,
unit: Unit.percentage,
}),
],
@@ -514,25 +554,25 @@ export function createMiningSection() {
line({
metric: blocks.mining.hashPriceThs,
name: "TH/s",
color: colors.emerald,
color: colors.usd,
unit: Unit.usdPerThsPerDay,
}),
line({
metric: blocks.mining.hashPricePhs,
name: "PH/s",
color: colors.emerald,
color: colors.usd,
unit: Unit.usdPerPhsPerDay,
}),
dotted({
metric: blocks.mining.hashPriceThsMin,
name: "TH/s Min",
color: colors.red,
color: colors.stat.min,
unit: Unit.usdPerThsPerDay,
}),
dotted({
metric: blocks.mining.hashPricePhsMin,
name: "PH/s Min",
color: colors.red,
color: colors.stat.min,
unit: Unit.usdPerPhsPerDay,
}),
],
@@ -544,25 +584,25 @@ export function createMiningSection() {
line({
metric: blocks.mining.hashValueThs,
name: "TH/s",
color: colors.orange,
color: colors.bitcoin,
unit: Unit.satsPerThsPerDay,
}),
line({
metric: blocks.mining.hashValuePhs,
name: "PH/s",
color: colors.orange,
color: colors.bitcoin,
unit: Unit.satsPerPhsPerDay,
}),
dotted({
metric: blocks.mining.hashValueThsMin,
name: "TH/s Min",
color: colors.red,
color: colors.stat.min,
unit: Unit.satsPerThsPerDay,
}),
dotted({
metric: blocks.mining.hashValuePhsMin,
name: "PH/s Min",
color: colors.red,
color: colors.stat.min,
unit: Unit.satsPerPhsPerDay,
}),
],
@@ -574,13 +614,13 @@ export function createMiningSection() {
line({
metric: blocks.mining.hashPriceRebound,
name: "Hash Price",
color: colors.emerald,
color: colors.usd,
unit: Unit.percentage,
}),
line({
metric: blocks.mining.hashValueRebound,
name: "Hash Value",
color: colors.orange,
color: colors.bitcoin,
unit: Unit.percentage,
}),
],
@@ -637,7 +677,7 @@ export function createMiningSection() {
line({
metric: p.pool._1mDominance,
name: p.name,
color: colorAt(i),
color: colors.at(i),
unit: Unit.percentage,
}),
),
@@ -649,7 +689,7 @@ export function createMiningSection() {
line({
metric: p.pool._1mBlocksMined,
name: p.name,
color: colorAt(i),
color: colors.at(i),
unit: Unit.count,
}),
),
@@ -662,7 +702,7 @@ export function createMiningSection() {
source: p.pool.coinbase,
key: "sum",
name: p.name,
color: colorAt(i),
color: colors.at(i),
}),
),
},
@@ -679,7 +719,7 @@ export function createMiningSection() {
line({
metric: p.pool._1mDominance,
name: p.name,
color: colorAt(i),
color: colors.at(i),
unit: Unit.percentage,
}),
),
@@ -691,7 +731,7 @@ export function createMiningSection() {
line({
metric: p.pool._1mBlocksMined,
name: p.name,
color: colorAt(i),
color: colors.at(i),
unit: Unit.count,
}),
),
@@ -704,7 +744,7 @@ export function createMiningSection() {
source: p.pool.coinbase,
key: "sum",
name: p.name,
color: colorAt(i),
color: colors.at(i),
}),
),
},

View File

@@ -7,6 +7,7 @@ import { priceLine } from "./constants.js";
import {
line,
dots,
baseline,
fromSupplyPattern,
fromBaseStatsPattern,
chartsFromFull,
@@ -126,17 +127,25 @@ export function createNetworkSection() {
]);
// Count types for comparison charts
// addrCount and emptyAddrCount have .count, totalAddrCount doesn't
const countTypes = /** @type {const} */ ([
{ key: "addrCount", name: "Funded", title: "Address Count by Type" },
{
key: "emptyAddrCount",
name: "Empty",
title: "Empty Address Count by Type",
name: "Funded",
title: "Address Count by Type",
/** @param {AddressableType} t */
getMetric: (t) => distribution.addrCount[t].count,
},
{
name: "Empty",
title: "Empty Address Count by Type",
/** @param {AddressableType} t */
getMetric: (t) => distribution.emptyAddrCount[t].count,
},
{
key: "totalAddrCount",
name: "Total",
title: "Total Address Count by Type",
/** @param {AddressableType} t */
getMetric: (t) => distribution.totalAddrCount[t],
},
]);
@@ -151,7 +160,7 @@ export function createNetworkSection() {
title: `${titlePrefix}Address Count`,
bottom: [
line({
metric: distribution.addrCount[key],
metric: distribution.addrCount[key].count,
name: "Funded",
unit: Unit.count,
}),
@@ -163,7 +172,7 @@ export function createNetworkSection() {
defaultActive: false,
}),
line({
metric: distribution.emptyAddrCount[key],
metric: distribution.emptyAddrCount[key].count,
name: "Empty",
color: colors.gray,
unit: Unit.count,
@@ -172,28 +181,44 @@ export function createNetworkSection() {
],
},
{
name: "New",
tree: chartsFromFull({
pattern: distribution.newAddrCount[key],
title: `${titlePrefix}New Address Count`,
unit: Unit.count,
}),
},
{
name: "Reactivated",
title: `${titlePrefix}Reactivated Address Count`,
bottom: fromBaseStatsPattern({
pattern: distribution.addressActivity[key].reactivated,
unit: Unit.count,
}),
},
{
name: "Growth Rate",
title: `${titlePrefix}Address Growth Rate`,
bottom: fromBaseStatsPattern({
pattern: distribution.growthRate[key],
unit: Unit.ratio,
}),
name: "Trends",
tree: [
{
name: "30d Change",
title: `${titlePrefix}Address Count 30d Change`,
bottom: [
baseline({
metric: distribution.addrCount[key]._30dChange,
name: "30d Change",
unit: Unit.count,
}),
],
},
{
name: "New",
tree: chartsFromFull({
pattern: distribution.newAddrCount[key],
title: `${titlePrefix}New Address Count`,
unit: Unit.count,
}),
},
{
name: "Reactivated",
title: `${titlePrefix}Reactivated Address Count`,
bottom: fromBaseStatsPattern({
pattern: distribution.addressActivity[key].reactivated,
unit: Unit.count,
}),
},
{
name: "Growth Rate",
title: `${titlePrefix}Address Growth Rate`,
bottom: fromBaseStatsPattern({
pattern: distribution.growthRate[key],
unit: Unit.ratio,
}),
},
],
},
{
name: "Transacting",
@@ -235,7 +260,7 @@ export function createNetworkSection() {
title: `${groupName} ${c.title}`,
bottom: types.map((t) =>
line({
metric: distribution[c.key][t.key],
metric: c.getMetric(t.key),
name: t.name,
color: t.color,
unit: Unit.count,
@@ -353,7 +378,7 @@ export function createNetworkSection() {
{ key: "p2ms", name: "P2MS", color: st.p2ms },
]);
const segwitScripts = /** @type {const} */ ([
{ key: "segwit", name: "All SegWit", color: colors.cyan },
{ key: "segwit", name: "All SegWit", color: colors.segwit },
{ key: "p2wsh", name: "P2WSH", color: st.p2wsh },
{ key: "p2wpkh", name: "P2WPKH", color: st.p2wpkh },
]);
@@ -492,7 +517,7 @@ export function createNetworkSection() {
...satsBtcUsd({
pattern: transactions.volume.receivedSum,
name: "Received",
color: colors.cyan,
color: colors.entity.output,
}),
],
},
@@ -530,19 +555,19 @@ export function createNetworkSection() {
line({
metric: transactions.versions.v1.sum,
name: "v1",
color: colors.orange,
color: colors.txVersion.v1,
unit: Unit.count,
}),
line({
metric: transactions.versions.v2.sum,
name: "v2",
color: colors.cyan,
color: colors.txVersion.v2,
unit: Unit.count,
}),
line({
metric: transactions.versions.v3.sum,
name: "v3",
color: colors.lime,
color: colors.txVersion.v3,
unit: Unit.count,
}),
],
@@ -554,19 +579,19 @@ export function createNetworkSection() {
line({
metric: transactions.versions.v1.cumulative,
name: "v1",
color: colors.orange,
color: colors.txVersion.v1,
unit: Unit.count,
}),
line({
metric: transactions.versions.v2.cumulative,
name: "v2",
color: colors.cyan,
color: colors.txVersion.v2,
unit: Unit.count,
}),
line({
metric: transactions.versions.v3.cumulative,
name: "v3",
color: colors.lime,
color: colors.txVersion.v3,
unit: Unit.count,
}),
],
@@ -585,7 +610,7 @@ export function createNetworkSection() {
line({
metric: supply.velocity.usd,
name: "USD",
color: colors.emerald,
color: colors.usd,
unit: Unit.ratio,
}),
],
@@ -625,25 +650,25 @@ export function createNetworkSection() {
line({
metric: blocks.count._24hBlockCount,
name: "24h",
color: colors.pink,
color: colors.time._24h,
unit: Unit.count,
}),
line({
metric: blocks.count._1wBlockCount,
name: "1w",
color: colors.red,
color: colors.time._1w,
unit: Unit.count,
}),
line({
metric: blocks.count._1mBlockCount,
name: "1m",
color: colors.orange,
color: colors.time._1m,
unit: Unit.count,
}),
line({
metric: blocks.count._1yBlockCount,
name: "1y",
color: colors.purple,
color: colors.time._1y,
unit: Unit.count,
}),
],
@@ -839,6 +864,17 @@ export function createNetworkSection() {
}),
],
},
{
name: "30d Change",
title: "UTXO Count 30d Change",
bottom: [
baseline({
metric: distribution.utxoCohorts.all.outputs.utxoCount30dChange,
name: "30d Change",
unit: Unit.count,
}),
],
},
{
name: "Flow",
title: "UTXO Flow",
@@ -846,13 +882,13 @@ export function createNetworkSection() {
line({
metric: outputs.count.totalCount.sum,
name: "Created",
color: colors.lime,
color: colors.entity.output,
unit: Unit.count,
}),
line({
metric: inputs.count.sum,
name: "Spent",
color: colors.red,
color: colors.entity.input,
unit: Unit.count,
}),
],
@@ -882,18 +918,19 @@ export function createNetworkSection() {
dots({
metric: transactions.volume.txPerSec,
name: "TX/sec",
color: colors.red,
color: colors.entity.tx,
unit: Unit.perSec,
}),
dots({
metric: transactions.volume.inputsPerSec,
name: "Inputs/sec",
color: colors.entity.input,
unit: Unit.perSec,
}),
dots({
metric: transactions.volume.outputsPerSec,
name: "Outputs/sec",
color: colors.cyan,
color: colors.entity.output,
unit: Unit.perSec,
}),
],
@@ -917,7 +954,7 @@ export function createNetworkSection() {
title: c.title,
bottom: addressTypes.map((t) =>
line({
metric: distribution[c.key][t.key],
metric: c.getMetric(t.key),
name: t.name,
color: t.color,
unit: Unit.count,
@@ -1198,13 +1235,13 @@ export function createNetworkSection() {
line({
metric: scripts.count.segwitAdoption.cumulative,
name: "SegWit",
color: colors.cyan,
color: colors.segwit,
unit: Unit.percentage,
}),
line({
metric: scripts.count.taprootAdoption.cumulative,
name: "Taproot",
color: colors.orange,
color: colors.scriptType.p2tr,
unit: Unit.percentage,
}),
],
@@ -1226,7 +1263,7 @@ export function createNetworkSection() {
line({
metric: scripts.count.segwitAdoption.cumulative,
name: "All-Time",
color: colors.red,
color: colors.time.all,
unit: Unit.percentage,
}),
],
@@ -1248,7 +1285,7 @@ export function createNetworkSection() {
line({
metric: scripts.count.taprootAdoption.cumulative,
name: "All-Time",
color: colors.red,
color: colors.time.all,
unit: Unit.percentage,
}),
],

View File

@@ -80,22 +80,22 @@ export function createPartialOptions() {
// Overview - All UTXOs (adjustedSopr + percentiles but no RelToMarketCap)
createCohortFolderAll({ ...cohortAll, name: "Overview" }),
// STH - Short term holder cohort (Full capability)
createCohortFolderFull(termShort),
// LTH - Long term holder cohort (nupl)
createCohortFolderWithNupl(termLong),
// STH vs LTH - Direct comparison
// STH vs LTH - Direct comparison (before individual cohorts)
createCohortFolderWithNupl({
name: "STH vs LTH",
title: "STH vs LTH",
list: [termShort, termLong],
}),
// STH - Short term holder cohort (Full capability)
createCohortFolderFull(termShort),
// LTH - Long term holder cohort (nupl)
createCohortFolderWithNupl(termLong),
// Ages cohorts
{
name: "Ages",
name: "UTXO Ages",
tree: [
// Younger Than (< X old)
{
@@ -138,7 +138,7 @@ export function createPartialOptions() {
// Sizes cohorts (UTXO size)
{
name: "Sizes",
name: "UTXO Sizes",
tree: [
// Less Than (< X sats)
{
@@ -187,7 +187,7 @@ export function createPartialOptions() {
// Balances cohorts (Address balance)
{
name: "Balances",
name: "Address Balances",
tree: [
// Less Than (< X sats)
{

View File

@@ -42,6 +42,35 @@ export function satsBtcUsd({ pattern, name, color, defaultActive }) {
];
}
/**
* Create sats/btc/usd baseline series from a value pattern
* @param {Object} args
* @param {{ bitcoin: AnyMetricPattern, sats: AnyMetricPattern, dollars: AnyMetricPattern }} args.pattern
* @param {string} args.name
* @param {Color} [args.color]
* @param {boolean} [args.defaultActive]
* @returns {FetchedBaselineSeriesBlueprint[]}
*/
export function satsBtcUsdBaseline({ pattern, name, color, defaultActive }) {
return [
baseline({
metric: pattern.bitcoin,
name,
color,
unit: Unit.btc,
defaultActive,
}),
baseline({ metric: pattern.sats, name, color, unit: Unit.sats, defaultActive }),
baseline({
metric: pattern.dollars,
name,
color,
unit: Unit.usd,
defaultActive,
}),
];
}
/**
* Create sats/btc/usd series from any value pattern using sum or cumulative key
* @param {Object} args
@@ -109,15 +138,15 @@ export function revenueBtcSatsUsd({ coinbase, subsidy, fee, key }) {
source: coinbase,
key,
name: "Coinbase",
color: colors.orange,
color: colors.mining.coinbase,
}),
...satsBtcUsdFrom({
source: subsidy,
key,
name: "Subsidy",
color: colors.lime,
color: colors.mining.subsidy,
}),
...satsBtcUsdFrom({ source: fee, key, name: "Fees", color: colors.cyan }),
...satsBtcUsdFrom({ source: fee, key, name: "Fees", color: colors.mining.fee }),
];
}
@@ -127,12 +156,12 @@ export function revenueBtcSatsUsd({ coinbase, subsidy, fee, key }) {
*/
export function percentileUsdMap(ratio) {
return /** @type {const} */ ([
{ name: "pct95", prop: ratio.ratioPct95Usd, color: colors.fuchsia },
{ name: "pct5", prop: ratio.ratioPct5Usd, color: colors.cyan },
{ name: "pct98", prop: ratio.ratioPct98Usd, color: colors.pink },
{ name: "pct2", prop: ratio.ratioPct2Usd, color: colors.sky },
{ name: "pct99", prop: ratio.ratioPct99Usd, color: colors.rose },
{ name: "pct1", prop: ratio.ratioPct1Usd, color: colors.blue },
{ name: "pct95", prop: ratio.ratioPct95Usd, color: colors.ratioPct._95 },
{ name: "pct5", prop: ratio.ratioPct5Usd, color: colors.ratioPct._5 },
{ name: "pct98", prop: ratio.ratioPct98Usd, color: colors.ratioPct._98 },
{ name: "pct2", prop: ratio.ratioPct2Usd, color: colors.ratioPct._2 },
{ name: "pct99", prop: ratio.ratioPct99Usd, color: colors.ratioPct._99 },
{ name: "pct1", prop: ratio.ratioPct1Usd, color: colors.ratioPct._1 },
]);
}
@@ -142,12 +171,12 @@ export function percentileUsdMap(ratio) {
*/
export function percentileMap(ratio) {
return /** @type {const} */ ([
{ name: "pct95", prop: ratio.ratioPct95, color: colors.fuchsia },
{ name: "pct5", prop: ratio.ratioPct5, color: colors.cyan },
{ name: "pct98", prop: ratio.ratioPct98, color: colors.pink },
{ name: "pct2", prop: ratio.ratioPct2, color: colors.sky },
{ name: "pct99", prop: ratio.ratioPct99, color: colors.rose },
{ name: "pct1", prop: ratio.ratioPct1, color: colors.blue },
{ name: "pct95", prop: ratio.ratioPct95, color: colors.ratioPct._95 },
{ name: "pct5", prop: ratio.ratioPct5, color: colors.ratioPct._5 },
{ name: "pct98", prop: ratio.ratioPct98, color: colors.ratioPct._98 },
{ name: "pct2", prop: ratio.ratioPct2, color: colors.ratioPct._2 },
{ name: "pct99", prop: ratio.ratioPct99, color: colors.ratioPct._99 },
{ name: "pct1", prop: ratio.ratioPct1, color: colors.ratioPct._1 },
]);
}
@@ -170,19 +199,19 @@ export function sdPatterns(ratio) {
*/
export function sdBandsUsd(sd) {
return /** @type {const} */ ([
{ name: "0σ", prop: sd._0sdUsd, color: colors.lime },
{ name: "+0.5σ", prop: sd.p05sdUsd, color: colors.yellow },
{ name: "0.5σ", prop: sd.m05sdUsd, color: colors.teal },
{ name: "+1σ", prop: sd.p1sdUsd, color: colors.amber },
{ name: "1σ", prop: sd.m1sdUsd, color: colors.cyan },
{ name: "+1.5σ", prop: sd.p15sdUsd, color: colors.orange },
{ name: "1.5σ", prop: sd.m15sdUsd, color: colors.sky },
{ name: "+2σ", prop: sd.p2sdUsd, color: colors.red },
{ name: "2σ", prop: sd.m2sdUsd, color: colors.blue },
{ name: "+2.5σ", prop: sd.p25sdUsd, color: colors.rose },
{ name: "2.5σ", prop: sd.m25sdUsd, color: colors.indigo },
{ name: "+3σ", prop: sd.p3sdUsd, color: colors.pink },
{ name: "3σ", prop: sd.m3sdUsd, color: colors.violet },
{ name: "0σ", prop: sd._0sdUsd, color: colors.sd._0 },
{ name: "+0.5σ", prop: sd.p05sdUsd, color: colors.sd.p05 },
{ name: "0.5σ", prop: sd.m05sdUsd, color: colors.sd.m05 },
{ name: "+1σ", prop: sd.p1sdUsd, color: colors.sd.p1 },
{ name: "1σ", prop: sd.m1sdUsd, color: colors.sd.m1 },
{ name: "+1.5σ", prop: sd.p15sdUsd, color: colors.sd.p15 },
{ name: "1.5σ", prop: sd.m15sdUsd, color: colors.sd.m15 },
{ name: "+2σ", prop: sd.p2sdUsd, color: colors.sd.p2 },
{ name: "2σ", prop: sd.m2sdUsd, color: colors.sd.m2 },
{ name: "+2.5σ", prop: sd.p25sdUsd, color: colors.sd.p25 },
{ name: "2.5σ", prop: sd.m25sdUsd, color: colors.sd.m25 },
{ name: "+3σ", prop: sd.p3sdUsd, color: colors.sd.p3 },
{ name: "3σ", prop: sd.m3sdUsd, color: colors.sd.m3 },
]);
}
@@ -192,19 +221,19 @@ export function sdBandsUsd(sd) {
*/
export function sdBandsRatio(sd) {
return /** @type {const} */ ([
{ name: "0σ", prop: sd.sma, color: colors.lime },
{ name: "+0.5σ", prop: sd.p05sd, color: colors.yellow },
{ name: "0.5σ", prop: sd.m05sd, color: colors.teal },
{ name: "+1σ", prop: sd.p1sd, color: colors.amber },
{ name: "1σ", prop: sd.m1sd, color: colors.cyan },
{ name: "+1.5σ", prop: sd.p15sd, color: colors.orange },
{ name: "1.5σ", prop: sd.m15sd, color: colors.sky },
{ name: "+2σ", prop: sd.p2sd, color: colors.red },
{ name: "2σ", prop: sd.m2sd, color: colors.blue },
{ name: "+2.5σ", prop: sd.p25sd, color: colors.rose },
{ name: "2.5σ", prop: sd.m25sd, color: colors.indigo },
{ name: "+3σ", prop: sd.p3sd, color: colors.pink },
{ name: "3σ", prop: sd.m3sd, color: colors.violet },
{ name: "0σ", prop: sd.sma, color: colors.sd._0 },
{ name: "+0.5σ", prop: sd.p05sd, color: colors.sd.p05 },
{ name: "0.5σ", prop: sd.m05sd, color: colors.sd.m05 },
{ name: "+1σ", prop: sd.p1sd, color: colors.sd.p1 },
{ name: "1σ", prop: sd.m1sd, color: colors.sd.m1 },
{ name: "+1.5σ", prop: sd.p15sd, color: colors.sd.p15 },
{ name: "1.5σ", prop: sd.m15sd, color: colors.sd.m15 },
{ name: "+2σ", prop: sd.p2sd, color: colors.sd.p2 },
{ name: "2σ", prop: sd.m2sd, color: colors.sd.m2 },
{ name: "+2.5σ", prop: sd.p25sd, color: colors.sd.p25 },
{ name: "2.5σ", prop: sd.m25sd, color: colors.sd.m25 },
{ name: "+3σ", prop: sd.p3sd, color: colors.sd.p3 },
{ name: "3σ", prop: sd.m3sd, color: colors.sd.m3 },
]);
}
@@ -214,12 +243,12 @@ export function sdBandsRatio(sd) {
*/
export function ratioSmas(ratio) {
return /** @type {const} */ ([
{ name: "1w SMA", metric: ratio.ratio1wSma, color: colors.lime },
{ name: "1m SMA", metric: ratio.ratio1mSma, color: colors.teal },
{ name: "1y SMA", metric: ratio.ratio1ySd.sma, color: colors.sky },
{ name: "2y SMA", metric: ratio.ratio2ySd.sma, color: colors.indigo },
{ name: "4y SMA", metric: ratio.ratio4ySd.sma, color: colors.purple },
{ name: "All SMA", metric: ratio.ratioSd.sma, color: colors.rose },
{ name: "1w SMA", metric: ratio.ratio1wSma, color: colors.ma._1w },
{ name: "1m SMA", metric: ratio.ratio1mSma, color: colors.ma._1m },
{ name: "1y SMA", metric: ratio.ratio1ySd.sma, color: colors.ma._1y },
{ name: "2y SMA", metric: ratio.ratio2ySd.sma, color: colors.ma._2y },
{ name: "4y SMA", metric: ratio.ratio4ySd.sma, color: colors.ma._4y },
{ name: "All SMA", metric: ratio.ratioSd.sma, color: colors.time.all },
]);
}
@@ -303,25 +332,25 @@ export function createZScoresFolder({
price({
metric: ratio.ratio1ySd._0sdUsd,
name: "1y 0σ",
color: colors.orange,
color: colors.ma._1y,
defaultActive: false,
}),
price({
metric: ratio.ratio2ySd._0sdUsd,
name: "2y 0σ",
color: colors.yellow,
color: colors.ma._2y,
defaultActive: false,
}),
price({
metric: ratio.ratio4ySd._0sdUsd,
name: "4y 0σ",
color: colors.lime,
color: colors.ma._4y,
defaultActive: false,
}),
price({
metric: ratio.ratioSd._0sdUsd,
name: "all 0σ",
color: colors.blue,
color: colors.time.all,
defaultActive: false,
}),
],
@@ -329,25 +358,25 @@ export function createZScoresFolder({
line({
metric: ratio.ratioSd.zscore,
name: "All",
color: colors.blue,
color: colors.time.all,
unit: Unit.sd,
}),
line({
metric: ratio.ratio4ySd.zscore,
name: "4y",
color: colors.lime,
color: colors.ma._4y,
unit: Unit.sd,
}),
line({
metric: ratio.ratio2ySd.zscore,
name: "2y",
color: colors.yellow,
color: colors.ma._2y,
unit: Unit.sd,
}),
line({
metric: ratio.ratio1ySd.zscore,
name: "1y",
color: colors.orange,
color: colors.ma._1y,
unit: Unit.sd,
}),
...priceLines({
@@ -422,9 +451,8 @@ export function createZScoresFolder({
* @param {AnyPricePattern} args.pricePattern - The price pattern
* @param {AnyRatioPattern} args.ratio - The ratio pattern
* @param {Color} args.color
* @param {string} [args.ratioName] - Optional custom name for ratio chart (default: "ratio")
* @param {string} [args.priceTitle] - Optional override for price chart title (default: context)
* @param {string} [args.zScoresSuffix] - Optional suffix appended to context for z-scores (e.g., "MVRV" gives "2y Z-Score: STH MVRV")
* @param {string} [args.titlePrefix] - Optional prefix for ratio/z-scores titles (e.g., "Realized Price" gives "Realized Price Ratio: STH")
* @param {FetchedPriceSeriesBlueprint[]} [args.priceReferences] - Optional additional price series to show in Price chart
* @returns {PartialOptionsTree}
*/
@@ -434,15 +462,11 @@ export function createPriceRatioCharts({
pricePattern,
ratio,
color,
ratioName,
priceTitle,
zScoresSuffix,
titlePrefix,
priceReferences,
}) {
const titleFn = formatCohortTitle(context);
const zScoresTitleFn = zScoresSuffix
? formatCohortTitle(`${context} ${zScoresSuffix}`)
: titleFn;
return [
{
name: "Price",
@@ -453,14 +477,15 @@ export function createPriceRatioCharts({
],
},
createRatioChart({
title: titleFn,
title: (name) =>
titleFn(titlePrefix ? `${titlePrefix} ${name}` : name),
pricePattern,
ratio,
color,
name: ratioName,
}),
createZScoresFolder({
formatTitle: zScoresTitleFn,
formatTitle: (name) =>
titleFn(titlePrefix ? `${titlePrefix} ${name}` : name),
legend,
pricePattern,
ratio,

View File

@@ -180,7 +180,7 @@
* @property {string} title
* @property {Color} color
* @property {PatternAll} tree
* @property {Brk.MetricPattern1<Brk.StoredU64>} addrCount
* @property {Brk._30dCountPattern} addrCount
*
* Full cohort: adjustedSopr + percentiles + RelToMarketCap (term.short)
* @typedef {Object} CohortFull
@@ -253,7 +253,7 @@
* ============================================================================
*
* Addressable cohort with address count (for "type" cohorts - no RelToMarketCap)
* @typedef {CohortBasicWithoutMarketCap & { addrCount: Brk.MetricPattern1<Brk.StoredU64> }} CohortAddress
* @typedef {CohortBasicWithoutMarketCap & { addrCount: Brk._30dCountPattern }} CohortAddress
*
* ============================================================================
* Cohort Group Types (by capability)

View File

@@ -61,7 +61,7 @@ export function init() {
type: "Candlestick",
title: "Price",
metric: brk.metrics.price.sats.ohlc,
colors: [colors.red, colors.green],
colors: colors.bi.profitLoss,
};
result.set(Unit.sats, [satsPrice, ...(optionTop.get(Unit.sats) ?? [])]);

View File

@@ -10,7 +10,7 @@
*
* @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 } from "./utils/colors.js"
*
* @import { WebSockets } from "./utils/ws.js"
*
@@ -77,20 +77,22 @@
* Relative patterns by capability:
* - BasicRelativePattern: minimal relative (investedCapitalIn*Pct, supplyIn*RelToOwnSupply only)
* - GlobalRelativePattern: has RelToMarketCap metrics (netUnrealizedPnlRelToMarketCap, etc)
* - GlobalPeakRelativePattern: GlobalRelativePattern + unrealizedPeakRegretRelToMarketCap
* - OwnRelativePattern: has RelToOwnMarketCap metrics (netUnrealizedPnlRelToOwnMarketCap, etc)
* - FullRelativePattern: has BOTH RelToMarketCap AND RelToOwnMarketCap
* - FullRelativePattern: has BOTH RelToMarketCap AND RelToOwnMarketCap + unrealizedPeakRegretRelToMarketCap
* @typedef {Brk.InvestedSupplyPattern} BasicRelativePattern
* @typedef {Brk.InvestedNegNetNuplSupplyUnrealizedPattern} GlobalRelativePattern
* @typedef {Brk.InvestedNegNetNuplSupplyUnrealizedPattern3} GlobalPeakRelativePattern
* @typedef {Brk.InvestedNegNetSupplyUnrealizedPattern} OwnRelativePattern
* @typedef {Brk.InvestedNegNetNuplSupplyUnrealizedPattern2} FullRelativePattern
* @typedef {Brk.InvestedNegNetNuplSupplyUnrealizedPattern4} FullRelativePattern
* @typedef {Brk.GreedInvestedInvestorNegNetPainSupplyTotalUnrealizedPattern} UnrealizedPattern
* @typedef {Brk.GreedInvestedInvestorNegNetPainPeakSupplyTotalUnrealizedPattern} UnrealizedFullPattern
*
* Realized patterns
* @typedef {Brk.CapCapitulationInvestorLossMvrvNegNetPeakProfitRealizedSellSoprTotalValuePattern} RealizedPattern
* @typedef {Brk.CapCapitulationInvestorLossMvrvNegNetPeakProfitRealizedSellSoprTotalValuePattern2} RealizedPattern2
* @typedef {Brk.AdjustedCapCapitulationInvestorLossMvrvNegNetPeakProfitRealizedSellSoprTotalValuePattern} RealizedPattern3
* @typedef {Brk.AdjustedCapCapitulationInvestorLossMvrvNegNetPeakProfitRealizedSellSoprTotalValuePattern2} RealizedPattern4
* @typedef {Brk.CapCapitulationInvestorLossMvrvNegNetPeakProfitRealizedSellSentSoprTotalValuePattern} RealizedPattern
* @typedef {Brk.CapCapitulationInvestorLossMvrvNegNetPeakProfitRealizedSellSentSoprTotalValuePattern2} RealizedPattern2
* @typedef {Brk.AdjustedCapCapitulationInvestorLossMvrvNegNetPeakProfitRealizedSellSentSoprTotalValuePattern} RealizedPattern3
* @typedef {Brk.AdjustedCapCapitulationInvestorLossMvrvNegNetPeakProfitRealizedSellSentSoprTotalValuePattern2} RealizedPattern4
*/
/**
@@ -153,10 +155,11 @@
* @typedef {UtxoCohortPattern | AddressCohortPattern} CohortPattern
*
* Relative pattern capability types
* @typedef {GlobalRelativePattern | FullRelativePattern} RelativeWithMarketCap
* @typedef {GlobalRelativePattern | FullRelativePattern | AllRelativePattern} RelativeWithMarketCap
* @typedef {OwnRelativePattern | FullRelativePattern} RelativeWithOwnMarketCap
* @typedef {OwnRelativePattern | FullRelativePattern | AllRelativePattern} RelativeWithOwnPnl
* @typedef {GlobalRelativePattern | FullRelativePattern} RelativeWithNupl
* @typedef {GlobalRelativePattern | FullRelativePattern | AllRelativePattern} RelativeWithNupl
* @typedef {GlobalPeakRelativePattern | FullRelativePattern | AllRelativePattern} RelativeWithPeakRegret
* @typedef {BasicRelativePattern | GlobalRelativePattern | OwnRelativePattern | FullRelativePattern | AllRelativePattern} RelativeWithInvestedCapitalPct
*
* Realized pattern capability types
@@ -173,6 +176,7 @@
* @typedef {AllUtxoPattern | AgeRangePattern | UtxoAmountPattern} PatternWithCostBasis
* @typedef {AllUtxoPattern | AgeRangePattern | UtxoAmountPattern} PatternWithActivity
* @typedef {AllUtxoPattern | AgeRangePattern} PatternWithCostBasisPercentiles
* @typedef {Brk.Pct05Pct10Pct15Pct20Pct25Pct30Pct35Pct40Pct45Pct50Pct55Pct60Pct65Pct70Pct75Pct80Pct85Pct90Pct95Pattern} PercentilesPattern
*
* Cohort objects with specific pattern capabilities
* @typedef {{ name: string, title: string, color: Color, tree: PatternWithRealizedPrice }} CohortWithRealizedPrice

View File

@@ -67,270 +67,381 @@ function getLightDarkValue(property) {
return dark ? _dark : light;
}
const red = createColor(() => getColor("red"));
const orange = createColor(() => getColor("orange"));
const amber = createColor(() => getColor("amber"));
const yellow = createColor(() => getColor("yellow"));
const avocado = createColor(() => getColor("avocado"));
const lime = createColor(() => getColor("lime"));
const green = createColor(() => getColor("green"));
const emerald = createColor(() => getColor("emerald"));
const teal = createColor(() => getColor("teal"));
const cyan = createColor(() => getColor("cyan"));
const sky = createColor(() => getColor("sky"));
const blue = createColor(() => getColor("blue"));
const indigo = createColor(() => getColor("indigo"));
const violet = createColor(() => getColor("violet"));
const purple = createColor(() => getColor("purple"));
const fuchsia = createColor(() => getColor("fuchsia"));
const pink = createColor(() => getColor("pink"));
const rose = createColor(() => getColor("rose"));
const spectrumColors = {
red,
orange,
amber,
yellow,
avocado,
lime,
green,
emerald,
teal,
cyan,
sky,
blue,
indigo,
violet,
purple,
fuchsia,
pink,
rose,
};
const baseColors = {
default: createColor(() => getLightDarkValue("--color")),
gray: createColor(() => getColor("gray")),
border: createColor(() => getLightDarkValue("--border-color")),
...spectrumColors,
const palette = {
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")),
};
export const colors = {
...baseColors,
default: createColor(() => getLightDarkValue("--color")),
gray: createColor(() => getColor("gray")),
border: createColor(() => getLightDarkValue("--border-color")),
// Directional
profit: palette.green,
loss: palette.red,
bitcoin: palette.orange,
usd: palette.green,
// Bi-color pairs for baselines
bi: {
/** @type {[Color, Color]} */
profitLoss: [palette.green, palette.red],
/** @type {[Color, Color]} */
sopr7d: [palette.lime, palette.rose],
/** @type {[Color, Color]} */
sopr30d: [palette.avocado, palette.pink],
/** @type {[Color, Color]} */
adjustedSopr: [palette.yellow, palette.fuchsia],
/** @type {[Color, Color]} */
adjustedSopr7d: [palette.amber, palette.purple],
/** @type {[Color, Color]} */
adjustedSopr30d: [palette.orange, palette.violet],
/** @type {[Color, Color]} */
lumpSum: [palette.cyan, palette.orange],
},
// Cointime economics
liveliness: palette.pink,
vaulted: palette.lime,
active: palette.rose,
activity: palette.purple,
cointime: palette.yellow,
destroyed: palette.red,
created: palette.orange,
stored: palette.green,
// Valuations
realized: palette.orange,
investor: palette.fuchsia,
thermo: palette.emerald,
trueMarketMean: palette.blue,
vocdd: palette.purple,
hodlBank: palette.blue,
reserveRisk: palette.orange,
// Comparisons (base vs adjusted)
base: palette.orange,
adjusted: palette.purple,
adjustedCreated: palette.lime,
adjustedDestroyed: palette.pink,
// Ratios
plRatio: palette.yellow,
// Mining
mining: {
coinbase: palette.orange,
subsidy: palette.lime,
fee: palette.cyan,
},
// Network
segwit: palette.cyan,
// Entity (transactions, inputs, outputs)
entity: {
tx: palette.orange,
input: palette.red,
output: palette.cyan,
},
// Technical indicators
indicator: {
main: palette.indigo,
fast: palette.blue,
slow: palette.orange,
upper: palette.green,
lower: palette.red,
mid: palette.yellow,
},
stat: {
sum: blue,
cumulative: indigo,
avg: orange,
max: green,
pct90: cyan,
pct75: blue,
median: yellow,
pct25: violet,
pct10: fuchsia,
min: red,
sum: palette.blue,
cumulative: palette.indigo,
avg: palette.orange,
max: palette.green,
pct90: palette.cyan,
pct75: palette.blue,
median: palette.yellow,
pct25: palette.violet,
pct10: palette.fuchsia,
min: palette.red,
},
// Ratio percentile bands (extreme values)
ratioPct: {
_99: palette.rose,
_98: palette.pink,
_95: palette.fuchsia,
_5: palette.cyan,
_2: palette.sky,
_1: palette.blue,
},
// Standard deviation bands (warm = positive, cool = negative)
sd: {
_0: palette.lime,
p05: palette.yellow,
m05: palette.teal,
p1: palette.amber,
m1: palette.cyan,
p15: palette.orange,
m15: palette.sky,
p2: palette.red,
m2: palette.blue,
p25: palette.rose,
m25: palette.indigo,
p3: palette.pink,
m3: palette.violet,
},
// Transaction versions
txVersion: {
v1: palette.orange,
v2: palette.cyan,
v3: palette.lime,
},
pct: {
_100: palette.red,
_95: palette.orange,
_90: palette.amber,
_85: palette.yellow,
_80: palette.avocado,
_75: palette.lime,
_70: palette.green,
_65: palette.emerald,
_60: palette.teal,
_55: palette.cyan,
_50: palette.sky,
_45: palette.blue,
_40: palette.indigo,
_35: palette.violet,
_30: palette.purple,
_25: palette.fuchsia,
_20: palette.pink,
_15: palette.rose,
_10: palette.pink,
_05: palette.fuchsia,
_0: palette.purple,
},
time: {
_24h: pink,
_1w: red,
_1m: yellow,
_1y: lime,
all: teal,
_24h: palette.pink,
_1w: palette.red,
_1m: palette.yellow,
_1y: palette.lime,
all: palette.teal,
},
term: {
short: yellow,
long: fuchsia,
short: palette.yellow,
long: palette.fuchsia,
},
age: {
_1d: red,
_1w: orange,
_1m: yellow,
_2m: lime,
_3m: green,
_4m: teal,
_5m: cyan,
_6m: blue,
_1y: indigo,
_2y: violet,
_3y: purple,
_4y: fuchsia,
_5y: pink,
_6y: rose,
_7y: red,
_8y: orange,
_10y: yellow,
_12y: lime,
_15y: green,
_1d: palette.red,
_1w: palette.orange,
_1m: palette.yellow,
_2m: palette.lime,
_3m: palette.green,
_4m: palette.teal,
_5m: palette.cyan,
_6m: palette.blue,
_1y: palette.indigo,
_2y: palette.violet,
_3y: palette.purple,
_4y: palette.fuchsia,
_5y: palette.pink,
_6y: palette.rose,
_7y: palette.red,
_8y: palette.orange,
_10y: palette.yellow,
_12y: palette.lime,
_15y: palette.green,
},
ageRange: {
upTo1h: rose,
_1hTo1d: pink,
_1dTo1w: red,
_1wTo1m: orange,
_1mTo2m: yellow,
_2mTo3m: yellow,
_3mTo4m: lime,
_4mTo5m: lime,
_5mTo6m: lime,
_6mTo1y: green,
_1yTo2y: cyan,
_2yTo3y: blue,
_3yTo4y: indigo,
_4yTo5y: violet,
_5yTo6y: purple,
_6yTo7y: purple,
_7yTo8y: fuchsia,
_8yTo10y: fuchsia,
_10yTo12y: pink,
_12yTo15y: red,
from15y: orange,
upTo1h: palette.rose,
_1hTo1d: palette.pink,
_1dTo1w: palette.red,
_1wTo1m: palette.orange,
_1mTo2m: palette.yellow,
_2mTo3m: palette.yellow,
_3mTo4m: palette.lime,
_4mTo5m: palette.lime,
_5mTo6m: palette.lime,
_6mTo1y: palette.green,
_1yTo2y: palette.cyan,
_2yTo3y: palette.blue,
_3yTo4y: palette.indigo,
_4yTo5y: palette.violet,
_5yTo6y: palette.purple,
_6yTo7y: palette.purple,
_7yTo8y: palette.fuchsia,
_8yTo10y: palette.fuchsia,
_10yTo12y: palette.pink,
_12yTo15y: palette.red,
from15y: palette.orange,
},
amount: {
_1sat: orange,
_10sats: orange,
_100sats: yellow,
_1kSats: lime,
_10kSats: green,
_100kSats: cyan,
_1mSats: blue,
_10mSats: indigo,
_1btc: purple,
_10btc: violet,
_100btc: fuchsia,
_1kBtc: pink,
_10kBtc: red,
_100kBtc: orange,
_1sat: palette.orange,
_10sats: palette.orange,
_100sats: palette.yellow,
_1kSats: palette.lime,
_10kSats: palette.green,
_100kSats: palette.cyan,
_1mSats: palette.blue,
_10mSats: palette.indigo,
_1btc: palette.purple,
_10btc: palette.violet,
_100btc: palette.fuchsia,
_1kBtc: palette.pink,
_10kBtc: palette.red,
_100kBtc: palette.orange,
},
amountRange: {
_0sats: red,
_1satTo10sats: orange,
_10satsTo100sats: yellow,
_100satsTo1kSats: lime,
_1kSatsTo10kSats: green,
_10kSatsTo100kSats: cyan,
_100kSatsTo1mSats: blue,
_1mSatsTo10mSats: indigo,
_10mSatsTo1btc: purple,
_1btcTo10btc: violet,
_10btcTo100btc: fuchsia,
_100btcTo1kBtc: pink,
_1kBtcTo10kBtc: red,
_10kBtcTo100kBtc: orange,
_100kBtcOrMore: yellow,
_0sats: palette.red,
_1satTo10sats: palette.orange,
_10satsTo100sats: palette.yellow,
_100satsTo1kSats: palette.lime,
_1kSatsTo10kSats: palette.green,
_10kSatsTo100kSats: palette.cyan,
_100kSatsTo1mSats: palette.blue,
_1mSatsTo10mSats: palette.indigo,
_10mSatsTo1btc: palette.purple,
_1btcTo10btc: palette.violet,
_10btcTo100btc: palette.fuchsia,
_100btcTo1kBtc: palette.pink,
_1kBtcTo10kBtc: palette.red,
_10kBtcTo100kBtc: palette.orange,
_100kBtcOrMore: palette.yellow,
},
epoch: {
_0: red,
_1: yellow,
_2: orange,
_3: lime,
_4: green,
_0: palette.red,
_1: palette.yellow,
_2: palette.orange,
_3: palette.lime,
_4: palette.green,
},
year: {
_2009: red,
_2010: orange,
_2011: amber,
_2012: yellow,
_2013: lime,
_2014: green,
_2015: teal,
_2016: cyan,
_2017: sky,
_2018: blue,
_2019: indigo,
_2020: violet,
_2021: purple,
_2022: fuchsia,
_2023: pink,
_2024: rose,
_2025: red,
_2026: orange,
_2009: palette.red,
_2010: palette.orange,
_2011: palette.amber,
_2012: palette.yellow,
_2013: palette.lime,
_2014: palette.green,
_2015: palette.teal,
_2016: palette.cyan,
_2017: palette.sky,
_2018: palette.blue,
_2019: palette.indigo,
_2020: palette.violet,
_2021: palette.purple,
_2022: palette.fuchsia,
_2023: palette.pink,
_2024: palette.rose,
_2025: palette.red,
_2026: palette.orange,
},
returns: {
_1d: red,
_1w: orange,
_1m: yellow,
_3m: lime,
_6m: green,
_1y: teal,
_2y: cyan,
_3y: sky,
_4y: blue,
_5y: indigo,
_6y: violet,
_8y: purple,
_10y: fuchsia,
_1d: palette.red,
_1w: palette.orange,
_1m: palette.yellow,
_3m: palette.lime,
_6m: palette.green,
_1y: palette.teal,
_2y: palette.cyan,
_3y: palette.sky,
_4y: palette.blue,
_5y: palette.indigo,
_6y: palette.violet,
_8y: palette.purple,
_10y: palette.fuchsia,
},
ma: {
_1w: red,
_8d: orange,
_12d: amber,
_13d: yellow,
_21d: avocado,
_26d: lime,
_1m: green,
_34d: emerald,
_55d: teal,
_89d: cyan,
_111d: sky,
_144d: blue,
_200d: indigo,
_350d: violet,
_1y: purple,
_2y: fuchsia,
_200w: pink,
_4y: rose,
_1w: palette.red,
_8d: palette.orange,
_12d: palette.amber,
_13d: palette.yellow,
_21d: palette.avocado,
_26d: palette.lime,
_1m: palette.green,
_34d: palette.emerald,
_55d: palette.teal,
_2m: palette.cyan,
_89d: palette.sky,
_111d: palette.blue,
_144d: palette.indigo,
_200d: palette.violet,
_350d: palette.purple,
_1y: palette.fuchsia,
_2y: palette.pink,
_200w: palette.rose,
_4y: palette.red,
},
dca: {
_1w: red,
_1m: orange,
_3m: yellow,
_6m: lime,
_1y: green,
_2y: teal,
_3y: cyan,
_4y: sky,
_5y: blue,
_6y: indigo,
_8y: violet,
_10y: purple,
_1w: palette.red,
_1m: palette.orange,
_3m: palette.yellow,
_6m: palette.lime,
_1y: palette.green,
_2y: palette.teal,
_3y: palette.cyan,
_4y: palette.sky,
_5y: palette.blue,
_6y: palette.indigo,
_8y: palette.violet,
_10y: palette.purple,
},
scriptType: {
p2pk65: red,
p2pk33: orange,
p2pkh: yellow,
p2ms: lime,
p2sh: green,
p2wpkh: teal,
p2wsh: blue,
p2tr: indigo,
p2a: purple,
opreturn: pink,
unknown: violet,
empty: fuchsia,
p2pk65: palette.red,
p2pk33: palette.orange,
p2pkh: palette.yellow,
p2ms: palette.lime,
p2sh: palette.green,
p2wpkh: palette.teal,
p2wsh: palette.blue,
p2tr: palette.indigo,
p2a: palette.purple,
opreturn: palette.pink,
unknown: palette.violet,
empty: palette.fuchsia,
},
arr: Object.values(palette),
/**
* Get a color by index (cycles through palette)
* @param {number} index
*/
at(index) {
return this.arr[index % this.arr.length];
},
};
/**
* @typedef {typeof colors} Colors
* @typedef {keyof typeof baseColors} ColorName
*/
/** Palette for indexed series */
const palette = Object.values(spectrumColors);
/**
* Get a color by index (cycles through palette)
* @param {number} index
*/
export const colorAt = (index) => palette[index % palette.length];

View File

@@ -45,12 +45,13 @@ export function throttle(callback, wait = 1000) {
* @template {(...args: any[]) => any} F
* @param {F} callback
* @param {number} [wait]
* @returns {((...args: Parameters<F>) => void) & { cancel: () => void }}
*/
export function debounce(callback, wait = 1000) {
/** @type {number | null} */
let timeoutId = null;
return (/** @type {Parameters<F>} */ ...args) => {
const fn = (/** @type {Parameters<F>} */ ...args) => {
if (timeoutId) {
clearTimeout(timeoutId);
}
@@ -59,4 +60,13 @@ export function debounce(callback, wait = 1000) {
timeoutId = null;
}, wait);
};
fn.cancel = () => {
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = null;
}
};
return fn;
}