diff --git a/crates/brk_bindgen/src/generate/constants.rs b/crates/brk_bindgen/src/generate/constants.rs index 44b38b229..d9f40d26f 100644 --- a/crates/brk_bindgen/src/generate/constants.rs +++ b/crates/brk_bindgen/src/generate/constants.rs @@ -6,8 +6,9 @@ use std::collections::BTreeMap; use brk_cohort::{ - AGE_RANGE_NAMES, AMOUNT_RANGE_NAMES, EPOCH_NAMES, OVER_AMOUNT_NAMES, UNDER_AMOUNT_NAMES, - UNDER_AGE_NAMES, OVER_AGE_NAMES, SPENDABLE_TYPE_NAMES, TERM_NAMES, CLASS_NAMES, + AGE_RANGE_NAMES, AMOUNT_RANGE_NAMES, CLASS_NAMES, EPOCH_NAMES, LOSS_NAMES, + OVER_AGE_NAMES, OVER_AMOUNT_NAMES, PROFITABILITY_RANGE_NAMES, PROFIT_NAMES, + SPENDABLE_TYPE_NAMES, TERM_NAMES, UNDER_AGE_NAMES, UNDER_AMOUNT_NAMES, }; use brk_types::{Index, PoolSlug, pools}; use serde::Serialize; @@ -63,6 +64,9 @@ impl CohortConstants { ("AMOUNT_RANGE_NAMES", to_value(&AMOUNT_RANGE_NAMES)), ("OVER_AMOUNT_NAMES", to_value(&OVER_AMOUNT_NAMES)), ("UNDER_AMOUNT_NAMES", to_value(&UNDER_AMOUNT_NAMES)), + ("PROFITABILITY_RANGE_NAMES", to_value(&PROFITABILITY_RANGE_NAMES)), + ("PROFIT_NAMES", to_value(&PROFIT_NAMES)), + ("LOSS_NAMES", to_value(&LOSS_NAMES)), ] } } diff --git a/crates/brk_client/src/lib.rs b/crates/brk_client/src/lib.rs index 0239ceb23..62ebdcc50 100644 --- a/crates/brk_client/src/lib.rs +++ b/crates/brk_client/src/lib.rs @@ -1871,6 +1871,28 @@ impl BpsCentsRatioSatsUsdPattern { } } +/// Pattern struct for repeated tree structure. +pub struct BtcCentsDeltaSatsUsdPattern { + pub btc: MetricPattern1, + pub cents: MetricPattern1, + pub delta: AbsoluteRatePattern, + pub sats: MetricPattern1, + pub usd: MetricPattern1, +} + +impl BtcCentsDeltaSatsUsdPattern { + /// Create a new pattern node with accumulated metric name. + pub fn new(client: Arc, acc: String) -> Self { + Self { + btc: MetricPattern1::new(client.clone(), acc.clone()), + cents: MetricPattern1::new(client.clone(), _m(&acc, "cents")), + delta: AbsoluteRatePattern::new(client.clone(), _m(&acc, "delta")), + sats: MetricPattern1::new(client.clone(), _m(&acc, "sats")), + usd: MetricPattern1::new(client.clone(), _m(&acc, "usd")), + } + } +} + /// Pattern struct for repeated tree structure. pub struct BtcCentsRelSatsUsdPattern { pub btc: MetricPattern1, @@ -1981,6 +2003,28 @@ impl InvestedMaxMinPercentilesSupplyPattern { } } +/// Pattern struct for repeated tree structure. +pub struct MvrvNuplRealizedSupplyPattern { + pub mvrv: MetricPattern1, + pub nupl: BpsRatioPattern, + pub realized_cap: AllSthPattern, + pub realized_price: BpsCentsRatioSatsUsdPattern, + pub supply: AllSthPattern2, +} + +impl MvrvNuplRealizedSupplyPattern { + /// Create a new pattern node with accumulated metric name. + pub fn new(client: Arc, acc: String) -> Self { + Self { + mvrv: MetricPattern1::new(client.clone(), _m(&acc, "mvrv")), + nupl: BpsRatioPattern::new(client.clone(), _m(&acc, "nupl")), + realized_cap: AllSthPattern::new(client.clone(), acc.clone()), + realized_price: BpsCentsRatioSatsUsdPattern::new(client.clone(), _m(&acc, "realized_price")), + supply: AllSthPattern2::new(client.clone(), acc.clone()), + } + } +} + /// Pattern struct for repeated tree structure. pub struct PhsReboundThsPattern { pub phs: MetricPattern1, @@ -2677,7 +2721,7 @@ impl GreedNetPainPattern { /// Pattern struct for repeated tree structure. pub struct LossNuplProfitPattern { - pub loss: BaseCumulativeSumPattern3, + pub loss: BaseCumulativeNegativeSumPattern, pub nupl: BpsRatioPattern, pub profit: BaseCumulativeSumPattern3, } @@ -2686,7 +2730,7 @@ impl LossNuplProfitPattern { /// Create a new pattern node with accumulated metric name. pub fn new(client: Arc, acc: String) -> Self { Self { - loss: BaseCumulativeSumPattern3::new(client.clone(), _m(&acc, "unrealized_loss")), + loss: BaseCumulativeNegativeSumPattern::new(client.clone(), acc.clone()), nupl: BpsRatioPattern::new(client.clone(), _m(&acc, "nupl")), profit: BaseCumulativeSumPattern3::new(client.clone(), _m(&acc, "unrealized_profit")), } @@ -2815,6 +2859,38 @@ impl AbsoluteRatePattern2 { } } +/// Pattern struct for repeated tree structure. +pub struct AllSthPattern2 { + pub all: BtcCentsDeltaSatsUsdPattern, + pub sth: BtcCentsSatsUsdPattern, +} + +impl AllSthPattern2 { + /// Create a new pattern node with accumulated metric name. + pub fn new(client: Arc, acc: String) -> Self { + Self { + all: BtcCentsDeltaSatsUsdPattern::new(client.clone(), _m(&acc, "supply")), + sth: BtcCentsSatsUsdPattern::new(client.clone(), _m(&acc, "sth_supply")), + } + } +} + +/// Pattern struct for repeated tree structure. +pub struct AllSthPattern { + pub all: MetricPattern1, + pub sth: MetricPattern1, +} + +impl AllSthPattern { + /// Create a new pattern node with accumulated metric name. + pub fn new(client: Arc, acc: String) -> Self { + Self { + all: MetricPattern1::new(client.clone(), _m(&acc, "realized_cap")), + sth: MetricPattern1::new(client.clone(), _m(&acc, "sth_realized_cap")), + } + } +} + /// Pattern struct for repeated tree structure. pub struct BlocksDominancePattern { pub blocks_mined: BaseCumulativeSumPattern2, @@ -2959,22 +3035,6 @@ impl PriceValuePattern { } } -/// Pattern struct for repeated tree structure. -pub struct RealizedSupplyPattern { - pub realized_cap: MetricPattern1, - pub supply: MetricPattern1, -} - -impl RealizedSupplyPattern { - /// Create a new pattern node with accumulated metric name. - pub fn new(client: Arc, acc: String) -> Self { - Self { - realized_cap: MetricPattern1::new(client.clone(), _m(&acc, "realized_cap")), - supply: MetricPattern1::new(client.clone(), _m(&acc, "supply")), - } - } -} - /// Pattern struct for repeated tree structure. pub struct RelPattern { pub rel_to_mcap: BpsPercentRatioPattern, @@ -4101,28 +4161,13 @@ impl MetricsTree_Scripts_Count { /// Metrics tree node. pub struct MetricsTree_Scripts_Value { - pub op_return: MetricsTree_Scripts_Value_OpReturn, + pub op_return: BaseCumulativeSumPattern4, } impl MetricsTree_Scripts_Value { pub fn new(client: Arc, base_path: String) -> Self { Self { - op_return: MetricsTree_Scripts_Value_OpReturn::new(client.clone(), format!("{base_path}_op_return")), - } - } -} - -/// Metrics tree node. -pub struct MetricsTree_Scripts_Value_OpReturn { - pub base: BtcCentsSatsUsdPattern, - pub cumulative: BtcCentsSatsUsdPattern, -} - -impl MetricsTree_Scripts_Value_OpReturn { - pub fn new(client: Arc, base_path: String) -> Self { - Self { - base: BtcCentsSatsUsdPattern::new(client.clone(), "op_return_value".to_string()), - cumulative: BtcCentsSatsUsdPattern::new(client.clone(), "op_return_value_cumulative".to_string()), + op_return: BaseCumulativeSumPattern4::new(client.clone(), "op_return_value".to_string()), } } } @@ -6448,14 +6493,12 @@ impl MetricsTree_Supply { /// Metrics tree node. pub struct MetricsTree_Supply_Burned { - pub op_return: BaseCumulativeSumPattern4, pub unspendable: BaseCumulativeSumPattern4, } impl MetricsTree_Supply_Burned { pub fn new(client: Arc, base_path: String) -> Self { Self { - op_return: BaseCumulativeSumPattern4::new(client.clone(), "op_return_supply".to_string()), unspendable: BaseCumulativeSumPattern4::new(client.clone(), "unspendable_supply".to_string()), } } @@ -7140,182 +7183,182 @@ impl MetricsTree_Cohorts_Utxo_Profitability { /// Metrics tree node. pub struct MetricsTree_Cohorts_Utxo_Profitability_Range { - pub over_1000pct_in_profit: RealizedSupplyPattern, - pub _500pct_to_1000pct_in_profit: RealizedSupplyPattern, - pub _300pct_to_500pct_in_profit: RealizedSupplyPattern, - pub _200pct_to_300pct_in_profit: RealizedSupplyPattern, - pub _100pct_to_200pct_in_profit: RealizedSupplyPattern, - pub _90pct_to_100pct_in_profit: RealizedSupplyPattern, - pub _80pct_to_90pct_in_profit: RealizedSupplyPattern, - pub _70pct_to_80pct_in_profit: RealizedSupplyPattern, - pub _60pct_to_70pct_in_profit: RealizedSupplyPattern, - pub _50pct_to_60pct_in_profit: RealizedSupplyPattern, - pub _40pct_to_50pct_in_profit: RealizedSupplyPattern, - pub _30pct_to_40pct_in_profit: RealizedSupplyPattern, - pub _20pct_to_30pct_in_profit: RealizedSupplyPattern, - pub _10pct_to_20pct_in_profit: RealizedSupplyPattern, - pub _0pct_to_10pct_in_profit: RealizedSupplyPattern, - pub _0pct_to_10pct_in_loss: RealizedSupplyPattern, - pub _10pct_to_20pct_in_loss: RealizedSupplyPattern, - pub _20pct_to_30pct_in_loss: RealizedSupplyPattern, - pub _30pct_to_40pct_in_loss: RealizedSupplyPattern, - pub _40pct_to_50pct_in_loss: RealizedSupplyPattern, - pub _50pct_to_60pct_in_loss: RealizedSupplyPattern, - pub _60pct_to_70pct_in_loss: RealizedSupplyPattern, - pub _70pct_to_80pct_in_loss: RealizedSupplyPattern, - pub _80pct_to_90pct_in_loss: RealizedSupplyPattern, - pub _90pct_to_100pct_in_loss: RealizedSupplyPattern, + pub over_1000pct_in_profit: MvrvNuplRealizedSupplyPattern, + pub _500pct_to_1000pct_in_profit: MvrvNuplRealizedSupplyPattern, + pub _300pct_to_500pct_in_profit: MvrvNuplRealizedSupplyPattern, + pub _200pct_to_300pct_in_profit: MvrvNuplRealizedSupplyPattern, + pub _100pct_to_200pct_in_profit: MvrvNuplRealizedSupplyPattern, + pub _90pct_to_100pct_in_profit: MvrvNuplRealizedSupplyPattern, + pub _80pct_to_90pct_in_profit: MvrvNuplRealizedSupplyPattern, + pub _70pct_to_80pct_in_profit: MvrvNuplRealizedSupplyPattern, + pub _60pct_to_70pct_in_profit: MvrvNuplRealizedSupplyPattern, + pub _50pct_to_60pct_in_profit: MvrvNuplRealizedSupplyPattern, + pub _40pct_to_50pct_in_profit: MvrvNuplRealizedSupplyPattern, + pub _30pct_to_40pct_in_profit: MvrvNuplRealizedSupplyPattern, + pub _20pct_to_30pct_in_profit: MvrvNuplRealizedSupplyPattern, + pub _10pct_to_20pct_in_profit: MvrvNuplRealizedSupplyPattern, + pub _0pct_to_10pct_in_profit: MvrvNuplRealizedSupplyPattern, + pub _0pct_to_10pct_in_loss: MvrvNuplRealizedSupplyPattern, + pub _10pct_to_20pct_in_loss: MvrvNuplRealizedSupplyPattern, + pub _20pct_to_30pct_in_loss: MvrvNuplRealizedSupplyPattern, + pub _30pct_to_40pct_in_loss: MvrvNuplRealizedSupplyPattern, + pub _40pct_to_50pct_in_loss: MvrvNuplRealizedSupplyPattern, + pub _50pct_to_60pct_in_loss: MvrvNuplRealizedSupplyPattern, + pub _60pct_to_70pct_in_loss: MvrvNuplRealizedSupplyPattern, + pub _70pct_to_80pct_in_loss: MvrvNuplRealizedSupplyPattern, + pub _80pct_to_90pct_in_loss: MvrvNuplRealizedSupplyPattern, + pub _90pct_to_100pct_in_loss: MvrvNuplRealizedSupplyPattern, } impl MetricsTree_Cohorts_Utxo_Profitability_Range { pub fn new(client: Arc, base_path: String) -> Self { Self { - over_1000pct_in_profit: RealizedSupplyPattern::new(client.clone(), "utxos_over_1000pct_in_profit".to_string()), - _500pct_to_1000pct_in_profit: RealizedSupplyPattern::new(client.clone(), "utxos_500pct_to_1000pct_in_profit".to_string()), - _300pct_to_500pct_in_profit: RealizedSupplyPattern::new(client.clone(), "utxos_300pct_to_500pct_in_profit".to_string()), - _200pct_to_300pct_in_profit: RealizedSupplyPattern::new(client.clone(), "utxos_200pct_to_300pct_in_profit".to_string()), - _100pct_to_200pct_in_profit: RealizedSupplyPattern::new(client.clone(), "utxos_100pct_to_200pct_in_profit".to_string()), - _90pct_to_100pct_in_profit: RealizedSupplyPattern::new(client.clone(), "utxos_90pct_to_100pct_in_profit".to_string()), - _80pct_to_90pct_in_profit: RealizedSupplyPattern::new(client.clone(), "utxos_80pct_to_90pct_in_profit".to_string()), - _70pct_to_80pct_in_profit: RealizedSupplyPattern::new(client.clone(), "utxos_70pct_to_80pct_in_profit".to_string()), - _60pct_to_70pct_in_profit: RealizedSupplyPattern::new(client.clone(), "utxos_60pct_to_70pct_in_profit".to_string()), - _50pct_to_60pct_in_profit: RealizedSupplyPattern::new(client.clone(), "utxos_50pct_to_60pct_in_profit".to_string()), - _40pct_to_50pct_in_profit: RealizedSupplyPattern::new(client.clone(), "utxos_40pct_to_50pct_in_profit".to_string()), - _30pct_to_40pct_in_profit: RealizedSupplyPattern::new(client.clone(), "utxos_30pct_to_40pct_in_profit".to_string()), - _20pct_to_30pct_in_profit: RealizedSupplyPattern::new(client.clone(), "utxos_20pct_to_30pct_in_profit".to_string()), - _10pct_to_20pct_in_profit: RealizedSupplyPattern::new(client.clone(), "utxos_10pct_to_20pct_in_profit".to_string()), - _0pct_to_10pct_in_profit: RealizedSupplyPattern::new(client.clone(), "utxos_0pct_to_10pct_in_profit".to_string()), - _0pct_to_10pct_in_loss: RealizedSupplyPattern::new(client.clone(), "utxos_0pct_to_10pct_in_loss".to_string()), - _10pct_to_20pct_in_loss: RealizedSupplyPattern::new(client.clone(), "utxos_10pct_to_20pct_in_loss".to_string()), - _20pct_to_30pct_in_loss: RealizedSupplyPattern::new(client.clone(), "utxos_20pct_to_30pct_in_loss".to_string()), - _30pct_to_40pct_in_loss: RealizedSupplyPattern::new(client.clone(), "utxos_30pct_to_40pct_in_loss".to_string()), - _40pct_to_50pct_in_loss: RealizedSupplyPattern::new(client.clone(), "utxos_40pct_to_50pct_in_loss".to_string()), - _50pct_to_60pct_in_loss: RealizedSupplyPattern::new(client.clone(), "utxos_50pct_to_60pct_in_loss".to_string()), - _60pct_to_70pct_in_loss: RealizedSupplyPattern::new(client.clone(), "utxos_60pct_to_70pct_in_loss".to_string()), - _70pct_to_80pct_in_loss: RealizedSupplyPattern::new(client.clone(), "utxos_70pct_to_80pct_in_loss".to_string()), - _80pct_to_90pct_in_loss: RealizedSupplyPattern::new(client.clone(), "utxos_80pct_to_90pct_in_loss".to_string()), - _90pct_to_100pct_in_loss: RealizedSupplyPattern::new(client.clone(), "utxos_90pct_to_100pct_in_loss".to_string()), + over_1000pct_in_profit: MvrvNuplRealizedSupplyPattern::new(client.clone(), "utxos_over_1000pct_in_profit".to_string()), + _500pct_to_1000pct_in_profit: MvrvNuplRealizedSupplyPattern::new(client.clone(), "utxos_500pct_to_1000pct_in_profit".to_string()), + _300pct_to_500pct_in_profit: MvrvNuplRealizedSupplyPattern::new(client.clone(), "utxos_300pct_to_500pct_in_profit".to_string()), + _200pct_to_300pct_in_profit: MvrvNuplRealizedSupplyPattern::new(client.clone(), "utxos_200pct_to_300pct_in_profit".to_string()), + _100pct_to_200pct_in_profit: MvrvNuplRealizedSupplyPattern::new(client.clone(), "utxos_100pct_to_200pct_in_profit".to_string()), + _90pct_to_100pct_in_profit: MvrvNuplRealizedSupplyPattern::new(client.clone(), "utxos_90pct_to_100pct_in_profit".to_string()), + _80pct_to_90pct_in_profit: MvrvNuplRealizedSupplyPattern::new(client.clone(), "utxos_80pct_to_90pct_in_profit".to_string()), + _70pct_to_80pct_in_profit: MvrvNuplRealizedSupplyPattern::new(client.clone(), "utxos_70pct_to_80pct_in_profit".to_string()), + _60pct_to_70pct_in_profit: MvrvNuplRealizedSupplyPattern::new(client.clone(), "utxos_60pct_to_70pct_in_profit".to_string()), + _50pct_to_60pct_in_profit: MvrvNuplRealizedSupplyPattern::new(client.clone(), "utxos_50pct_to_60pct_in_profit".to_string()), + _40pct_to_50pct_in_profit: MvrvNuplRealizedSupplyPattern::new(client.clone(), "utxos_40pct_to_50pct_in_profit".to_string()), + _30pct_to_40pct_in_profit: MvrvNuplRealizedSupplyPattern::new(client.clone(), "utxos_30pct_to_40pct_in_profit".to_string()), + _20pct_to_30pct_in_profit: MvrvNuplRealizedSupplyPattern::new(client.clone(), "utxos_20pct_to_30pct_in_profit".to_string()), + _10pct_to_20pct_in_profit: MvrvNuplRealizedSupplyPattern::new(client.clone(), "utxos_10pct_to_20pct_in_profit".to_string()), + _0pct_to_10pct_in_profit: MvrvNuplRealizedSupplyPattern::new(client.clone(), "utxos_0pct_to_10pct_in_profit".to_string()), + _0pct_to_10pct_in_loss: MvrvNuplRealizedSupplyPattern::new(client.clone(), "utxos_0pct_to_10pct_in_loss".to_string()), + _10pct_to_20pct_in_loss: MvrvNuplRealizedSupplyPattern::new(client.clone(), "utxos_10pct_to_20pct_in_loss".to_string()), + _20pct_to_30pct_in_loss: MvrvNuplRealizedSupplyPattern::new(client.clone(), "utxos_20pct_to_30pct_in_loss".to_string()), + _30pct_to_40pct_in_loss: MvrvNuplRealizedSupplyPattern::new(client.clone(), "utxos_30pct_to_40pct_in_loss".to_string()), + _40pct_to_50pct_in_loss: MvrvNuplRealizedSupplyPattern::new(client.clone(), "utxos_40pct_to_50pct_in_loss".to_string()), + _50pct_to_60pct_in_loss: MvrvNuplRealizedSupplyPattern::new(client.clone(), "utxos_50pct_to_60pct_in_loss".to_string()), + _60pct_to_70pct_in_loss: MvrvNuplRealizedSupplyPattern::new(client.clone(), "utxos_60pct_to_70pct_in_loss".to_string()), + _70pct_to_80pct_in_loss: MvrvNuplRealizedSupplyPattern::new(client.clone(), "utxos_70pct_to_80pct_in_loss".to_string()), + _80pct_to_90pct_in_loss: MvrvNuplRealizedSupplyPattern::new(client.clone(), "utxos_80pct_to_90pct_in_loss".to_string()), + _90pct_to_100pct_in_loss: MvrvNuplRealizedSupplyPattern::new(client.clone(), "utxos_90pct_to_100pct_in_loss".to_string()), } } } /// Metrics tree node. pub struct MetricsTree_Cohorts_Utxo_Profitability_Profit { - pub breakeven: RealizedSupplyPattern, - pub _10pct: RealizedSupplyPattern, - pub _20pct: RealizedSupplyPattern, - pub _30pct: RealizedSupplyPattern, - pub _40pct: RealizedSupplyPattern, - pub _50pct: RealizedSupplyPattern, - pub _60pct: RealizedSupplyPattern, - pub _70pct: RealizedSupplyPattern, - pub _80pct: RealizedSupplyPattern, - pub _90pct: RealizedSupplyPattern, - pub _100pct: RealizedSupplyPattern, - pub _200pct: RealizedSupplyPattern, - pub _300pct: RealizedSupplyPattern, - pub _500pct: RealizedSupplyPattern, + pub breakeven: MvrvNuplRealizedSupplyPattern, + pub _10pct: MvrvNuplRealizedSupplyPattern, + pub _20pct: MvrvNuplRealizedSupplyPattern, + pub _30pct: MvrvNuplRealizedSupplyPattern, + pub _40pct: MvrvNuplRealizedSupplyPattern, + pub _50pct: MvrvNuplRealizedSupplyPattern, + pub _60pct: MvrvNuplRealizedSupplyPattern, + pub _70pct: MvrvNuplRealizedSupplyPattern, + pub _80pct: MvrvNuplRealizedSupplyPattern, + pub _90pct: MvrvNuplRealizedSupplyPattern, + pub _100pct: MvrvNuplRealizedSupplyPattern, + pub _200pct: MvrvNuplRealizedSupplyPattern, + pub _300pct: MvrvNuplRealizedSupplyPattern, + pub _500pct: MvrvNuplRealizedSupplyPattern, } impl MetricsTree_Cohorts_Utxo_Profitability_Profit { pub fn new(client: Arc, base_path: String) -> Self { Self { - breakeven: RealizedSupplyPattern::new(client.clone(), "utxos_in_profit".to_string()), - _10pct: RealizedSupplyPattern::new(client.clone(), "utxos_over_10pct_in_profit".to_string()), - _20pct: RealizedSupplyPattern::new(client.clone(), "utxos_over_20pct_in_profit".to_string()), - _30pct: RealizedSupplyPattern::new(client.clone(), "utxos_over_30pct_in_profit".to_string()), - _40pct: RealizedSupplyPattern::new(client.clone(), "utxos_over_40pct_in_profit".to_string()), - _50pct: RealizedSupplyPattern::new(client.clone(), "utxos_over_50pct_in_profit".to_string()), - _60pct: RealizedSupplyPattern::new(client.clone(), "utxos_over_60pct_in_profit".to_string()), - _70pct: RealizedSupplyPattern::new(client.clone(), "utxos_over_70pct_in_profit".to_string()), - _80pct: RealizedSupplyPattern::new(client.clone(), "utxos_over_80pct_in_profit".to_string()), - _90pct: RealizedSupplyPattern::new(client.clone(), "utxos_over_90pct_in_profit".to_string()), - _100pct: RealizedSupplyPattern::new(client.clone(), "utxos_over_100pct_in_profit".to_string()), - _200pct: RealizedSupplyPattern::new(client.clone(), "utxos_over_200pct_in_profit".to_string()), - _300pct: RealizedSupplyPattern::new(client.clone(), "utxos_over_300pct_in_profit".to_string()), - _500pct: RealizedSupplyPattern::new(client.clone(), "utxos_over_500pct_in_profit".to_string()), + breakeven: MvrvNuplRealizedSupplyPattern::new(client.clone(), "utxos_in_profit".to_string()), + _10pct: MvrvNuplRealizedSupplyPattern::new(client.clone(), "utxos_over_10pct_in_profit".to_string()), + _20pct: MvrvNuplRealizedSupplyPattern::new(client.clone(), "utxos_over_20pct_in_profit".to_string()), + _30pct: MvrvNuplRealizedSupplyPattern::new(client.clone(), "utxos_over_30pct_in_profit".to_string()), + _40pct: MvrvNuplRealizedSupplyPattern::new(client.clone(), "utxos_over_40pct_in_profit".to_string()), + _50pct: MvrvNuplRealizedSupplyPattern::new(client.clone(), "utxos_over_50pct_in_profit".to_string()), + _60pct: MvrvNuplRealizedSupplyPattern::new(client.clone(), "utxos_over_60pct_in_profit".to_string()), + _70pct: MvrvNuplRealizedSupplyPattern::new(client.clone(), "utxos_over_70pct_in_profit".to_string()), + _80pct: MvrvNuplRealizedSupplyPattern::new(client.clone(), "utxos_over_80pct_in_profit".to_string()), + _90pct: MvrvNuplRealizedSupplyPattern::new(client.clone(), "utxos_over_90pct_in_profit".to_string()), + _100pct: MvrvNuplRealizedSupplyPattern::new(client.clone(), "utxos_over_100pct_in_profit".to_string()), + _200pct: MvrvNuplRealizedSupplyPattern::new(client.clone(), "utxos_over_200pct_in_profit".to_string()), + _300pct: MvrvNuplRealizedSupplyPattern::new(client.clone(), "utxos_over_300pct_in_profit".to_string()), + _500pct: MvrvNuplRealizedSupplyPattern::new(client.clone(), "utxos_over_500pct_in_profit".to_string()), } } } /// Metrics tree node. pub struct MetricsTree_Cohorts_Utxo_Profitability_Loss { - pub breakeven: RealizedSupplyPattern, - pub _10pct: RealizedSupplyPattern, - pub _20pct: RealizedSupplyPattern, - pub _30pct: RealizedSupplyPattern, - pub _40pct: RealizedSupplyPattern, - pub _50pct: RealizedSupplyPattern, - pub _60pct: RealizedSupplyPattern, - pub _70pct: RealizedSupplyPattern, - pub _80pct: RealizedSupplyPattern, + pub breakeven: MvrvNuplRealizedSupplyPattern, + pub _10pct: MvrvNuplRealizedSupplyPattern, + pub _20pct: MvrvNuplRealizedSupplyPattern, + pub _30pct: MvrvNuplRealizedSupplyPattern, + pub _40pct: MvrvNuplRealizedSupplyPattern, + pub _50pct: MvrvNuplRealizedSupplyPattern, + pub _60pct: MvrvNuplRealizedSupplyPattern, + pub _70pct: MvrvNuplRealizedSupplyPattern, + pub _80pct: MvrvNuplRealizedSupplyPattern, } impl MetricsTree_Cohorts_Utxo_Profitability_Loss { pub fn new(client: Arc, base_path: String) -> Self { Self { - breakeven: RealizedSupplyPattern::new(client.clone(), "utxos_in_loss".to_string()), - _10pct: RealizedSupplyPattern::new(client.clone(), "utxos_over_10pct_in_loss".to_string()), - _20pct: RealizedSupplyPattern::new(client.clone(), "utxos_over_20pct_in_loss".to_string()), - _30pct: RealizedSupplyPattern::new(client.clone(), "utxos_over_30pct_in_loss".to_string()), - _40pct: RealizedSupplyPattern::new(client.clone(), "utxos_over_40pct_in_loss".to_string()), - _50pct: RealizedSupplyPattern::new(client.clone(), "utxos_over_50pct_in_loss".to_string()), - _60pct: RealizedSupplyPattern::new(client.clone(), "utxos_over_60pct_in_loss".to_string()), - _70pct: RealizedSupplyPattern::new(client.clone(), "utxos_over_70pct_in_loss".to_string()), - _80pct: RealizedSupplyPattern::new(client.clone(), "utxos_over_80pct_in_loss".to_string()), + breakeven: MvrvNuplRealizedSupplyPattern::new(client.clone(), "utxos_in_loss".to_string()), + _10pct: MvrvNuplRealizedSupplyPattern::new(client.clone(), "utxos_over_10pct_in_loss".to_string()), + _20pct: MvrvNuplRealizedSupplyPattern::new(client.clone(), "utxos_over_20pct_in_loss".to_string()), + _30pct: MvrvNuplRealizedSupplyPattern::new(client.clone(), "utxos_over_30pct_in_loss".to_string()), + _40pct: MvrvNuplRealizedSupplyPattern::new(client.clone(), "utxos_over_40pct_in_loss".to_string()), + _50pct: MvrvNuplRealizedSupplyPattern::new(client.clone(), "utxos_over_50pct_in_loss".to_string()), + _60pct: MvrvNuplRealizedSupplyPattern::new(client.clone(), "utxos_over_60pct_in_loss".to_string()), + _70pct: MvrvNuplRealizedSupplyPattern::new(client.clone(), "utxos_over_70pct_in_loss".to_string()), + _80pct: MvrvNuplRealizedSupplyPattern::new(client.clone(), "utxos_over_80pct_in_loss".to_string()), } } } /// Metrics tree node. pub struct MetricsTree_Cohorts_Utxo_Matured { - pub under_1h: BtcCentsSatsUsdPattern, - pub _1h_to_1d: BtcCentsSatsUsdPattern, - pub _1d_to_1w: BtcCentsSatsUsdPattern, - pub _1w_to_1m: BtcCentsSatsUsdPattern, - pub _1m_to_2m: BtcCentsSatsUsdPattern, - pub _2m_to_3m: BtcCentsSatsUsdPattern, - pub _3m_to_4m: BtcCentsSatsUsdPattern, - pub _4m_to_5m: BtcCentsSatsUsdPattern, - pub _5m_to_6m: BtcCentsSatsUsdPattern, - pub _6m_to_1y: BtcCentsSatsUsdPattern, - pub _1y_to_2y: BtcCentsSatsUsdPattern, - pub _2y_to_3y: BtcCentsSatsUsdPattern, - pub _3y_to_4y: BtcCentsSatsUsdPattern, - pub _4y_to_5y: BtcCentsSatsUsdPattern, - pub _5y_to_6y: BtcCentsSatsUsdPattern, - pub _6y_to_7y: BtcCentsSatsUsdPattern, - pub _7y_to_8y: BtcCentsSatsUsdPattern, - pub _8y_to_10y: BtcCentsSatsUsdPattern, - pub _10y_to_12y: BtcCentsSatsUsdPattern, - pub _12y_to_15y: BtcCentsSatsUsdPattern, - pub over_15y: BtcCentsSatsUsdPattern, + pub under_1h: BaseCumulativeSumPattern4, + pub _1h_to_1d: BaseCumulativeSumPattern4, + pub _1d_to_1w: BaseCumulativeSumPattern4, + pub _1w_to_1m: BaseCumulativeSumPattern4, + pub _1m_to_2m: BaseCumulativeSumPattern4, + pub _2m_to_3m: BaseCumulativeSumPattern4, + pub _3m_to_4m: BaseCumulativeSumPattern4, + pub _4m_to_5m: BaseCumulativeSumPattern4, + pub _5m_to_6m: BaseCumulativeSumPattern4, + pub _6m_to_1y: BaseCumulativeSumPattern4, + pub _1y_to_2y: BaseCumulativeSumPattern4, + pub _2y_to_3y: BaseCumulativeSumPattern4, + pub _3y_to_4y: BaseCumulativeSumPattern4, + pub _4y_to_5y: BaseCumulativeSumPattern4, + pub _5y_to_6y: BaseCumulativeSumPattern4, + pub _6y_to_7y: BaseCumulativeSumPattern4, + pub _7y_to_8y: BaseCumulativeSumPattern4, + pub _8y_to_10y: BaseCumulativeSumPattern4, + pub _10y_to_12y: BaseCumulativeSumPattern4, + pub _12y_to_15y: BaseCumulativeSumPattern4, + pub over_15y: BaseCumulativeSumPattern4, } impl MetricsTree_Cohorts_Utxo_Matured { pub fn new(client: Arc, base_path: String) -> Self { Self { - under_1h: BtcCentsSatsUsdPattern::new(client.clone(), "utxo_under_1h_old_matured".to_string()), - _1h_to_1d: BtcCentsSatsUsdPattern::new(client.clone(), "utxo_1h_to_1d_old_matured".to_string()), - _1d_to_1w: BtcCentsSatsUsdPattern::new(client.clone(), "utxo_1d_to_1w_old_matured".to_string()), - _1w_to_1m: BtcCentsSatsUsdPattern::new(client.clone(), "utxo_1w_to_1m_old_matured".to_string()), - _1m_to_2m: BtcCentsSatsUsdPattern::new(client.clone(), "utxo_1m_to_2m_old_matured".to_string()), - _2m_to_3m: BtcCentsSatsUsdPattern::new(client.clone(), "utxo_2m_to_3m_old_matured".to_string()), - _3m_to_4m: BtcCentsSatsUsdPattern::new(client.clone(), "utxo_3m_to_4m_old_matured".to_string()), - _4m_to_5m: BtcCentsSatsUsdPattern::new(client.clone(), "utxo_4m_to_5m_old_matured".to_string()), - _5m_to_6m: BtcCentsSatsUsdPattern::new(client.clone(), "utxo_5m_to_6m_old_matured".to_string()), - _6m_to_1y: BtcCentsSatsUsdPattern::new(client.clone(), "utxo_6m_to_1y_old_matured".to_string()), - _1y_to_2y: BtcCentsSatsUsdPattern::new(client.clone(), "utxo_1y_to_2y_old_matured".to_string()), - _2y_to_3y: BtcCentsSatsUsdPattern::new(client.clone(), "utxo_2y_to_3y_old_matured".to_string()), - _3y_to_4y: BtcCentsSatsUsdPattern::new(client.clone(), "utxo_3y_to_4y_old_matured".to_string()), - _4y_to_5y: BtcCentsSatsUsdPattern::new(client.clone(), "utxo_4y_to_5y_old_matured".to_string()), - _5y_to_6y: BtcCentsSatsUsdPattern::new(client.clone(), "utxo_5y_to_6y_old_matured".to_string()), - _6y_to_7y: BtcCentsSatsUsdPattern::new(client.clone(), "utxo_6y_to_7y_old_matured".to_string()), - _7y_to_8y: BtcCentsSatsUsdPattern::new(client.clone(), "utxo_7y_to_8y_old_matured".to_string()), - _8y_to_10y: BtcCentsSatsUsdPattern::new(client.clone(), "utxo_8y_to_10y_old_matured".to_string()), - _10y_to_12y: BtcCentsSatsUsdPattern::new(client.clone(), "utxo_10y_to_12y_old_matured".to_string()), - _12y_to_15y: BtcCentsSatsUsdPattern::new(client.clone(), "utxo_12y_to_15y_old_matured".to_string()), - over_15y: BtcCentsSatsUsdPattern::new(client.clone(), "utxo_over_15y_old_matured".to_string()), + under_1h: BaseCumulativeSumPattern4::new(client.clone(), "utxos_under_1h_old_matured_supply".to_string()), + _1h_to_1d: BaseCumulativeSumPattern4::new(client.clone(), "utxos_1h_to_1d_old_matured_supply".to_string()), + _1d_to_1w: BaseCumulativeSumPattern4::new(client.clone(), "utxos_1d_to_1w_old_matured_supply".to_string()), + _1w_to_1m: BaseCumulativeSumPattern4::new(client.clone(), "utxos_1w_to_1m_old_matured_supply".to_string()), + _1m_to_2m: BaseCumulativeSumPattern4::new(client.clone(), "utxos_1m_to_2m_old_matured_supply".to_string()), + _2m_to_3m: BaseCumulativeSumPattern4::new(client.clone(), "utxos_2m_to_3m_old_matured_supply".to_string()), + _3m_to_4m: BaseCumulativeSumPattern4::new(client.clone(), "utxos_3m_to_4m_old_matured_supply".to_string()), + _4m_to_5m: BaseCumulativeSumPattern4::new(client.clone(), "utxos_4m_to_5m_old_matured_supply".to_string()), + _5m_to_6m: BaseCumulativeSumPattern4::new(client.clone(), "utxos_5m_to_6m_old_matured_supply".to_string()), + _6m_to_1y: BaseCumulativeSumPattern4::new(client.clone(), "utxos_6m_to_1y_old_matured_supply".to_string()), + _1y_to_2y: BaseCumulativeSumPattern4::new(client.clone(), "utxos_1y_to_2y_old_matured_supply".to_string()), + _2y_to_3y: BaseCumulativeSumPattern4::new(client.clone(), "utxos_2y_to_3y_old_matured_supply".to_string()), + _3y_to_4y: BaseCumulativeSumPattern4::new(client.clone(), "utxos_3y_to_4y_old_matured_supply".to_string()), + _4y_to_5y: BaseCumulativeSumPattern4::new(client.clone(), "utxos_4y_to_5y_old_matured_supply".to_string()), + _5y_to_6y: BaseCumulativeSumPattern4::new(client.clone(), "utxos_5y_to_6y_old_matured_supply".to_string()), + _6y_to_7y: BaseCumulativeSumPattern4::new(client.clone(), "utxos_6y_to_7y_old_matured_supply".to_string()), + _7y_to_8y: BaseCumulativeSumPattern4::new(client.clone(), "utxos_7y_to_8y_old_matured_supply".to_string()), + _8y_to_10y: BaseCumulativeSumPattern4::new(client.clone(), "utxos_8y_to_10y_old_matured_supply".to_string()), + _10y_to_12y: BaseCumulativeSumPattern4::new(client.clone(), "utxos_10y_to_12y_old_matured_supply".to_string()), + _12y_to_15y: BaseCumulativeSumPattern4::new(client.clone(), "utxos_12y_to_15y_old_matured_supply".to_string()), + over_15y: BaseCumulativeSumPattern4::new(client.clone(), "utxos_over_15y_old_matured_supply".to_string()), } } } diff --git a/crates/brk_computer/src/blocks/compute.rs b/crates/brk_computer/src/blocks/compute.rs index 20545d49e..6a0b411d4 100644 --- a/crates/brk_computer/src/blocks/compute.rs +++ b/crates/brk_computer/src/blocks/compute.rs @@ -1,3 +1,5 @@ +use std::thread; + use brk_error::Result; use brk_indexer::Indexer; use brk_types::Indexes; @@ -15,22 +17,40 @@ impl Vecs { starting_indexes: &Indexes, exit: &Exit, ) -> Result<()> { + // Sequential: time → lookback (dependency chain) self.time .timestamp .compute(indexer, indexes, starting_indexes, exit)?; self.lookback .compute(&self.time, starting_indexes, exit)?; - self.count - .compute(indexer, starting_indexes, exit)?; - self.interval - .compute(indexer, starting_indexes, exit)?; - self.size - .compute(indexer, &self.lookback, starting_indexes, exit)?; - self.weight - .compute(indexer, starting_indexes, exit)?; - self.difficulty - .compute(indexer, indexes, starting_indexes, exit)?; - self.halving.compute(indexes, starting_indexes, exit)?; + + // Parallel: remaining sub-modules are independent of each other. + // size depends on lookback (already computed above). + let Vecs { + lookback, + count, + interval, + size, + weight, + difficulty, + halving, + .. + } = self; + thread::scope(|s| -> Result<()> { + let r1 = s.spawn(|| count.compute(indexer, starting_indexes, exit)); + let r2 = s.spawn(|| interval.compute(indexer, starting_indexes, exit)); + let r3 = s.spawn(|| weight.compute(indexer, starting_indexes, exit)); + let r4 = + s.spawn(|| difficulty.compute(indexer, indexes, starting_indexes, exit)); + let r5 = s.spawn(|| halving.compute(indexes, starting_indexes, exit)); + size.compute(indexer, &*lookback, starting_indexes, exit)?; + r1.join().unwrap()?; + r2.join().unwrap()?; + r3.join().unwrap()?; + r4.join().unwrap()?; + r5.join().unwrap()?; + Ok(()) + })?; let _lock = exit.lock(); self.db.compact()?; diff --git a/crates/brk_computer/src/distribution/cohorts/utxo/fenwick.rs b/crates/brk_computer/src/distribution/cohorts/utxo/fenwick.rs index b3c42d5b4..4bc73eaad 100644 --- a/crates/brk_computer/src/distribution/cohorts/utxo/fenwick.rs +++ b/crates/brk_computer/src/distribution/cohorts/utxo/fenwick.rs @@ -32,7 +32,7 @@ pub(super) struct CostBasisNode { } impl CostBasisNode { - #[inline] + #[inline(always)] fn new(sats: i64, usd: i128, is_sth: bool) -> Self { Self { all_sats: sats, @@ -237,7 +237,7 @@ impl CostBasisFenwick { let mut sat_buckets = [0usize; PERCENTILES_LEN + 2]; self.tree - .batch_kth(&sat_targets, &sat_field, &mut sat_buckets); + .kth(&sat_targets, &sat_field, &mut sat_buckets); result.min_price = bucket_to_cents(sat_buckets[0]); (0..PERCENTILES_LEN).for_each(|i| { @@ -254,7 +254,7 @@ impl CostBasisFenwick { let mut usd_buckets = [0usize; PERCENTILES_LEN]; self.tree - .batch_kth(&usd_targets, &usd_field, &mut usd_buckets); + .kth(&usd_targets, &usd_field, &mut usd_buckets); (0..PERCENTILES_LEN).for_each(|i| { result.usd_prices[i] = bucket_to_cents(usd_buckets[i]); @@ -310,16 +310,16 @@ impl CostBasisFenwick { } // ----------------------------------------------------------------------- - // Profitability queries (all cohort only) + // Profitability queries // ----------------------------------------------------------------------- /// Compute profitability range buckets from current spot price. - /// Returns 25 ranges: (sats, usd_raw) per range. + /// Returns 25 ranges with all/sth splits. pub(super) fn profitability( &self, spot_price: Cents, - ) -> [(u64, u128); PROFITABILITY_RANGE_COUNT] { - let mut result = [(0u64, 0u128); PROFITABILITY_RANGE_COUNT]; + ) -> [ProfitabilityRangeResult; PROFITABILITY_RANGE_COUNT] { + let mut result = [ProfitabilityRangeResult::ZERO; PROFITABILITY_RANGE_COUNT]; if self.totals.all_sats <= 0 { return result; @@ -327,34 +327,54 @@ impl CostBasisFenwick { let boundaries = compute_profitability_boundaries(spot_price); - let mut prev_sats: i64 = 0; - let mut prev_usd: i128 = 0; + let mut prev = CostBasisNode::default(); for (i, &boundary) in boundaries.iter().enumerate() { let boundary_bucket = cents_to_bucket(boundary); - // prefix_sum through the bucket BEFORE the boundary let cum = if boundary_bucket > 0 { self.tree.prefix_sum(boundary_bucket - 1) } else { CostBasisNode::default() }; - let range_sats = cum.all_sats - prev_sats; - let range_usd = cum.all_usd - prev_usd; - result[i] = (range_sats.max(0) as u64, range_usd.max(0) as u128); - prev_sats = cum.all_sats; - prev_usd = cum.all_usd; + result[i] = ProfitabilityRangeResult { + all_sats: (cum.all_sats - prev.all_sats).max(0) as u64, + all_usd: (cum.all_usd - prev.all_usd).max(0) as u128, + sth_sats: (cum.sth_sats - prev.sth_sats).max(0) as u64, + sth_usd: (cum.sth_usd - prev.sth_usd).max(0) as u128, + }; + prev = cum; } // Last range: everything >= last boundary - let remaining_sats = self.totals.all_sats - prev_sats; - let remaining_usd = self.totals.all_usd - prev_usd; - result[PROFITABILITY_RANGE_COUNT - 1] = - (remaining_sats.max(0) as u64, remaining_usd.max(0) as u128); + result[PROFITABILITY_RANGE_COUNT - 1] = ProfitabilityRangeResult { + all_sats: (self.totals.all_sats - prev.all_sats).max(0) as u64, + all_usd: (self.totals.all_usd - prev.all_usd).max(0) as u128, + sth_sats: (self.totals.sth_sats - prev.sth_sats).max(0) as u64, + sth_usd: (self.totals.sth_usd - prev.sth_usd).max(0) as u128, + }; result } } +/// Per-range profitability result with all/sth split. +#[derive(Clone, Copy)] +pub(super) struct ProfitabilityRangeResult { + pub all_sats: u64, + pub all_usd: u128, + pub sth_sats: u64, + pub sth_usd: u128, +} + +impl ProfitabilityRangeResult { + const ZERO: Self = Self { + all_sats: 0, + all_usd: 0, + sth_sats: 0, + sth_usd: 0, + }; +} + /// Result of a percentile computation for one cohort. #[derive(Default)] pub(super) struct PercentileResult { diff --git a/crates/brk_computer/src/distribution/cohorts/utxo/groups.rs b/crates/brk_computer/src/distribution/cohorts/utxo/groups.rs index ebd65bf12..7366d628d 100644 --- a/crates/brk_computer/src/distribution/cohorts/utxo/groups.rs +++ b/crates/brk_computer/src/distribution/cohorts/utxo/groups.rs @@ -25,7 +25,7 @@ use crate::{ state::UTXOCohortState, }, indexes, - internal::{AmountPerBlock, CachedWindowStarts}, + internal::{AmountPerBlockCumulativeWithSums, CachedWindowStarts}, prices, }; @@ -50,7 +50,7 @@ pub struct UTXOCohorts { #[traversable(rename = "type")] pub type_: SpendableType>>, pub profitability: ProfitabilityMetrics, - pub matured: AgeRange>, + pub matured: AgeRange>, #[traversable(skip)] pub(super) fenwick: CostBasisFenwick, /// Cached partition_point positions for tick_tock boundary searches. @@ -178,7 +178,7 @@ impl UTXOCohorts { ); // Phase 3b: Import profitability metrics (derived from "all" during k-way merge). - let profitability = ProfitabilityMetrics::forced_import(db, v, indexes)?; + let profitability = ProfitabilityMetrics::forced_import(db, v, indexes, cached_starts)?; // Phase 4: Import aggregate cohorts. @@ -256,10 +256,17 @@ impl UTXOCohorts { let under_amount = UnderAmount::try_new(&minimal_no_state)?; let over_amount = OverAmount::try_new(&minimal_no_state)?; + let prefix = CohortContext::Utxo.prefix(); let matured = AgeRange::try_new(&|_f: Filter, name: &'static str| - -> Result { - AmountPerBlock::forced_import(db, &format!("utxo_{name}_matured"), v, indexes) + -> Result { + AmountPerBlockCumulativeWithSums::forced_import( + db, + &format!("{prefix}_{name}_matured_supply"), + v, + indexes, + cached_starts, + ) })?; Ok(Self { @@ -338,7 +345,7 @@ impl UTXOCohorts { matured: &AgeRange, ) -> Result<()> { for (v, &sats) in self.matured.iter_mut().zip(matured.iter()) { - v.sats.height.truncate_push(height, sats)?; + v.base.sats.height.truncate_push(height, sats)?; } Ok(()) } @@ -509,10 +516,13 @@ impl UTXOCohorts { .try_for_each(|v| v.compute_rest_part1(prices, starting_indexes, exit))?; } - // Compute matured cents from sats × price + // Compute matured cumulative + cents from sats × price self.matured .par_iter_mut() - .try_for_each(|v| v.compute(prices, starting_indexes.height, exit))?; + .try_for_each(|v| v.compute_rest(starting_indexes.height, prices, exit))?; + + // Compute profitability supply cents and realized price + self.profitability.compute(prices, starting_indexes, exit)?; Ok(()) } @@ -709,8 +719,10 @@ impl UTXOCohorts { } vecs.extend(self.profitability.collect_all_vecs_mut()); for v in self.matured.iter_mut() { - vecs.push(&mut v.sats.height); - vecs.push(&mut v.cents.height); + vecs.push(&mut v.base.sats.height); + vecs.push(&mut v.base.cents.height); + vecs.push(&mut v.cumulative.sats.height); + vecs.push(&mut v.cumulative.cents.height); } vecs.into_par_iter() } @@ -727,7 +739,7 @@ impl UTXOCohorts { .chain( self.matured .iter() - .map(|v| Height::from(v.min_stateful_len())), + .map(|v| Height::from(v.base.min_stateful_len())), ) .min() .unwrap_or_default() diff --git a/crates/brk_computer/src/distribution/cohorts/utxo/percentiles.rs b/crates/brk_computer/src/distribution/cohorts/utxo/percentiles.rs index d48918759..c445a146a 100644 --- a/crates/brk_computer/src/distribution/cohorts/utxo/percentiles.rs +++ b/crates/brk_computer/src/distribution/cohorts/utxo/percentiles.rs @@ -6,7 +6,7 @@ use brk_types::{BasisPoints16, Cents, CentsCompact, CostBasisDistribution, Date, use crate::distribution::metrics::{CostBasis, ProfitabilityMetrics}; -use super::fenwick::PercentileResult; +use super::fenwick::{PercentileResult, ProfitabilityRangeResult}; use super::groups::UTXOCohorts; use super::COST_BASIS_PRICE_DIGITS; @@ -104,7 +104,7 @@ fn push_cost_basis( } /// Convert raw (cents × sats) accumulator to Dollars (÷ 100 for cents→dollars, ÷ 1e8 for sats). -#[inline] +#[inline(always)] fn raw_usd_to_dollars(raw: u128) -> Dollars { Dollars::from(raw as f64 / 1e10) } @@ -112,25 +112,41 @@ fn raw_usd_to_dollars(raw: u128) -> Dollars { /// Push profitability range + profit/loss aggregate values to vecs. fn push_profitability( height: Height, - buckets: &[(u64, u128); PROFITABILITY_RANGE_COUNT], + buckets: &[ProfitabilityRangeResult; PROFITABILITY_RANGE_COUNT], metrics: &mut ProfitabilityMetrics, ) -> Result<()> { + // Truncate all buckets once upfront to avoid per-push checks + metrics.truncate(height)?; + // Push 25 range buckets for (i, bucket) in metrics.range.as_array_mut().into_iter().enumerate() { - let (sats, usd_raw) = buckets[i]; - bucket.truncate_push(height, Sats::from(sats), raw_usd_to_dollars(usd_raw))?; + let r = &buckets[i]; + bucket.push( + Sats::from(r.all_sats), + Sats::from(r.sth_sats), + raw_usd_to_dollars(r.all_usd), + raw_usd_to_dollars(r.sth_usd), + ); } // Profit: forward cumulative sum over ranges[0..15], pushed in reverse. // profit[0] (breakeven) = sum(0..=13), ..., profit[13] (_500pct) = ranges[0] let profit_arr = metrics.profit.as_array_mut(); let mut cum_sats = 0u64; + let mut cum_sth_sats = 0u64; let mut cum_usd = 0u128; + let mut cum_sth_usd = 0u128; for i in 0..PROFIT_COUNT { - cum_sats += buckets[i].0; - cum_usd += buckets[i].1; - profit_arr[PROFIT_COUNT - 1 - i] - .truncate_push(height, Sats::from(cum_sats), raw_usd_to_dollars(cum_usd))?; + cum_sats += buckets[i].all_sats; + cum_sth_sats += buckets[i].sth_sats; + cum_usd += buckets[i].all_usd; + cum_sth_usd += buckets[i].sth_usd; + profit_arr[PROFIT_COUNT - 1 - i].push( + Sats::from(cum_sats), + Sats::from(cum_sth_sats), + raw_usd_to_dollars(cum_usd), + raw_usd_to_dollars(cum_sth_usd), + ); } // Loss: backward cumulative sum over ranges[15..25], pushed in reverse. @@ -138,12 +154,21 @@ fn push_profitability( let loss_arr = metrics.loss.as_array_mut(); let loss_count = loss_arr.len(); cum_sats = 0; + cum_sth_sats = 0; cum_usd = 0; + cum_sth_usd = 0; for i in 0..loss_count { - cum_sats += buckets[PROFITABILITY_RANGE_COUNT - 1 - i].0; - cum_usd += buckets[PROFITABILITY_RANGE_COUNT - 1 - i].1; - loss_arr[loss_count - 1 - i] - .truncate_push(height, Sats::from(cum_sats), raw_usd_to_dollars(cum_usd))?; + let r = &buckets[PROFITABILITY_RANGE_COUNT - 1 - i]; + cum_sats += r.all_sats; + cum_sth_sats += r.sth_sats; + cum_usd += r.all_usd; + cum_sth_usd += r.sth_usd; + loss_arr[loss_count - 1 - i].push( + Sats::from(cum_sats), + Sats::from(cum_sth_sats), + raw_usd_to_dollars(cum_usd), + raw_usd_to_dollars(cum_sth_usd), + ); } Ok(()) diff --git a/crates/brk_computer/src/distribution/cohorts/utxo/tick_tock.rs b/crates/brk_computer/src/distribution/cohorts/utxo/tick_tock.rs index ba08d716f..a7df31039 100644 --- a/crates/brk_computer/src/distribution/cohorts/utxo/tick_tock.rs +++ b/crates/brk_computer/src/distribution/cohorts/utxo/tick_tock.rs @@ -16,8 +16,8 @@ impl UTXOCohorts { /// Since timestamps are monotonic, positions only advance forward. /// Complexity: O(k * c) where k = 20 boundaries, c = ~1 (forward scan steps). /// - /// Returns how many sats matured INTO each cohort from the younger adjacent one. - /// `under_1h` is always zero since nothing ages into the youngest cohort. + /// Returns how many sats matured OUT OF each cohort into the older adjacent one. + /// `over_15y` is always zero since nothing ages out of the oldest cohort. pub(crate) fn tick_tock_next_block( &mut self, chain_state: &[BlockState], @@ -92,7 +92,7 @@ impl UTXOCohorts { if let Some(state) = age_cohorts[boundary_idx + 1].as_mut() { state.increment_snapshot(&snapshot); } - matured[boundary_idx + 1] += block_state.supply.value; + matured[boundary_idx] += block_state.supply.value; } } diff --git a/crates/brk_computer/src/distribution/metrics/profitability.rs b/crates/brk_computer/src/distribution/metrics/profitability.rs index de284075d..97f9cb4e4 100644 --- a/crates/brk_computer/src/distribution/metrics/profitability.rs +++ b/crates/brk_computer/src/distribution/metrics/profitability.rs @@ -1,21 +1,43 @@ use brk_cohort::{Loss, Profit, ProfitabilityRange}; use brk_error::Result; use brk_traversable::Traversable; -use brk_types::{Dollars, Height, Sats, Version}; -use vecdb::{AnyStoredVec, AnyVec, Database, Rw, StorageMode, WritableVec}; +use brk_types::{ + BasisPoints32, BasisPointsSigned32, Cents, Dollars, Height, Indexes, Sats, StoredF32, Version, +}; +use vecdb::{AnyStoredVec, AnyVec, Database, Exit, Rw, StorageMode, WritableVec}; -use crate::{indexes, internal::PerBlock}; +use crate::{ + indexes, + internal::{ + AmountPerBlock, AmountPerBlockWithDeltas, CachedWindowStarts, Identity, LazyPerBlock, + PerBlock, PriceWithRatioPerBlock, RatioPerBlock, + }, + prices, +}; + +#[derive(Traversable)] +pub struct WithSth { + pub all: All, + pub sth: Sth, +} -/// Supply + realized cap for a single profitability bucket. #[derive(Traversable)] pub struct ProfitabilityBucket { - pub supply: PerBlock, - pub realized_cap: PerBlock, + pub supply: WithSth, AmountPerBlock>, + pub realized_cap: WithSth>, + pub realized_price: PriceWithRatioPerBlock, + pub mvrv: LazyPerBlock, + pub nupl: RatioPerBlock, } impl ProfitabilityBucket { fn min_len(&self) -> usize { - self.supply.height.len().min(self.realized_cap.height.len()) + self.supply + .all + .sats + .height + .len() + .min(self.realized_cap.all.height.len()) } } @@ -25,45 +47,154 @@ impl ProfitabilityBucket { name: &str, version: Version, indexes: &indexes::Vecs, + cached_starts: &CachedWindowStarts, ) -> Result { + let realized_price = PriceWithRatioPerBlock::forced_import( + db, + &format!("{name}_realized_price"), + version, + indexes, + )?; + + let mvrv = LazyPerBlock::from_lazy::, BasisPoints32>( + &format!("{name}_mvrv"), + version, + &realized_price.ratio, + ); + Ok(Self { - supply: PerBlock::forced_import( + supply: WithSth { + all: AmountPerBlockWithDeltas::forced_import( + db, + &format!("{name}_supply"), + version, + indexes, + cached_starts, + )?, + sth: AmountPerBlock::forced_import( + db, + &format!("{name}_sth_supply"), + version, + indexes, + )?, + }, + realized_cap: WithSth { + all: PerBlock::forced_import( + db, + &format!("{name}_realized_cap"), + version, + indexes, + )?, + sth: PerBlock::forced_import( + db, + &format!("{name}_sth_realized_cap"), + version, + indexes, + )?, + }, + realized_price, + mvrv, + nupl: RatioPerBlock::forced_import_raw( db, - &format!("{name}_supply"), - version, - indexes, - )?, - realized_cap: PerBlock::forced_import( - db, - &format!("{name}_realized_cap"), - version, + &format!("{name}_nupl"), + version + Version::ONE, indexes, )?, }) } - pub(crate) fn truncate_push( + #[inline(always)] + pub(crate) fn truncate(&mut self, height: Height) -> Result<()> { + self.supply.all.sats.height.truncate_if_needed(height)?; + self.supply.sth.sats.height.truncate_if_needed(height)?; + self.realized_cap.all.height.truncate_if_needed(height)?; + self.realized_cap.sth.height.truncate_if_needed(height)?; + Ok(()) + } + + #[inline(always)] + pub(crate) fn push( &mut self, - height: Height, supply: Sats, + sth_supply: Sats, realized_cap: Dollars, + sth_realized_cap: Dollars, + ) { + self.supply.all.sats.height.push(supply); + self.supply.sth.sats.height.push(sth_supply); + self.realized_cap.all.height.push(realized_cap); + self.realized_cap.sth.height.push(sth_realized_cap); + } + + pub(crate) fn compute( + &mut self, + prices: &prices::Vecs, + starting_indexes: &Indexes, + exit: &Exit, ) -> Result<()> { - self.supply.height.truncate_push(height, supply)?; - self.realized_cap - .height - .truncate_push(height, realized_cap)?; + let max_from = starting_indexes.height; + + self.supply.all.compute(prices, max_from, exit)?; + self.supply.sth.compute(prices, max_from, exit)?; + + // Realized price cents = realized_cap_cents × ONE_BTC / supply_sats + self.realized_price.cents.height.compute_transform2( + max_from, + &self.realized_cap.all.height, + &self.supply.all.sats.height, + |(i, cap_dollars, supply_sats, ..)| { + let cap_cents = Cents::from(cap_dollars).as_u128(); + let supply = supply_sats.as_u128(); + if supply == 0 { + (i, Cents::ZERO) + } else { + (i, Cents::from(cap_cents * Sats::ONE_BTC_U128 / supply)) + } + }, + exit, + )?; + + // Ratio (spot / realized_price) → feeds MVRV lazily + self.realized_price + .compute_ratio(starting_indexes, &prices.spot.cents.height, exit)?; + + // NUPL = (spot - realized_price) / spot + self.nupl.bps.height.compute_transform2( + max_from, + &prices.spot.cents.height, + &self.realized_price.cents.height, + |(i, spot, realized, ..)| { + let p = spot.as_u128(); + if p == 0 { + (i, BasisPointsSigned32::ZERO) + } else { + let rp = realized.as_u128(); + let bps = ((p as i128 - rp as i128) * 10000) / p as i128; + (i, BasisPointsSigned32::from(bps as i32)) + } + }, + exit, + )?; + Ok(()) } pub(crate) fn collect_all_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> { vec![ - &mut self.supply.height as &mut dyn AnyStoredVec, - &mut self.realized_cap.height as &mut dyn AnyStoredVec, + &mut self.supply.all.inner.sats.height as &mut dyn AnyStoredVec, + &mut self.supply.all.inner.cents.height as &mut dyn AnyStoredVec, + &mut self.supply.sth.sats.height as &mut dyn AnyStoredVec, + &mut self.supply.sth.cents.height as &mut dyn AnyStoredVec, + &mut self.realized_cap.all.height as &mut dyn AnyStoredVec, + &mut self.realized_cap.sth.height as &mut dyn AnyStoredVec, + &mut self.realized_price.cents.height as &mut dyn AnyStoredVec, + &mut self.realized_price.bps.height as &mut dyn AnyStoredVec, + &mut self.nupl.bps.height as &mut dyn AnyStoredVec, ] } } -/// All profitability metrics: 25 ranges + 15 profit thresholds + 10 loss thresholds. +/// All profitability metrics: 25 ranges + 14 profit thresholds + 9 loss thresholds. #[derive(Traversable)] pub struct ProfitabilityMetrics { pub range: ProfitabilityRange>, @@ -72,32 +203,46 @@ pub struct ProfitabilityMetrics { } impl ProfitabilityMetrics { - pub(crate) fn min_stateful_len(&self) -> usize { - self.range.iter() + pub fn iter(&self) -> impl Iterator> { + self.range + .iter() .chain(self.profit.iter()) .chain(self.loss.iter()) - .map(|b| b.min_len()) - .min() - .unwrap_or(0) + } + + pub fn iter_mut(&mut self) -> impl Iterator> { + self.range + .iter_mut() + .chain(self.profit.iter_mut()) + .chain(self.loss.iter_mut()) + } + + pub(crate) fn min_stateful_len(&self) -> usize { + self.iter().map(|b| b.min_len()).min().unwrap_or(0) } } impl ProfitabilityMetrics { + pub(crate) fn truncate(&mut self, height: Height) -> Result<()> { + self.iter_mut().try_for_each(|b| b.truncate(height)) + } + pub(crate) fn forced_import( db: &Database, version: Version, indexes: &indexes::Vecs, + cached_starts: &CachedWindowStarts, ) -> Result { let range = ProfitabilityRange::try_new(|name| { - ProfitabilityBucket::forced_import(db, name, version, indexes) + ProfitabilityBucket::forced_import(db, name, version, indexes, cached_starts) })?; let profit = Profit::try_new(|name| { - ProfitabilityBucket::forced_import(db, name, version, indexes) + ProfitabilityBucket::forced_import(db, name, version, indexes, cached_starts) })?; let loss = Loss::try_new(|name| { - ProfitabilityBucket::forced_import(db, name, version, indexes) + ProfitabilityBucket::forced_import(db, name, version, indexes, cached_starts) })?; Ok(Self { @@ -107,18 +252,21 @@ impl ProfitabilityMetrics { }) } + pub(crate) fn compute( + &mut self, + prices: &prices::Vecs, + starting_indexes: &Indexes, + exit: &Exit, + ) -> Result<()> { + self.iter_mut() + .try_for_each(|b| b.compute(prices, starting_indexes, exit)) + } + pub(crate) fn collect_all_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> { let mut vecs = Vec::new(); - for bucket in self.range.iter_mut() { - vecs.extend(bucket.collect_all_vecs_mut()); - } - for bucket in self.profit.iter_mut() { - vecs.extend(bucket.collect_all_vecs_mut()); - } - for bucket in self.loss.iter_mut() { + for bucket in self.iter_mut() { vecs.extend(bucket.collect_all_vecs_mut()); } vecs } - } diff --git a/crates/brk_computer/src/distribution/metrics/unrealized/basic.rs b/crates/brk_computer/src/distribution/metrics/unrealized/basic.rs index c010bd936..37cf5b893 100644 --- a/crates/brk_computer/src/distribution/metrics/unrealized/basic.rs +++ b/crates/brk_computer/src/distribution/metrics/unrealized/basic.rs @@ -1,12 +1,12 @@ use brk_error::Result; use brk_traversable::Traversable; -use brk_types::{Cents, Height, Indexes, Version}; +use brk_types::{Cents, Dollars, Height, Indexes, Version}; use derive_more::{Deref, DerefMut}; -use vecdb::{AnyStoredVec, AnyVec, Exit, Rw, StorageMode, WritableVec}; +use vecdb::{AnyStoredVec, AnyVec, Exit, ReadableCloneableVec, Rw, StorageMode, WritableVec}; use crate::{ distribution::{metrics::ImportConfig, state::UnrealizedState}, - internal::FiatPerBlockCumulativeWithSums, + internal::{FiatPerBlockCumulativeWithSums, LazyPerBlock, NegCentsUnsignedToDollars}, }; use super::UnrealizedMinimal; @@ -19,16 +19,28 @@ pub struct UnrealizedBasic { pub minimal: UnrealizedMinimal, pub profit: FiatPerBlockCumulativeWithSums, pub loss: FiatPerBlockCumulativeWithSums, + #[traversable(wrap = "loss", rename = "negative")] + pub neg_loss: LazyPerBlock, } impl UnrealizedBasic { pub(crate) fn forced_import(cfg: &ImportConfig) -> Result { let v1 = Version::ONE; + let loss: FiatPerBlockCumulativeWithSums = cfg.import("unrealized_loss", v1)?; + + let neg_loss = LazyPerBlock::from_computed::( + &cfg.name("neg_unrealized_loss"), + cfg.version, + loss.base.cents.height.read_only_boxed_clone(), + &loss.base.cents, + ); + Ok(Self { minimal: UnrealizedMinimal::forced_import(cfg)?, profit: cfg.import("unrealized_profit", v1)?, - loss: cfg.import("unrealized_loss", v1)?, + loss, + neg_loss, }) } diff --git a/crates/brk_computer/src/distribution/metrics/unrealized/core.rs b/crates/brk_computer/src/distribution/metrics/unrealized/core.rs index 432ee8494..b827fb9ff 100644 --- a/crates/brk_computer/src/distribution/metrics/unrealized/core.rs +++ b/crates/brk_computer/src/distribution/metrics/unrealized/core.rs @@ -2,18 +2,16 @@ use brk_error::Result; use brk_traversable::Traversable; use brk_types::{Cents, CentsSigned, Height, Indexes, Version}; use derive_more::{Deref, DerefMut}; -use vecdb::{AnyStoredVec, Exit, ReadableCloneableVec, Rw, StorageMode}; +use vecdb::{AnyStoredVec, Exit, Rw, StorageMode}; use crate::{ distribution::{ metrics::ImportConfig, state::UnrealizedState, }, - internal::{CentsSubtractToCentsSigned, FiatPerBlock, LazyPerBlock, NegCentsUnsignedToDollars}, + internal::{CentsSubtractToCentsSigned, FiatPerBlock}, }; -use brk_types::Dollars; - use super::UnrealizedBasic; #[derive(Deref, DerefMut, Traversable)] @@ -23,27 +21,16 @@ pub struct UnrealizedCore { #[traversable(flatten)] pub basic: UnrealizedBasic, - #[traversable(wrap = "loss", rename = "negative")] - pub neg_loss: LazyPerBlock, pub net_pnl: FiatPerBlock, } impl UnrealizedCore { pub(crate) fn forced_import(cfg: &ImportConfig) -> Result { let basic = UnrealizedBasic::forced_import(cfg)?; - - let neg_unrealized_loss = LazyPerBlock::from_computed::( - &cfg.name("neg_unrealized_loss"), - cfg.version, - basic.loss.base.cents.height.read_only_boxed_clone(), - &basic.loss.base.cents, - ); - let net_unrealized_pnl = cfg.import("net_unrealized_pnl", Version::ZERO)?; Ok(Self { basic, - neg_loss: neg_unrealized_loss, net_pnl: net_unrealized_pnl, }) } diff --git a/crates/brk_computer/src/indicators/compute.rs b/crates/brk_computer/src/indicators/compute.rs index 701ef2b17..823b6733c 100644 --- a/crates/brk_computer/src/indicators/compute.rs +++ b/crates/brk_computer/src/indicators/compute.rs @@ -1,6 +1,6 @@ use brk_error::Result; -use brk_types::{Bitcoin, Dollars, Indexes, Sats, StoredF32}; -use vecdb::{Exit, ReadableVec}; +use brk_types::{Bitcoin, Dollars, Indexes, StoredF32}; +use vecdb::Exit; use super::{gini, Vecs}; use crate::{distribution, internal::RatioDollarsBp32, market, mining, transactions}; @@ -180,14 +180,12 @@ impl Vecs { // Seller Exhaustion Constant: % supply_in_profit × 30d_volatility self.seller_exhaustion_constant .height - .compute_transform2( + .compute_transform3( starting_indexes.height, &all_metrics.supply.in_profit.sats.height, &market.volatility._1m.height, - |(i, profit_sats, volatility, ..)| { - let total_sats: Sats = supply_total_sats - .collect_one(i) - .unwrap_or_default(); + supply_total_sats, + |(i, profit_sats, volatility, total_sats, ..)| { let total = total_sats.as_u128() as f64; if total == 0.0 { (i, StoredF32::from(0.0f32)) diff --git a/crates/brk_computer/src/inputs/spent/compute.rs b/crates/brk_computer/src/inputs/spent/compute.rs index 8a1a11241..a2ce4c5f3 100644 --- a/crates/brk_computer/src/inputs/spent/compute.rs +++ b/crates/brk_computer/src/inputs/spent/compute.rs @@ -1,6 +1,6 @@ use brk_error::Result; use brk_indexer::Indexer; -use brk_types::{Indexes, Sats, TxInIndex, TxIndex, TxOutIndex, Vout}; +use brk_types::{Indexes, Sats, TxIndex, TxOutIndex, Vout}; use tracing::info; use vecdb::{AnyStoredVec, AnyVec, Database, Exit, ReadableVec, VecIndex, WritableVec}; @@ -98,11 +98,11 @@ impl Vecs { out_value[entry.original_idx] = entry.value; } + self.txout_index.truncate_if_needed_at(batch_start)?; + self.value.truncate_if_needed_at(batch_start)?; for i in 0..batch_len { - let txin_index = TxInIndex::from(batch_start + i); - self.txout_index - .truncate_push(txin_index, out_txout_index[i])?; - self.value.truncate_push(txin_index, out_value[i])?; + self.txout_index.push(out_txout_index[i]); + self.value.push(out_value[i]); } if batch_end < target { diff --git a/crates/brk_computer/src/internal/algo/expanding_percentiles.rs b/crates/brk_computer/src/internal/algo/expanding_percentiles.rs index 3b3258ad7..2a8f530a1 100644 --- a/crates/brk_computer/src/internal/algo/expanding_percentiles.rs +++ b/crates/brk_computer/src/internal/algo/expanding_percentiles.rs @@ -71,18 +71,23 @@ impl ExpandingPercentiles { self.tree.add(Self::to_bucket(value), &1); } - /// Compute 6 percentiles in one call. O(6 × log N). - /// Quantiles q must be in (0, 1). Output is in BPS. + /// Compute 6 percentiles in one call via kth. O(6 × log N) but with + /// shared tree traversal across all 6 targets for better cache locality. + /// Quantiles q must be sorted ascending in (0, 1). Output is in BPS. pub fn quantiles(&self, qs: &[f64; 6], out: &mut [u32; 6]) { if self.count == 0 { out.iter_mut().for_each(|o| *o = 0); return; } + let mut targets = [0u32; 6]; for (i, &q) in qs.iter().enumerate() { let k = ((q * self.count as f64).ceil() as u32).clamp(1, self.count); - // kth with 0-indexed k: k-1; result is 0-indexed bucket - let bucket = self.tree.kth(k - 1, |n| *n); - out[i] = bucket as u32 * BUCKET_BPS as u32; + targets[i] = k - 1; // 0-indexed + } + let mut buckets = [0usize; 6]; + self.tree.kth(&targets, &|n: &u32| *n, &mut buckets); + for (i, bucket) in buckets.iter().enumerate() { + out[i] = *bucket as u32 * BUCKET_BPS as u32; } } } diff --git a/crates/brk_computer/src/internal/algo/fenwick.rs b/crates/brk_computer/src/internal/algo/fenwick.rs index a38e189d3..22d31a3a5 100644 --- a/crates/brk_computer/src/internal/algo/fenwick.rs +++ b/crates/brk_computer/src/internal/algo/fenwick.rs @@ -54,42 +54,15 @@ impl FenwickTree { result } - /// Find the 0-indexed bucket containing the k-th element (0-indexed k). + /// Find the 0-indexed bucket containing the k-th element for each target. /// /// `field_fn` extracts the relevant count field from a node. - /// The value type `V` must support comparison and subtraction - /// (works with `u32`, `i64`, `i128`). - #[inline] - pub fn kth(&self, k: V, field_fn: F) -> usize - where - V: Copy + PartialOrd + std::ops::SubAssign, - F: Fn(&N) -> V, - { - debug_assert!(self.size > 0); - let mut pos = 0usize; - let mut remaining = k; - let mut bit = 1usize << (usize::BITS - 1 - self.size.leading_zeros()); - while bit > 0 { - let next = pos + bit; - if next <= self.size { - let val = field_fn(&self.tree[next]); - if remaining >= val { - remaining -= val; - pos = next; - } - } - bit >>= 1; - } - pos // 0-indexed bucket - } - - /// Batch kth for sorted targets. Processes all targets at each tree level - /// for better cache locality vs individual kth() calls. + /// `sorted_targets` must be sorted ascending. `out` receives the 0-indexed + /// bucket for each target. Both slices must have the same length. /// - /// `sorted_targets` must be sorted ascending. `out` receives the 0-indexed bucket - /// for each target. Both slices must have the same length. + /// Processes all targets at each tree level for better cache locality. #[inline] - pub fn batch_kth(&self, sorted_targets: &[V], field_fn: &F, out: &mut [usize]) + pub fn kth(&self, sorted_targets: &[V], field_fn: &F, out: &mut [usize]) where V: Copy + PartialOrd + std::ops::SubAssign, F: Fn(&N) -> V, @@ -162,18 +135,14 @@ mod tests { tree.add(3, &5); tree.add(4, &1); - // kth(0) = first element → bucket 0 - assert_eq!(tree.kth(0u32, |n| *n), 0); - // kth(2) = 3rd element → bucket 0 (last of bucket 0) - assert_eq!(tree.kth(2u32, |n| *n), 0); - // kth(3) = 4th element → bucket 1 - assert_eq!(tree.kth(3u32, |n| *n), 1); - // kth(4) = 5th element → bucket 1 - assert_eq!(tree.kth(4u32, |n| *n), 1); - // kth(5) = 6th element → bucket 3 (bucket 2 is empty) - assert_eq!(tree.kth(5u32, |n| *n), 3); - // kth(10) = 11th element → bucket 4 - assert_eq!(tree.kth(10u32, |n| *n), 4); + let mut out = [0usize; 6]; + tree.kth(&[0u32, 2, 3, 4, 5, 10], &|n: &u32| *n, &mut out); + assert_eq!(out[0], 0); // kth(0) → bucket 0 + assert_eq!(out[1], 0); // kth(2) → bucket 0 (last of bucket 0) + assert_eq!(out[2], 1); // kth(3) → bucket 1 + assert_eq!(out[3], 1); // kth(4) → bucket 1 + assert_eq!(out[4], 3); // kth(5) → bucket 3 (bucket 2 is empty) + assert_eq!(out[5], 4); // kth(10) → bucket 4 } #[test] diff --git a/crates/brk_computer/src/internal/algo/sliding_distribution.rs b/crates/brk_computer/src/internal/algo/sliding_distribution.rs index 3a7c67ef3..815c87dbd 100644 --- a/crates/brk_computer/src/internal/algo/sliding_distribution.rs +++ b/crates/brk_computer/src/internal/algo/sliding_distribution.rs @@ -8,6 +8,11 @@ use super::sliding_window::SlidingWindowSorted; /// Compute all 8 rolling distribution stats (avg, min, max, p10, p25, median, p75, p90) /// in a single sorted-vec pass per window. +/// +/// When computing multiple windows from the same source, pass the same +/// `&mut Option<(usize, Vec)>` cache to each call — the first call reads +/// and caches, subsequent calls reuse if their range is covered. +/// Process the largest window first (1y) so its cache covers all smaller windows. #[allow(clippy::too_many_arguments)] pub fn compute_rolling_distribution_from_starts( max_from: I, @@ -22,6 +27,7 @@ pub fn compute_rolling_distribution_from_starts( p75_out: &mut EagerVec>, p90_out: &mut EagerVec>, exit: &Exit, + values_cache: &mut Option<(usize, Vec)>, ) -> Result<()> where I: VecIndex, @@ -68,8 +74,21 @@ where } else { 0 }; - let mut partial_values: Vec = Vec::with_capacity(end - range_start); - values.for_each_range_at(range_start, end, |a: A| partial_values.push(f64::from(a))); + + // Reuse cached values if the cache covers our range, otherwise read and cache. + let need_read = match values_cache.as_ref() { + Some((cached_start, cached)) => { + range_start < *cached_start || end > *cached_start + cached.len() + } + None => true, + }; + if need_read { + let mut v = Vec::with_capacity(end - range_start); + values.for_each_range_at(range_start, end, |a: A| v.push(f64::from(a))); + *values_cache = Some((range_start, v)); + } + let (cached_start, cached) = values_cache.as_ref().unwrap(); + let partial_values = &cached[(range_start - cached_start)..(end - cached_start)]; let capacity = if skip > 0 && skip < end { let first_start = window_starts.collect_one_at(skip).unwrap().to_usize(); @@ -83,7 +102,7 @@ where let mut window = SlidingWindowSorted::with_capacity(capacity); if skip > 0 { - window.reconstruct(&partial_values, range_start, skip); + window.reconstruct(partial_values, range_start, skip); } let starts_batch = window_starts.collect_range_at(skip, end); @@ -92,7 +111,7 @@ where let i = skip + j; let v = partial_values[i - range_start]; let start_usize = start.to_usize(); - window.advance(v, start_usize, &partial_values, range_start); + window.advance(v, start_usize, partial_values, range_start); if window.is_empty() { let zero = T::from(0.0); diff --git a/crates/brk_computer/src/internal/containers/windows.rs b/crates/brk_computer/src/internal/containers/windows.rs index fab1fc86c..ec381c72c 100644 --- a/crates/brk_computer/src/internal/containers/windows.rs +++ b/crates/brk_computer/src/internal/containers/windows.rs @@ -26,10 +26,20 @@ impl Windows { [&self._24h, &self._1w, &self._1m, &self._1y] } + /// Largest window first (1y, 1m, 1w, 24h). + pub fn as_array_largest_first(&self) -> [&A; 4] { + [&self._1y, &self._1m, &self._1w, &self._24h] + } + pub fn as_mut_array(&mut self) -> [&mut A; 4] { [&mut self._24h, &mut self._1w, &mut self._1m, &mut self._1y] } + /// Largest window first (1y, 1m, 1w, 24h). + pub fn as_mut_array_largest_first(&mut self) -> [&mut A; 4] { + [&mut self._1y, &mut self._1m, &mut self._1w, &mut self._24h] + } + pub fn as_mut_array_from_1w(&mut self) -> [&mut A; 3] { [&mut self._1w, &mut self._1m, &mut self._1y] } diff --git a/crates/brk_computer/src/internal/per_block/amount/cumulative.rs b/crates/brk_computer/src/internal/per_block/amount/cumulative.rs index d3d7ddfc1..181e200cb 100644 --- a/crates/brk_computer/src/internal/per_block/amount/cumulative.rs +++ b/crates/brk_computer/src/internal/per_block/amount/cumulative.rs @@ -1,7 +1,7 @@ use brk_error::Result; use brk_traversable::Traversable; use brk_types::{Cents, Height, Sats, Version}; -use vecdb::{Database, EagerVec, Exit, PcoVec, Rw, StorageMode}; +use vecdb::{Database, Exit, Rw, StorageMode}; use crate::{ indexes, @@ -37,17 +37,6 @@ impl AmountPerBlockCumulative { }) } - pub(crate) fn compute_with( - &mut self, - max_from: Height, - prices: &prices::Vecs, - exit: &Exit, - compute_sats: impl FnOnce(&mut EagerVec>) -> Result<()>, - ) -> Result<()> { - compute_sats(&mut self.base.sats.height)?; - self.compute(prices, max_from, exit) - } - pub(crate) fn compute( &mut self, prices: &prices::Vecs, diff --git a/crates/brk_computer/src/internal/per_block/amount/mod.rs b/crates/brk_computer/src/internal/per_block/amount/mod.rs index dc76e8114..227e2fabf 100644 --- a/crates/brk_computer/src/internal/per_block/amount/mod.rs +++ b/crates/brk_computer/src/internal/per_block/amount/mod.rs @@ -6,6 +6,7 @@ mod lazy; mod lazy_derived_resolutions; mod lazy_rolling_sum; mod rolling_distribution; +mod with_deltas; pub use base::*; pub use cumulative::*; @@ -15,3 +16,4 @@ pub use lazy::*; pub use lazy_derived_resolutions::*; pub use lazy_rolling_sum::*; pub use rolling_distribution::*; +pub use with_deltas::*; diff --git a/crates/brk_computer/src/internal/per_block/amount/rolling_distribution.rs b/crates/brk_computer/src/internal/per_block/amount/rolling_distribution.rs index dbed72c49..105d38def 100644 --- a/crates/brk_computer/src/internal/per_block/amount/rolling_distribution.rs +++ b/crates/brk_computer/src/internal/per_block/amount/rolling_distribution.rs @@ -42,11 +42,13 @@ impl RollingDistributionSlot { sats_source: &impl ReadableVec, cents_source: &impl ReadableVec, exit: &Exit, + sats_cache: &mut Option<(usize, Vec)>, + cents_cache: &mut Option<(usize, Vec)>, ) -> Result<()> { let d = &mut self.distribution; macro_rules! compute_unit { - ($unit:ident, $source:expr) => { + ($unit:ident, $source:expr, $cache:expr) => { compute_rolling_distribution_from_starts( max_from, starts, @@ -60,11 +62,12 @@ impl RollingDistributionSlot { &mut d.pct75.$unit.height, &mut d.pct90.$unit.height, exit, + $cache, )? }; } - compute_unit!(sats, sats_source); - compute_unit!(cents, cents_source); + compute_unit!(sats, sats_source, sats_cache); + compute_unit!(cents, cents_source, cents_cache); Ok(()) } @@ -104,8 +107,23 @@ impl RollingDistributionAmountPerBlock { cents_source: &impl ReadableVec, exit: &Exit, ) -> Result<()> { - for (slot, starts) in self.0.as_mut_array().into_iter().zip(windows.as_array()) { - slot.compute(max_from, *starts, sats_source, cents_source, exit)?; + let mut sats_cache = None; + let mut cents_cache = None; + for (slot, starts) in self + .0 + .as_mut_array_largest_first() + .into_iter() + .zip(windows.as_array_largest_first()) + { + slot.compute( + max_from, + *starts, + sats_source, + cents_source, + exit, + &mut sats_cache, + &mut cents_cache, + )?; } Ok(()) } diff --git a/crates/brk_computer/src/internal/per_block/amount/with_deltas.rs b/crates/brk_computer/src/internal/per_block/amount/with_deltas.rs new file mode 100644 index 000000000..2b22a63e1 --- /dev/null +++ b/crates/brk_computer/src/internal/per_block/amount/with_deltas.rs @@ -0,0 +1,41 @@ +use brk_error::Result; +use brk_traversable::Traversable; +use brk_types::{BasisPointsSigned32, Sats, SatsSigned, Version}; +use derive_more::{Deref, DerefMut}; +use vecdb::{Database, Rw, StorageMode}; + +use crate::{ + indexes, + internal::{AmountPerBlock, CachedWindowStarts, LazyRollingDeltasFromHeight}, +}; + +#[derive(Deref, DerefMut, Traversable)] +pub struct AmountPerBlockWithDeltas { + #[deref] + #[deref_mut] + #[traversable(flatten)] + pub inner: AmountPerBlock, + pub delta: LazyRollingDeltasFromHeight, +} + +impl AmountPerBlockWithDeltas { + pub(crate) fn forced_import( + db: &Database, + name: &str, + version: Version, + indexes: &indexes::Vecs, + cached_starts: &CachedWindowStarts, + ) -> Result { + let inner = AmountPerBlock::forced_import(db, name, version, indexes)?; + + let delta = LazyRollingDeltasFromHeight::new( + &format!("{name}_delta"), + version + Version::ONE, + &inner.sats.height, + cached_starts, + indexes, + ); + + Ok(Self { inner, delta }) + } +} diff --git a/crates/brk_computer/src/internal/per_block/rolling/distribution.rs b/crates/brk_computer/src/internal/per_block/rolling/distribution.rs index 6c051ce1a..746270985 100644 --- a/crates/brk_computer/src/internal/per_block/rolling/distribution.rs +++ b/crates/brk_computer/src/internal/per_block/rolling/distribution.rs @@ -46,6 +46,7 @@ where T: Copy + Ord + From + Default, f64: From, { + let mut values_cache = None; macro_rules! compute_window { ($w:ident) => { compute_rolling_distribution_from_starts( @@ -61,13 +62,15 @@ where &mut self.0.pct75.$w.height, &mut self.0.pct90.$w.height, exit, + &mut values_cache, )? }; } - compute_window!(_24h); - compute_window!(_1w); - compute_window!(_1m); + // Largest window first: its cache covers all smaller windows. compute_window!(_1y); + compute_window!(_1m); + compute_window!(_1w); + compute_window!(_24h); Ok(()) } diff --git a/crates/brk_computer/src/lib.rs b/crates/brk_computer/src/lib.rs index d8f5947dd..e13e707c5 100644 --- a/crates/brk_computer/src/lib.rs +++ b/crates/brk_computer/src/lib.rs @@ -325,25 +325,33 @@ impl Computer { .compute(indexer, &self.indexes, &starting_indexes, exit) })?; - timed("Computed inputs", || { - self.inputs.compute( - indexer, - &self.indexes, - &self.blocks, - &starting_indexes, - exit, - ) - })?; - - timed("Computed scripts", || { - self.scripts.compute( - indexer, - &self.outputs, - &self.prices, - &starting_indexes, - exit, - ) - })?; + // inputs and scripts are independent — parallelize + let (inputs_result, scripts_result) = rayon::join( + || { + timed("Computed inputs", || { + self.inputs.compute( + indexer, + &self.indexes, + &self.blocks, + &starting_indexes, + exit, + ) + }) + }, + || { + timed("Computed scripts", || { + self.scripts.compute( + indexer, + &self.outputs, + &self.prices, + &starting_indexes, + exit, + ) + }) + }, + ); + inputs_result?; + scripts_result?; timed("Computed outputs", || { self.outputs.compute( diff --git a/crates/brk_computer/src/mining/rewards/compute.rs b/crates/brk_computer/src/mining/rewards/compute.rs index 5f4d07845..d043c812b 100644 --- a/crates/brk_computer/src/mining/rewards/compute.rs +++ b/crates/brk_computer/src/mining/rewards/compute.rs @@ -18,62 +18,68 @@ impl Vecs { starting_indexes: &Indexes, exit: &Exit, ) -> Result<()> { - self.coinbase.compute( - starting_indexes.height, - prices, - exit, - |vec| { - // Cursors avoid per-height PcoVec page decompression for the - // tx-indexed lookups. Coinbase tx_index values are strictly - // increasing, so the cursors only advance forward. - let mut txout_cursor = indexer.vecs.transactions.first_txout_index.cursor(); - let mut count_cursor = indexes.tx_index.output_count.cursor(); - - vec.compute_transform( - starting_indexes.height, - &indexer.vecs.transactions.first_tx_index, - |(height, tx_index, ..)| { - let ti = tx_index.to_usize(); - - txout_cursor.advance(ti - txout_cursor.position()); - let first_txout_index = txout_cursor.next().unwrap().to_usize(); - - count_cursor.advance(ti - count_cursor.position()); - let output_count: usize = count_cursor.next().unwrap().into(); - - let sats = indexer.vecs.outputs.value.fold_range_at( - first_txout_index, - first_txout_index + output_count, - Sats::ZERO, - |acc, v| acc + v, - ); - (height, sats) - }, - exit, - )?; - Ok(()) - }, - )?; - - // Coinbase fee is 0, so including it in the sum doesn't affect the result + // coinbase and fees are independent — parallelize let window_starts = lookback.window_starts(); - - self.fees.compute( - starting_indexes.height, - &window_starts, - prices, - exit, - |vec| { - vec.compute_sum_from_indexes( + let (r_coinbase, r_fees) = rayon::join( + || { + self.coinbase.compute( starting_indexes.height, - &indexer.vecs.transactions.first_tx_index, - &indexes.height.tx_index_count, - &transactions_fees.fee.tx_index, + prices, exit, - )?; - Ok(()) + |vec| { + let mut txout_cursor = + indexer.vecs.transactions.first_txout_index.cursor(); + let mut count_cursor = indexes.tx_index.output_count.cursor(); + + vec.compute_transform( + starting_indexes.height, + &indexer.vecs.transactions.first_tx_index, + |(height, tx_index, ..)| { + let ti = tx_index.to_usize(); + + txout_cursor.advance(ti - txout_cursor.position()); + let first_txout_index = + txout_cursor.next().unwrap().to_usize(); + + count_cursor.advance(ti - count_cursor.position()); + let output_count: usize = + count_cursor.next().unwrap().into(); + + let sats = indexer.vecs.outputs.value.fold_range_at( + first_txout_index, + first_txout_index + output_count, + Sats::ZERO, + |acc, v| acc + v, + ); + (height, sats) + }, + exit, + )?; + Ok(()) + }, + ) }, - )?; + || { + self.fees.compute( + starting_indexes.height, + &window_starts, + prices, + exit, + |vec| { + vec.compute_sum_from_indexes( + starting_indexes.height, + &indexer.vecs.transactions.first_tx_index, + &indexes.height.tx_index_count, + &transactions_fees.fee.tx_index, + exit, + )?; + Ok(()) + }, + ) + }, + ); + r_coinbase?; + r_fees?; self.subsidy.base.sats.height.compute_transform2( starting_indexes.height, @@ -110,7 +116,6 @@ impl Vecs { }, )?; - // All-time cumulative fee dominance self.fee_dominance .compute_binary::( starting_indexes.height, @@ -119,7 +124,6 @@ impl Vecs { exit, )?; - // Rolling fee dominance = sum(fees) / sum(coinbase) self.fee_dominance_rolling .compute_binary::( starting_indexes.height, @@ -128,7 +132,6 @@ impl Vecs { exit, )?; - // All-time cumulative subsidy dominance self.subsidy_dominance .compute_binary::( starting_indexes.height, @@ -144,7 +147,6 @@ impl Vecs { exit, )?; - // Fee Ratio Multiple: sum(coinbase) / sum(fees) per rolling window self.fee_ratio_multiple .compute_binary::( starting_indexes.height, diff --git a/crates/brk_computer/src/scripts/import.rs b/crates/brk_computer/src/scripts/import.rs index 3d9e63d02..156d0340b 100644 --- a/crates/brk_computer/src/scripts/import.rs +++ b/crates/brk_computer/src/scripts/import.rs @@ -22,7 +22,7 @@ impl Vecs { let version = parent_version; let count = CountVecs::forced_import(&db, version, indexes, cached_starts)?; - let value = ValueVecs::forced_import(&db, version, indexes)?; + let value = ValueVecs::forced_import(&db, version, indexes, cached_starts)?; let adoption = AdoptionVecs::forced_import(&db, version, indexes)?; let this = Self { diff --git a/crates/brk_computer/src/scripts/value/compute.rs b/crates/brk_computer/src/scripts/value/compute.rs index 380bd03d2..03e17ff2f 100644 --- a/crates/brk_computer/src/scripts/value/compute.rs +++ b/crates/brk_computer/src/scripts/value/compute.rs @@ -14,7 +14,7 @@ impl Vecs { starting_indexes: &Indexes, exit: &Exit, ) -> Result<()> { - self.op_return.compute_with( + self.op_return.compute( starting_indexes.height, prices, exit, diff --git a/crates/brk_computer/src/scripts/value/import.rs b/crates/brk_computer/src/scripts/value/import.rs index 6d45eacb0..449e0638c 100644 --- a/crates/brk_computer/src/scripts/value/import.rs +++ b/crates/brk_computer/src/scripts/value/import.rs @@ -3,20 +3,22 @@ use brk_types::Version; use vecdb::Database; use super::Vecs; -use crate::{indexes, internal::AmountPerBlockCumulative}; +use crate::{indexes, internal::{AmountPerBlockCumulativeWithSums, CachedWindowStarts}}; impl Vecs { pub(crate) fn forced_import( db: &Database, version: Version, indexes: &indexes::Vecs, + cached_starts: &CachedWindowStarts, ) -> Result { Ok(Self { - op_return: AmountPerBlockCumulative::forced_import( + op_return: AmountPerBlockCumulativeWithSums::forced_import( db, "op_return_value", version, indexes, + cached_starts, )?, }) } diff --git a/crates/brk_computer/src/scripts/value/vecs.rs b/crates/brk_computer/src/scripts/value/vecs.rs index 511f88aac..485744b05 100644 --- a/crates/brk_computer/src/scripts/value/vecs.rs +++ b/crates/brk_computer/src/scripts/value/vecs.rs @@ -1,9 +1,9 @@ use brk_traversable::Traversable; use vecdb::{Rw, StorageMode}; -use crate::internal::AmountPerBlockCumulative; +use crate::internal::AmountPerBlockCumulativeWithSums; #[derive(Traversable)] pub struct Vecs { - pub op_return: AmountPerBlockCumulative, + pub op_return: AmountPerBlockCumulativeWithSums, } diff --git a/crates/brk_computer/src/supply/burned/compute.rs b/crates/brk_computer/src/supply/burned/compute.rs index 09dae8273..f59e1dafd 100644 --- a/crates/brk_computer/src/supply/burned/compute.rs +++ b/crates/brk_computer/src/supply/burned/compute.rs @@ -14,47 +14,7 @@ impl Vecs { starting_indexes: &Indexes, exit: &Exit, ) -> Result<()> { - self.op_return.compute( - starting_indexes.height, - prices, - exit, - |height_vec| { - // Validate computed versions against dependencies - - let op_return_dep_version = scripts.value.op_return.base.sats.height.version(); - height_vec.validate_computed_version_or_reset(op_return_dep_version)?; - - // Copy per-block op_return values from scripts - let scripts_target = scripts.value.op_return.base.sats.height.len(); - if scripts_target > 0 { - let target_height = Height::from(scripts_target - 1); - let current_len = height_vec.len(); - let starting_height = - Height::from(current_len.min(starting_indexes.height.to_usize())); - - if starting_height <= target_height { - let start = starting_height.to_usize(); - let end = target_height.to_usize() + 1; - scripts.value.op_return.base.sats.height.fold_range_at( - start, - end, - start, - |idx, value| { - height_vec.truncate_push(Height::from(idx), value).unwrap(); - idx + 1 - }, - ); - } - } - - height_vec.write()?; - Ok(()) - }, - )?; - - // 2. Compute unspendable supply = op_return + unclaimed_rewards + genesis (at height 0) - // Get reference to op_return height vec for computing unspendable - let op_return_height = &self.op_return.base.sats.height; + let op_return_height = &scripts.value.op_return.base.sats.height; let unclaimed_height = &mining.rewards.unclaimed.base.sats.height; self.unspendable.compute( diff --git a/crates/brk_computer/src/supply/burned/import.rs b/crates/brk_computer/src/supply/burned/import.rs index 111184e6c..0bdc9d520 100644 --- a/crates/brk_computer/src/supply/burned/import.rs +++ b/crates/brk_computer/src/supply/burned/import.rs @@ -13,13 +13,6 @@ impl Vecs { cached_starts: &CachedWindowStarts, ) -> Result { Ok(Self { - op_return: AmountPerBlockCumulativeWithSums::forced_import( - db, - "op_return_supply", - version, - indexes, - cached_starts, - )?, unspendable: AmountPerBlockCumulativeWithSums::forced_import( db, "unspendable_supply", diff --git a/crates/brk_computer/src/supply/burned/vecs.rs b/crates/brk_computer/src/supply/burned/vecs.rs index 74998a0a1..3ecf34763 100644 --- a/crates/brk_computer/src/supply/burned/vecs.rs +++ b/crates/brk_computer/src/supply/burned/vecs.rs @@ -5,6 +5,5 @@ use crate::internal::AmountPerBlockCumulativeWithSums; #[derive(Traversable)] pub struct Vecs { - pub op_return: AmountPerBlockCumulativeWithSums, pub unspendable: AmountPerBlockCumulativeWithSums, } diff --git a/crates/brk_computer/src/transactions/compute.rs b/crates/brk_computer/src/transactions/compute.rs index c8c867873..6b2f17af1 100644 --- a/crates/brk_computer/src/transactions/compute.rs +++ b/crates/brk_computer/src/transactions/compute.rs @@ -20,17 +20,19 @@ impl Vecs { starting_indexes: &Indexes, exit: &Exit, ) -> Result<()> { - // Count computes first - self.count - .compute(indexer, &blocks.lookback, starting_indexes, exit)?; - - // Versions depends on count - self.versions - .compute(indexer, starting_indexes, exit)?; - - // Size computes next (uses 6-block rolling window) - self.size - .compute(indexer, indexes, starting_indexes, exit)?; + // count, versions, size are independent — parallelize + let (r1, (r2, r3)) = rayon::join( + || self.count.compute(indexer, &blocks.lookback, starting_indexes, exit), + || { + rayon::join( + || self.versions.compute(indexer, starting_indexes, exit), + || self.size.compute(indexer, indexes, starting_indexes, exit), + ) + }, + ); + r1?; + r2?; + r3?; // Fees depends on size self.fees diff --git a/crates/brk_computer/src/transactions/fees/compute.rs b/crates/brk_computer/src/transactions/fees/compute.rs index d35a05bba..227cd5b50 100644 --- a/crates/brk_computer/src/transactions/fees/compute.rs +++ b/crates/brk_computer/src/transactions/fees/compute.rs @@ -18,21 +18,29 @@ impl Vecs { starting_indexes: &Indexes, exit: &Exit, ) -> Result<()> { - self.input_value.compute_sum_from_indexes( - starting_indexes.tx_index, - &indexer.vecs.transactions.first_txin_index, - &indexes.tx_index.input_count, - &txins.spent.value, - exit, - )?; - - self.output_value.compute_sum_from_indexes( - starting_indexes.tx_index, - &indexer.vecs.transactions.first_txout_index, - &indexes.tx_index.output_count, - &indexer.vecs.outputs.value, - exit, - )?; + // input_value and output_value are independent — parallelize + let (r1, r2) = rayon::join( + || { + self.input_value.compute_sum_from_indexes( + starting_indexes.tx_index, + &indexer.vecs.transactions.first_txin_index, + &indexes.tx_index.input_count, + &txins.spent.value, + exit, + ) + }, + || { + self.output_value.compute_sum_from_indexes( + starting_indexes.tx_index, + &indexer.vecs.transactions.first_txout_index, + &indexes.tx_index.output_count, + &indexer.vecs.outputs.value, + exit, + ) + }, + ); + r1?; + r2?; self.fee.tx_index.compute_transform2( starting_indexes.tx_index, diff --git a/crates/brk_computer/src/transactions/versions/compute.rs b/crates/brk_computer/src/transactions/versions/compute.rs index 6028fd013..e0db54377 100644 --- a/crates/brk_computer/src/transactions/versions/compute.rs +++ b/crates/brk_computer/src/transactions/versions/compute.rs @@ -1,10 +1,9 @@ use brk_error::Result; use brk_indexer::Indexer; use brk_types::{Indexes, StoredU64, TxVersion}; -use vecdb::{Exit, ReadableVec, VecIndex}; +use vecdb::{AnyStoredVec, AnyVec, Exit, ReadableVec, VecIndex, WritableVec}; use super::Vecs; -use crate::internal::PerBlockCumulativeWithSums; impl Vecs { pub(crate) fn compute( @@ -13,30 +12,86 @@ impl Vecs { starting_indexes: &Indexes, exit: &Exit, ) -> Result<()> { - let tx_vany = |tx_vany: &mut PerBlockCumulativeWithSums, - tx_version: TxVersion| { - let tx_version_vec = &indexer.vecs.transactions.tx_version; - // Cursor avoids per-transaction PcoVec page decompression. - // Txindex values are sequential, so the cursor only advances forward. - let mut cursor = tx_version_vec.cursor(); - tx_vany.compute(starting_indexes.height, exit, |vec| { - vec.compute_filtered_count_from_indexes( - starting_indexes.height, - &indexer.vecs.transactions.first_tx_index, - &indexer.vecs.transactions.txid, - |tx_index| { - let ti = tx_index.to_usize(); - cursor.advance(ti - cursor.position()); - cursor.next().unwrap() == tx_version - }, - exit, - )?; - Ok(()) - }) - }; - tx_vany(&mut self.v1, TxVersion::ONE)?; - tx_vany(&mut self.v2, TxVersion::TWO)?; - tx_vany(&mut self.v3, TxVersion::THREE)?; + let dep_version = indexer.vecs.transactions.tx_version.version() + + indexer.vecs.transactions.first_tx_index.version() + + indexer.vecs.transactions.txid.version(); + + for vec in [ + &mut self.v1.base.height, + &mut self.v2.base.height, + &mut self.v3.base.height, + ] { + vec.validate_and_truncate(dep_version, starting_indexes.height)?; + } + + let skip = self + .v1 + .base + .height + .len() + .min(self.v2.base.height.len()) + .min(self.v3.base.height.len()); + + let first_tx_index = &indexer.vecs.transactions.first_tx_index; + let end = first_tx_index.len(); + if skip >= end { + return Ok(()); + } + + // Truncate all 3 to skip, then push (no per-element bounds checks). + self.v1.base.height.truncate_if_needed_at(skip)?; + self.v2.base.height.truncate_if_needed_at(skip)?; + self.v3.base.height.truncate_if_needed_at(skip)?; + + // Single cursor over tx_version — scanned once for all 3 version counts. + let mut cursor = indexer.vecs.transactions.tx_version.cursor(); + let fi_batch = first_tx_index.collect_range_at(skip, end); + let txid_len = indexer.vecs.transactions.txid.len(); + + for (j, first_index) in fi_batch.iter().enumerate() { + let next_first = fi_batch + .get(j + 1) + .map(|fi| fi.to_usize()) + .unwrap_or(txid_len); + + let mut c1: usize = 0; + let mut c2: usize = 0; + let mut c3: usize = 0; + + let fi = first_index.to_usize(); + cursor.advance(fi - cursor.position()); + for _ in fi..next_first { + match cursor.next().unwrap() { + TxVersion::ONE => c1 += 1, + TxVersion::TWO => c2 += 1, + TxVersion::THREE => c3 += 1, + _ => {} + } + } + + self.v1.base.height.push(StoredU64::from(c1 as u64)); + self.v2.base.height.push(StoredU64::from(c2 as u64)); + self.v3.base.height.push(StoredU64::from(c3 as u64)); + + if self.v1.base.height.batch_limit_reached() { + let _lock = exit.lock(); + self.v1.base.height.write()?; + self.v2.base.height.write()?; + self.v3.base.height.write()?; + } + } + + { + let _lock = exit.lock(); + self.v1.base.height.write()?; + self.v2.base.height.write()?; + self.v3.base.height.write()?; + } + + // Derive cumulative + sums from base + self.v1.compute_rest(starting_indexes.height, exit)?; + self.v2.compute_rest(starting_indexes.height, exit)?; + self.v3.compute_rest(starting_indexes.height, exit)?; Ok(()) } diff --git a/crates/brk_computer/src/transactions/volume/compute.rs b/crates/brk_computer/src/transactions/volume/compute.rs index 7b6c81fac..0d038e762 100644 --- a/crates/brk_computer/src/transactions/volume/compute.rs +++ b/crates/brk_computer/src/transactions/volume/compute.rs @@ -22,36 +22,44 @@ impl Vecs { starting_indexes: &Indexes, exit: &Exit, ) -> Result<()> { - self.sent_sum.compute( - starting_indexes.height, - prices, - exit, - |sats_vec| { - Ok(sats_vec.compute_filtered_sum_from_indexes( + // sent_sum and received_sum are independent — parallelize + let (r1, r2) = rayon::join( + || { + self.sent_sum.compute( starting_indexes.height, - &indexer.vecs.transactions.first_tx_index, - &indexes.height.tx_index_count, - &fees_vecs.input_value, - |sats| !sats.is_max(), + prices, exit, - )?) + |sats_vec| { + Ok(sats_vec.compute_filtered_sum_from_indexes( + starting_indexes.height, + &indexer.vecs.transactions.first_tx_index, + &indexes.height.tx_index_count, + &fees_vecs.input_value, + |sats| !sats.is_max(), + exit, + )?) + }, + ) }, - )?; - - self.received_sum.compute( - starting_indexes.height, - prices, - exit, - |sats_vec| { - Ok(sats_vec.compute_sum_from_indexes( + || { + self.received_sum.compute( starting_indexes.height, - &indexer.vecs.transactions.first_tx_index, - &indexes.height.tx_index_count, - &fees_vecs.output_value, + prices, exit, - )?) + |sats_vec| { + Ok(sats_vec.compute_sum_from_indexes( + starting_indexes.height, + &indexer.vecs.transactions.first_tx_index, + &indexes.height.tx_index_count, + &fees_vecs.output_value, + exit, + )?) + }, + ) }, - )?; + ); + r1?; + r2?; self.tx_per_sec .height diff --git a/crates/brk_types/src/range_map.rs b/crates/brk_types/src/range_map.rs index 9c049222f..d6fa4c57e 100644 --- a/crates/brk_types/src/range_map.rs +++ b/crates/brk_types/src/range_map.rs @@ -1,7 +1,8 @@ use std::marker::PhantomData; /// Direct-mapped cache size. Power of 2 for fast masking. -const CACHE_SIZE: usize = 128; +/// 1024 entries × ~32 bytes = 32 KB (fits in L1 cache). +const CACHE_SIZE: usize = 1024; const CACHE_MASK: usize = CACHE_SIZE - 1; /// Cache entry: (range_low, range_high, value, occupied). diff --git a/modules/brk-client/index.js b/modules/brk-client/index.js index 3b619bcca..1db9eff35 100644 --- a/modules/brk-client/index.js +++ b/modules/brk-client/index.js @@ -2633,6 +2633,31 @@ function createBpsCentsRatioSatsUsdPattern(client, acc) { }; } +/** + * @typedef {Object} BtcCentsDeltaSatsUsdPattern + * @property {MetricPattern1} btc + * @property {MetricPattern1} cents + * @property {AbsoluteRatePattern} delta + * @property {MetricPattern1} sats + * @property {MetricPattern1} usd + */ + +/** + * Create a BtcCentsDeltaSatsUsdPattern pattern node + * @param {BrkClientBase} client + * @param {string} acc - Accumulated metric name + * @returns {BtcCentsDeltaSatsUsdPattern} + */ +function createBtcCentsDeltaSatsUsdPattern(client, acc) { + return { + btc: createMetricPattern1(client, acc), + cents: createMetricPattern1(client, _m(acc, 'cents')), + delta: createAbsoluteRatePattern(client, _m(acc, 'delta')), + sats: createMetricPattern1(client, _m(acc, 'sats')), + usd: createMetricPattern1(client, _m(acc, 'usd')), + }; +} + /** * @typedef {Object} BtcCentsRelSatsUsdPattern * @property {MetricPattern1} btc @@ -2758,6 +2783,31 @@ function createInvestedMaxMinPercentilesSupplyPattern(client, acc) { }; } +/** + * @typedef {Object} MvrvNuplRealizedSupplyPattern + * @property {MetricPattern1} mvrv + * @property {BpsRatioPattern} nupl + * @property {AllSthPattern} realizedCap + * @property {BpsCentsRatioSatsUsdPattern} realizedPrice + * @property {AllSthPattern2} supply + */ + +/** + * Create a MvrvNuplRealizedSupplyPattern pattern node + * @param {BrkClientBase} client + * @param {string} acc - Accumulated metric name + * @returns {MvrvNuplRealizedSupplyPattern} + */ +function createMvrvNuplRealizedSupplyPattern(client, acc) { + return { + mvrv: createMetricPattern1(client, _m(acc, 'mvrv')), + nupl: createBpsRatioPattern(client, _m(acc, 'nupl')), + realizedCap: createAllSthPattern(client, acc), + realizedPrice: createBpsCentsRatioSatsUsdPattern(client, _m(acc, 'realized_price')), + supply: createAllSthPattern2(client, acc), + }; +} + /** * @typedef {Object} PhsReboundThsPattern * @property {MetricPattern1} phs @@ -3566,7 +3616,7 @@ function createGreedNetPainPattern(client, acc) { /** * @typedef {Object} LossNuplProfitPattern - * @property {BaseCumulativeSumPattern3} loss + * @property {BaseCumulativeNegativeSumPattern} loss * @property {BpsRatioPattern} nupl * @property {BaseCumulativeSumPattern3} profit */ @@ -3579,7 +3629,7 @@ function createGreedNetPainPattern(client, acc) { */ function createLossNuplProfitPattern(client, acc) { return { - loss: createBaseCumulativeSumPattern3(client, _m(acc, 'unrealized_loss')), + loss: createBaseCumulativeNegativeSumPattern(client, acc), nupl: createBpsRatioPattern(client, _m(acc, 'nupl')), profit: createBaseCumulativeSumPattern3(client, _m(acc, 'unrealized_profit')), }; @@ -3732,6 +3782,44 @@ function createAbsoluteRatePattern2(client, acc) { }; } +/** + * @typedef {Object} AllSthPattern2 + * @property {BtcCentsDeltaSatsUsdPattern} all + * @property {BtcCentsSatsUsdPattern} sth + */ + +/** + * Create a AllSthPattern2 pattern node + * @param {BrkClientBase} client + * @param {string} acc - Accumulated metric name + * @returns {AllSthPattern2} + */ +function createAllSthPattern2(client, acc) { + return { + all: createBtcCentsDeltaSatsUsdPattern(client, _m(acc, 'supply')), + sth: createBtcCentsSatsUsdPattern(client, _m(acc, 'sth_supply')), + }; +} + +/** + * @typedef {Object} AllSthPattern + * @property {MetricPattern1} all + * @property {MetricPattern1} sth + */ + +/** + * Create a AllSthPattern pattern node + * @param {BrkClientBase} client + * @param {string} acc - Accumulated metric name + * @returns {AllSthPattern} + */ +function createAllSthPattern(client, acc) { + return { + all: createMetricPattern1(client, _m(acc, 'realized_cap')), + sth: createMetricPattern1(client, _m(acc, 'sth_realized_cap')), + }; +} + /** * @typedef {Object} BlocksDominancePattern * @property {BaseCumulativeSumPattern2} blocksMined @@ -3903,25 +3991,6 @@ function createPriceValuePattern(client, acc) { }; } -/** - * @typedef {Object} RealizedSupplyPattern - * @property {MetricPattern1} realizedCap - * @property {MetricPattern1} supply - */ - -/** - * Create a RealizedSupplyPattern pattern node - * @param {BrkClientBase} client - * @param {string} acc - Accumulated metric name - * @returns {RealizedSupplyPattern} - */ -function createRealizedSupplyPattern(client, acc) { - return { - realizedCap: createMetricPattern1(client, _m(acc, 'realized_cap')), - supply: createMetricPattern1(client, _m(acc, 'supply')), - }; -} - /** * @typedef {Object} RelPattern * @property {BpsPercentRatioPattern} relToMcap @@ -4481,13 +4550,7 @@ function createUnspentPattern(client, acc) { /** * @typedef {Object} MetricsTree_Scripts_Value - * @property {MetricsTree_Scripts_Value_OpReturn} opReturn - */ - -/** - * @typedef {Object} MetricsTree_Scripts_Value_OpReturn - * @property {BtcCentsSatsUsdPattern} base - * @property {BtcCentsSatsUsdPattern} cumulative + * @property {BaseCumulativeSumPattern4} opReturn */ /** @@ -5509,7 +5572,6 @@ function createUnspentPattern(client, acc) { /** * @typedef {Object} MetricsTree_Supply_Burned - * @property {BaseCumulativeSumPattern4} opReturn * @property {BaseCumulativeSumPattern4} unspendable */ @@ -5819,87 +5881,87 @@ function createUnspentPattern(client, acc) { /** * @typedef {Object} MetricsTree_Cohorts_Utxo_Profitability_Range - * @property {RealizedSupplyPattern} over1000pctInProfit - * @property {RealizedSupplyPattern} _500pctTo1000pctInProfit - * @property {RealizedSupplyPattern} _300pctTo500pctInProfit - * @property {RealizedSupplyPattern} _200pctTo300pctInProfit - * @property {RealizedSupplyPattern} _100pctTo200pctInProfit - * @property {RealizedSupplyPattern} _90pctTo100pctInProfit - * @property {RealizedSupplyPattern} _80pctTo90pctInProfit - * @property {RealizedSupplyPattern} _70pctTo80pctInProfit - * @property {RealizedSupplyPattern} _60pctTo70pctInProfit - * @property {RealizedSupplyPattern} _50pctTo60pctInProfit - * @property {RealizedSupplyPattern} _40pctTo50pctInProfit - * @property {RealizedSupplyPattern} _30pctTo40pctInProfit - * @property {RealizedSupplyPattern} _20pctTo30pctInProfit - * @property {RealizedSupplyPattern} _10pctTo20pctInProfit - * @property {RealizedSupplyPattern} _0pctTo10pctInProfit - * @property {RealizedSupplyPattern} _0pctTo10pctInLoss - * @property {RealizedSupplyPattern} _10pctTo20pctInLoss - * @property {RealizedSupplyPattern} _20pctTo30pctInLoss - * @property {RealizedSupplyPattern} _30pctTo40pctInLoss - * @property {RealizedSupplyPattern} _40pctTo50pctInLoss - * @property {RealizedSupplyPattern} _50pctTo60pctInLoss - * @property {RealizedSupplyPattern} _60pctTo70pctInLoss - * @property {RealizedSupplyPattern} _70pctTo80pctInLoss - * @property {RealizedSupplyPattern} _80pctTo90pctInLoss - * @property {RealizedSupplyPattern} _90pctTo100pctInLoss + * @property {MvrvNuplRealizedSupplyPattern} over1000pctInProfit + * @property {MvrvNuplRealizedSupplyPattern} _500pctTo1000pctInProfit + * @property {MvrvNuplRealizedSupplyPattern} _300pctTo500pctInProfit + * @property {MvrvNuplRealizedSupplyPattern} _200pctTo300pctInProfit + * @property {MvrvNuplRealizedSupplyPattern} _100pctTo200pctInProfit + * @property {MvrvNuplRealizedSupplyPattern} _90pctTo100pctInProfit + * @property {MvrvNuplRealizedSupplyPattern} _80pctTo90pctInProfit + * @property {MvrvNuplRealizedSupplyPattern} _70pctTo80pctInProfit + * @property {MvrvNuplRealizedSupplyPattern} _60pctTo70pctInProfit + * @property {MvrvNuplRealizedSupplyPattern} _50pctTo60pctInProfit + * @property {MvrvNuplRealizedSupplyPattern} _40pctTo50pctInProfit + * @property {MvrvNuplRealizedSupplyPattern} _30pctTo40pctInProfit + * @property {MvrvNuplRealizedSupplyPattern} _20pctTo30pctInProfit + * @property {MvrvNuplRealizedSupplyPattern} _10pctTo20pctInProfit + * @property {MvrvNuplRealizedSupplyPattern} _0pctTo10pctInProfit + * @property {MvrvNuplRealizedSupplyPattern} _0pctTo10pctInLoss + * @property {MvrvNuplRealizedSupplyPattern} _10pctTo20pctInLoss + * @property {MvrvNuplRealizedSupplyPattern} _20pctTo30pctInLoss + * @property {MvrvNuplRealizedSupplyPattern} _30pctTo40pctInLoss + * @property {MvrvNuplRealizedSupplyPattern} _40pctTo50pctInLoss + * @property {MvrvNuplRealizedSupplyPattern} _50pctTo60pctInLoss + * @property {MvrvNuplRealizedSupplyPattern} _60pctTo70pctInLoss + * @property {MvrvNuplRealizedSupplyPattern} _70pctTo80pctInLoss + * @property {MvrvNuplRealizedSupplyPattern} _80pctTo90pctInLoss + * @property {MvrvNuplRealizedSupplyPattern} _90pctTo100pctInLoss */ /** * @typedef {Object} MetricsTree_Cohorts_Utxo_Profitability_Profit - * @property {RealizedSupplyPattern} breakeven - * @property {RealizedSupplyPattern} _10pct - * @property {RealizedSupplyPattern} _20pct - * @property {RealizedSupplyPattern} _30pct - * @property {RealizedSupplyPattern} _40pct - * @property {RealizedSupplyPattern} _50pct - * @property {RealizedSupplyPattern} _60pct - * @property {RealizedSupplyPattern} _70pct - * @property {RealizedSupplyPattern} _80pct - * @property {RealizedSupplyPattern} _90pct - * @property {RealizedSupplyPattern} _100pct - * @property {RealizedSupplyPattern} _200pct - * @property {RealizedSupplyPattern} _300pct - * @property {RealizedSupplyPattern} _500pct + * @property {MvrvNuplRealizedSupplyPattern} breakeven + * @property {MvrvNuplRealizedSupplyPattern} _10pct + * @property {MvrvNuplRealizedSupplyPattern} _20pct + * @property {MvrvNuplRealizedSupplyPattern} _30pct + * @property {MvrvNuplRealizedSupplyPattern} _40pct + * @property {MvrvNuplRealizedSupplyPattern} _50pct + * @property {MvrvNuplRealizedSupplyPattern} _60pct + * @property {MvrvNuplRealizedSupplyPattern} _70pct + * @property {MvrvNuplRealizedSupplyPattern} _80pct + * @property {MvrvNuplRealizedSupplyPattern} _90pct + * @property {MvrvNuplRealizedSupplyPattern} _100pct + * @property {MvrvNuplRealizedSupplyPattern} _200pct + * @property {MvrvNuplRealizedSupplyPattern} _300pct + * @property {MvrvNuplRealizedSupplyPattern} _500pct */ /** * @typedef {Object} MetricsTree_Cohorts_Utxo_Profitability_Loss - * @property {RealizedSupplyPattern} breakeven - * @property {RealizedSupplyPattern} _10pct - * @property {RealizedSupplyPattern} _20pct - * @property {RealizedSupplyPattern} _30pct - * @property {RealizedSupplyPattern} _40pct - * @property {RealizedSupplyPattern} _50pct - * @property {RealizedSupplyPattern} _60pct - * @property {RealizedSupplyPattern} _70pct - * @property {RealizedSupplyPattern} _80pct + * @property {MvrvNuplRealizedSupplyPattern} breakeven + * @property {MvrvNuplRealizedSupplyPattern} _10pct + * @property {MvrvNuplRealizedSupplyPattern} _20pct + * @property {MvrvNuplRealizedSupplyPattern} _30pct + * @property {MvrvNuplRealizedSupplyPattern} _40pct + * @property {MvrvNuplRealizedSupplyPattern} _50pct + * @property {MvrvNuplRealizedSupplyPattern} _60pct + * @property {MvrvNuplRealizedSupplyPattern} _70pct + * @property {MvrvNuplRealizedSupplyPattern} _80pct */ /** * @typedef {Object} MetricsTree_Cohorts_Utxo_Matured - * @property {BtcCentsSatsUsdPattern} under1h - * @property {BtcCentsSatsUsdPattern} _1hTo1d - * @property {BtcCentsSatsUsdPattern} _1dTo1w - * @property {BtcCentsSatsUsdPattern} _1wTo1m - * @property {BtcCentsSatsUsdPattern} _1mTo2m - * @property {BtcCentsSatsUsdPattern} _2mTo3m - * @property {BtcCentsSatsUsdPattern} _3mTo4m - * @property {BtcCentsSatsUsdPattern} _4mTo5m - * @property {BtcCentsSatsUsdPattern} _5mTo6m - * @property {BtcCentsSatsUsdPattern} _6mTo1y - * @property {BtcCentsSatsUsdPattern} _1yTo2y - * @property {BtcCentsSatsUsdPattern} _2yTo3y - * @property {BtcCentsSatsUsdPattern} _3yTo4y - * @property {BtcCentsSatsUsdPattern} _4yTo5y - * @property {BtcCentsSatsUsdPattern} _5yTo6y - * @property {BtcCentsSatsUsdPattern} _6yTo7y - * @property {BtcCentsSatsUsdPattern} _7yTo8y - * @property {BtcCentsSatsUsdPattern} _8yTo10y - * @property {BtcCentsSatsUsdPattern} _10yTo12y - * @property {BtcCentsSatsUsdPattern} _12yTo15y - * @property {BtcCentsSatsUsdPattern} over15y + * @property {BaseCumulativeSumPattern4} under1h + * @property {BaseCumulativeSumPattern4} _1hTo1d + * @property {BaseCumulativeSumPattern4} _1dTo1w + * @property {BaseCumulativeSumPattern4} _1wTo1m + * @property {BaseCumulativeSumPattern4} _1mTo2m + * @property {BaseCumulativeSumPattern4} _2mTo3m + * @property {BaseCumulativeSumPattern4} _3mTo4m + * @property {BaseCumulativeSumPattern4} _4mTo5m + * @property {BaseCumulativeSumPattern4} _5mTo6m + * @property {BaseCumulativeSumPattern4} _6mTo1y + * @property {BaseCumulativeSumPattern4} _1yTo2y + * @property {BaseCumulativeSumPattern4} _2yTo3y + * @property {BaseCumulativeSumPattern4} _3yTo4y + * @property {BaseCumulativeSumPattern4} _4yTo5y + * @property {BaseCumulativeSumPattern4} _5yTo6y + * @property {BaseCumulativeSumPattern4} _6yTo7y + * @property {BaseCumulativeSumPattern4} _7yTo8y + * @property {BaseCumulativeSumPattern4} _8yTo10y + * @property {BaseCumulativeSumPattern4} _10yTo12y + * @property {BaseCumulativeSumPattern4} _12yTo15y + * @property {BaseCumulativeSumPattern4} over15y */ /** @@ -6870,6 +6932,255 @@ class BrkClient extends BrkClientBase { } }); + PROFITABILITY_RANGE_NAMES = /** @type {const} */ ({ + "over1000pctInProfit": { + "id": "utxos_over_1000pct_in_profit", + "short": ">1000%", + "long": "Over 1000% Profit" + }, + "_500pctTo1000pctInProfit": { + "id": "utxos_500pct_to_1000pct_in_profit", + "short": "500-1000%", + "long": "500-1000% Profit" + }, + "_300pctTo500pctInProfit": { + "id": "utxos_300pct_to_500pct_in_profit", + "short": "300-500%", + "long": "300-500% Profit" + }, + "_200pctTo300pctInProfit": { + "id": "utxos_200pct_to_300pct_in_profit", + "short": "200-300%", + "long": "200-300% Profit" + }, + "_100pctTo200pctInProfit": { + "id": "utxos_100pct_to_200pct_in_profit", + "short": "100-200%", + "long": "100-200% Profit" + }, + "_90pctTo100pctInProfit": { + "id": "utxos_90pct_to_100pct_in_profit", + "short": "90-100%", + "long": "90-100% Profit" + }, + "_80pctTo90pctInProfit": { + "id": "utxos_80pct_to_90pct_in_profit", + "short": "80-90%", + "long": "80-90% Profit" + }, + "_70pctTo80pctInProfit": { + "id": "utxos_70pct_to_80pct_in_profit", + "short": "70-80%", + "long": "70-80% Profit" + }, + "_60pctTo70pctInProfit": { + "id": "utxos_60pct_to_70pct_in_profit", + "short": "60-70%", + "long": "60-70% Profit" + }, + "_50pctTo60pctInProfit": { + "id": "utxos_50pct_to_60pct_in_profit", + "short": "50-60%", + "long": "50-60% Profit" + }, + "_40pctTo50pctInProfit": { + "id": "utxos_40pct_to_50pct_in_profit", + "short": "40-50%", + "long": "40-50% Profit" + }, + "_30pctTo40pctInProfit": { + "id": "utxos_30pct_to_40pct_in_profit", + "short": "30-40%", + "long": "30-40% Profit" + }, + "_20pctTo30pctInProfit": { + "id": "utxos_20pct_to_30pct_in_profit", + "short": "20-30%", + "long": "20-30% Profit" + }, + "_10pctTo20pctInProfit": { + "id": "utxos_10pct_to_20pct_in_profit", + "short": "10-20%", + "long": "10-20% Profit" + }, + "_0pctTo10pctInProfit": { + "id": "utxos_0pct_to_10pct_in_profit", + "short": "0-10%", + "long": "0-10% Profit" + }, + "_0pctTo10pctInLoss": { + "id": "utxos_0pct_to_10pct_in_loss", + "short": "0-10%L", + "long": "0-10% Loss" + }, + "_10pctTo20pctInLoss": { + "id": "utxos_10pct_to_20pct_in_loss", + "short": "10-20%L", + "long": "10-20% Loss" + }, + "_20pctTo30pctInLoss": { + "id": "utxos_20pct_to_30pct_in_loss", + "short": "20-30%L", + "long": "20-30% Loss" + }, + "_30pctTo40pctInLoss": { + "id": "utxos_30pct_to_40pct_in_loss", + "short": "30-40%L", + "long": "30-40% Loss" + }, + "_40pctTo50pctInLoss": { + "id": "utxos_40pct_to_50pct_in_loss", + "short": "40-50%L", + "long": "40-50% Loss" + }, + "_50pctTo60pctInLoss": { + "id": "utxos_50pct_to_60pct_in_loss", + "short": "50-60%L", + "long": "50-60% Loss" + }, + "_60pctTo70pctInLoss": { + "id": "utxos_60pct_to_70pct_in_loss", + "short": "60-70%L", + "long": "60-70% Loss" + }, + "_70pctTo80pctInLoss": { + "id": "utxos_70pct_to_80pct_in_loss", + "short": "70-80%L", + "long": "70-80% Loss" + }, + "_80pctTo90pctInLoss": { + "id": "utxos_80pct_to_90pct_in_loss", + "short": "80-90%L", + "long": "80-90% Loss" + }, + "_90pctTo100pctInLoss": { + "id": "utxos_90pct_to_100pct_in_loss", + "short": "90-100%L", + "long": "90-100% Loss" + } + }); + + PROFIT_NAMES = /** @type {const} */ ({ + "breakeven": { + "id": "utxos_in_profit", + "short": "≥0%", + "long": "In Profit (Breakeven+)" + }, + "_10pct": { + "id": "utxos_over_10pct_in_profit", + "short": "≥10%", + "long": "10%+ Profit" + }, + "_20pct": { + "id": "utxos_over_20pct_in_profit", + "short": "≥20%", + "long": "20%+ Profit" + }, + "_30pct": { + "id": "utxos_over_30pct_in_profit", + "short": "≥30%", + "long": "30%+ Profit" + }, + "_40pct": { + "id": "utxos_over_40pct_in_profit", + "short": "≥40%", + "long": "40%+ Profit" + }, + "_50pct": { + "id": "utxos_over_50pct_in_profit", + "short": "≥50%", + "long": "50%+ Profit" + }, + "_60pct": { + "id": "utxos_over_60pct_in_profit", + "short": "≥60%", + "long": "60%+ Profit" + }, + "_70pct": { + "id": "utxos_over_70pct_in_profit", + "short": "≥70%", + "long": "70%+ Profit" + }, + "_80pct": { + "id": "utxos_over_80pct_in_profit", + "short": "≥80%", + "long": "80%+ Profit" + }, + "_90pct": { + "id": "utxos_over_90pct_in_profit", + "short": "≥90%", + "long": "90%+ Profit" + }, + "_100pct": { + "id": "utxos_over_100pct_in_profit", + "short": "≥100%", + "long": "100%+ Profit" + }, + "_200pct": { + "id": "utxos_over_200pct_in_profit", + "short": "≥200%", + "long": "200%+ Profit" + }, + "_300pct": { + "id": "utxos_over_300pct_in_profit", + "short": "≥300%", + "long": "300%+ Profit" + }, + "_500pct": { + "id": "utxos_over_500pct_in_profit", + "short": "≥500%", + "long": "500%+ Profit" + } + }); + + LOSS_NAMES = /** @type {const} */ ({ + "breakeven": { + "id": "utxos_in_loss", + "short": "<0%", + "long": "In Loss (Below Breakeven)" + }, + "_10pct": { + "id": "utxos_over_10pct_in_loss", + "short": "≥10%L", + "long": "10%+ Loss" + }, + "_20pct": { + "id": "utxos_over_20pct_in_loss", + "short": "≥20%L", + "long": "20%+ Loss" + }, + "_30pct": { + "id": "utxos_over_30pct_in_loss", + "short": "≥30%L", + "long": "30%+ Loss" + }, + "_40pct": { + "id": "utxos_over_40pct_in_loss", + "short": "≥40%L", + "long": "40%+ Loss" + }, + "_50pct": { + "id": "utxos_over_50pct_in_loss", + "short": "≥50%L", + "long": "50%+ Loss" + }, + "_60pct": { + "id": "utxos_over_60pct_in_loss", + "short": "≥60%L", + "long": "60%+ Loss" + }, + "_70pct": { + "id": "utxos_over_70pct_in_loss", + "short": "≥70%L", + "long": "70%+ Loss" + }, + "_80pct": { + "id": "utxos_over_80pct_in_loss", + "short": "≥80%L", + "long": "80%+ Loss" + } + }); + /** * Convert an index value to a Date for date-based indexes. * @param {Index} index - The index type @@ -7203,10 +7514,7 @@ class BrkClient extends BrkClientBase { segwit: createBaseCumulativeSumPattern(this, 'segwit_count'), }, value: { - opReturn: { - base: createBtcCentsSatsUsdPattern(this, 'op_return_value'), - cumulative: createBtcCentsSatsUsdPattern(this, 'op_return_value_cumulative'), - }, + opReturn: createBaseCumulativeSumPattern4(this, 'op_return_value'), }, adoption: { taproot: createBpsPercentRatioPattern3(this, 'taproot_adoption'), @@ -7945,7 +8253,6 @@ class BrkClient extends BrkClientBase { state: createMetricPattern18(this, 'supply_state'), circulating: createBtcCentsSatsUsdPattern(this, 'circulating_supply'), burned: { - opReturn: createBaseCumulativeSumPattern4(this, 'op_return_supply'), unspendable: createBaseCumulativeSumPattern4(this, 'unspendable_supply'), }, inflationRate: createBpsPercentRatioPattern(this, 'inflation_rate'), @@ -8183,82 +8490,82 @@ class BrkClient extends BrkClientBase { }, profitability: { range: { - over1000pctInProfit: createRealizedSupplyPattern(this, 'utxos_over_1000pct_in_profit'), - _500pctTo1000pctInProfit: createRealizedSupplyPattern(this, 'utxos_500pct_to_1000pct_in_profit'), - _300pctTo500pctInProfit: createRealizedSupplyPattern(this, 'utxos_300pct_to_500pct_in_profit'), - _200pctTo300pctInProfit: createRealizedSupplyPattern(this, 'utxos_200pct_to_300pct_in_profit'), - _100pctTo200pctInProfit: createRealizedSupplyPattern(this, 'utxos_100pct_to_200pct_in_profit'), - _90pctTo100pctInProfit: createRealizedSupplyPattern(this, 'utxos_90pct_to_100pct_in_profit'), - _80pctTo90pctInProfit: createRealizedSupplyPattern(this, 'utxos_80pct_to_90pct_in_profit'), - _70pctTo80pctInProfit: createRealizedSupplyPattern(this, 'utxos_70pct_to_80pct_in_profit'), - _60pctTo70pctInProfit: createRealizedSupplyPattern(this, 'utxos_60pct_to_70pct_in_profit'), - _50pctTo60pctInProfit: createRealizedSupplyPattern(this, 'utxos_50pct_to_60pct_in_profit'), - _40pctTo50pctInProfit: createRealizedSupplyPattern(this, 'utxos_40pct_to_50pct_in_profit'), - _30pctTo40pctInProfit: createRealizedSupplyPattern(this, 'utxos_30pct_to_40pct_in_profit'), - _20pctTo30pctInProfit: createRealizedSupplyPattern(this, 'utxos_20pct_to_30pct_in_profit'), - _10pctTo20pctInProfit: createRealizedSupplyPattern(this, 'utxos_10pct_to_20pct_in_profit'), - _0pctTo10pctInProfit: createRealizedSupplyPattern(this, 'utxos_0pct_to_10pct_in_profit'), - _0pctTo10pctInLoss: createRealizedSupplyPattern(this, 'utxos_0pct_to_10pct_in_loss'), - _10pctTo20pctInLoss: createRealizedSupplyPattern(this, 'utxos_10pct_to_20pct_in_loss'), - _20pctTo30pctInLoss: createRealizedSupplyPattern(this, 'utxos_20pct_to_30pct_in_loss'), - _30pctTo40pctInLoss: createRealizedSupplyPattern(this, 'utxos_30pct_to_40pct_in_loss'), - _40pctTo50pctInLoss: createRealizedSupplyPattern(this, 'utxos_40pct_to_50pct_in_loss'), - _50pctTo60pctInLoss: createRealizedSupplyPattern(this, 'utxos_50pct_to_60pct_in_loss'), - _60pctTo70pctInLoss: createRealizedSupplyPattern(this, 'utxos_60pct_to_70pct_in_loss'), - _70pctTo80pctInLoss: createRealizedSupplyPattern(this, 'utxos_70pct_to_80pct_in_loss'), - _80pctTo90pctInLoss: createRealizedSupplyPattern(this, 'utxos_80pct_to_90pct_in_loss'), - _90pctTo100pctInLoss: createRealizedSupplyPattern(this, 'utxos_90pct_to_100pct_in_loss'), + over1000pctInProfit: createMvrvNuplRealizedSupplyPattern(this, 'utxos_over_1000pct_in_profit'), + _500pctTo1000pctInProfit: createMvrvNuplRealizedSupplyPattern(this, 'utxos_500pct_to_1000pct_in_profit'), + _300pctTo500pctInProfit: createMvrvNuplRealizedSupplyPattern(this, 'utxos_300pct_to_500pct_in_profit'), + _200pctTo300pctInProfit: createMvrvNuplRealizedSupplyPattern(this, 'utxos_200pct_to_300pct_in_profit'), + _100pctTo200pctInProfit: createMvrvNuplRealizedSupplyPattern(this, 'utxos_100pct_to_200pct_in_profit'), + _90pctTo100pctInProfit: createMvrvNuplRealizedSupplyPattern(this, 'utxos_90pct_to_100pct_in_profit'), + _80pctTo90pctInProfit: createMvrvNuplRealizedSupplyPattern(this, 'utxos_80pct_to_90pct_in_profit'), + _70pctTo80pctInProfit: createMvrvNuplRealizedSupplyPattern(this, 'utxos_70pct_to_80pct_in_profit'), + _60pctTo70pctInProfit: createMvrvNuplRealizedSupplyPattern(this, 'utxos_60pct_to_70pct_in_profit'), + _50pctTo60pctInProfit: createMvrvNuplRealizedSupplyPattern(this, 'utxos_50pct_to_60pct_in_profit'), + _40pctTo50pctInProfit: createMvrvNuplRealizedSupplyPattern(this, 'utxos_40pct_to_50pct_in_profit'), + _30pctTo40pctInProfit: createMvrvNuplRealizedSupplyPattern(this, 'utxos_30pct_to_40pct_in_profit'), + _20pctTo30pctInProfit: createMvrvNuplRealizedSupplyPattern(this, 'utxos_20pct_to_30pct_in_profit'), + _10pctTo20pctInProfit: createMvrvNuplRealizedSupplyPattern(this, 'utxos_10pct_to_20pct_in_profit'), + _0pctTo10pctInProfit: createMvrvNuplRealizedSupplyPattern(this, 'utxos_0pct_to_10pct_in_profit'), + _0pctTo10pctInLoss: createMvrvNuplRealizedSupplyPattern(this, 'utxos_0pct_to_10pct_in_loss'), + _10pctTo20pctInLoss: createMvrvNuplRealizedSupplyPattern(this, 'utxos_10pct_to_20pct_in_loss'), + _20pctTo30pctInLoss: createMvrvNuplRealizedSupplyPattern(this, 'utxos_20pct_to_30pct_in_loss'), + _30pctTo40pctInLoss: createMvrvNuplRealizedSupplyPattern(this, 'utxos_30pct_to_40pct_in_loss'), + _40pctTo50pctInLoss: createMvrvNuplRealizedSupplyPattern(this, 'utxos_40pct_to_50pct_in_loss'), + _50pctTo60pctInLoss: createMvrvNuplRealizedSupplyPattern(this, 'utxos_50pct_to_60pct_in_loss'), + _60pctTo70pctInLoss: createMvrvNuplRealizedSupplyPattern(this, 'utxos_60pct_to_70pct_in_loss'), + _70pctTo80pctInLoss: createMvrvNuplRealizedSupplyPattern(this, 'utxos_70pct_to_80pct_in_loss'), + _80pctTo90pctInLoss: createMvrvNuplRealizedSupplyPattern(this, 'utxos_80pct_to_90pct_in_loss'), + _90pctTo100pctInLoss: createMvrvNuplRealizedSupplyPattern(this, 'utxos_90pct_to_100pct_in_loss'), }, profit: { - breakeven: createRealizedSupplyPattern(this, 'utxos_in_profit'), - _10pct: createRealizedSupplyPattern(this, 'utxos_over_10pct_in_profit'), - _20pct: createRealizedSupplyPattern(this, 'utxos_over_20pct_in_profit'), - _30pct: createRealizedSupplyPattern(this, 'utxos_over_30pct_in_profit'), - _40pct: createRealizedSupplyPattern(this, 'utxos_over_40pct_in_profit'), - _50pct: createRealizedSupplyPattern(this, 'utxos_over_50pct_in_profit'), - _60pct: createRealizedSupplyPattern(this, 'utxos_over_60pct_in_profit'), - _70pct: createRealizedSupplyPattern(this, 'utxos_over_70pct_in_profit'), - _80pct: createRealizedSupplyPattern(this, 'utxos_over_80pct_in_profit'), - _90pct: createRealizedSupplyPattern(this, 'utxos_over_90pct_in_profit'), - _100pct: createRealizedSupplyPattern(this, 'utxos_over_100pct_in_profit'), - _200pct: createRealizedSupplyPattern(this, 'utxos_over_200pct_in_profit'), - _300pct: createRealizedSupplyPattern(this, 'utxos_over_300pct_in_profit'), - _500pct: createRealizedSupplyPattern(this, 'utxos_over_500pct_in_profit'), + breakeven: createMvrvNuplRealizedSupplyPattern(this, 'utxos_in_profit'), + _10pct: createMvrvNuplRealizedSupplyPattern(this, 'utxos_over_10pct_in_profit'), + _20pct: createMvrvNuplRealizedSupplyPattern(this, 'utxos_over_20pct_in_profit'), + _30pct: createMvrvNuplRealizedSupplyPattern(this, 'utxos_over_30pct_in_profit'), + _40pct: createMvrvNuplRealizedSupplyPattern(this, 'utxos_over_40pct_in_profit'), + _50pct: createMvrvNuplRealizedSupplyPattern(this, 'utxos_over_50pct_in_profit'), + _60pct: createMvrvNuplRealizedSupplyPattern(this, 'utxos_over_60pct_in_profit'), + _70pct: createMvrvNuplRealizedSupplyPattern(this, 'utxos_over_70pct_in_profit'), + _80pct: createMvrvNuplRealizedSupplyPattern(this, 'utxos_over_80pct_in_profit'), + _90pct: createMvrvNuplRealizedSupplyPattern(this, 'utxos_over_90pct_in_profit'), + _100pct: createMvrvNuplRealizedSupplyPattern(this, 'utxos_over_100pct_in_profit'), + _200pct: createMvrvNuplRealizedSupplyPattern(this, 'utxos_over_200pct_in_profit'), + _300pct: createMvrvNuplRealizedSupplyPattern(this, 'utxos_over_300pct_in_profit'), + _500pct: createMvrvNuplRealizedSupplyPattern(this, 'utxos_over_500pct_in_profit'), }, loss: { - breakeven: createRealizedSupplyPattern(this, 'utxos_in_loss'), - _10pct: createRealizedSupplyPattern(this, 'utxos_over_10pct_in_loss'), - _20pct: createRealizedSupplyPattern(this, 'utxos_over_20pct_in_loss'), - _30pct: createRealizedSupplyPattern(this, 'utxos_over_30pct_in_loss'), - _40pct: createRealizedSupplyPattern(this, 'utxos_over_40pct_in_loss'), - _50pct: createRealizedSupplyPattern(this, 'utxos_over_50pct_in_loss'), - _60pct: createRealizedSupplyPattern(this, 'utxos_over_60pct_in_loss'), - _70pct: createRealizedSupplyPattern(this, 'utxos_over_70pct_in_loss'), - _80pct: createRealizedSupplyPattern(this, 'utxos_over_80pct_in_loss'), + breakeven: createMvrvNuplRealizedSupplyPattern(this, 'utxos_in_loss'), + _10pct: createMvrvNuplRealizedSupplyPattern(this, 'utxos_over_10pct_in_loss'), + _20pct: createMvrvNuplRealizedSupplyPattern(this, 'utxos_over_20pct_in_loss'), + _30pct: createMvrvNuplRealizedSupplyPattern(this, 'utxos_over_30pct_in_loss'), + _40pct: createMvrvNuplRealizedSupplyPattern(this, 'utxos_over_40pct_in_loss'), + _50pct: createMvrvNuplRealizedSupplyPattern(this, 'utxos_over_50pct_in_loss'), + _60pct: createMvrvNuplRealizedSupplyPattern(this, 'utxos_over_60pct_in_loss'), + _70pct: createMvrvNuplRealizedSupplyPattern(this, 'utxos_over_70pct_in_loss'), + _80pct: createMvrvNuplRealizedSupplyPattern(this, 'utxos_over_80pct_in_loss'), }, }, matured: { - under1h: createBtcCentsSatsUsdPattern(this, 'utxo_under_1h_old_matured'), - _1hTo1d: createBtcCentsSatsUsdPattern(this, 'utxo_1h_to_1d_old_matured'), - _1dTo1w: createBtcCentsSatsUsdPattern(this, 'utxo_1d_to_1w_old_matured'), - _1wTo1m: createBtcCentsSatsUsdPattern(this, 'utxo_1w_to_1m_old_matured'), - _1mTo2m: createBtcCentsSatsUsdPattern(this, 'utxo_1m_to_2m_old_matured'), - _2mTo3m: createBtcCentsSatsUsdPattern(this, 'utxo_2m_to_3m_old_matured'), - _3mTo4m: createBtcCentsSatsUsdPattern(this, 'utxo_3m_to_4m_old_matured'), - _4mTo5m: createBtcCentsSatsUsdPattern(this, 'utxo_4m_to_5m_old_matured'), - _5mTo6m: createBtcCentsSatsUsdPattern(this, 'utxo_5m_to_6m_old_matured'), - _6mTo1y: createBtcCentsSatsUsdPattern(this, 'utxo_6m_to_1y_old_matured'), - _1yTo2y: createBtcCentsSatsUsdPattern(this, 'utxo_1y_to_2y_old_matured'), - _2yTo3y: createBtcCentsSatsUsdPattern(this, 'utxo_2y_to_3y_old_matured'), - _3yTo4y: createBtcCentsSatsUsdPattern(this, 'utxo_3y_to_4y_old_matured'), - _4yTo5y: createBtcCentsSatsUsdPattern(this, 'utxo_4y_to_5y_old_matured'), - _5yTo6y: createBtcCentsSatsUsdPattern(this, 'utxo_5y_to_6y_old_matured'), - _6yTo7y: createBtcCentsSatsUsdPattern(this, 'utxo_6y_to_7y_old_matured'), - _7yTo8y: createBtcCentsSatsUsdPattern(this, 'utxo_7y_to_8y_old_matured'), - _8yTo10y: createBtcCentsSatsUsdPattern(this, 'utxo_8y_to_10y_old_matured'), - _10yTo12y: createBtcCentsSatsUsdPattern(this, 'utxo_10y_to_12y_old_matured'), - _12yTo15y: createBtcCentsSatsUsdPattern(this, 'utxo_12y_to_15y_old_matured'), - over15y: createBtcCentsSatsUsdPattern(this, 'utxo_over_15y_old_matured'), + under1h: createBaseCumulativeSumPattern4(this, 'utxos_under_1h_old_matured_supply'), + _1hTo1d: createBaseCumulativeSumPattern4(this, 'utxos_1h_to_1d_old_matured_supply'), + _1dTo1w: createBaseCumulativeSumPattern4(this, 'utxos_1d_to_1w_old_matured_supply'), + _1wTo1m: createBaseCumulativeSumPattern4(this, 'utxos_1w_to_1m_old_matured_supply'), + _1mTo2m: createBaseCumulativeSumPattern4(this, 'utxos_1m_to_2m_old_matured_supply'), + _2mTo3m: createBaseCumulativeSumPattern4(this, 'utxos_2m_to_3m_old_matured_supply'), + _3mTo4m: createBaseCumulativeSumPattern4(this, 'utxos_3m_to_4m_old_matured_supply'), + _4mTo5m: createBaseCumulativeSumPattern4(this, 'utxos_4m_to_5m_old_matured_supply'), + _5mTo6m: createBaseCumulativeSumPattern4(this, 'utxos_5m_to_6m_old_matured_supply'), + _6mTo1y: createBaseCumulativeSumPattern4(this, 'utxos_6m_to_1y_old_matured_supply'), + _1yTo2y: createBaseCumulativeSumPattern4(this, 'utxos_1y_to_2y_old_matured_supply'), + _2yTo3y: createBaseCumulativeSumPattern4(this, 'utxos_2y_to_3y_old_matured_supply'), + _3yTo4y: createBaseCumulativeSumPattern4(this, 'utxos_3y_to_4y_old_matured_supply'), + _4yTo5y: createBaseCumulativeSumPattern4(this, 'utxos_4y_to_5y_old_matured_supply'), + _5yTo6y: createBaseCumulativeSumPattern4(this, 'utxos_5y_to_6y_old_matured_supply'), + _6yTo7y: createBaseCumulativeSumPattern4(this, 'utxos_6y_to_7y_old_matured_supply'), + _7yTo8y: createBaseCumulativeSumPattern4(this, 'utxos_7y_to_8y_old_matured_supply'), + _8yTo10y: createBaseCumulativeSumPattern4(this, 'utxos_8y_to_10y_old_matured_supply'), + _10yTo12y: createBaseCumulativeSumPattern4(this, 'utxos_10y_to_12y_old_matured_supply'), + _12yTo15y: createBaseCumulativeSumPattern4(this, 'utxos_12y_to_15y_old_matured_supply'), + over15y: createBaseCumulativeSumPattern4(this, 'utxos_over_15y_old_matured_supply'), }, }, address: { diff --git a/packages/brk_client/brk_client/__init__.py b/packages/brk_client/brk_client/__init__.py index 0213b27f2..601dd1cdb 100644 --- a/packages/brk_client/brk_client/__init__.py +++ b/packages/brk_client/brk_client/__init__.py @@ -2585,6 +2585,17 @@ class BpsCentsRatioSatsUsdPattern: self.sats: MetricPattern1[SatsFract] = MetricPattern1(client, _m(acc, 'sats')) self.usd: MetricPattern1[Dollars] = MetricPattern1(client, acc) +class BtcCentsDeltaSatsUsdPattern: + """Pattern struct for repeated tree structure.""" + + def __init__(self, client: BrkClientBase, acc: str): + """Create pattern node with accumulated metric name.""" + self.btc: MetricPattern1[Bitcoin] = MetricPattern1(client, acc) + self.cents: MetricPattern1[Cents] = MetricPattern1(client, _m(acc, 'cents')) + self.delta: AbsoluteRatePattern = AbsoluteRatePattern(client, _m(acc, 'delta')) + self.sats: MetricPattern1[Sats] = MetricPattern1(client, _m(acc, 'sats')) + self.usd: MetricPattern1[Dollars] = MetricPattern1(client, _m(acc, 'usd')) + class BtcCentsRelSatsUsdPattern: """Pattern struct for repeated tree structure.""" @@ -2640,6 +2651,17 @@ class InvestedMaxMinPercentilesSupplyPattern: self.percentiles: Pct05Pct10Pct15Pct20Pct25Pct30Pct35Pct40Pct45Pct50Pct55Pct60Pct65Pct70Pct75Pct80Pct85Pct90Pct95Pattern = Pct05Pct10Pct15Pct20Pct25Pct30Pct35Pct40Pct45Pct50Pct55Pct60Pct65Pct70Pct75Pct80Pct85Pct90Pct95Pattern(client, _m(acc, 'cost_basis')) self.supply_density: BpsPercentRatioPattern3 = BpsPercentRatioPattern3(client, _m(acc, 'supply_density')) +class MvrvNuplRealizedSupplyPattern: + """Pattern struct for repeated tree structure.""" + + def __init__(self, client: BrkClientBase, acc: str): + """Create pattern node with accumulated metric name.""" + self.mvrv: MetricPattern1[StoredF32] = MetricPattern1(client, _m(acc, 'mvrv')) + self.nupl: BpsRatioPattern = BpsRatioPattern(client, _m(acc, 'nupl')) + self.realized_cap: AllSthPattern = AllSthPattern(client, acc) + self.realized_price: BpsCentsRatioSatsUsdPattern = BpsCentsRatioSatsUsdPattern(client, _m(acc, 'realized_price')) + self.supply: AllSthPattern2 = AllSthPattern2(client, acc) + class PhsReboundThsPattern: """Pattern struct for repeated tree structure.""" @@ -2992,7 +3014,7 @@ class LossNuplProfitPattern: def __init__(self, client: BrkClientBase, acc: str): """Create pattern node with accumulated metric name.""" - self.loss: BaseCumulativeSumPattern3 = BaseCumulativeSumPattern3(client, _m(acc, 'unrealized_loss')) + self.loss: BaseCumulativeNegativeSumPattern = BaseCumulativeNegativeSumPattern(client, acc) self.nupl: BpsRatioPattern = BpsRatioPattern(client, _m(acc, 'nupl')) self.profit: BaseCumulativeSumPattern3 = BaseCumulativeSumPattern3(client, _m(acc, 'unrealized_profit')) @@ -3057,6 +3079,22 @@ class AbsoluteRatePattern2: self.absolute: _1m1w1y24hPattern3 = _1m1w1y24hPattern3(client, acc) self.rate: _1m1w1y24hPattern2 = _1m1w1y24hPattern2(client, acc) +class AllSthPattern2: + """Pattern struct for repeated tree structure.""" + + def __init__(self, client: BrkClientBase, acc: str): + """Create pattern node with accumulated metric name.""" + self.all: BtcCentsDeltaSatsUsdPattern = BtcCentsDeltaSatsUsdPattern(client, _m(acc, 'supply')) + self.sth: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, _m(acc, 'sth_supply')) + +class AllSthPattern: + """Pattern struct for repeated tree structure.""" + + def __init__(self, client: BrkClientBase, acc: str): + """Create pattern node with accumulated metric name.""" + self.all: MetricPattern1[Dollars] = MetricPattern1(client, _m(acc, 'realized_cap')) + self.sth: MetricPattern1[Dollars] = MetricPattern1(client, _m(acc, 'sth_realized_cap')) + class BlocksDominancePattern: """Pattern struct for repeated tree structure.""" @@ -3129,14 +3167,6 @@ class PriceValuePattern: self.price: CentsSatsUsdPattern = CentsSatsUsdPattern(client, _m(acc, 'p3sd_4y')) self.value: MetricPattern1[StoredF32] = MetricPattern1(client, _m(acc, 'ratio_p3sd_4y')) -class RealizedSupplyPattern: - """Pattern struct for repeated tree structure.""" - - def __init__(self, client: BrkClientBase, acc: str): - """Create pattern node with accumulated metric name.""" - self.realized_cap: MetricPattern1[Dollars] = MetricPattern1(client, _m(acc, 'realized_cap')) - self.supply: MetricPattern1[Sats] = MetricPattern1(client, _m(acc, 'supply')) - class RelPattern: """Pattern struct for repeated tree structure.""" @@ -3647,18 +3677,11 @@ class MetricsTree_Scripts_Count: self.unknown_output: BaseCumulativeSumPattern[StoredU64] = BaseCumulativeSumPattern(client, 'unknown_output_count') self.segwit: BaseCumulativeSumPattern[StoredU64] = BaseCumulativeSumPattern(client, 'segwit_count') -class MetricsTree_Scripts_Value_OpReturn: - """Metrics tree node.""" - - def __init__(self, client: BrkClientBase, base_path: str = ''): - self.base: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'op_return_value') - self.cumulative: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'op_return_value_cumulative') - class MetricsTree_Scripts_Value: """Metrics tree node.""" def __init__(self, client: BrkClientBase, base_path: str = ''): - self.op_return: MetricsTree_Scripts_Value_OpReturn = MetricsTree_Scripts_Value_OpReturn(client) + self.op_return: BaseCumulativeSumPattern4 = BaseCumulativeSumPattern4(client, 'op_return_value') class MetricsTree_Scripts_Adoption: """Metrics tree node.""" @@ -4772,7 +4795,6 @@ class MetricsTree_Supply_Burned: """Metrics tree node.""" def __init__(self, client: BrkClientBase, base_path: str = ''): - self.op_return: BaseCumulativeSumPattern4 = BaseCumulativeSumPattern4(client, 'op_return_supply') self.unspendable: BaseCumulativeSumPattern4 = BaseCumulativeSumPattern4(client, 'unspendable_supply') class MetricsTree_Supply_Velocity: @@ -5085,64 +5107,64 @@ class MetricsTree_Cohorts_Utxo_Profitability_Range: """Metrics tree node.""" def __init__(self, client: BrkClientBase, base_path: str = ''): - self.over_1000pct_in_profit: RealizedSupplyPattern = RealizedSupplyPattern(client, 'utxos_over_1000pct_in_profit') - self._500pct_to_1000pct_in_profit: RealizedSupplyPattern = RealizedSupplyPattern(client, 'utxos_500pct_to_1000pct_in_profit') - self._300pct_to_500pct_in_profit: RealizedSupplyPattern = RealizedSupplyPattern(client, 'utxos_300pct_to_500pct_in_profit') - self._200pct_to_300pct_in_profit: RealizedSupplyPattern = RealizedSupplyPattern(client, 'utxos_200pct_to_300pct_in_profit') - self._100pct_to_200pct_in_profit: RealizedSupplyPattern = RealizedSupplyPattern(client, 'utxos_100pct_to_200pct_in_profit') - self._90pct_to_100pct_in_profit: RealizedSupplyPattern = RealizedSupplyPattern(client, 'utxos_90pct_to_100pct_in_profit') - self._80pct_to_90pct_in_profit: RealizedSupplyPattern = RealizedSupplyPattern(client, 'utxos_80pct_to_90pct_in_profit') - self._70pct_to_80pct_in_profit: RealizedSupplyPattern = RealizedSupplyPattern(client, 'utxos_70pct_to_80pct_in_profit') - self._60pct_to_70pct_in_profit: RealizedSupplyPattern = RealizedSupplyPattern(client, 'utxos_60pct_to_70pct_in_profit') - self._50pct_to_60pct_in_profit: RealizedSupplyPattern = RealizedSupplyPattern(client, 'utxos_50pct_to_60pct_in_profit') - self._40pct_to_50pct_in_profit: RealizedSupplyPattern = RealizedSupplyPattern(client, 'utxos_40pct_to_50pct_in_profit') - self._30pct_to_40pct_in_profit: RealizedSupplyPattern = RealizedSupplyPattern(client, 'utxos_30pct_to_40pct_in_profit') - self._20pct_to_30pct_in_profit: RealizedSupplyPattern = RealizedSupplyPattern(client, 'utxos_20pct_to_30pct_in_profit') - self._10pct_to_20pct_in_profit: RealizedSupplyPattern = RealizedSupplyPattern(client, 'utxos_10pct_to_20pct_in_profit') - self._0pct_to_10pct_in_profit: RealizedSupplyPattern = RealizedSupplyPattern(client, 'utxos_0pct_to_10pct_in_profit') - self._0pct_to_10pct_in_loss: RealizedSupplyPattern = RealizedSupplyPattern(client, 'utxos_0pct_to_10pct_in_loss') - self._10pct_to_20pct_in_loss: RealizedSupplyPattern = RealizedSupplyPattern(client, 'utxos_10pct_to_20pct_in_loss') - self._20pct_to_30pct_in_loss: RealizedSupplyPattern = RealizedSupplyPattern(client, 'utxos_20pct_to_30pct_in_loss') - self._30pct_to_40pct_in_loss: RealizedSupplyPattern = RealizedSupplyPattern(client, 'utxos_30pct_to_40pct_in_loss') - self._40pct_to_50pct_in_loss: RealizedSupplyPattern = RealizedSupplyPattern(client, 'utxos_40pct_to_50pct_in_loss') - self._50pct_to_60pct_in_loss: RealizedSupplyPattern = RealizedSupplyPattern(client, 'utxos_50pct_to_60pct_in_loss') - self._60pct_to_70pct_in_loss: RealizedSupplyPattern = RealizedSupplyPattern(client, 'utxos_60pct_to_70pct_in_loss') - self._70pct_to_80pct_in_loss: RealizedSupplyPattern = RealizedSupplyPattern(client, 'utxos_70pct_to_80pct_in_loss') - self._80pct_to_90pct_in_loss: RealizedSupplyPattern = RealizedSupplyPattern(client, 'utxos_80pct_to_90pct_in_loss') - self._90pct_to_100pct_in_loss: RealizedSupplyPattern = RealizedSupplyPattern(client, 'utxos_90pct_to_100pct_in_loss') + self.over_1000pct_in_profit: MvrvNuplRealizedSupplyPattern = MvrvNuplRealizedSupplyPattern(client, 'utxos_over_1000pct_in_profit') + self._500pct_to_1000pct_in_profit: MvrvNuplRealizedSupplyPattern = MvrvNuplRealizedSupplyPattern(client, 'utxos_500pct_to_1000pct_in_profit') + self._300pct_to_500pct_in_profit: MvrvNuplRealizedSupplyPattern = MvrvNuplRealizedSupplyPattern(client, 'utxos_300pct_to_500pct_in_profit') + self._200pct_to_300pct_in_profit: MvrvNuplRealizedSupplyPattern = MvrvNuplRealizedSupplyPattern(client, 'utxos_200pct_to_300pct_in_profit') + self._100pct_to_200pct_in_profit: MvrvNuplRealizedSupplyPattern = MvrvNuplRealizedSupplyPattern(client, 'utxos_100pct_to_200pct_in_profit') + self._90pct_to_100pct_in_profit: MvrvNuplRealizedSupplyPattern = MvrvNuplRealizedSupplyPattern(client, 'utxos_90pct_to_100pct_in_profit') + self._80pct_to_90pct_in_profit: MvrvNuplRealizedSupplyPattern = MvrvNuplRealizedSupplyPattern(client, 'utxos_80pct_to_90pct_in_profit') + self._70pct_to_80pct_in_profit: MvrvNuplRealizedSupplyPattern = MvrvNuplRealizedSupplyPattern(client, 'utxos_70pct_to_80pct_in_profit') + self._60pct_to_70pct_in_profit: MvrvNuplRealizedSupplyPattern = MvrvNuplRealizedSupplyPattern(client, 'utxos_60pct_to_70pct_in_profit') + self._50pct_to_60pct_in_profit: MvrvNuplRealizedSupplyPattern = MvrvNuplRealizedSupplyPattern(client, 'utxos_50pct_to_60pct_in_profit') + self._40pct_to_50pct_in_profit: MvrvNuplRealizedSupplyPattern = MvrvNuplRealizedSupplyPattern(client, 'utxos_40pct_to_50pct_in_profit') + self._30pct_to_40pct_in_profit: MvrvNuplRealizedSupplyPattern = MvrvNuplRealizedSupplyPattern(client, 'utxos_30pct_to_40pct_in_profit') + self._20pct_to_30pct_in_profit: MvrvNuplRealizedSupplyPattern = MvrvNuplRealizedSupplyPattern(client, 'utxos_20pct_to_30pct_in_profit') + self._10pct_to_20pct_in_profit: MvrvNuplRealizedSupplyPattern = MvrvNuplRealizedSupplyPattern(client, 'utxos_10pct_to_20pct_in_profit') + self._0pct_to_10pct_in_profit: MvrvNuplRealizedSupplyPattern = MvrvNuplRealizedSupplyPattern(client, 'utxos_0pct_to_10pct_in_profit') + self._0pct_to_10pct_in_loss: MvrvNuplRealizedSupplyPattern = MvrvNuplRealizedSupplyPattern(client, 'utxos_0pct_to_10pct_in_loss') + self._10pct_to_20pct_in_loss: MvrvNuplRealizedSupplyPattern = MvrvNuplRealizedSupplyPattern(client, 'utxos_10pct_to_20pct_in_loss') + self._20pct_to_30pct_in_loss: MvrvNuplRealizedSupplyPattern = MvrvNuplRealizedSupplyPattern(client, 'utxos_20pct_to_30pct_in_loss') + self._30pct_to_40pct_in_loss: MvrvNuplRealizedSupplyPattern = MvrvNuplRealizedSupplyPattern(client, 'utxos_30pct_to_40pct_in_loss') + self._40pct_to_50pct_in_loss: MvrvNuplRealizedSupplyPattern = MvrvNuplRealizedSupplyPattern(client, 'utxos_40pct_to_50pct_in_loss') + self._50pct_to_60pct_in_loss: MvrvNuplRealizedSupplyPattern = MvrvNuplRealizedSupplyPattern(client, 'utxos_50pct_to_60pct_in_loss') + self._60pct_to_70pct_in_loss: MvrvNuplRealizedSupplyPattern = MvrvNuplRealizedSupplyPattern(client, 'utxos_60pct_to_70pct_in_loss') + self._70pct_to_80pct_in_loss: MvrvNuplRealizedSupplyPattern = MvrvNuplRealizedSupplyPattern(client, 'utxos_70pct_to_80pct_in_loss') + self._80pct_to_90pct_in_loss: MvrvNuplRealizedSupplyPattern = MvrvNuplRealizedSupplyPattern(client, 'utxos_80pct_to_90pct_in_loss') + self._90pct_to_100pct_in_loss: MvrvNuplRealizedSupplyPattern = MvrvNuplRealizedSupplyPattern(client, 'utxos_90pct_to_100pct_in_loss') class MetricsTree_Cohorts_Utxo_Profitability_Profit: """Metrics tree node.""" def __init__(self, client: BrkClientBase, base_path: str = ''): - self.breakeven: RealizedSupplyPattern = RealizedSupplyPattern(client, 'utxos_in_profit') - self._10pct: RealizedSupplyPattern = RealizedSupplyPattern(client, 'utxos_over_10pct_in_profit') - self._20pct: RealizedSupplyPattern = RealizedSupplyPattern(client, 'utxos_over_20pct_in_profit') - self._30pct: RealizedSupplyPattern = RealizedSupplyPattern(client, 'utxos_over_30pct_in_profit') - self._40pct: RealizedSupplyPattern = RealizedSupplyPattern(client, 'utxos_over_40pct_in_profit') - self._50pct: RealizedSupplyPattern = RealizedSupplyPattern(client, 'utxos_over_50pct_in_profit') - self._60pct: RealizedSupplyPattern = RealizedSupplyPattern(client, 'utxos_over_60pct_in_profit') - self._70pct: RealizedSupplyPattern = RealizedSupplyPattern(client, 'utxos_over_70pct_in_profit') - self._80pct: RealizedSupplyPattern = RealizedSupplyPattern(client, 'utxos_over_80pct_in_profit') - self._90pct: RealizedSupplyPattern = RealizedSupplyPattern(client, 'utxos_over_90pct_in_profit') - self._100pct: RealizedSupplyPattern = RealizedSupplyPattern(client, 'utxos_over_100pct_in_profit') - self._200pct: RealizedSupplyPattern = RealizedSupplyPattern(client, 'utxos_over_200pct_in_profit') - self._300pct: RealizedSupplyPattern = RealizedSupplyPattern(client, 'utxos_over_300pct_in_profit') - self._500pct: RealizedSupplyPattern = RealizedSupplyPattern(client, 'utxos_over_500pct_in_profit') + self.breakeven: MvrvNuplRealizedSupplyPattern = MvrvNuplRealizedSupplyPattern(client, 'utxos_in_profit') + self._10pct: MvrvNuplRealizedSupplyPattern = MvrvNuplRealizedSupplyPattern(client, 'utxos_over_10pct_in_profit') + self._20pct: MvrvNuplRealizedSupplyPattern = MvrvNuplRealizedSupplyPattern(client, 'utxos_over_20pct_in_profit') + self._30pct: MvrvNuplRealizedSupplyPattern = MvrvNuplRealizedSupplyPattern(client, 'utxos_over_30pct_in_profit') + self._40pct: MvrvNuplRealizedSupplyPattern = MvrvNuplRealizedSupplyPattern(client, 'utxos_over_40pct_in_profit') + self._50pct: MvrvNuplRealizedSupplyPattern = MvrvNuplRealizedSupplyPattern(client, 'utxos_over_50pct_in_profit') + self._60pct: MvrvNuplRealizedSupplyPattern = MvrvNuplRealizedSupplyPattern(client, 'utxos_over_60pct_in_profit') + self._70pct: MvrvNuplRealizedSupplyPattern = MvrvNuplRealizedSupplyPattern(client, 'utxos_over_70pct_in_profit') + self._80pct: MvrvNuplRealizedSupplyPattern = MvrvNuplRealizedSupplyPattern(client, 'utxos_over_80pct_in_profit') + self._90pct: MvrvNuplRealizedSupplyPattern = MvrvNuplRealizedSupplyPattern(client, 'utxos_over_90pct_in_profit') + self._100pct: MvrvNuplRealizedSupplyPattern = MvrvNuplRealizedSupplyPattern(client, 'utxos_over_100pct_in_profit') + self._200pct: MvrvNuplRealizedSupplyPattern = MvrvNuplRealizedSupplyPattern(client, 'utxos_over_200pct_in_profit') + self._300pct: MvrvNuplRealizedSupplyPattern = MvrvNuplRealizedSupplyPattern(client, 'utxos_over_300pct_in_profit') + self._500pct: MvrvNuplRealizedSupplyPattern = MvrvNuplRealizedSupplyPattern(client, 'utxos_over_500pct_in_profit') class MetricsTree_Cohorts_Utxo_Profitability_Loss: """Metrics tree node.""" def __init__(self, client: BrkClientBase, base_path: str = ''): - self.breakeven: RealizedSupplyPattern = RealizedSupplyPattern(client, 'utxos_in_loss') - self._10pct: RealizedSupplyPattern = RealizedSupplyPattern(client, 'utxos_over_10pct_in_loss') - self._20pct: RealizedSupplyPattern = RealizedSupplyPattern(client, 'utxos_over_20pct_in_loss') - self._30pct: RealizedSupplyPattern = RealizedSupplyPattern(client, 'utxos_over_30pct_in_loss') - self._40pct: RealizedSupplyPattern = RealizedSupplyPattern(client, 'utxos_over_40pct_in_loss') - self._50pct: RealizedSupplyPattern = RealizedSupplyPattern(client, 'utxos_over_50pct_in_loss') - self._60pct: RealizedSupplyPattern = RealizedSupplyPattern(client, 'utxos_over_60pct_in_loss') - self._70pct: RealizedSupplyPattern = RealizedSupplyPattern(client, 'utxos_over_70pct_in_loss') - self._80pct: RealizedSupplyPattern = RealizedSupplyPattern(client, 'utxos_over_80pct_in_loss') + self.breakeven: MvrvNuplRealizedSupplyPattern = MvrvNuplRealizedSupplyPattern(client, 'utxos_in_loss') + self._10pct: MvrvNuplRealizedSupplyPattern = MvrvNuplRealizedSupplyPattern(client, 'utxos_over_10pct_in_loss') + self._20pct: MvrvNuplRealizedSupplyPattern = MvrvNuplRealizedSupplyPattern(client, 'utxos_over_20pct_in_loss') + self._30pct: MvrvNuplRealizedSupplyPattern = MvrvNuplRealizedSupplyPattern(client, 'utxos_over_30pct_in_loss') + self._40pct: MvrvNuplRealizedSupplyPattern = MvrvNuplRealizedSupplyPattern(client, 'utxos_over_40pct_in_loss') + self._50pct: MvrvNuplRealizedSupplyPattern = MvrvNuplRealizedSupplyPattern(client, 'utxos_over_50pct_in_loss') + self._60pct: MvrvNuplRealizedSupplyPattern = MvrvNuplRealizedSupplyPattern(client, 'utxos_over_60pct_in_loss') + self._70pct: MvrvNuplRealizedSupplyPattern = MvrvNuplRealizedSupplyPattern(client, 'utxos_over_70pct_in_loss') + self._80pct: MvrvNuplRealizedSupplyPattern = MvrvNuplRealizedSupplyPattern(client, 'utxos_over_80pct_in_loss') class MetricsTree_Cohorts_Utxo_Profitability: """Metrics tree node.""" @@ -5156,27 +5178,27 @@ class MetricsTree_Cohorts_Utxo_Matured: """Metrics tree node.""" def __init__(self, client: BrkClientBase, base_path: str = ''): - self.under_1h: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'utxo_under_1h_old_matured') - self._1h_to_1d: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'utxo_1h_to_1d_old_matured') - self._1d_to_1w: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'utxo_1d_to_1w_old_matured') - self._1w_to_1m: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'utxo_1w_to_1m_old_matured') - self._1m_to_2m: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'utxo_1m_to_2m_old_matured') - self._2m_to_3m: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'utxo_2m_to_3m_old_matured') - self._3m_to_4m: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'utxo_3m_to_4m_old_matured') - self._4m_to_5m: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'utxo_4m_to_5m_old_matured') - self._5m_to_6m: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'utxo_5m_to_6m_old_matured') - self._6m_to_1y: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'utxo_6m_to_1y_old_matured') - self._1y_to_2y: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'utxo_1y_to_2y_old_matured') - self._2y_to_3y: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'utxo_2y_to_3y_old_matured') - self._3y_to_4y: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'utxo_3y_to_4y_old_matured') - self._4y_to_5y: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'utxo_4y_to_5y_old_matured') - self._5y_to_6y: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'utxo_5y_to_6y_old_matured') - self._6y_to_7y: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'utxo_6y_to_7y_old_matured') - self._7y_to_8y: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'utxo_7y_to_8y_old_matured') - self._8y_to_10y: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'utxo_8y_to_10y_old_matured') - self._10y_to_12y: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'utxo_10y_to_12y_old_matured') - self._12y_to_15y: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'utxo_12y_to_15y_old_matured') - self.over_15y: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'utxo_over_15y_old_matured') + self.under_1h: BaseCumulativeSumPattern4 = BaseCumulativeSumPattern4(client, 'utxos_under_1h_old_matured_supply') + self._1h_to_1d: BaseCumulativeSumPattern4 = BaseCumulativeSumPattern4(client, 'utxos_1h_to_1d_old_matured_supply') + self._1d_to_1w: BaseCumulativeSumPattern4 = BaseCumulativeSumPattern4(client, 'utxos_1d_to_1w_old_matured_supply') + self._1w_to_1m: BaseCumulativeSumPattern4 = BaseCumulativeSumPattern4(client, 'utxos_1w_to_1m_old_matured_supply') + self._1m_to_2m: BaseCumulativeSumPattern4 = BaseCumulativeSumPattern4(client, 'utxos_1m_to_2m_old_matured_supply') + self._2m_to_3m: BaseCumulativeSumPattern4 = BaseCumulativeSumPattern4(client, 'utxos_2m_to_3m_old_matured_supply') + self._3m_to_4m: BaseCumulativeSumPattern4 = BaseCumulativeSumPattern4(client, 'utxos_3m_to_4m_old_matured_supply') + self._4m_to_5m: BaseCumulativeSumPattern4 = BaseCumulativeSumPattern4(client, 'utxos_4m_to_5m_old_matured_supply') + self._5m_to_6m: BaseCumulativeSumPattern4 = BaseCumulativeSumPattern4(client, 'utxos_5m_to_6m_old_matured_supply') + self._6m_to_1y: BaseCumulativeSumPattern4 = BaseCumulativeSumPattern4(client, 'utxos_6m_to_1y_old_matured_supply') + self._1y_to_2y: BaseCumulativeSumPattern4 = BaseCumulativeSumPattern4(client, 'utxos_1y_to_2y_old_matured_supply') + self._2y_to_3y: BaseCumulativeSumPattern4 = BaseCumulativeSumPattern4(client, 'utxos_2y_to_3y_old_matured_supply') + self._3y_to_4y: BaseCumulativeSumPattern4 = BaseCumulativeSumPattern4(client, 'utxos_3y_to_4y_old_matured_supply') + self._4y_to_5y: BaseCumulativeSumPattern4 = BaseCumulativeSumPattern4(client, 'utxos_4y_to_5y_old_matured_supply') + self._5y_to_6y: BaseCumulativeSumPattern4 = BaseCumulativeSumPattern4(client, 'utxos_5y_to_6y_old_matured_supply') + self._6y_to_7y: BaseCumulativeSumPattern4 = BaseCumulativeSumPattern4(client, 'utxos_6y_to_7y_old_matured_supply') + self._7y_to_8y: BaseCumulativeSumPattern4 = BaseCumulativeSumPattern4(client, 'utxos_7y_to_8y_old_matured_supply') + self._8y_to_10y: BaseCumulativeSumPattern4 = BaseCumulativeSumPattern4(client, 'utxos_8y_to_10y_old_matured_supply') + self._10y_to_12y: BaseCumulativeSumPattern4 = BaseCumulativeSumPattern4(client, 'utxos_10y_to_12y_old_matured_supply') + self._12y_to_15y: BaseCumulativeSumPattern4 = BaseCumulativeSumPattern4(client, 'utxos_12y_to_15y_old_matured_supply') + self.over_15y: BaseCumulativeSumPattern4 = BaseCumulativeSumPattern4(client, 'utxos_over_15y_old_matured_supply') class MetricsTree_Cohorts_Utxo: """Metrics tree node.""" @@ -6195,6 +6217,255 @@ class BrkClient(BrkClientBase): } } + PROFITABILITY_RANGE_NAMES = { + "over_1000pct_in_profit": { + "id": "utxos_over_1000pct_in_profit", + "short": ">1000%", + "long": "Over 1000% Profit" + }, + "_500pct_to_1000pct_in_profit": { + "id": "utxos_500pct_to_1000pct_in_profit", + "short": "500-1000%", + "long": "500-1000% Profit" + }, + "_300pct_to_500pct_in_profit": { + "id": "utxos_300pct_to_500pct_in_profit", + "short": "300-500%", + "long": "300-500% Profit" + }, + "_200pct_to_300pct_in_profit": { + "id": "utxos_200pct_to_300pct_in_profit", + "short": "200-300%", + "long": "200-300% Profit" + }, + "_100pct_to_200pct_in_profit": { + "id": "utxos_100pct_to_200pct_in_profit", + "short": "100-200%", + "long": "100-200% Profit" + }, + "_90pct_to_100pct_in_profit": { + "id": "utxos_90pct_to_100pct_in_profit", + "short": "90-100%", + "long": "90-100% Profit" + }, + "_80pct_to_90pct_in_profit": { + "id": "utxos_80pct_to_90pct_in_profit", + "short": "80-90%", + "long": "80-90% Profit" + }, + "_70pct_to_80pct_in_profit": { + "id": "utxos_70pct_to_80pct_in_profit", + "short": "70-80%", + "long": "70-80% Profit" + }, + "_60pct_to_70pct_in_profit": { + "id": "utxos_60pct_to_70pct_in_profit", + "short": "60-70%", + "long": "60-70% Profit" + }, + "_50pct_to_60pct_in_profit": { + "id": "utxos_50pct_to_60pct_in_profit", + "short": "50-60%", + "long": "50-60% Profit" + }, + "_40pct_to_50pct_in_profit": { + "id": "utxos_40pct_to_50pct_in_profit", + "short": "40-50%", + "long": "40-50% Profit" + }, + "_30pct_to_40pct_in_profit": { + "id": "utxos_30pct_to_40pct_in_profit", + "short": "30-40%", + "long": "30-40% Profit" + }, + "_20pct_to_30pct_in_profit": { + "id": "utxos_20pct_to_30pct_in_profit", + "short": "20-30%", + "long": "20-30% Profit" + }, + "_10pct_to_20pct_in_profit": { + "id": "utxos_10pct_to_20pct_in_profit", + "short": "10-20%", + "long": "10-20% Profit" + }, + "_0pct_to_10pct_in_profit": { + "id": "utxos_0pct_to_10pct_in_profit", + "short": "0-10%", + "long": "0-10% Profit" + }, + "_0pct_to_10pct_in_loss": { + "id": "utxos_0pct_to_10pct_in_loss", + "short": "0-10%L", + "long": "0-10% Loss" + }, + "_10pct_to_20pct_in_loss": { + "id": "utxos_10pct_to_20pct_in_loss", + "short": "10-20%L", + "long": "10-20% Loss" + }, + "_20pct_to_30pct_in_loss": { + "id": "utxos_20pct_to_30pct_in_loss", + "short": "20-30%L", + "long": "20-30% Loss" + }, + "_30pct_to_40pct_in_loss": { + "id": "utxos_30pct_to_40pct_in_loss", + "short": "30-40%L", + "long": "30-40% Loss" + }, + "_40pct_to_50pct_in_loss": { + "id": "utxos_40pct_to_50pct_in_loss", + "short": "40-50%L", + "long": "40-50% Loss" + }, + "_50pct_to_60pct_in_loss": { + "id": "utxos_50pct_to_60pct_in_loss", + "short": "50-60%L", + "long": "50-60% Loss" + }, + "_60pct_to_70pct_in_loss": { + "id": "utxos_60pct_to_70pct_in_loss", + "short": "60-70%L", + "long": "60-70% Loss" + }, + "_70pct_to_80pct_in_loss": { + "id": "utxos_70pct_to_80pct_in_loss", + "short": "70-80%L", + "long": "70-80% Loss" + }, + "_80pct_to_90pct_in_loss": { + "id": "utxos_80pct_to_90pct_in_loss", + "short": "80-90%L", + "long": "80-90% Loss" + }, + "_90pct_to_100pct_in_loss": { + "id": "utxos_90pct_to_100pct_in_loss", + "short": "90-100%L", + "long": "90-100% Loss" + } + } + + PROFIT_NAMES = { + "breakeven": { + "id": "utxos_in_profit", + "short": "≥0%", + "long": "In Profit (Breakeven+)" + }, + "_10pct": { + "id": "utxos_over_10pct_in_profit", + "short": "≥10%", + "long": "10%+ Profit" + }, + "_20pct": { + "id": "utxos_over_20pct_in_profit", + "short": "≥20%", + "long": "20%+ Profit" + }, + "_30pct": { + "id": "utxos_over_30pct_in_profit", + "short": "≥30%", + "long": "30%+ Profit" + }, + "_40pct": { + "id": "utxos_over_40pct_in_profit", + "short": "≥40%", + "long": "40%+ Profit" + }, + "_50pct": { + "id": "utxos_over_50pct_in_profit", + "short": "≥50%", + "long": "50%+ Profit" + }, + "_60pct": { + "id": "utxos_over_60pct_in_profit", + "short": "≥60%", + "long": "60%+ Profit" + }, + "_70pct": { + "id": "utxos_over_70pct_in_profit", + "short": "≥70%", + "long": "70%+ Profit" + }, + "_80pct": { + "id": "utxos_over_80pct_in_profit", + "short": "≥80%", + "long": "80%+ Profit" + }, + "_90pct": { + "id": "utxos_over_90pct_in_profit", + "short": "≥90%", + "long": "90%+ Profit" + }, + "_100pct": { + "id": "utxos_over_100pct_in_profit", + "short": "≥100%", + "long": "100%+ Profit" + }, + "_200pct": { + "id": "utxos_over_200pct_in_profit", + "short": "≥200%", + "long": "200%+ Profit" + }, + "_300pct": { + "id": "utxos_over_300pct_in_profit", + "short": "≥300%", + "long": "300%+ Profit" + }, + "_500pct": { + "id": "utxos_over_500pct_in_profit", + "short": "≥500%", + "long": "500%+ Profit" + } + } + + LOSS_NAMES = { + "breakeven": { + "id": "utxos_in_loss", + "short": "<0%", + "long": "In Loss (Below Breakeven)" + }, + "_10pct": { + "id": "utxos_over_10pct_in_loss", + "short": "≥10%L", + "long": "10%+ Loss" + }, + "_20pct": { + "id": "utxos_over_20pct_in_loss", + "short": "≥20%L", + "long": "20%+ Loss" + }, + "_30pct": { + "id": "utxos_over_30pct_in_loss", + "short": "≥30%L", + "long": "30%+ Loss" + }, + "_40pct": { + "id": "utxos_over_40pct_in_loss", + "short": "≥40%L", + "long": "40%+ Loss" + }, + "_50pct": { + "id": "utxos_over_50pct_in_loss", + "short": "≥50%L", + "long": "50%+ Loss" + }, + "_60pct": { + "id": "utxos_over_60pct_in_loss", + "short": "≥60%L", + "long": "60%+ Loss" + }, + "_70pct": { + "id": "utxos_over_70pct_in_loss", + "short": "≥70%L", + "long": "70%+ Loss" + }, + "_80pct": { + "id": "utxos_over_80pct_in_loss", + "short": "≥80%L", + "long": "80%+ Loss" + } + } + def __init__(self, base_url: str = 'http://localhost:3000', timeout: float = 30.0): super().__init__(base_url, timeout) self.metrics = MetricsTree(self) diff --git a/website/scripts/options/distribution/activity.js b/website/scripts/options/distribution/activity.js index cda84667e..159312ca6 100644 --- a/website/scripts/options/distribution/activity.js +++ b/website/scripts/options/distribution/activity.js @@ -77,6 +77,127 @@ function volumeAndCoinsTree(activity, color, title) { ]; } +/** + * Sent in profit/loss breakdown tree (shared by full and mid-level activity) + * @param {Brk.BaseCumulativeInSumPattern} sent + * @param {(metric: string) => string} title + * @returns {PartialOptionsTree} + */ +function sentProfitLossTree(sent, title) { + return [ + { + name: "Sent In Profit", + tree: [ + { + name: "USD", + title: title("Sent Volume In Profit"), + bottom: [ + line({ metric: sent.inProfit.base.usd, name: "Base", color: colors.profit, unit: Unit.usd }), + line({ metric: sent.inProfit.sum._24h.usd, name: "24h", color: colors.time._24h, unit: Unit.usd, defaultActive: false }), + line({ metric: sent.inProfit.sum._1w.usd, name: "1w", color: colors.time._1w, unit: Unit.usd, defaultActive: false }), + line({ metric: sent.inProfit.sum._1m.usd, name: "1m", color: colors.time._1m, unit: Unit.usd, defaultActive: false }), + line({ metric: sent.inProfit.sum._1y.usd, name: "1y", color: colors.time._1y, unit: Unit.usd, defaultActive: false }), + ], + }, + { + name: "BTC", + title: title("Sent Volume In Profit (BTC)"), + bottom: [ + line({ metric: sent.inProfit.base.btc, name: "Base", color: colors.profit, unit: Unit.btc }), + line({ metric: sent.inProfit.sum._24h.btc, name: "24h", color: colors.time._24h, unit: Unit.btc, defaultActive: false }), + line({ metric: sent.inProfit.sum._1w.btc, name: "1w", color: colors.time._1w, unit: Unit.btc, defaultActive: false }), + line({ metric: sent.inProfit.sum._1m.btc, name: "1m", color: colors.time._1m, unit: Unit.btc, defaultActive: false }), + line({ metric: sent.inProfit.sum._1y.btc, name: "1y", color: colors.time._1y, unit: Unit.btc, defaultActive: false }), + ], + }, + { + name: "Sats", + title: title("Sent Volume In Profit (Sats)"), + bottom: [ + line({ metric: sent.inProfit.base.sats, name: "Base", color: colors.profit, unit: Unit.sats }), + line({ metric: sent.inProfit.sum._24h.sats, name: "24h", color: colors.time._24h, unit: Unit.sats, defaultActive: false }), + line({ metric: sent.inProfit.sum._1w.sats, name: "1w", color: colors.time._1w, unit: Unit.sats, defaultActive: false }), + line({ metric: sent.inProfit.sum._1m.sats, name: "1m", color: colors.time._1m, unit: Unit.sats, defaultActive: false }), + line({ metric: sent.inProfit.sum._1y.sats, name: "1y", color: colors.time._1y, unit: Unit.sats, defaultActive: false }), + ], + }, + { name: "Cumulative", title: title("Cumulative Sent In Profit"), bottom: [ + line({ metric: sent.inProfit.cumulative.usd, name: "USD", color: colors.profit, unit: Unit.usd }), + line({ metric: sent.inProfit.cumulative.btc, name: "BTC", color: colors.profit, unit: Unit.btc, defaultActive: false }), + line({ metric: sent.inProfit.cumulative.sats, name: "Sats", color: colors.profit, unit: Unit.sats, defaultActive: false }), + ]}, + ], + }, + { + name: "Sent In Loss", + tree: [ + { + name: "USD", + title: title("Sent Volume In Loss"), + bottom: [ + line({ metric: sent.inLoss.base.usd, name: "Base", color: colors.loss, unit: Unit.usd }), + line({ metric: sent.inLoss.sum._24h.usd, name: "24h", color: colors.time._24h, unit: Unit.usd, defaultActive: false }), + line({ metric: sent.inLoss.sum._1w.usd, name: "1w", color: colors.time._1w, unit: Unit.usd, defaultActive: false }), + line({ metric: sent.inLoss.sum._1m.usd, name: "1m", color: colors.time._1m, unit: Unit.usd, defaultActive: false }), + line({ metric: sent.inLoss.sum._1y.usd, name: "1y", color: colors.time._1y, unit: Unit.usd, defaultActive: false }), + ], + }, + { + name: "BTC", + title: title("Sent Volume In Loss (BTC)"), + bottom: [ + line({ metric: sent.inLoss.base.btc, name: "Base", color: colors.loss, unit: Unit.btc }), + line({ metric: sent.inLoss.sum._24h.btc, name: "24h", color: colors.time._24h, unit: Unit.btc, defaultActive: false }), + line({ metric: sent.inLoss.sum._1w.btc, name: "1w", color: colors.time._1w, unit: Unit.btc, defaultActive: false }), + line({ metric: sent.inLoss.sum._1m.btc, name: "1m", color: colors.time._1m, unit: Unit.btc, defaultActive: false }), + line({ metric: sent.inLoss.sum._1y.btc, name: "1y", color: colors.time._1y, unit: Unit.btc, defaultActive: false }), + ], + }, + { + name: "Sats", + title: title("Sent Volume In Loss (Sats)"), + bottom: [ + line({ metric: sent.inLoss.base.sats, name: "Base", color: colors.loss, unit: Unit.sats }), + line({ metric: sent.inLoss.sum._24h.sats, name: "24h", color: colors.time._24h, unit: Unit.sats, defaultActive: false }), + line({ metric: sent.inLoss.sum._1w.sats, name: "1w", color: colors.time._1w, unit: Unit.sats, defaultActive: false }), + line({ metric: sent.inLoss.sum._1m.sats, name: "1m", color: colors.time._1m, unit: Unit.sats, defaultActive: false }), + line({ metric: sent.inLoss.sum._1y.sats, name: "1y", color: colors.time._1y, unit: Unit.sats, defaultActive: false }), + ], + }, + { name: "Cumulative", title: title("Cumulative Sent In Loss"), bottom: [ + line({ metric: sent.inLoss.cumulative.usd, name: "USD", color: colors.loss, unit: Unit.usd }), + line({ metric: sent.inLoss.cumulative.btc, name: "BTC", color: colors.loss, unit: Unit.btc, defaultActive: false }), + line({ metric: sent.inLoss.cumulative.sats, name: "Sats", color: colors.loss, unit: Unit.sats, defaultActive: false }), + ]}, + ], + }, + ]; +} + +/** + * Volume and coins tree with coinyears, dormancy, and sent in profit/loss (All/STH/LTH) + * @param {Brk.CoindaysCoinyearsDormancySentPattern} activity + * @param {Color} color + * @param {(metric: string) => string} title + * @returns {PartialOptionsTree} + */ +function fullVolumeTree(activity, color, title) { + return [ + ...volumeAndCoinsTree(activity, color, title), + ...sentProfitLossTree(activity.sent, title), + { + name: "Coinyears Destroyed", + title: title("Coinyears Destroyed"), + bottom: [line({ metric: activity.coinyearsDestroyed, name: "CYD", color, unit: Unit.years })], + }, + { + name: "Dormancy", + title: title("Dormancy"), + bottom: [line({ metric: activity.dormancy, name: "Dormancy", color, unit: Unit.days })], + }, + ]; +} + // ============================================================================ // Shared SOPR Helpers // ============================================================================ @@ -349,7 +470,7 @@ export function createActivitySectionWithAdjusted({ cohort, title }) { return { name: "Activity", tree: [ - ...volumeAndCoinsTree(tree.activity, color, title), + ...fullVolumeTree(tree.activity, color, title), { name: "SOPR", tree: [ @@ -400,7 +521,7 @@ export function createActivitySection({ cohort, title }) { return { name: "Activity", tree: [ - ...volumeAndCoinsTree(tree.activity, color, title), + ...fullVolumeTree(tree.activity, color, title), { name: "SOPR", tree: singleRollingSoprTree(sopr.ratio, title), @@ -430,6 +551,7 @@ export function createActivitySectionWithActivity({ cohort, title }) { name: "Activity", tree: [ ...volumeAndCoinsTree(tree.activity, color, title), + ...sentProfitLossTree(tree.activity.sent, title), { name: "SOPR", title: title("SOPR (24h)"), diff --git a/website/scripts/options/distribution/data.js b/website/scripts/options/distribution/data.js index 7650078c7..1ebdcb1ae 100644 --- a/website/scripts/options/distribution/data.js +++ b/website/scripts/options/distribution/data.js @@ -83,6 +83,7 @@ export function buildCohortData() { title: `UTXOs ${names.long}`, color: colors.at(i, arr.length), tree: utxoCohorts.ageRange[key], + matured: utxoCohorts.matured[key], })); const epoch = entries(EPOCH_NAMES).map(([key, names], i, arr) => ({ diff --git a/website/scripts/options/distribution/holdings.js b/website/scripts/options/distribution/holdings.js index d4dd2c45f..536a00e8a 100644 --- a/website/scripts/options/distribution/holdings.js +++ b/website/scripts/options/distribution/holdings.js @@ -115,6 +115,34 @@ function circulatingSupplyPctSeries(supply) { ]; } +/** + * Ratio of Circulating Supply series (total, profit, loss) + * @param {{ relToCirculating: { ratio: AnyMetricPattern }, inProfit: { relToCirculating: { ratio: AnyMetricPattern } }, inLoss: { relToCirculating: { ratio: AnyMetricPattern } } }} supply + * @returns {AnyFetchedSeriesBlueprint[]} + */ +function circulatingSupplyRatioSeries(supply) { + return [ + line({ + metric: supply.relToCirculating.ratio, + name: "Total", + color: colors.default, + unit: Unit.ratio, + }), + line({ + metric: supply.inProfit.relToCirculating.ratio, + name: "In Profit", + color: colors.profit, + unit: Unit.ratio, + }), + line({ + metric: supply.inLoss.relToCirculating.ratio, + name: "In Loss", + color: colors.loss, + unit: Unit.ratio, + }), + ]; +} + /** * @param {readonly (UtxoCohortObject | CohortWithoutRelative)[]} list * @param {CohortAll} all @@ -157,7 +185,7 @@ function singleDeltaTree(delta, unit, title, name) { * @template {{ name: string, color: Color }} A * @param {readonly T[]} list * @param {A} all - * @param {(c: T | A) => ChangeRatePattern | ChangeRatePattern2} getDelta + * @param {(c: T | A) => DeltaPattern} getDelta * @param {Unit} unit * @param {(metric: string) => string} title * @param {string} name @@ -309,11 +337,21 @@ export function createHoldingsSectionWithRelative({ cohort, title }) { tree: [ { name: "Supply", - title: title("Supply"), - bottom: [ - ...fullSupplySeries(supply), - ...circulatingSupplyPctSeries(supply), - ...ownSupplyPctSeries(supply), + tree: [ + { + name: "Overview", + title: title("Supply"), + bottom: [ + ...fullSupplySeries(supply), + ...circulatingSupplyPctSeries(supply), + ...ownSupplyPctSeries(supply), + ], + }, + { + name: "Ratio", + title: title("Supply (% of Circulating)"), + bottom: circulatingSupplyRatioSeries(supply), + }, ], }, singleUtxoCountChart(cohort, title), @@ -341,10 +379,20 @@ export function createHoldingsSectionWithOwnSupply({ cohort, title }) { tree: [ { name: "Supply", - title: title("Supply"), - bottom: [ - ...fullSupplySeries(supply), - ...circulatingSupplyPctSeries(supply), + tree: [ + { + name: "Overview", + title: title("Supply"), + bottom: [ + ...fullSupplySeries(supply), + ...circulatingSupplyPctSeries(supply), + ], + }, + { + name: "Ratio", + title: title("Supply (% of Circulating)"), + bottom: circulatingSupplyRatioSeries(supply), + }, ], }, singleUtxoCountChart(cohort, title), @@ -359,6 +407,33 @@ export function createHoldingsSectionWithOwnSupply({ cohort, title }) { }; } +/** + * Holdings with inProfit/inLoss (no rel, no address count) + * For: CohortWithoutRelative (p2ms, unknown, empty) + * @param {{ cohort: CohortWithoutRelative, title: (metric: string) => string }} args + * @returns {PartialOptionsGroup} + */ +export function createHoldingsSectionWithProfitLoss({ cohort, title }) { + return { + name: "Holdings", + tree: [ + { + name: "Supply", + title: title("Supply"), + bottom: fullSupplySeries(cohort.tree.supply), + }, + singleUtxoCountChart(cohort, title), + { + name: "Change", + tree: [ + singleDeltaTree(cohort.tree.supply.delta, Unit.sats, title, "Supply"), + singleDeltaTree(cohort.tree.outputs.unspentCount.delta, Unit.count, title, "UTXO Count"), + ], + }, + ], + }; +} + /** * Holdings for CohortAddress (has inProfit/inLoss but no rel, plus address count) * @param {{ cohort: CohortAddress, title: (metric: string) => string }} args @@ -559,6 +634,66 @@ export function createGroupedHoldingsSection({ list, all, title }) { }; } +/** + * Grouped holdings with inProfit/inLoss (no rel, no address count) + * For: CohortWithoutRelative (p2ms, unknown, empty) + * @param {{ list: readonly CohortWithoutRelative[], all: CohortAll, title: (metric: string) => string }} args + * @returns {PartialOptionsGroup} + */ +export function createGroupedHoldingsSectionWithProfitLoss({ + list, + all, + title, +}) { + return { + name: "Holdings", + tree: [ + { + name: "Supply", + tree: [ + { + name: "Total", + title: title("Supply"), + bottom: flatMapCohortsWithAll(list, all, ({ name, color, tree }) => + satsBtcUsd({ pattern: tree.supply.total, name, color }), + ), + }, + { + name: "In Profit", + title: title("Supply In Profit"), + bottom: flatMapCohortsWithAll(list, all, ({ name, color, tree }) => + satsBtcUsd({ + pattern: tree.supply.inProfit, + name, + color, + }), + ), + }, + { + name: "In Loss", + title: title("Supply In Loss"), + bottom: flatMapCohortsWithAll(list, all, ({ name, color, tree }) => + satsBtcUsd({ + pattern: tree.supply.inLoss, + name, + color, + }), + ), + }, + ], + }, + groupedUtxoCountChart(list, all, title), + { + name: "Change", + tree: [ + groupedDeltaTree(list, all, (c) => c.tree.supply.delta, Unit.sats, title, "Supply"), + groupedDeltaTree(list, all, (c) => c.tree.outputs.unspentCount.delta, Unit.count, title, "UTXO Count"), + ], + }, + ], + }; +} + /** * Grouped holdings with inProfit/inLoss + relToCirculating (no relToOwn) * For: CohortWithAdjusted, CohortAgeRange diff --git a/website/scripts/options/distribution/index.js b/website/scripts/options/distribution/index.js index 2f8c0be95..fa6e82846 100644 --- a/website/scripts/options/distribution/index.js +++ b/website/scripts/options/distribution/index.js @@ -10,7 +10,7 @@ * - activity.js: SOPR, Volume, Lifespan */ -import { formatCohortTitle } from "../shared.js"; +import { formatCohortTitle, satsBtcUsd } from "../shared.js"; // Section builders import { @@ -18,9 +18,11 @@ import { createHoldingsSectionAll, createHoldingsSectionAddress, createHoldingsSectionAddressAmount, + createHoldingsSectionWithProfitLoss, createHoldingsSectionWithRelative, createHoldingsSectionWithOwnSupply, createGroupedHoldingsSection, + createGroupedHoldingsSectionWithProfitLoss, createGroupedHoldingsSectionAddress, createGroupedHoldingsSectionAddressAmount, createGroupedHoldingsSectionWithRelative, @@ -42,14 +44,16 @@ import { createGroupedCostBasisSectionWithPercentiles, } from "./cost-basis.js"; import { - createProfitabilitySection, createProfitabilitySectionAll, createProfitabilitySectionFull, createProfitabilitySectionWithNupl, createProfitabilitySectionWithInvestedCapitalPct, createProfitabilitySectionBasicWithInvestedCapitalPct, + createProfitabilitySectionAddress, + createProfitabilitySectionWithProfitLoss, createProfitabilitySectionLongTerm, createGroupedProfitabilitySection, + createGroupedProfitabilitySectionWithProfitLoss, createGroupedProfitabilitySectionWithNupl, createGroupedProfitabilitySectionWithInvestedCapitalPct, createGroupedProfitabilitySectionBasicWithInvestedCapitalPct, @@ -126,7 +130,7 @@ export function createCohortFolderWithAdjusted(cohort) { createHoldingsSectionWithOwnSupply({ cohort, title }), createValuationSection({ cohort, title }), createPricesSectionBasic({ cohort, title }), - createProfitabilitySectionWithNupl({ cohort, title }), + createProfitabilitySectionWithInvestedCapitalPct({ cohort, title }), createActivitySectionWithActivity({ cohort, title }), ], }; @@ -191,6 +195,22 @@ export function createCohortFolderAgeRange(cohort) { }; } +/** + * Age range folder with matured supply + * @param {CohortAgeRangeWithMatured} cohort + * @returns {PartialOptionsGroup} + */ +export function createCohortFolderAgeRangeWithMatured(cohort) { + const folder = createCohortFolderAgeRange(cohort); + const title = formatCohortTitle(cohort.name); + folder.tree.push({ + name: "Matured", + title: title("Matured Supply"), + bottom: satsBtcUsd({ pattern: cohort.matured, name: cohort.name }), + }); + return folder; +} + /** * Basic folder WITH RelToMarketCap * @param {CohortBasicWithMarketCap} cohort @@ -242,7 +262,7 @@ export function createCohortFolderAddress(cohort) { createHoldingsSectionAddress({ cohort, title }), createValuationSection({ cohort, title }), createPricesSectionBasic({ cohort, title }), - createProfitabilitySectionBasicWithInvestedCapitalPct({ cohort, title }), + createProfitabilitySectionAddress({ cohort, title }), createActivitySectionMinimal({ cohort, title }), ], }; @@ -258,10 +278,10 @@ export function createCohortFolderWithoutRelative(cohort) { return { name: cohort.name || "all", tree: [ - createHoldingsSection({ cohort, title }), + createHoldingsSectionWithProfitLoss({ cohort, title }), createValuationSection({ cohort, title }), createPricesSectionBasic({ cohort, title }), - createProfitabilitySection({ cohort, title }), + createProfitabilitySectionWithProfitLoss({ cohort, title }), createActivitySectionMinimal({ cohort, title }), ], }; @@ -331,7 +351,11 @@ export function createGroupedCohortFolderWithAdjusted({ createGroupedHoldingsSectionWithOwnSupply({ list, all, title }), createGroupedValuationSection({ list, all, title }), createGroupedPricesSection({ list, all, title }), - createGroupedProfitabilitySectionWithInvestedCapitalPct({ list, all, title }), + createGroupedProfitabilitySectionWithInvestedCapitalPct({ + list, + all, + title, + }), createGroupedActivitySectionWithActivity({ list, all, title }), ], }; @@ -412,6 +436,28 @@ export function createGroupedCohortFolderAgeRange({ }; } +/** + * @param {{ name: string, title: string, list: readonly CohortAgeRangeWithMatured[], all: CohortAll }} args + * @returns {PartialOptionsGroup} + */ +export function createGroupedCohortFolderAgeRangeWithMatured({ + name, + title: groupTitle, + list, + all, +}) { + const folder = createGroupedCohortFolderAgeRange({ name, title: groupTitle, list, all }); + const title = formatCohortTitle(groupTitle); + folder.tree.push({ + name: "Matured", + title: title("Matured Supply"), + bottom: list.flatMap((cohort) => + satsBtcUsd({ pattern: cohort.matured, name: cohort.name, color: cohort.color }), + ), + }); + return folder; +} + /** * @param {CohortGroupBasicWithMarketCap} args * @returns {PartialOptionsGroup} @@ -503,10 +549,10 @@ export function createGroupedCohortFolderWithoutRelative({ return { name: name || "all", tree: [ - createGroupedHoldingsSection({ list, all, title }), + createGroupedHoldingsSectionWithProfitLoss({ list, all, title }), createGroupedValuationSection({ list, all, title }), createGroupedPricesSection({ list, all, title }), - createGroupedProfitabilitySection({ list, all, title }), + createGroupedProfitabilitySectionWithProfitLoss({ list, all, title }), createGroupedActivitySectionMinimal({ list, all, title }), ], }; diff --git a/website/scripts/options/distribution/prices.js b/website/scripts/options/distribution/prices.js index 4a72efd70..a4cc4666c 100644 --- a/website/scripts/options/distribution/prices.js +++ b/website/scripts/options/distribution/prices.js @@ -98,8 +98,8 @@ export function createPricesSectionBasic({ cohort, title }) { top: [price({ metric: tree.realized.price, name: "Realized", color })], }, { - name: "Ratio", - title: title("Realized Price Ratio"), + name: "MVRV", + title: title("MVRV"), bottom: [ baseline({ metric: tree.realized.mvrv, @@ -109,6 +109,18 @@ export function createPricesSectionBasic({ cohort, title }) { }), ], }, + { + name: "Price Ratio", + title: title("Realized Price Ratio"), + bottom: [ + baseline({ + metric: tree.realized.price.ratio, + name: "Price Ratio", + unit: Unit.ratio, + base: 1, + }), + ], + }, ], }, ], diff --git a/website/scripts/options/distribution/profitability.js b/website/scripts/options/distribution/profitability.js index 536d7b9e8..4aab50b7b 100644 --- a/website/scripts/options/distribution/profitability.js +++ b/website/scripts/options/distribution/profitability.js @@ -105,6 +105,7 @@ function unrealizedPnlTreeAll(u, title) { { name: "USD", title: title("Unrealized P&L"), bottom: unrealizedUsdSeries(u) }, relPnlChart(u.profit.relToMcap, u.loss.relToMcap, "% of Mcap", title), relPnlChart(u.profit.relToOwnGross, u.loss.relToOwnGross, "% of Own P&L", title), + ...unrealizedCumulativeRollingTree(u.profit, u.loss, title), ]; } @@ -120,6 +121,7 @@ function unrealizedPnlTreeFull(u, title) { relPnlChart(u.profit.relToMcap, u.loss.relToMcap, "% of Mcap", title), relPnlChart(u.profit.relToOwnMcap, u.loss.relToOwnMcap, "% of Own Mcap", title), relPnlChart(u.profit.relToOwnGross, u.loss.relToOwnGross, "% of Own P&L", title), + ...unrealizedCumulativeRollingTree(u.profit, u.loss, title), ]; } @@ -139,21 +141,89 @@ function unrealizedPnlTreeLongTerm(u, title) { }, relPnlChart(u.profit.relToOwnMcap, u.loss.relToOwnMcap, "% of Own Mcap", title), relPnlChart(u.profit.relToOwnGross, u.loss.relToOwnGross, "% of Own P&L", title), + ...unrealizedCumulativeRollingTree(u.profit, u.loss, title), ]; } /** - * Unrealized P&L (USD only) for mid-tier cohorts (AgeRange/MaxAge) + * Unrealized P&L tree for mid-tier cohorts (AgeRange/MaxAge) * @param {Brk.LossNetNuplProfitPattern} u - * @returns {AnyFetchedSeriesBlueprint[]} + * @param {(metric: string) => string} title + * @returns {PartialOptionsTree} */ -function unrealizedMid(u) { +function unrealizedPnlTreeMid(u, title) { return [ - ...pnlLines( - { profit: u.profit.base.usd, loss: u.loss.base.usd, negLoss: u.loss.negative }, - Unit.usd, - ), - priceLine({ unit: Unit.usd, defaultActive: false }), + { + name: "USD", + title: title("Unrealized P&L"), + bottom: [ + ...pnlLines( + { profit: u.profit.base.usd, loss: u.loss.base.usd, negLoss: u.loss.negative }, + Unit.usd, + ), + priceLine({ unit: Unit.usd, defaultActive: false }), + ], + }, + ...unrealizedCumulativeRollingTree(u.profit, u.loss, title), + ]; +} + +/** + * Unrealized cumulative + rolling P&L tree (profit and loss have cumulative.usd + sum[w].usd) + * @param {{ cumulative: { usd: AnyMetricPattern }, sum: { _24h: { usd: AnyMetricPattern }, _1w: { usd: AnyMetricPattern }, _1m: { usd: AnyMetricPattern }, _1y: { usd: AnyMetricPattern } } }} profit + * @param {{ cumulative: { usd: AnyMetricPattern }, sum: { _24h: { usd: AnyMetricPattern }, _1w: { usd: AnyMetricPattern }, _1m: { usd: AnyMetricPattern }, _1y: { usd: AnyMetricPattern } } }} loss + * @param {(metric: string) => string} title + * @returns {PartialOptionsTree} + */ +function unrealizedCumulativeRollingTree(profit, loss, title) { + return [ + { + name: "Cumulative", + title: title("Cumulative Unrealized P&L"), + bottom: [ + line({ metric: profit.cumulative.usd, name: "Profit", color: colors.profit, unit: Unit.usd }), + line({ metric: loss.cumulative.usd, name: "Loss", color: colors.loss, unit: Unit.usd }), + ], + }, + { + name: "Rolling", + tree: [ + { + name: "Profit", + tree: [ + { + name: "Compare", + title: title("Rolling Unrealized Profit"), + bottom: ROLLING_WINDOWS.map((w) => + line({ metric: profit.sum[w.key].usd, name: w.name, color: w.color, unit: Unit.usd }), + ), + }, + ...ROLLING_WINDOWS.map((w) => ({ + name: w.name, + title: title(`Unrealized Profit (${w.name})`), + bottom: [line({ metric: profit.sum[w.key].usd, name: "Profit", color: colors.profit, unit: Unit.usd })], + })), + ], + }, + { + name: "Loss", + tree: [ + { + name: "Compare", + title: title("Rolling Unrealized Loss"), + bottom: ROLLING_WINDOWS.map((w) => + line({ metric: loss.sum[w.key].usd, name: w.name, color: w.color, unit: Unit.usd }), + ), + }, + ...ROLLING_WINDOWS.map((w) => ({ + name: w.name, + title: title(`Unrealized Loss (${w.name})`), + bottom: [line({ metric: loss.sum[w.key].usd, name: "Loss", color: colors.loss, unit: Unit.usd })], + })), + ], + }, + ], + }, ]; } @@ -320,24 +390,103 @@ function realizedPnlCumulativeTreeFull(r, title) { } /** + * Net realized P&L delta tree (absolute + rate across all rolling windows) + * @param {Brk.BaseChangeCumulativeDeltaRelSumPattern | Brk.BaseCumulativeDeltaSumPattern} netPnl + * @param {(metric: string) => string} title + * @returns {PartialOptionsGroup} + */ +function realizedNetPnlDeltaTree(netPnl, title) { + return { + name: "Change", + tree: [ + { + name: "Absolute", + tree: [ + { + name: "Compare", + title: title("Net Realized P&L Change"), + bottom: ROLLING_WINDOWS.map((w) => + baseline({ metric: netPnl.delta.absolute[w.key].usd, name: w.name, color: w.color, unit: Unit.usd }), + ), + }, + ...ROLLING_WINDOWS.map((w) => ({ + name: w.name, + title: title(`Net Realized P&L Change (${w.name})`), + bottom: [baseline({ metric: netPnl.delta.absolute[w.key].usd, name: "Change", unit: Unit.usd })], + })), + ], + }, + { + name: "Rate", + tree: [ + { + name: "Compare", + title: title("Net Realized P&L Rate"), + bottom: ROLLING_WINDOWS.flatMap((w) => + percentRatio({ pattern: netPnl.delta.rate[w.key], name: w.name, color: w.color }), + ), + }, + ...ROLLING_WINDOWS.map((w) => ({ + name: w.name, + title: title(`Net Realized P&L Rate (${w.name})`), + bottom: percentRatioBaseline({ pattern: netPnl.delta.rate[w.key], name: "Rate" }), + })), + ], + }, + ], + }; +} + +/** + * Full realized delta tree (absolute + rate + rel to mcap/rcap) * @param {Brk.CapGrossInvestorLossMvrvNetPeakPriceProfitSellSoprPattern | Brk.MetricsTree_Cohorts_Utxo_Lth_Realized} r * @param {(metric: string) => string} title - * @returns {PartialOptionsTree} + * @returns {PartialOptionsGroup} */ -function realized30dChangeTreeFull(r, title) { - return [ - { name: "USD", title: title("Realized P&L 30d Change"), bottom: [baseline({ metric: r.netPnl.delta.absolute._1m.usd, name: "30d Change", unit: Unit.usd })] }, - { - name: "% of Mcap", - title: title("Realized 30d Change (% of Mcap)"), - bottom: percentRatioBaseline({ pattern: r.netPnl.change1m.relToMcap, name: "30d Change" }), - }, - { - name: "% of Rcap", - title: title("Realized 30d Change (% of Realized Cap)"), - bottom: percentRatioBaseline({ pattern: r.netPnl.change1m.relToRcap, name: "30d Change" }), - }, - ]; +function realizedNetPnlDeltaTreeFull(r, title) { + const base = realizedNetPnlDeltaTree(r.netPnl, title); + return { + ...base, + tree: [ + ...base.tree, + { + name: "% of Mcap", + title: title("Net Realized P&L Change (% of Mcap)"), + bottom: percentRatioBaseline({ pattern: r.netPnl.change1m.relToMcap, name: "30d Change" }), + }, + { + name: "% of Rcap", + title: title("Net Realized P&L Change (% of Rcap)"), + bottom: percentRatioBaseline({ pattern: r.netPnl.change1m.relToRcap, name: "30d Change" }), + }, + ], + }; +} + +/** + * Rolling net realized P&L tree (reusable by full and mid realized) + * @param {{ sum: { _24h: { usd: AnyMetricPattern }, _1w: { usd: AnyMetricPattern }, _1m: { usd: AnyMetricPattern }, _1y: { usd: AnyMetricPattern } } }} netPnl + * @param {(metric: string) => string} title + * @returns {PartialOptionsGroup} + */ +function rollingNetRealizedTree(netPnl, title) { + return { + name: "Net", + tree: [ + { + name: "Compare", + title: title("Rolling Net Realized P&L"), + bottom: ROLLING_WINDOWS.map((w) => + baseline({ metric: netPnl.sum[w.key].usd, name: w.name, color: w.color, unit: Unit.usd }), + ), + }, + ...ROLLING_WINDOWS.map((w) => ({ + name: w.name, + title: title(`Net Realized P&L (${w.name})`), + bottom: [baseline({ metric: netPnl.sum[w.key].usd, name: "Net", unit: Unit.usd })], + })), + ], + }; } /** @@ -382,23 +531,7 @@ function singleRollingRealizedTreeFull(r, title) { })), ], }, - { - name: "Net", - tree: [ - { - name: "Compare", - title: title("Rolling Net Realized P&L"), - bottom: ROLLING_WINDOWS.map((w) => - baseline({ metric: r.netPnl.sum[w.key].usd, name: w.name, color: w.color, unit: Unit.usd }), - ), - }, - ...ROLLING_WINDOWS.map((w) => ({ - name: w.name, - title: title(`Net Realized P&L (${w.name})`), - bottom: [baseline({ metric: r.netPnl.sum[w.key].usd, name: "Net", unit: Unit.usd })], - })), - ], - }, + rollingNetRealizedTree(r.netPnl, title), { name: "P/L Ratio", tree: [ @@ -451,6 +584,96 @@ function singleRollingRealizedTreeBasic(profit, loss, title) { // Realized Subfolder Builders // ============================================================================ +/** + * Value Created/Destroyed tree for a single P&L side (profit or loss) + * @param {CountPattern} valueCreated + * @param {CountPattern} valueDestroyed + * @param {string} label - "Profit" or "Loss" + * @param {(metric: string) => string} title + * @returns {PartialOptionsGroup} + */ +function realizedValueTree(valueCreated, valueDestroyed, label, title) { + return { + name: label, + tree: [ + { + name: "Rolling", + tree: [ + { + name: "Compare", + title: title(`${label} Value Created vs Destroyed`), + bottom: ROLLING_WINDOWS.flatMap((w) => [ + line({ metric: valueCreated.sum[w.key], name: `Created (${w.name})`, color: w.color, unit: Unit.usd }), + line({ metric: valueDestroyed.sum[w.key], name: `Destroyed (${w.name})`, color: w.color, unit: Unit.usd, style: 2 }), + ]), + }, + ...ROLLING_WINDOWS.map((w) => ({ + name: w.name, + title: title(`${label} Value (${w.name})`), + bottom: [ + line({ metric: valueCreated.sum[w.key], name: "Created", color: colors.profit, unit: Unit.usd }), + line({ metric: valueDestroyed.sum[w.key], name: "Destroyed", color: colors.loss, unit: Unit.usd }), + ], + })), + ], + }, + { + name: "Cumulative", + title: title(`Cumulative ${label} Value`), + bottom: [ + line({ metric: valueCreated.cumulative, name: "Created", color: colors.profit, unit: Unit.usd }), + line({ metric: valueDestroyed.cumulative, name: "Destroyed", color: colors.loss, unit: Unit.usd }), + ], + }, + ], + }; +} + +/** + * Investor price percentiles tree (pct1/2/5/95/98/99) + * @param {InvestorPercentilesPattern} percentiles + * @param {(metric: string) => string} title + * @returns {PartialOptionsGroup} + */ +function investorPricePercentilesTree(percentiles, title) { + /** @type {readonly [InvestorPercentileEntry, string, Color][]} */ + const pcts = [ + [percentiles.pct99, "p99", colors.stat.max], + [percentiles.pct98, "p98", colors.stat.pct90], + [percentiles.pct95, "p95", colors.stat.pct75], + [percentiles.pct5, "p5", colors.stat.pct25], + [percentiles.pct2, "p2", colors.stat.pct10], + [percentiles.pct1, "p1", colors.stat.min], + ]; + + return { + name: "Percentiles", + tree: [ + { + name: "USD", + title: title("Investor Price Percentiles"), + bottom: pcts.map(([p, name, color]) => + line({ metric: p.price.usd, name, color, unit: Unit.usd }), + ), + }, + { + name: "Sats", + title: title("Investor Price Percentiles (Sats)"), + bottom: pcts.map(([p, name, color]) => + line({ metric: p.price.sats, name, color, unit: Unit.sats }), + ), + }, + { + name: "Ratio", + title: title("Investor Price Percentile Ratios"), + bottom: pcts.map(([p, name, color]) => + baseline({ metric: p.ratio, name, color, unit: Unit.ratio }), + ), + }, + ], + }; +} + /** * Full realized subfolder (All/STH/LTH) * @param {Brk.CapGrossInvestorLossMvrvNetPeakPriceProfitSellSoprPattern | Brk.MetricsTree_Cohorts_Utxo_Lth_Realized} r @@ -463,7 +686,7 @@ function realizedSubfolderFull(r, title) { tree: [ { name: "P&L", tree: realizedPnlSumTreeFull(r, title) }, { name: "Net", tree: realizedNetPnlSumTreeFull(r, title) }, - { name: "30d Change", tree: realized30dChangeTreeFull(r, title) }, + realizedNetPnlDeltaTreeFull(r, title), { name: "Gross P&L", tree: [ @@ -488,6 +711,13 @@ function realizedSubfolderFull(r, title) { { name: "Cumulative", title: title("Total Realized P&L"), bottom: [line({ metric: r.grossPnl.cumulative.usd, name: "Total", unit: Unit.usd, color: colors.bitcoin })] }, ], }, + { + name: "Value", + tree: [ + realizedValueTree(r.profit.valueCreated, r.profit.valueDestroyed, "Profit", title), + realizedValueTree(r.loss.valueCreated, r.loss.valueDestroyed, "Loss", title), + ], + }, { name: "P/L Ratio", title: title("Realized Profit/Loss Ratio"), @@ -498,6 +728,12 @@ function realizedSubfolderFull(r, title) { title: title("Realized Peak Regret"), bottom: [line({ metric: r.peakRegret.base, name: "Peak Regret", unit: Unit.usd })], }, + { + name: "Investor Price", + tree: [ + investorPricePercentilesTree(r.investor.price.percentiles, title), + ], + }, { name: "Rolling", tree: singleRollingRealizedTreeFull(r, title) }, { name: "Cumulative", @@ -555,12 +791,14 @@ function realizedSubfolderMid(r, title) { title: title("Net Realized P&L"), bottom: [dotsBaseline({ metric: r.netPnl.base.usd, name: "Net", unit: Unit.usd })], }, + realizedNetPnlDeltaTree(r.netPnl, title), { - name: "30d Change", - title: title("Realized P&L 30d Change"), - bottom: [baseline({ metric: r.netPnl.delta.absolute._1m.usd, name: "30d Change", unit: Unit.usd })], + name: "Rolling", + tree: [ + ...singleRollingRealizedTreeBasic(r.profit, r.loss, title), + rollingNetRealizedTree(r.netPnl, title), + ], }, - { name: "Rolling", tree: singleRollingRealizedTreeBasic(r.profit, r.loss, title) }, { name: "Cumulative", tree: [ @@ -620,7 +858,7 @@ function realizedSubfolderBasic(r, title) { /** * Basic profitability section (NUPL only unrealized, basic realized) - * @param {{ cohort: UtxoCohortObject | CohortWithoutRelative, title: (metric: string) => string }} args + * @param {{ cohort: UtxoCohortObject, title: (metric: string) => string }} args * @returns {PartialOptionsGroup} */ export function createProfitabilitySection({ cohort, title }) { @@ -639,6 +877,42 @@ export function createProfitabilitySection({ cohort, title }) { }; } +/** + * Profitability section with unrealized P&L + NUPL (no netPnl, no rel) + * For: CohortWithoutRelative (p2ms, unknown, empty) + * @param {{ cohort: CohortWithoutRelative, title: (metric: string) => string }} args + * @returns {PartialOptionsGroup} + */ +export function createProfitabilitySectionWithProfitLoss({ cohort, title }) { + const u = cohort.tree.unrealized; + return { + name: "Profitability", + tree: [ + { + name: "Unrealized", + tree: [ + { + name: "P&L", + tree: [ + { + name: "USD", + title: title("Unrealized P&L"), + bottom: [ + ...pnlLines({ profit: u.profit.base.usd, loss: u.loss.base.usd, negLoss: u.loss.negative }, Unit.usd), + priceLine({ unit: Unit.usd, defaultActive: false }), + ], + }, + ...unrealizedCumulativeRollingTree(u.profit, u.loss, title), + ], + }, + { name: "NUPL", title: title("NUPL"), bottom: nuplSeries(u.nupl) }, + ], + }, + realizedSubfolderBasic(cohort.tree.realized, title), + ], + }; +} + /** * Section for All cohort * @param {{ cohort: CohortAll, title: (metric: string) => string }} args @@ -764,7 +1038,7 @@ export function createProfitabilitySectionWithInvestedCapitalPct({ cohort, title { name: "Unrealized", tree: [ - { name: "P&L", title: title("Unrealized P&L"), bottom: unrealizedMid(u) }, + { name: "P&L", tree: unrealizedPnlTreeMid(u, title) }, { name: "Net P&L", title: title("Net Unrealized P&L"), bottom: netUnrealizedMid(u) }, { name: "NUPL", title: title("NUPL"), bottom: nuplSeries(u.nupl) }, ], @@ -795,6 +1069,41 @@ export function createProfitabilitySectionBasicWithInvestedCapitalPct({ cohort, }; } +/** + * Section for CohortAddress (has unrealized profit/loss + NUPL, basic realized) + * @param {{ cohort: CohortAddress, title: (metric: string) => string }} args + * @returns {PartialOptionsGroup} + */ +export function createProfitabilitySectionAddress({ cohort, title }) { + const u = cohort.tree.unrealized; + return { + name: "Profitability", + tree: [ + { + name: "Unrealized", + tree: [ + { + name: "P&L", + tree: [ + { + name: "USD", + title: title("Unrealized P&L"), + bottom: [ + ...pnlLines({ profit: u.profit.base.usd, loss: u.loss.base.usd, negLoss: u.loss.negative }, Unit.usd), + priceLine({ unit: Unit.usd, defaultActive: false }), + ], + }, + ...unrealizedCumulativeRollingTree(u.profit, u.loss, title), + ], + }, + { name: "NUPL", title: title("NUPL"), bottom: nuplSeries(u.nupl) }, + ], + }, + realizedSubfolderBasic(cohort.tree.realized, title), + ], + }; +} + // ============================================================================ // Grouped Cohort Helpers // ============================================================================ @@ -945,6 +1254,63 @@ function groupedRealizedSubfolder(list, all, title) { }; } +/** + * Grouped net realized P&L delta (Absolute + Rate with all rolling windows) + * @param {readonly (CohortAll | CohortFull | CohortLongTerm)[]} list + * @param {CohortAll} all + * @param {(metric: string) => string} title + * @returns {PartialOptionsGroup} + */ +function groupedRealizedNetPnlDeltaTree(list, all, title) { + return { + name: "Change", + tree: [ + { + name: "Absolute", + tree: [ + { + name: "Compare", + title: title("Net Realized P&L Change"), + bottom: ROLLING_WINDOWS.flatMap((w) => + mapCohortsWithAll(list, all, ({ name, tree }) => + baseline({ metric: tree.realized.netPnl.delta.absolute[w.key].usd, name: `${name} (${w.name})`, color: w.color, unit: Unit.usd }), + ), + ), + }, + ...ROLLING_WINDOWS.map((w) => ({ + name: w.name, + title: title(`Net Realized P&L Change (${w.name})`), + bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => + baseline({ metric: tree.realized.netPnl.delta.absolute[w.key].usd, name, color, unit: Unit.usd }), + ), + })), + ], + }, + { + name: "Rate", + tree: [ + { + name: "Compare", + title: title("Net Realized P&L Rate"), + bottom: ROLLING_WINDOWS.flatMap((w) => + flatMapCohortsWithAll(list, all, ({ name, tree }) => + percentRatio({ pattern: tree.realized.netPnl.delta.rate[w.key], name: `${name} (${w.name})`, color: w.color }), + ), + ), + }, + ...ROLLING_WINDOWS.map((w) => ({ + name: w.name, + title: title(`Net Realized P&L Rate (${w.name})`), + bottom: flatMapCohortsWithAll(list, all, ({ name, color, tree }) => + percentRatio({ pattern: tree.realized.netPnl.delta.rate[w.key], name, color }), + ), + })), + ], + }, + ], + }; +} + /** * Grouped realized subfolder for full cohorts * @param {readonly (CohortAll | CohortFull | CohortLongTerm)[]} list @@ -964,13 +1330,7 @@ function groupedRealizedSubfolderFull(list, all, title) { baseline({ metric: tree.realized.netPnl.base.usd, name, color, unit: Unit.usd }), ), }, - { - name: "30d Change", - title: title("Realized P&L 30d Change"), - bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => - baseline({ metric: tree.realized.netPnl.delta.absolute._1m.usd, name, color, unit: Unit.usd }), - ), - }, + groupedRealizedNetPnlDeltaTree(list, all, title), { name: "Rolling", tree: groupedRollingRealizedChartsFull(list, all, title) }, { name: "Cumulative", @@ -1265,6 +1625,41 @@ export function createGroupedProfitabilitySection({ list, all, title }) { }; } +/** + * Grouped profitability with unrealized profit/loss + NUPL + * For: CohortWithoutRelative (p2ms, unknown, empty) + * @param {{ list: readonly CohortWithoutRelative[], all: CohortAll, title: (metric: string) => string }} args + * @returns {PartialOptionsGroup} + */ +export function createGroupedProfitabilitySectionWithProfitLoss({ list, all, title }) { + return { + name: "Profitability", + tree: [ + { + name: "Unrealized", + tree: [ + { + name: "Profit", + title: title("Unrealized Profit"), + bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => + line({ metric: tree.unrealized.profit.base.usd, name, color, unit: Unit.usd }), + ), + }, + { + name: "Loss", + title: title("Unrealized Loss"), + bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => + line({ metric: tree.unrealized.loss.base.usd, name, color, unit: Unit.usd }), + ), + }, + ...groupedNuplCharts(list, all, title), + ], + }, + groupedRealizedSubfolder(list, all, title), + ], + }; +} + /** * Grouped section with invested capital % (basic cohorts — uses NUPL only) * @param {{ list: readonly CohortBasicWithoutMarketCap[], all: CohortAll, title: (metric: string) => string }} args diff --git a/website/scripts/options/distribution/utxo-profitability.js b/website/scripts/options/distribution/utxo-profitability.js new file mode 100644 index 000000000..180af46a9 --- /dev/null +++ b/website/scripts/options/distribution/utxo-profitability.js @@ -0,0 +1,89 @@ +/** UTXO Profitability section — range bands, cumulative profit/loss thresholds */ + +import { colors } from "../../utils/colors.js"; +import { entries } from "../../utils/array.js"; +import { Unit } from "../../utils/units.js"; +import { line, price } from "../series.js"; +import { brk } from "../../client.js"; +import { satsBtcUsd } from "../shared.js"; + +/** + * @param {{ name: string, color: Color, pattern: RealizedSupplyPattern }[]} list + * @param {string} titlePrefix + * @returns {PartialOptionsTree} + */ +function bucketCharts(list, titlePrefix) { + return [ + { + name: "Supply", + title: `${titlePrefix}: Supply`, + bottom: list.flatMap(({ name, color, pattern }) => + satsBtcUsd({ pattern: pattern.supply, name, color }), + ), + }, + { + name: "Realized Cap", + title: `${titlePrefix}: Realized Cap`, + bottom: list.map(({ name, color, pattern }) => + line({ metric: pattern.realizedCap, name, color, unit: Unit.usd }), + ), + }, + { + name: "Realized Price", + title: `${titlePrefix}: Realized Price`, + top: list.map(({ name, color, pattern }) => + price({ metric: pattern.realizedPrice, name, color }), + ), + }, + ]; +} + +/** + * @returns {PartialOptionsGroup} + */ +export function createUtxoProfitabilitySection() { + const { range, profit, loss } = brk.metrics.cohorts.utxo.profitability; + const { + PROFITABILITY_RANGE_NAMES, + PROFIT_NAMES, + LOSS_NAMES, + } = brk; + + const rangeList = entries(PROFITABILITY_RANGE_NAMES).map( + ([key, names], i, arr) => ({ + name: names.short, + color: colors.at(i, arr.length), + pattern: range[key], + }), + ); + + const profitList = entries(PROFIT_NAMES).map(([key, names], i, arr) => ({ + name: names.short, + color: colors.at(i, arr.length), + pattern: profit[key], + })); + + const lossList = entries(LOSS_NAMES).map(([key, names], i, arr) => ({ + name: names.short, + color: colors.at(i, arr.length), + pattern: loss[key], + })); + + return { + name: "UTXO Profitability", + tree: [ + { + name: "Range", + tree: bucketCharts(rangeList, "Profitability Range"), + }, + { + name: "In Profit", + tree: bucketCharts(profitList, "In Profit"), + }, + { + name: "In Loss", + tree: bucketCharts(lossList, "In Loss"), + }, + ], + }; +} diff --git a/website/scripts/options/market.js b/website/scripts/options/market.js index 1273e7f90..28bb4da14 100644 --- a/website/scripts/options/market.js +++ b/website/scripts/options/market.js @@ -36,6 +36,18 @@ import { periodIdToName } from "./utils.js"; * @property {Brk.BpsCentsRatioSatsUsdPattern} ratio */ +/** + * Create index (percent) + ratio line pair from a BpsPercentRatioPattern + * @param {{ pattern: { percent: AnyMetricPattern, ratio: AnyMetricPattern }, name: string, color?: Color, defaultActive?: boolean }} args + * @returns {AnyFetchedSeriesBlueprint[]} + */ +function indexRatio({ pattern, name, color, defaultActive }) { + return [ + line({ metric: pattern.percent, name, color, defaultActive, unit: Unit.index }), + line({ metric: pattern.ratio, name, color, defaultActive, unit: Unit.ratio }), + ]; +} + const commonMaIds = /** @type {const} */ ([ "1w", "1m", @@ -671,166 +683,43 @@ export function createMarketSection() { name: "Compare", title: "RSI Comparison", bottom: [ - line({ - metric: technical.rsi._24h.rsi.percent, - name: "1d", - color: colors.time._24h, - unit: Unit.index, - }), - line({ - metric: technical.rsi._1w.rsi.percent, - name: "1w", - color: colors.time._1w, - unit: Unit.index, - }), - line({ - metric: technical.rsi._1m.rsi.percent, - name: "1m", - color: colors.time._1m, - unit: Unit.index, - }), - line({ - metric: technical.rsi._1y.rsi.percent, - name: "1y", - color: colors.time._1y, - unit: Unit.index, - }), + ...ROLLING_WINDOWS.flatMap((w) => + indexRatio({ pattern: technical.rsi[w.key].rsi, name: w.name, color: w.color }), + ), priceLine({ unit: Unit.index, number: 70 }), priceLine({ unit: Unit.index, number: 30 }), ], }, - { - name: "1 Day", - title: "RSI (1d)", - bottom: [ - line({ - metric: technical.rsi._24h.rsi.percent, - name: "RSI", - color: colors.indicator.main, - unit: Unit.index, - }), - line({ - metric: technical.rsi._24h.rsiMax.percent, - name: "Max", - color: colors.stat.max, - defaultActive: false, - unit: Unit.index, - }), - line({ - metric: technical.rsi._24h.rsiMin.percent, - name: "Min", - color: colors.stat.min, - defaultActive: false, - unit: Unit.index, - }), - priceLine({ unit: Unit.index, number: 70 }), - priceLine({ - unit: Unit.index, - number: 50, - defaultActive: false, - }), - priceLine({ unit: Unit.index, number: 30 }), - ], - }, - { - name: "1 Week", - title: "RSI (1w)", - bottom: [ - line({ - metric: technical.rsi._1w.rsi.percent, - name: "RSI", - color: colors.indicator.main, - unit: Unit.index, - }), - line({ - metric: technical.rsi._1w.rsiMax.percent, - name: "Max", - color: colors.stat.max, - defaultActive: false, - unit: Unit.index, - }), - line({ - metric: technical.rsi._1w.rsiMin.percent, - name: "Min", - color: colors.stat.min, - defaultActive: false, - unit: Unit.index, - }), - priceLine({ unit: Unit.index, number: 70 }), - priceLine({ - unit: Unit.index, - number: 50, - defaultActive: false, - }), - priceLine({ unit: Unit.index, number: 30 }), - ], - }, - { - name: "1 Month", - title: "RSI (1m)", - bottom: [ - line({ - metric: technical.rsi._1m.rsi.percent, - name: "RSI", - color: colors.indicator.main, - unit: Unit.index, - }), - line({ - metric: technical.rsi._1m.rsiMax.percent, - name: "Max", - color: colors.stat.max, - defaultActive: false, - unit: Unit.index, - }), - line({ - metric: technical.rsi._1m.rsiMin.percent, - name: "Min", - color: colors.stat.min, - defaultActive: false, - unit: Unit.index, - }), - priceLine({ unit: Unit.index, number: 70 }), - priceLine({ - unit: Unit.index, - number: 50, - defaultActive: false, - }), - priceLine({ unit: Unit.index, number: 30 }), - ], - }, - { - name: "1 Year", - title: "RSI (1y)", - bottom: [ - line({ - metric: technical.rsi._1y.rsi.percent, - name: "RSI", - color: colors.indicator.main, - unit: Unit.index, - }), - line({ - metric: technical.rsi._1y.rsiMax.percent, - name: "Max", - color: colors.stat.max, - defaultActive: false, - unit: Unit.index, - }), - line({ - metric: technical.rsi._1y.rsiMin.percent, - name: "Min", - color: colors.stat.min, - defaultActive: false, - unit: Unit.index, - }), - priceLine({ unit: Unit.index, number: 70 }), - priceLine({ - unit: Unit.index, - number: 50, - defaultActive: false, - }), - priceLine({ unit: Unit.index, number: 30 }), - ], - }, + ...ROLLING_WINDOWS.map((w) => { + const rsi = technical.rsi[w.key]; + return { + name: w.name, + tree: [ + { + name: "Value", + title: `RSI (${w.name})`, + bottom: [ + ...indexRatio({ pattern: rsi.rsi, name: "RSI", color: colors.indicator.main }), + ...indexRatio({ pattern: rsi.rsiMax, name: "Max", color: colors.stat.max, defaultActive: false }), + ...indexRatio({ pattern: rsi.rsiMin, name: "Min", color: colors.stat.min, defaultActive: false }), + priceLine({ unit: Unit.index, number: 70 }), + priceLine({ unit: Unit.index, number: 50, defaultActive: false }), + priceLine({ unit: Unit.index, number: 30 }), + ], + }, + { + name: "Components", + title: `RSI Components (${w.name})`, + bottom: [ + line({ metric: rsi.averageGain, name: "Avg Gain", color: colors.profit, unit: Unit.usd }), + line({ metric: rsi.averageLoss, name: "Avg Loss", color: colors.loss, unit: Unit.usd }), + line({ metric: rsi.gains, name: "Gains", color: colors.profit, defaultActive: false, unit: Unit.usd }), + line({ metric: rsi.losses, name: "Losses", color: colors.loss, defaultActive: false, unit: Unit.usd }), + ], + }, + ], + }; + }), ], }, { @@ -840,127 +729,33 @@ export function createMarketSection() { name: "Compare", title: "Stochastic RSI Comparison", bottom: [ - line({ - metric: technical.rsi._24h.stochRsiK.percent, - name: "1d K", - color: colors.time._24h, - unit: Unit.index, - }), - line({ - metric: technical.rsi._1w.stochRsiK.percent, - name: "1w K", - color: colors.time._1w, - unit: Unit.index, - }), - line({ - metric: technical.rsi._1m.stochRsiK.percent, - name: "1m K", - color: colors.time._1m, - unit: Unit.index, - }), - line({ - metric: technical.rsi._1y.stochRsiK.percent, - name: "1y K", - color: colors.time._1y, - unit: Unit.index, - }), - ...priceLines({ unit: Unit.index, numbers: [80, 20] }), - ], - }, - { - name: "1 Day", - title: "Stochastic RSI (1d)", - bottom: [ - line({ - metric: technical.rsi._24h.stochRsiK.percent, - name: "K", - color: colors.indicator.fast, - unit: Unit.index, - }), - line({ - metric: technical.rsi._24h.stochRsiD.percent, - name: "D", - color: colors.indicator.slow, - unit: Unit.index, - }), - ...priceLines({ unit: Unit.index, numbers: [80, 20] }), - ], - }, - { - name: "1 Week", - title: "Stochastic RSI (1w)", - bottom: [ - line({ - metric: technical.rsi._1w.stochRsiK.percent, - name: "K", - color: colors.indicator.fast, - unit: Unit.index, - }), - line({ - metric: technical.rsi._1w.stochRsiD.percent, - name: "D", - color: colors.indicator.slow, - unit: Unit.index, - }), - ...priceLines({ unit: Unit.index, numbers: [80, 20] }), - ], - }, - { - name: "1 Month", - title: "Stochastic RSI (1m)", - bottom: [ - line({ - metric: technical.rsi._1m.stochRsiK.percent, - name: "K", - color: colors.indicator.fast, - unit: Unit.index, - }), - line({ - metric: technical.rsi._1m.stochRsiD.percent, - name: "D", - color: colors.indicator.slow, - unit: Unit.index, - }), - ...priceLines({ unit: Unit.index, numbers: [80, 20] }), - ], - }, - { - name: "1 Year", - title: "Stochastic RSI (1y)", - bottom: [ - line({ - metric: technical.rsi._1y.stochRsiK.percent, - name: "K", - color: colors.indicator.fast, - unit: Unit.index, - }), - line({ - metric: technical.rsi._1y.stochRsiD.percent, - name: "D", - color: colors.indicator.slow, - unit: Unit.index, - }), + ...ROLLING_WINDOWS.flatMap((w) => + indexRatio({ pattern: technical.rsi[w.key].stochRsiK, name: `${w.name} K`, color: w.color }), + ), ...priceLines({ unit: Unit.index, numbers: [80, 20] }), ], }, + ...ROLLING_WINDOWS.map((w) => { + const rsi = technical.rsi[w.key]; + return { + name: w.name, + title: `Stochastic RSI (${w.name})`, + bottom: [ + ...indexRatio({ pattern: rsi.stochRsi, name: "Raw", color: colors.indicator.main, defaultActive: false }), + ...indexRatio({ pattern: rsi.stochRsiK, name: "K", color: colors.indicator.fast }), + ...indexRatio({ pattern: rsi.stochRsiD, name: "D", color: colors.indicator.slow }), + ...priceLines({ unit: Unit.index, numbers: [80, 20] }), + ], + }; + }), ], }, { name: "Stochastic", title: "Stochastic Oscillator", bottom: [ - line({ - metric: technical.stochK.percent, - name: "K", - color: colors.indicator.fast, - unit: Unit.index, - }), - line({ - metric: technical.stochD.percent, - name: "D", - color: colors.indicator.slow, - unit: Unit.index, - }), + ...indexRatio({ pattern: technical.stochK, name: "K", color: colors.indicator.fast }), + ...indexRatio({ pattern: technical.stochD, name: "D", color: colors.indicator.slow }), ...priceLines({ unit: Unit.index, numbers: [80, 20] }), ], }, @@ -970,32 +765,9 @@ export function createMarketSection() { { name: "Compare", title: "MACD Comparison", - bottom: [ - line({ - metric: technical.macd._24h.line, - name: "1d", - color: colors.time._24h, - unit: Unit.usd, - }), - line({ - metric: technical.macd._1w.line, - name: "1w", - color: colors.time._1w, - unit: Unit.usd, - }), - line({ - metric: technical.macd._1m.line, - name: "1m", - color: colors.time._1m, - unit: Unit.usd, - }), - line({ - metric: technical.macd._1y.line, - name: "1y", - color: colors.time._1y, - unit: Unit.usd, - }), - ], + bottom: ROLLING_WINDOWS.map((w) => + line({ metric: technical.macd[w.key].line, name: w.name, color: w.color, unit: Unit.usd }), + ), }, ...ROLLING_WINDOWS.map((w) => ({ name: w.name, diff --git a/website/scripts/options/network.js b/website/scripts/options/network.js index ff4a0c36f..1e2e168c8 100644 --- a/website/scripts/options/network.js +++ b/website/scripts/options/network.js @@ -118,7 +118,6 @@ export function createNetworkSection() { }, ]); - const countTypes = /** @type {const} */ ([ { name: "Funded", @@ -568,7 +567,7 @@ export function createNetworkSection() { name: "Base", title: "OP_RETURN Burned", bottom: satsBtcUsd({ - pattern: supply.burned.opReturn.base, + pattern: scripts.value.opReturn.base, name: "sum", }), }, @@ -580,7 +579,7 @@ export function createNetworkSection() { title: "OP_RETURN Burned Rolling", bottom: ROLLING_WINDOWS.flatMap((w) => satsBtcUsd({ - pattern: supply.burned.opReturn.sum[w.key], + pattern: scripts.value.opReturn.sum[w.key], name: w.name, color: w.color, }), @@ -590,7 +589,7 @@ export function createNetworkSection() { name: w.name, title: `OP_RETURN Burned ${w.name}`, bottom: satsBtcUsd({ - pattern: supply.burned.opReturn.sum[w.key], + pattern: scripts.value.opReturn.sum[w.key], name: w.name, color: w.color, }), @@ -601,7 +600,7 @@ export function createNetworkSection() { name: "Cumulative", title: "OP_RETURN Burned (Total)", bottom: satsBtcUsd({ - pattern: supply.burned.opReturn.cumulative, + pattern: scripts.value.opReturn.cumulative, name: "all-time", }), }, @@ -1074,7 +1073,8 @@ export function createNetworkSection() { title: "UTXO Count 30d Change", bottom: [ baseline({ - metric: cohorts.utxo.all.outputs.unspentCount.delta.absolute._1m, + metric: + cohorts.utxo.all.outputs.unspentCount.delta.absolute._1m, name: "30d Change", unit: Unit.count, }), diff --git a/website/scripts/options/partial.js b/website/scripts/options/partial.js index af6e8ab21..3a6158634 100644 --- a/website/scripts/options/partial.js +++ b/website/scripts/options/partial.js @@ -6,7 +6,7 @@ import { createCohortFolderFull, createCohortFolderWithAdjusted, createCohortFolderLongTerm, - createCohortFolderAgeRange, + createCohortFolderAgeRangeWithMatured, createCohortFolderBasicWithMarketCap, createCohortFolderBasicWithoutMarketCap, createCohortFolderWithoutRelative, @@ -14,12 +14,13 @@ import { createAddressCohortFolder, createGroupedCohortFolderWithAdjusted, createGroupedCohortFolderWithNupl, - createGroupedCohortFolderAgeRange, + createGroupedCohortFolderAgeRangeWithMatured, createGroupedCohortFolderBasicWithMarketCap, createGroupedCohortFolderBasicWithoutMarketCap, createGroupedCohortFolderAddress, createGroupedAddressCohortFolder, } from "./distribution/index.js"; +import { createUtxoProfitabilitySection } from "./distribution/utxo-profitability.js"; import { createMarketSection } from "./market.js"; import { createNetworkSection } from "./network.js"; import { createMiningSection } from "./mining.js"; @@ -110,26 +111,26 @@ export function createPartialOptions() { { name: "Older Than", tree: [ - createGroupedCohortFolderBasicWithMarketCap({ + createGroupedCohortFolderWithAdjusted({ name: "Compare", title: "Over Age", list: overAge, all: cohortAll, }), - ...overAge.map(createCohortFolderBasicWithMarketCap), + ...overAge.map(createCohortFolderWithAdjusted), ], }, // Range { name: "Range", tree: [ - createGroupedCohortFolderAgeRange({ + createGroupedCohortFolderAgeRangeWithMatured({ name: "Compare", title: "Age Ranges", list: ageRange, all: cohortAll, }), - ...ageRange.map(createCohortFolderAgeRange), + ...ageRange.map(createCohortFolderAgeRangeWithMatured), ], }, ], @@ -246,13 +247,13 @@ export function createPartialOptions() { { name: "Epochs", tree: [ - createGroupedCohortFolderBasicWithoutMarketCap({ + createGroupedCohortFolderWithAdjusted({ name: "Compare", title: "Epochs", list: epoch, all: cohortAll, }), - ...epoch.map(createCohortFolderBasicWithoutMarketCap), + ...epoch.map(createCohortFolderWithAdjusted), ], }, @@ -260,15 +261,18 @@ export function createPartialOptions() { { name: "Years", tree: [ - createGroupedCohortFolderBasicWithoutMarketCap({ + createGroupedCohortFolderWithAdjusted({ name: "Compare", title: "Years", list: class_, all: cohortAll, }), - ...class_.map(createCohortFolderBasicWithoutMarketCap), + ...class_.map(createCohortFolderWithAdjusted), ], }, + + // UTXO Profitability bands + createUtxoProfitabilitySection(), ], }, diff --git a/website/scripts/options/types.js b/website/scripts/options/types.js index 4f1daf769..b63e5071b 100644 --- a/website/scripts/options/types.js +++ b/website/scripts/options/types.js @@ -233,6 +233,9 @@ * @property {Color} color * @property {AgeRangePattern} tree * + * Age range cohort with matured supply + * @typedef {CohortAgeRange & { matured: AnyValuePattern }} CohortAgeRangeWithMatured + * * Basic cohort WITH RelToMarketCap (geAmount.*, ltAmount.*) * @typedef {Object} CohortBasicWithMarketCap * @property {string} name diff --git a/website/scripts/types.js b/website/scripts/types.js index 1fc684808..58c81d259 100644 --- a/website/scripts/types.js +++ b/website/scripts/types.js @@ -12,7 +12,7 @@ * * @import { Color } from "./utils/colors.js" * - * @import { Option, PartialChartOption, ChartOption, AnyPartialOption, ProcessedOptionAddons, OptionsTree, SimulationOption, AnySeriesBlueprint, SeriesType, AnyFetchedSeriesBlueprint, TableOption, ExplorerOption, UrlOption, PartialOptionsGroup, OptionsGroup, PartialOptionsTree, UtxoCohortObject, AddressCohortObject, CohortObject, CohortGroupObject, FetchedLineSeriesBlueprint, FetchedBaselineSeriesBlueprint, FetchedHistogramSeriesBlueprint, FetchedDotsBaselineSeriesBlueprint, PatternAll, PatternFull, PatternWithAdjusted, PatternWithPercentiles, PatternBasic, PatternBasicWithMarketCap, PatternBasicWithoutMarketCap, PatternWithoutRelative, CohortAll, CohortFull, CohortWithAdjusted, CohortWithPercentiles, CohortBasic, CohortBasicWithMarketCap, CohortBasicWithoutMarketCap, CohortWithoutRelative, CohortAddress, CohortLongTerm, CohortAgeRange, CohortGroupFull, CohortGroupWithAdjusted, CohortGroupWithPercentiles, CohortGroupLongTerm, CohortGroupAgeRange, CohortGroupBasic, CohortGroupBasicWithMarketCap, CohortGroupBasicWithoutMarketCap, CohortGroupWithoutRelative, CohortGroupAddress, UtxoCohortGroupObject, AddressCohortGroupObject, FetchedDotsSeriesBlueprint, FetchedCandlestickSeriesBlueprint, FetchedPriceSeriesBlueprint, AnyPricePattern, AnyValuePattern } from "./options/partial.js" + * @import { Option, PartialChartOption, ChartOption, AnyPartialOption, ProcessedOptionAddons, OptionsTree, SimulationOption, AnySeriesBlueprint, SeriesType, AnyFetchedSeriesBlueprint, TableOption, ExplorerOption, UrlOption, PartialOptionsGroup, OptionsGroup, PartialOptionsTree, UtxoCohortObject, AddressCohortObject, CohortObject, CohortGroupObject, FetchedLineSeriesBlueprint, FetchedBaselineSeriesBlueprint, FetchedHistogramSeriesBlueprint, FetchedDotsBaselineSeriesBlueprint, PatternAll, PatternFull, PatternWithAdjusted, PatternWithPercentiles, PatternBasic, PatternBasicWithMarketCap, PatternBasicWithoutMarketCap, PatternWithoutRelative, CohortAll, CohortFull, CohortWithAdjusted, CohortWithPercentiles, CohortBasic, CohortBasicWithMarketCap, CohortBasicWithoutMarketCap, CohortWithoutRelative, CohortAddress, CohortLongTerm, CohortAgeRange, CohortAgeRangeWithMatured, CohortGroupFull, CohortGroupWithAdjusted, CohortGroupWithPercentiles, CohortGroupLongTerm, CohortGroupAgeRange, CohortGroupBasic, CohortGroupBasicWithMarketCap, CohortGroupBasicWithoutMarketCap, CohortGroupWithoutRelative, CohortGroupAddress, UtxoCohortGroupObject, AddressCohortGroupObject, FetchedDotsSeriesBlueprint, FetchedCandlestickSeriesBlueprint, FetchedPriceSeriesBlueprint, AnyPricePattern, AnyValuePattern } from "./options/partial.js" * * * @import { UnitObject as Unit } from "./utils/units.js" @@ -49,7 +49,7 @@ * @typedef {Brk.AddressOutputsRealizedSupplyUnrealizedPattern} AddressAmountPattern * @typedef {Brk.ActivityOutputsRealizedSupplyUnrealizedPattern} BasicUtxoPattern * @typedef {Brk.ActivityOutputsRealizedSupplyUnrealizedPattern} EpochPattern - * @typedef {Brk.OutputsRealizedSupplyUnrealizedPattern} EmptyPattern + * @typedef {Brk.OutputsRealizedSupplyUnrealizedPattern2} EmptyPattern * @typedef {Brk._0sdM0M1M1sdM2M2sdM3sdP0P1P1sdP2P2sdP3sdSdZscorePattern} Ratio1ySdPattern * @typedef {Brk.Dollars} Dollars * CoinbasePattern: base + cumulative + rolling windows (flattened) @@ -82,6 +82,9 @@ * @typedef {Brk.GrossInvestedLossNetNuplProfitSentimentPattern2} FullRelativePattern * @typedef {Brk.GrossInvestedLossNetNuplProfitSentimentPattern2} UnrealizedPattern * + * Profitability bucket pattern + * @typedef {Brk.RealizedSupplyPattern} RealizedSupplyPattern + * * Realized patterns * @typedef {Brk.CapGrossInvestorLossMvrvNetPeakPriceProfitSellSoprPattern} RealizedPattern * @typedef {Brk.CapGrossInvestorLossMvrvNetPeakPriceProfitSellSoprPattern} RealizedPattern2 @@ -204,6 +207,14 @@ * All cohorts with circulating supply relative metrics * @typedef {UtxoCohortWithCirculatingSupplyRelative | AddressCohortWithCirculatingSupplyRelative} CohortWithCirculatingSupplyRelative * + * Delta patterns with absolute + rate rolling windows + * @typedef {Brk.AbsoluteRatePattern} DeltaPattern + * @typedef {Brk.AbsoluteRatePattern2} FiatDeltaPattern + * + * Investor price percentiles (pct1/2/5/95/98/99) + * @typedef {Brk.Pct1Pct2Pct5Pct95Pct98Pct99Pattern} InvestorPercentilesPattern + * @typedef {Brk.BpsPriceRatioPattern} InvestorPercentileEntry + * * Generic tree node type for walking * @typedef {AnyMetricPattern | Record} TreeNode * diff --git a/website/scripts/utils/units.js b/website/scripts/utils/units.js index 7a9b36fb4..0fe3deb54 100644 --- a/website/scripts/utils/units.js +++ b/website/scripts/utils/units.js @@ -19,11 +19,6 @@ export const Unit = /** @type {const} */ ({ // Relative percentages pctSupply: { id: "pct-supply", name: "% of circulating" }, pctOwn: { id: "pct-own", name: "% of Own" }, - pctMcap: { id: "pct-mcap", name: "% of Market Cap" }, - pctRcap: { id: "pct-rcap", name: "% of Realized Cap" }, - pctOwnRcap: { id: "pct-own-rcap", name: "% of Own Realized Cap" }, - pctOwnMcap: { id: "pct-own-mcap", name: "% of Own Market Cap" }, - pctOwnPnl: { id: "pct-own-pnl", name: "% of Own P&L" }, // Time days: { id: "days", name: "Days" },