diff --git a/crates/brk_client/src/lib.rs b/crates/brk_client/src/lib.rs index 85ec56903..fc49f6c98 100644 --- a/crates/brk_client/src/lib.rs +++ b/crates/brk_client/src/lib.rs @@ -4352,20 +4352,20 @@ pub struct MetricsTree_Market_Dca { pub period_cagr: _10y2y3y4y5y6y8yPattern, pub period_days_in_profit: _10y1m1w1y2y3m3y4y5y6m6y8yPattern2, pub period_days_in_loss: _10y1m1w1y2y3m3y4y5y6m6y8yPattern2, - pub period_max_drawdown: _10y1m1w1y2y3m3y4y5y6m6y8yPattern2, + pub period_min_return: _10y1m1w1y2y3m3y4y5y6m6y8yPattern2, pub period_max_return: _10y1m1w1y2y3m3y4y5y6m6y8yPattern2, pub period_lump_sum_stack: _10y1m1w1y2y3m3y4y5y6m6y8yPattern3, pub period_lump_sum_returns: _10y1m1w1y2y3m3y4y5y6m6y8yPattern2, pub period_lump_sum_days_in_profit: _10y1m1w1y2y3m3y4y5y6m6y8yPattern2, pub period_lump_sum_days_in_loss: _10y1m1w1y2y3m3y4y5y6m6y8yPattern2, - pub period_lump_sum_max_drawdown: _10y1m1w1y2y3m3y4y5y6m6y8yPattern2, + pub period_lump_sum_min_return: _10y1m1w1y2y3m3y4y5y6m6y8yPattern2, pub period_lump_sum_max_return: _10y1m1w1y2y3m3y4y5y6m6y8yPattern2, pub class_stack: MetricsTree_Market_Dca_ClassStack, pub class_average_price: MetricsTree_Market_Dca_ClassAveragePrice, pub class_returns: _201520162017201820192020202120222023202420252026Pattern2, pub class_days_in_profit: MetricsTree_Market_Dca_ClassDaysInProfit, pub class_days_in_loss: MetricsTree_Market_Dca_ClassDaysInLoss, - pub class_max_drawdown: MetricsTree_Market_Dca_ClassMaxDrawdown, + pub class_min_return: MetricsTree_Market_Dca_ClassMinReturn, pub class_max_return: MetricsTree_Market_Dca_ClassMaxReturn, } @@ -4378,20 +4378,20 @@ impl MetricsTree_Market_Dca { period_cagr: _10y2y3y4y5y6y8yPattern::new(client.clone(), "dca_cagr".to_string()), period_days_in_profit: _10y1m1w1y2y3m3y4y5y6m6y8yPattern2::new(client.clone(), "dca_days_in_profit".to_string()), period_days_in_loss: _10y1m1w1y2y3m3y4y5y6m6y8yPattern2::new(client.clone(), "dca_days_in_loss".to_string()), - period_max_drawdown: _10y1m1w1y2y3m3y4y5y6m6y8yPattern2::new(client.clone(), "dca_max_drawdown".to_string()), + period_min_return: _10y1m1w1y2y3m3y4y5y6m6y8yPattern2::new(client.clone(), "dca_min_return".to_string()), period_max_return: _10y1m1w1y2y3m3y4y5y6m6y8yPattern2::new(client.clone(), "dca_max_return".to_string()), period_lump_sum_stack: _10y1m1w1y2y3m3y4y5y6m6y8yPattern3::new(client.clone(), "lump_sum_stack".to_string()), period_lump_sum_returns: _10y1m1w1y2y3m3y4y5y6m6y8yPattern2::new(client.clone(), "lump_sum_returns".to_string()), period_lump_sum_days_in_profit: _10y1m1w1y2y3m3y4y5y6m6y8yPattern2::new(client.clone(), "lump_sum_days_in_profit".to_string()), period_lump_sum_days_in_loss: _10y1m1w1y2y3m3y4y5y6m6y8yPattern2::new(client.clone(), "lump_sum_days_in_loss".to_string()), - period_lump_sum_max_drawdown: _10y1m1w1y2y3m3y4y5y6m6y8yPattern2::new(client.clone(), "lump_sum_max_drawdown".to_string()), + period_lump_sum_min_return: _10y1m1w1y2y3m3y4y5y6m6y8yPattern2::new(client.clone(), "lump_sum_min_return".to_string()), period_lump_sum_max_return: _10y1m1w1y2y3m3y4y5y6m6y8yPattern2::new(client.clone(), "lump_sum_max_return".to_string()), class_stack: MetricsTree_Market_Dca_ClassStack::new(client.clone(), format!("{base_path}_class_stack")), class_average_price: MetricsTree_Market_Dca_ClassAveragePrice::new(client.clone(), format!("{base_path}_class_average_price")), class_returns: _201520162017201820192020202120222023202420252026Pattern2::new(client.clone(), "dca_class".to_string()), class_days_in_profit: MetricsTree_Market_Dca_ClassDaysInProfit::new(client.clone(), format!("{base_path}_class_days_in_profit")), class_days_in_loss: MetricsTree_Market_Dca_ClassDaysInLoss::new(client.clone(), format!("{base_path}_class_days_in_loss")), - class_max_drawdown: MetricsTree_Market_Dca_ClassMaxDrawdown::new(client.clone(), format!("{base_path}_class_max_drawdown")), + class_min_return: MetricsTree_Market_Dca_ClassMinReturn::new(client.clone(), format!("{base_path}_class_min_return")), class_max_return: MetricsTree_Market_Dca_ClassMaxReturn::new(client.clone(), format!("{base_path}_class_max_return")), } } @@ -4573,7 +4573,7 @@ impl MetricsTree_Market_Dca_ClassDaysInLoss { } /// Metrics tree node. -pub struct MetricsTree_Market_Dca_ClassMaxDrawdown { +pub struct MetricsTree_Market_Dca_ClassMinReturn { pub _2015: MetricPattern4, pub _2016: MetricPattern4, pub _2017: MetricPattern4, @@ -4588,21 +4588,21 @@ pub struct MetricsTree_Market_Dca_ClassMaxDrawdown { pub _2026: MetricPattern4, } -impl MetricsTree_Market_Dca_ClassMaxDrawdown { +impl MetricsTree_Market_Dca_ClassMinReturn { pub fn new(client: Arc, base_path: String) -> Self { Self { - _2015: MetricPattern4::new(client.clone(), "dca_class_2015_max_drawdown".to_string()), - _2016: MetricPattern4::new(client.clone(), "dca_class_2016_max_drawdown".to_string()), - _2017: MetricPattern4::new(client.clone(), "dca_class_2017_max_drawdown".to_string()), - _2018: MetricPattern4::new(client.clone(), "dca_class_2018_max_drawdown".to_string()), - _2019: MetricPattern4::new(client.clone(), "dca_class_2019_max_drawdown".to_string()), - _2020: MetricPattern4::new(client.clone(), "dca_class_2020_max_drawdown".to_string()), - _2021: MetricPattern4::new(client.clone(), "dca_class_2021_max_drawdown".to_string()), - _2022: MetricPattern4::new(client.clone(), "dca_class_2022_max_drawdown".to_string()), - _2023: MetricPattern4::new(client.clone(), "dca_class_2023_max_drawdown".to_string()), - _2024: MetricPattern4::new(client.clone(), "dca_class_2024_max_drawdown".to_string()), - _2025: MetricPattern4::new(client.clone(), "dca_class_2025_max_drawdown".to_string()), - _2026: MetricPattern4::new(client.clone(), "dca_class_2026_max_drawdown".to_string()), + _2015: MetricPattern4::new(client.clone(), "dca_class_2015_min_return".to_string()), + _2016: MetricPattern4::new(client.clone(), "dca_class_2016_min_return".to_string()), + _2017: MetricPattern4::new(client.clone(), "dca_class_2017_min_return".to_string()), + _2018: MetricPattern4::new(client.clone(), "dca_class_2018_min_return".to_string()), + _2019: MetricPattern4::new(client.clone(), "dca_class_2019_min_return".to_string()), + _2020: MetricPattern4::new(client.clone(), "dca_class_2020_min_return".to_string()), + _2021: MetricPattern4::new(client.clone(), "dca_class_2021_min_return".to_string()), + _2022: MetricPattern4::new(client.clone(), "dca_class_2022_min_return".to_string()), + _2023: MetricPattern4::new(client.clone(), "dca_class_2023_min_return".to_string()), + _2024: MetricPattern4::new(client.clone(), "dca_class_2024_min_return".to_string()), + _2025: MetricPattern4::new(client.clone(), "dca_class_2025_min_return".to_string()), + _2026: MetricPattern4::new(client.clone(), "dca_class_2026_min_return".to_string()), } } } diff --git a/crates/brk_computer/src/market/dca/compute.rs b/crates/brk_computer/src/market/dca/compute.rs index cf51fe26f..e35b9e1ba 100644 --- a/crates/brk_computer/src/market/dca/compute.rs +++ b/crates/brk_computer/src/market/dca/compute.rs @@ -67,7 +67,7 @@ impl Vecs { compute_period_profitability( &mut self.period_days_in_profit, &mut self.period_days_in_loss, - &mut self.period_max_drawdown, + &mut self.period_min_return, &mut self.period_max_return, &self.period_returns, starting_indexes, @@ -95,7 +95,7 @@ impl Vecs { compute_period_profitability( &mut self.period_lump_sum_days_in_profit, &mut self.period_lump_sum_days_in_loss, - &mut self.period_lump_sum_max_drawdown, + &mut self.period_lump_sum_min_return, &mut self.period_lump_sum_max_return, &self.period_lump_sum_returns, starting_indexes, @@ -130,7 +130,7 @@ impl Vecs { compute_class_profitability( &mut self.class_days_in_profit, &mut self.class_days_in_loss, - &mut self.class_max_drawdown, + &mut self.class_min_return, &mut self.class_max_return, &self.class_returns, starting_indexes, @@ -144,16 +144,16 @@ impl Vecs { fn compute_period_profitability( days_in_profit: &mut ByDcaPeriod>, days_in_loss: &mut ByDcaPeriod>, - max_drawdown: &mut ByDcaPeriod>, + min_return: &mut ByDcaPeriod>, max_return: &mut ByDcaPeriod>, returns: &ByDcaPeriod, Dollars>>, starting_indexes: &ComputeIndexes, exit: &Exit, ) -> Result<()> { - for ((((dip, dil), md), mr), (ret, days)) in days_in_profit + for ((((dip, dil), minr), maxr), (ret, days)) in days_in_profit .iter_mut() .zip(days_in_loss.iter_mut()) - .zip(max_drawdown.iter_mut()) + .zip(min_return.iter_mut()) .zip(max_return.iter_mut()) .zip(returns.iter_with_days()) { @@ -177,7 +177,7 @@ fn compute_period_profitability( )?) })?; - md.compute_all(starting_indexes, exit, |v| { + minr.compute_all(starting_indexes, exit, |v| { Ok(v.compute_min( starting_indexes.dateindex, &ret.dateindex, @@ -186,7 +186,7 @@ fn compute_period_profitability( )?) })?; - mr.compute_all(starting_indexes, exit, |v| { + maxr.compute_all(starting_indexes, exit, |v| { Ok(v.compute_max( starting_indexes.dateindex, &ret.dateindex, @@ -201,7 +201,7 @@ fn compute_period_profitability( fn compute_class_profitability( days_in_profit: &mut ByDcaClass>, days_in_loss: &mut ByDcaClass>, - max_drawdown: &mut ByDcaClass>, + min_return: &mut ByDcaClass>, max_return: &mut ByDcaClass>, returns: &ByDcaClass, Dollars>>, starting_indexes: &ComputeIndexes, @@ -209,10 +209,10 @@ fn compute_class_profitability( ) -> Result<()> { let dateindexes = ByDcaClass::<()>::dateindexes(); - for (((((dip, dil), md), mr), ret), from) in days_in_profit + for (((((dip, dil), minr), maxr), ret), from) in days_in_profit .iter_mut() .zip(days_in_loss.iter_mut()) - .zip(max_drawdown.iter_mut()) + .zip(min_return.iter_mut()) .zip(max_return.iter_mut()) .zip(returns.iter()) .zip(dateindexes) @@ -237,7 +237,7 @@ fn compute_class_profitability( )?) })?; - md.compute_all(starting_indexes, exit, |v| { + minr.compute_all(starting_indexes, exit, |v| { Ok(v.compute_all_time_low_from( starting_indexes.dateindex, &ret.dateindex, @@ -246,7 +246,7 @@ fn compute_class_profitability( )?) })?; - mr.compute_all(starting_indexes, exit, |v| { + maxr.compute_all(starting_indexes, exit, |v| { Ok(v.compute_all_time_high_from( starting_indexes.dateindex, &ret.dateindex, diff --git a/crates/brk_computer/src/market/dca/import.rs b/crates/brk_computer/src/market/dca/import.rs index 91b265901..29e7db226 100644 --- a/crates/brk_computer/src/market/dca/import.rs +++ b/crates/brk_computer/src/market/dca/import.rs @@ -67,10 +67,10 @@ impl Vecs { ) })?; - let period_max_drawdown = ByDcaPeriod::try_new(|name, _days| { + let period_min_return = ByDcaPeriod::try_new(|name, _days| { ComputedFromDateLast::forced_import( db, - &format!("{name}_dca_max_drawdown"), + &format!("{name}_dca_min_return"), version, indexes, ) @@ -130,10 +130,10 @@ impl Vecs { ) })?; - let period_lump_sum_max_drawdown = ByDcaPeriod::try_new(|name, _days| { + let period_lump_sum_min_return = ByDcaPeriod::try_new(|name, _days| { ComputedFromDateLast::forced_import( db, - &format!("{name}_lump_sum_max_drawdown"), + &format!("{name}_lump_sum_min_return"), version, indexes, ) @@ -189,10 +189,10 @@ impl Vecs { ) })?; - let class_max_drawdown = ByDcaClass::try_new(|name, _year, _dateindex| { + let class_min_return = ByDcaClass::try_new(|name, _year, _dateindex| { ComputedFromDateLast::forced_import( db, - &format!("{name}_max_drawdown"), + &format!("{name}_min_return"), version, indexes, ) @@ -214,20 +214,20 @@ impl Vecs { period_cagr, period_days_in_profit, period_days_in_loss, - period_max_drawdown, + period_min_return, period_max_return, period_lump_sum_stack, period_lump_sum_returns, period_lump_sum_days_in_profit, period_lump_sum_days_in_loss, - period_lump_sum_max_drawdown, + period_lump_sum_min_return, period_lump_sum_max_return, class_stack, class_average_price, class_returns, class_days_in_profit, class_days_in_loss, - class_max_drawdown, + class_min_return, class_max_return, }) } diff --git a/crates/brk_computer/src/market/dca/vecs.rs b/crates/brk_computer/src/market/dca/vecs.rs index f4de22fc8..1e2ba26e7 100644 --- a/crates/brk_computer/src/market/dca/vecs.rs +++ b/crates/brk_computer/src/market/dca/vecs.rs @@ -16,7 +16,7 @@ pub struct Vecs { // DCA by period - profitability pub period_days_in_profit: ByDcaPeriod>, pub period_days_in_loss: ByDcaPeriod>, - pub period_max_drawdown: ByDcaPeriod>, + pub period_min_return: ByDcaPeriod>, pub period_max_return: ByDcaPeriod>, // Lump sum by period (for comparison with DCA) - KISS types @@ -26,7 +26,7 @@ pub struct Vecs { // Lump sum by period - profitability pub period_lump_sum_days_in_profit: ByDcaPeriod>, pub period_lump_sum_days_in_loss: ByDcaPeriod>, - pub period_lump_sum_max_drawdown: ByDcaPeriod>, + pub period_lump_sum_min_return: ByDcaPeriod>, pub period_lump_sum_max_return: ByDcaPeriod>, // DCA by year class - KISS types @@ -37,6 +37,6 @@ pub struct Vecs { // DCA by year class - profitability pub class_days_in_profit: ByDcaClass>, pub class_days_in_loss: ByDcaClass>, - pub class_max_drawdown: ByDcaClass>, + pub class_min_return: ByDcaClass>, pub class_max_return: ByDcaClass>, } diff --git a/modules/brk-client/index.js b/modules/brk-client/index.js index 43605644d..d5afb60c8 100644 --- a/modules/brk-client/index.js +++ b/modules/brk-client/index.js @@ -4121,20 +4121,20 @@ function createUtxoPattern(client, acc) { * @property {_10y2y3y4y5y6y8yPattern} periodCagr * @property {_10y1m1w1y2y3m3y4y5y6m6y8yPattern2} periodDaysInProfit * @property {_10y1m1w1y2y3m3y4y5y6m6y8yPattern2} periodDaysInLoss - * @property {_10y1m1w1y2y3m3y4y5y6m6y8yPattern2} periodMaxDrawdown + * @property {_10y1m1w1y2y3m3y4y5y6m6y8yPattern2} periodMinReturn * @property {_10y1m1w1y2y3m3y4y5y6m6y8yPattern2} periodMaxReturn * @property {_10y1m1w1y2y3m3y4y5y6m6y8yPattern3} periodLumpSumStack * @property {_10y1m1w1y2y3m3y4y5y6m6y8yPattern2} periodLumpSumReturns * @property {_10y1m1w1y2y3m3y4y5y6m6y8yPattern2} periodLumpSumDaysInProfit * @property {_10y1m1w1y2y3m3y4y5y6m6y8yPattern2} periodLumpSumDaysInLoss - * @property {_10y1m1w1y2y3m3y4y5y6m6y8yPattern2} periodLumpSumMaxDrawdown + * @property {_10y1m1w1y2y3m3y4y5y6m6y8yPattern2} periodLumpSumMinReturn * @property {_10y1m1w1y2y3m3y4y5y6m6y8yPattern2} periodLumpSumMaxReturn * @property {MetricsTree_Market_Dca_ClassStack} classStack * @property {MetricsTree_Market_Dca_ClassAveragePrice} classAveragePrice * @property {_201520162017201820192020202120222023202420252026Pattern2} classReturns * @property {MetricsTree_Market_Dca_ClassDaysInProfit} classDaysInProfit * @property {MetricsTree_Market_Dca_ClassDaysInLoss} classDaysInLoss - * @property {MetricsTree_Market_Dca_ClassMaxDrawdown} classMaxDrawdown + * @property {MetricsTree_Market_Dca_ClassMinReturn} classMinReturn * @property {MetricsTree_Market_Dca_ClassMaxReturn} classMaxReturn */ @@ -4219,7 +4219,7 @@ function createUtxoPattern(client, acc) { */ /** - * @typedef {Object} MetricsTree_Market_Dca_ClassMaxDrawdown + * @typedef {Object} MetricsTree_Market_Dca_ClassMinReturn * @property {MetricPattern4} _2015 * @property {MetricPattern4} _2016 * @property {MetricPattern4} _2017 @@ -6317,13 +6317,13 @@ class BrkClient extends BrkClientBase { periodCagr: create_10y2y3y4y5y6y8yPattern(this, 'dca_cagr'), periodDaysInProfit: create_10y1m1w1y2y3m3y4y5y6m6y8yPattern2(this, 'dca_days_in_profit'), periodDaysInLoss: create_10y1m1w1y2y3m3y4y5y6m6y8yPattern2(this, 'dca_days_in_loss'), - periodMaxDrawdown: create_10y1m1w1y2y3m3y4y5y6m6y8yPattern2(this, 'dca_max_drawdown'), + periodMinReturn: create_10y1m1w1y2y3m3y4y5y6m6y8yPattern2(this, 'dca_min_return'), periodMaxReturn: create_10y1m1w1y2y3m3y4y5y6m6y8yPattern2(this, 'dca_max_return'), periodLumpSumStack: create_10y1m1w1y2y3m3y4y5y6m6y8yPattern3(this, 'lump_sum_stack'), periodLumpSumReturns: create_10y1m1w1y2y3m3y4y5y6m6y8yPattern2(this, 'lump_sum_returns'), periodLumpSumDaysInProfit: create_10y1m1w1y2y3m3y4y5y6m6y8yPattern2(this, 'lump_sum_days_in_profit'), periodLumpSumDaysInLoss: create_10y1m1w1y2y3m3y4y5y6m6y8yPattern2(this, 'lump_sum_days_in_loss'), - periodLumpSumMaxDrawdown: create_10y1m1w1y2y3m3y4y5y6m6y8yPattern2(this, 'lump_sum_max_drawdown'), + periodLumpSumMinReturn: create_10y1m1w1y2y3m3y4y5y6m6y8yPattern2(this, 'lump_sum_min_return'), periodLumpSumMaxReturn: create_10y1m1w1y2y3m3y4y5y6m6y8yPattern2(this, 'lump_sum_max_return'), classStack: { _2015: createBitcoinDollarsSatsPattern5(this, 'dca_class_2015_stack'), @@ -6382,19 +6382,19 @@ class BrkClient extends BrkClientBase { _2025: createMetricPattern4(this, 'dca_class_2025_days_in_loss'), _2026: createMetricPattern4(this, 'dca_class_2026_days_in_loss'), }, - classMaxDrawdown: { - _2015: createMetricPattern4(this, 'dca_class_2015_max_drawdown'), - _2016: createMetricPattern4(this, 'dca_class_2016_max_drawdown'), - _2017: createMetricPattern4(this, 'dca_class_2017_max_drawdown'), - _2018: createMetricPattern4(this, 'dca_class_2018_max_drawdown'), - _2019: createMetricPattern4(this, 'dca_class_2019_max_drawdown'), - _2020: createMetricPattern4(this, 'dca_class_2020_max_drawdown'), - _2021: createMetricPattern4(this, 'dca_class_2021_max_drawdown'), - _2022: createMetricPattern4(this, 'dca_class_2022_max_drawdown'), - _2023: createMetricPattern4(this, 'dca_class_2023_max_drawdown'), - _2024: createMetricPattern4(this, 'dca_class_2024_max_drawdown'), - _2025: createMetricPattern4(this, 'dca_class_2025_max_drawdown'), - _2026: createMetricPattern4(this, 'dca_class_2026_max_drawdown'), + classMinReturn: { + _2015: createMetricPattern4(this, 'dca_class_2015_min_return'), + _2016: createMetricPattern4(this, 'dca_class_2016_min_return'), + _2017: createMetricPattern4(this, 'dca_class_2017_min_return'), + _2018: createMetricPattern4(this, 'dca_class_2018_min_return'), + _2019: createMetricPattern4(this, 'dca_class_2019_min_return'), + _2020: createMetricPattern4(this, 'dca_class_2020_min_return'), + _2021: createMetricPattern4(this, 'dca_class_2021_min_return'), + _2022: createMetricPattern4(this, 'dca_class_2022_min_return'), + _2023: createMetricPattern4(this, 'dca_class_2023_min_return'), + _2024: createMetricPattern4(this, 'dca_class_2024_min_return'), + _2025: createMetricPattern4(this, 'dca_class_2025_min_return'), + _2026: createMetricPattern4(this, 'dca_class_2026_min_return'), }, classMaxReturn: { _2015: createMetricPattern4(this, 'dca_class_2015_max_return'), diff --git a/packages/brk_client/brk_client/__init__.py b/packages/brk_client/brk_client/__init__.py index b0b6aa504..ff6a405c6 100644 --- a/packages/brk_client/brk_client/__init__.py +++ b/packages/brk_client/brk_client/__init__.py @@ -3590,22 +3590,22 @@ class MetricsTree_Market_Dca_ClassDaysInLoss: self._2025: MetricPattern4[StoredU32] = MetricPattern4(client, 'dca_class_2025_days_in_loss') self._2026: MetricPattern4[StoredU32] = MetricPattern4(client, 'dca_class_2026_days_in_loss') -class MetricsTree_Market_Dca_ClassMaxDrawdown: +class MetricsTree_Market_Dca_ClassMinReturn: """Metrics tree node.""" def __init__(self, client: BrkClientBase, base_path: str = ''): - self._2015: MetricPattern4[StoredF32] = MetricPattern4(client, 'dca_class_2015_max_drawdown') - self._2016: MetricPattern4[StoredF32] = MetricPattern4(client, 'dca_class_2016_max_drawdown') - self._2017: MetricPattern4[StoredF32] = MetricPattern4(client, 'dca_class_2017_max_drawdown') - self._2018: MetricPattern4[StoredF32] = MetricPattern4(client, 'dca_class_2018_max_drawdown') - self._2019: MetricPattern4[StoredF32] = MetricPattern4(client, 'dca_class_2019_max_drawdown') - self._2020: MetricPattern4[StoredF32] = MetricPattern4(client, 'dca_class_2020_max_drawdown') - self._2021: MetricPattern4[StoredF32] = MetricPattern4(client, 'dca_class_2021_max_drawdown') - self._2022: MetricPattern4[StoredF32] = MetricPattern4(client, 'dca_class_2022_max_drawdown') - self._2023: MetricPattern4[StoredF32] = MetricPattern4(client, 'dca_class_2023_max_drawdown') - self._2024: MetricPattern4[StoredF32] = MetricPattern4(client, 'dca_class_2024_max_drawdown') - self._2025: MetricPattern4[StoredF32] = MetricPattern4(client, 'dca_class_2025_max_drawdown') - self._2026: MetricPattern4[StoredF32] = MetricPattern4(client, 'dca_class_2026_max_drawdown') + self._2015: MetricPattern4[StoredF32] = MetricPattern4(client, 'dca_class_2015_min_return') + self._2016: MetricPattern4[StoredF32] = MetricPattern4(client, 'dca_class_2016_min_return') + self._2017: MetricPattern4[StoredF32] = MetricPattern4(client, 'dca_class_2017_min_return') + self._2018: MetricPattern4[StoredF32] = MetricPattern4(client, 'dca_class_2018_min_return') + self._2019: MetricPattern4[StoredF32] = MetricPattern4(client, 'dca_class_2019_min_return') + self._2020: MetricPattern4[StoredF32] = MetricPattern4(client, 'dca_class_2020_min_return') + self._2021: MetricPattern4[StoredF32] = MetricPattern4(client, 'dca_class_2021_min_return') + self._2022: MetricPattern4[StoredF32] = MetricPattern4(client, 'dca_class_2022_min_return') + self._2023: MetricPattern4[StoredF32] = MetricPattern4(client, 'dca_class_2023_min_return') + self._2024: MetricPattern4[StoredF32] = MetricPattern4(client, 'dca_class_2024_min_return') + self._2025: MetricPattern4[StoredF32] = MetricPattern4(client, 'dca_class_2025_min_return') + self._2026: MetricPattern4[StoredF32] = MetricPattern4(client, 'dca_class_2026_min_return') class MetricsTree_Market_Dca_ClassMaxReturn: """Metrics tree node.""" @@ -3634,20 +3634,20 @@ class MetricsTree_Market_Dca: self.period_cagr: _10y2y3y4y5y6y8yPattern = _10y2y3y4y5y6y8yPattern(client, 'dca_cagr') self.period_days_in_profit: _10y1m1w1y2y3m3y4y5y6m6y8yPattern2[StoredU32] = _10y1m1w1y2y3m3y4y5y6m6y8yPattern2(client, 'dca_days_in_profit') self.period_days_in_loss: _10y1m1w1y2y3m3y4y5y6m6y8yPattern2[StoredU32] = _10y1m1w1y2y3m3y4y5y6m6y8yPattern2(client, 'dca_days_in_loss') - self.period_max_drawdown: _10y1m1w1y2y3m3y4y5y6m6y8yPattern2[StoredF32] = _10y1m1w1y2y3m3y4y5y6m6y8yPattern2(client, 'dca_max_drawdown') + self.period_min_return: _10y1m1w1y2y3m3y4y5y6m6y8yPattern2[StoredF32] = _10y1m1w1y2y3m3y4y5y6m6y8yPattern2(client, 'dca_min_return') self.period_max_return: _10y1m1w1y2y3m3y4y5y6m6y8yPattern2[StoredF32] = _10y1m1w1y2y3m3y4y5y6m6y8yPattern2(client, 'dca_max_return') self.period_lump_sum_stack: _10y1m1w1y2y3m3y4y5y6m6y8yPattern3 = _10y1m1w1y2y3m3y4y5y6m6y8yPattern3(client, 'lump_sum_stack') self.period_lump_sum_returns: _10y1m1w1y2y3m3y4y5y6m6y8yPattern2[StoredF32] = _10y1m1w1y2y3m3y4y5y6m6y8yPattern2(client, 'lump_sum_returns') self.period_lump_sum_days_in_profit: _10y1m1w1y2y3m3y4y5y6m6y8yPattern2[StoredU32] = _10y1m1w1y2y3m3y4y5y6m6y8yPattern2(client, 'lump_sum_days_in_profit') self.period_lump_sum_days_in_loss: _10y1m1w1y2y3m3y4y5y6m6y8yPattern2[StoredU32] = _10y1m1w1y2y3m3y4y5y6m6y8yPattern2(client, 'lump_sum_days_in_loss') - self.period_lump_sum_max_drawdown: _10y1m1w1y2y3m3y4y5y6m6y8yPattern2[StoredF32] = _10y1m1w1y2y3m3y4y5y6m6y8yPattern2(client, 'lump_sum_max_drawdown') + self.period_lump_sum_min_return: _10y1m1w1y2y3m3y4y5y6m6y8yPattern2[StoredF32] = _10y1m1w1y2y3m3y4y5y6m6y8yPattern2(client, 'lump_sum_min_return') self.period_lump_sum_max_return: _10y1m1w1y2y3m3y4y5y6m6y8yPattern2[StoredF32] = _10y1m1w1y2y3m3y4y5y6m6y8yPattern2(client, 'lump_sum_max_return') self.class_stack: MetricsTree_Market_Dca_ClassStack = MetricsTree_Market_Dca_ClassStack(client) self.class_average_price: MetricsTree_Market_Dca_ClassAveragePrice = MetricsTree_Market_Dca_ClassAveragePrice(client) self.class_returns: _201520162017201820192020202120222023202420252026Pattern2[StoredF32] = _201520162017201820192020202120222023202420252026Pattern2(client, 'dca_class') self.class_days_in_profit: MetricsTree_Market_Dca_ClassDaysInProfit = MetricsTree_Market_Dca_ClassDaysInProfit(client) self.class_days_in_loss: MetricsTree_Market_Dca_ClassDaysInLoss = MetricsTree_Market_Dca_ClassDaysInLoss(client) - self.class_max_drawdown: MetricsTree_Market_Dca_ClassMaxDrawdown = MetricsTree_Market_Dca_ClassMaxDrawdown(client) + self.class_min_return: MetricsTree_Market_Dca_ClassMinReturn = MetricsTree_Market_Dca_ClassMinReturn(client) self.class_max_return: MetricsTree_Market_Dca_ClassMaxReturn = MetricsTree_Market_Dca_ClassMaxReturn(client) class MetricsTree_Market_Indicators: diff --git a/website/scripts/chart/colors.js b/website/scripts/chart/colors.js index 002ef81fe..f79be0a9b 100644 --- a/website/scripts/chart/colors.js +++ b/website/scripts/chart/colors.js @@ -86,11 +86,10 @@ const fuchsia = createColor(() => getColor("fuchsia")); const pink = createColor(() => getColor("pink")); const rose = createColor(() => getColor("rose")); -export const colors = { +const baseColors = { default: createColor(() => getLightDarkValue("--color")), gray: createColor(() => getColor("gray")), border: createColor(() => getLightDarkValue("--border-color")), - red, orange, amber, @@ -109,6 +108,10 @@ export const colors = { fuchsia, pink, rose, +}; + +export const colors = { + ...baseColors, /** Semantic stat colors for pattern helpers */ stat: { @@ -123,9 +126,47 @@ export const colors = { pct10: fuchsia, min: red, }, + + /** DCA period colors by term */ + dcaPeriods: { + // Short term + _1w: red, + _1m: orange, + _3m: yellow, + _6m: lime, + // Medium term + _1y: green, + _2y: teal, + _3y: cyan, + // Long term + _4y: sky, + _5y: blue, + _6y: indigo, + _8y: violet, + _10y: purple, + }, + + /** DCA year colors by halving epoch */ + dcaYears: { + // Epoch 5 (2024+) + _2026: rose, + _2025: fuchsia, + _2024: purple, + // Epoch 4 (2020-2023) + _2023: violet, + _2022: blue, + _2021: sky, + _2020: teal, + // Epoch 3 (2016-2019) + _2019: green, + _2018: yellow, + _2017: orange, + _2016: red, + _2015: pink, + }, }; /** * @typedef {typeof colors} Colors - * @typedef {Exclude} ColorName + * @typedef {keyof typeof baseColors} ColorName */ diff --git a/website/scripts/options/chain.js b/website/scripts/options/chain.js index c13c4324b..813150597 100644 --- a/website/scripts/options/chain.js +++ b/website/scripts/options/chain.js @@ -2,7 +2,7 @@ import { Unit } from "../utils/units.js"; import { priceLine } from "./constants.js"; -import { line, baseline, dots } from "./series.js"; +import { line, baseline, dots, dotted } from "./series.js"; import { satsBtcUsd } from "./shared.js"; import { spendableTypeColors } from "./colors/index.js"; @@ -70,31 +70,88 @@ export function createChainSection(ctx) { const addressTypes = [ { key: "p2pkh", name: "P2PKH", color: colors[spendableTypeColors.p2pkh] }, { key: "p2sh", name: "P2SH", color: colors[spendableTypeColors.p2sh] }, - { key: "p2wpkh", name: "P2WPKH", color: colors[spendableTypeColors.p2wpkh] }, + { + key: "p2wpkh", + name: "P2WPKH", + color: colors[spendableTypeColors.p2wpkh], + }, { key: "p2wsh", name: "P2WSH", color: colors[spendableTypeColors.p2wsh] }, { key: "p2tr", name: "P2TR", color: colors[spendableTypeColors.p2tr] }, - { key: "p2pk65", name: "P2PK65", color: colors[spendableTypeColors.p2pk65], defaultActive: false }, - { key: "p2pk33", name: "P2PK33", color: colors[spendableTypeColors.p2pk33], defaultActive: false }, - { key: "p2a", name: "P2A", color: colors[spendableTypeColors.p2a], defaultActive: false }, + { + key: "p2pk65", + name: "P2PK65", + color: colors[spendableTypeColors.p2pk65], + defaultActive: false, + }, + { + key: "p2pk33", + name: "P2PK33", + color: colors[spendableTypeColors.p2pk33], + defaultActive: false, + }, + { + key: "p2a", + name: "P2A", + color: colors[spendableTypeColors.p2a], + defaultActive: false, + }, ]; // Activity types for mapping /** @type {ReadonlyArray<{key: "sending" | "receiving" | "both" | "reactivated" | "balanceIncreased" | "balanceDecreased", name: string, title: string, compareTitle: string}>} */ const activityTypes = [ - { key: "sending", name: "Sending", title: "Sending Address Count", compareTitle: "Sending Address Count by Type" }, - { key: "receiving", name: "Receiving", title: "Receiving Address Count", compareTitle: "Receiving Address Count by Type" }, - { key: "both", name: "Both", title: "Addresses Sending & Receiving (Same Block)", compareTitle: "Addresses Sending & Receiving by Type" }, - { key: "reactivated", name: "Reactivated", title: "Reactivated Address Count (Was Empty)", compareTitle: "Reactivated Address Count by Type" }, - { key: "balanceIncreased", name: "Balance Increased", title: "Addresses with Increased Balance", compareTitle: "Addresses with Increased Balance by Type" }, - { key: "balanceDecreased", name: "Balance Decreased", title: "Addresses with Decreased Balance", compareTitle: "Addresses with Decreased Balance by Type" }, + { + key: "sending", + name: "Sending", + title: "Sending Address Count", + compareTitle: "Sending Address Count by Type", + }, + { + key: "receiving", + name: "Receiving", + title: "Receiving Address Count", + compareTitle: "Receiving Address Count by Type", + }, + { + key: "both", + name: "Both", + title: "Addresses Sending & Receiving (Same Block)", + compareTitle: "Addresses Sending & Receiving by Type", + }, + { + key: "reactivated", + name: "Reactivated", + title: "Reactivated Address Count (Was Empty)", + compareTitle: "Reactivated Address Count by Type", + }, + { + key: "balanceIncreased", + name: "Balance Increased", + title: "Addresses with Increased Balance", + compareTitle: "Addresses with Increased Balance by Type", + }, + { + key: "balanceDecreased", + name: "Balance Decreased", + title: "Addresses with Decreased Balance", + compareTitle: "Addresses with Decreased Balance by Type", + }, ]; // Count types for comparison charts /** @type {ReadonlyArray<{key: "addrCount" | "emptyAddrCount" | "totalAddrCount", name: string, title: string}>} */ const countTypes = [ { key: "addrCount", name: "Loaded", title: "Address Count by Type" }, - { key: "emptyAddrCount", name: "Empty", title: "Empty Address Count by Type" }, - { key: "totalAddrCount", name: "Total", title: "Total Address Count by Type" }, + { + key: "emptyAddrCount", + name: "Empty", + title: "Empty Address Count by Type", + }, + { + key: "totalAddrCount", + name: "Total", + title: "Total Address Count by Type", + }, ]; /** @@ -131,12 +188,18 @@ export function createChainSection(ctx) { { name: "New", title: `${titlePrefix}New Address Count`, - bottom: fromFullStatsPattern({ pattern: distribution.newAddrCount[key], unit: Unit.count }), + bottom: fromFullStatsPattern({ + pattern: distribution.newAddrCount[key], + unit: Unit.count, + }), }, { name: "Growth Rate", title: `${titlePrefix}Address Growth Rate`, - bottom: fromBaseStatsPattern({ pattern: distribution.growthRate[key], unit: Unit.ratio }), + bottom: fromBaseStatsPattern({ + pattern: distribution.growthRate[key], + unit: Unit.ratio, + }), }, { name: "Activity", @@ -262,7 +325,12 @@ export function createChainSection(ctx) { sumColor: colors.lime, cumulativeColor: colors.emerald, }), - ...fromValuePattern({ pattern: pool.fee, title: "fee", sumColor: colors.cyan, cumulativeColor: colors.indigo }), + ...fromValuePattern({ + pattern: pool.fee, + title: "fee", + sumColor: colors.cyan, + cumulativeColor: colors.indigo, + }), ], }, { @@ -296,7 +364,10 @@ export function createChainSection(ctx) { name: "Count", title: "Block Count", bottom: [ - ...fromCountPattern({ pattern: blocks.count.blockCount, unit: Unit.count }), + ...fromCountPattern({ + pattern: blocks.count.blockCount, + unit: Unit.count, + }), line({ metric: blocks.count.blockCountTarget, name: "Target", @@ -338,7 +409,11 @@ export function createChainSection(ctx) { name: "Interval", title: "Block Interval", bottom: [ - ...fromBaseStatsPattern({ pattern: blocks.interval, unit: Unit.secs, avgActive: false }), + ...fromBaseStatsPattern({ + pattern: blocks.interval, + unit: Unit.secs, + avgActive: false, + }), priceLine({ ctx, unit: Unit.secs, name: "Target", number: 600 }), ], }, @@ -346,7 +421,10 @@ export function createChainSection(ctx) { name: "Size", title: "Block Size", bottom: [ - ...fromSumStatsPattern({ pattern: blocks.size, unit: Unit.bytes }), + ...fromSumStatsPattern({ + pattern: blocks.size, + unit: Unit.bytes, + }), line({ metric: blocks.totalSize, name: "Total", @@ -354,8 +432,14 @@ export function createChainSection(ctx) { unit: Unit.bytes, defaultActive: false, }), - ...fromBaseStatsPattern({ pattern: blocks.vbytes, unit: Unit.vb }), - ...fromBaseStatsPattern({ pattern: blocks.weight, unit: Unit.wu }), + ...fromBaseStatsPattern({ + pattern: blocks.vbytes, + unit: Unit.vb, + }), + ...fromBaseStatsPattern({ + pattern: blocks.weight, + unit: Unit.wu, + }), line({ metric: blocks.weight.sum, name: "Sum", @@ -375,7 +459,10 @@ export function createChainSection(ctx) { { name: "Fullness", title: "Block Fullness", - bottom: fromBaseStatsPattern({ pattern: blocks.fullness, unit: Unit.percentage }), + bottom: fromBaseStatsPattern({ + pattern: blocks.fullness, + unit: Unit.percentage, + }), }, ], }, @@ -387,7 +474,10 @@ export function createChainSection(ctx) { { name: "Count", title: "Transaction Count", - bottom: fromFullStatsPattern({ pattern: transactions.count.txCount, unit: Unit.count }), + bottom: fromFullStatsPattern({ + pattern: transactions.count.txCount, + unit: Unit.count, + }), }, { name: "Speed", @@ -404,7 +494,10 @@ export function createChainSection(ctx) { name: "Volume", title: "Transaction Volume", bottom: [ - ...satsBtcUsd({ pattern: transactions.volume.sentSum, name: "Sent" }), + ...satsBtcUsd({ + pattern: transactions.volume.sentSum, + name: "Sent", + }), ...satsBtcUsd({ pattern: transactions.volume.receivedSum, name: "Received", @@ -423,14 +516,23 @@ export function createChainSection(ctx) { name: "Size", title: "Transaction Size", bottom: [ - ...fromStatsPattern({ pattern: transactions.size.weight, unit: Unit.wu }), - ...fromStatsPattern({ pattern: transactions.size.vsize, unit: Unit.vb }), + ...fromStatsPattern({ + pattern: transactions.size.weight, + unit: Unit.wu, + }), + ...fromStatsPattern({ + pattern: transactions.size.vsize, + unit: Unit.vb, + }), ], }, { name: "Fee Rate", title: "Fee Rate", - bottom: fromStatsPattern({ pattern: transactions.fees.feeRate, unit: Unit.feeRate }), + bottom: fromStatsPattern({ + pattern: transactions.fees.feeRate, + unit: Unit.feeRate, + }), }, { name: "Versions", @@ -486,12 +588,22 @@ export function createChainSection(ctx) { { name: "Input Count", title: "Input Count", - bottom: [...fromSumStatsPattern({ pattern: inputs.count, unit: Unit.count })], + bottom: [ + ...fromSumStatsPattern({ + pattern: inputs.count, + unit: Unit.count, + }), + ], }, { name: "Output Count", title: "Output Count", - bottom: [...fromSumStatsPattern({ pattern: outputs.count.totalCount, unit: Unit.count })], + bottom: [ + ...fromSumStatsPattern({ + pattern: outputs.count.totalCount, + unit: Unit.count, + }), + ], }, { name: "Inputs/sec", @@ -543,17 +655,26 @@ export function createChainSection(ctx) { { name: "P2PKH", title: "P2PKH Output Count", - bottom: fromFullStatsPattern({ pattern: scripts.count.p2pkh, unit: Unit.count }), + bottom: fromFullStatsPattern({ + pattern: scripts.count.p2pkh, + unit: Unit.count, + }), }, { name: "P2PK33", title: "P2PK33 Output Count", - bottom: fromFullStatsPattern({ pattern: scripts.count.p2pk33, unit: Unit.count }), + bottom: fromFullStatsPattern({ + pattern: scripts.count.p2pk33, + unit: Unit.count, + }), }, { name: "P2PK65", title: "P2PK65 Output Count", - bottom: fromFullStatsPattern({ pattern: scripts.count.p2pk65, unit: Unit.count }), + bottom: fromFullStatsPattern({ + pattern: scripts.count.p2pk65, + unit: Unit.count, + }), }, ], }, @@ -564,12 +685,18 @@ export function createChainSection(ctx) { { name: "P2SH", title: "P2SH Output Count", - bottom: fromFullStatsPattern({ pattern: scripts.count.p2sh, unit: Unit.count }), + bottom: fromFullStatsPattern({ + pattern: scripts.count.p2sh, + unit: Unit.count, + }), }, { name: "P2MS", title: "P2MS Output Count", - bottom: fromFullStatsPattern({ pattern: scripts.count.p2ms, unit: Unit.count }), + bottom: fromFullStatsPattern({ + pattern: scripts.count.p2ms, + unit: Unit.count, + }), }, ], }, @@ -580,17 +707,26 @@ export function createChainSection(ctx) { { name: "All SegWit", title: "SegWit Output Count", - bottom: fromFullStatsPattern({ pattern: scripts.count.segwit, unit: Unit.count }), + bottom: fromFullStatsPattern({ + pattern: scripts.count.segwit, + unit: Unit.count, + }), }, { name: "P2WPKH", title: "P2WPKH Output Count", - bottom: fromFullStatsPattern({ pattern: scripts.count.p2wpkh, unit: Unit.count }), + bottom: fromFullStatsPattern({ + pattern: scripts.count.p2wpkh, + unit: Unit.count, + }), }, { name: "P2WSH", title: "P2WSH Output Count", - bottom: fromFullStatsPattern({ pattern: scripts.count.p2wsh, unit: Unit.count }), + bottom: fromFullStatsPattern({ + pattern: scripts.count.p2wsh, + unit: Unit.count, + }), }, ], }, @@ -601,12 +737,18 @@ export function createChainSection(ctx) { { name: "P2TR", title: "P2TR Output Count", - bottom: fromFullStatsPattern({ pattern: scripts.count.p2tr, unit: Unit.count }), + bottom: fromFullStatsPattern({ + pattern: scripts.count.p2tr, + unit: Unit.count, + }), }, { name: "P2A", title: "P2A Output Count", - bottom: fromFullStatsPattern({ pattern: scripts.count.p2a, unit: Unit.count }), + bottom: fromFullStatsPattern({ + pattern: scripts.count.p2a, + unit: Unit.count, + }), }, ], }, @@ -617,17 +759,26 @@ export function createChainSection(ctx) { { name: "OP_RETURN", title: "OP_RETURN Output Count", - bottom: fromFullStatsPattern({ pattern: scripts.count.opreturn, unit: Unit.count }), + bottom: fromFullStatsPattern({ + pattern: scripts.count.opreturn, + unit: Unit.count, + }), }, { name: "Empty", title: "Empty Output Count", - bottom: fromFullStatsPattern({ pattern: scripts.count.emptyoutput, unit: Unit.count }), + bottom: fromFullStatsPattern({ + pattern: scripts.count.emptyoutput, + unit: Unit.count, + }), }, { name: "Unknown", title: "Unknown Output Count", - bottom: fromFullStatsPattern({ pattern: scripts.count.unknownoutput, unit: Unit.count }), + bottom: fromFullStatsPattern({ + pattern: scripts.count.unknownoutput, + unit: Unit.count, + }), }, ], }, @@ -701,7 +852,10 @@ export function createChainSection(ctx) { { name: "Circulating", title: "Circulating Supply", - bottom: fromSupplyPattern({ pattern: supply.circulating, title: "Supply" }), + bottom: fromSupplyPattern({ + pattern: supply.circulating, + title: "Supply", + }), }, { name: "Inflation", @@ -769,9 +923,18 @@ export function createChainSection(ctx) { name: "Fee", title: "Transaction Fees", bottom: [ - ...fromSumStatsPattern({ pattern: transactions.fees.fee.bitcoin, unit: Unit.btc }), - ...fromSumStatsPattern({ pattern: transactions.fees.fee.sats, unit: Unit.sats }), - ...fromSumStatsPattern({ pattern: transactions.fees.fee.dollars, unit: Unit.usd }), + ...fromSumStatsPattern({ + pattern: transactions.fees.fee.bitcoin, + unit: Unit.btc, + }), + ...fromSumStatsPattern({ + pattern: transactions.fees.fee.sats, + unit: Unit.sats, + }), + ...fromSumStatsPattern({ + pattern: transactions.fees.fee.dollars, + unit: Unit.usd, + }), line({ metric: blocks.rewards.feeDominance, name: "Dominance", @@ -873,7 +1036,8 @@ export function createChainSection(ctx) { defaultActive: t.defaultActive, }), line({ - metric: distribution.addressActivity[t.key][a.key].average, + metric: + distribution.addressActivity[t.key][a.key].average, name: t.name, color: t.color, unit: Unit.count, @@ -935,12 +1099,11 @@ export function createChainSection(ctx) { unit: Unit.hashRate, defaultActive: false, }), - line({ + dotted({ metric: blocks.difficulty.asHash, name: "Difficulty", color: colors.default, unit: Unit.hashRate, - options: { lineStyle: 1 }, }), ], }, @@ -1025,19 +1188,17 @@ export function createChainSection(ctx) { color: colors.yellow, unit: Unit.percentage, }), - line({ + dotted({ metric: blocks.mining.hashPriceThsMin, name: "TH/s Min", color: colors.red, unit: Unit.usdPerThsPerDay, - options: { lineStyle: 1 }, }), - line({ + dotted({ metric: blocks.mining.hashPricePhsMin, name: "PH/s Min", color: colors.red, unit: Unit.usdPerPhsPerDay, - options: { lineStyle: 1 }, }), ], }, @@ -1063,19 +1224,17 @@ export function createChainSection(ctx) { color: colors.yellow, unit: Unit.percentage, }), - line({ + dotted({ metric: blocks.mining.hashValueThsMin, name: "TH/s Min", color: colors.red, unit: Unit.satsPerThsPerDay, - options: { lineStyle: 1 }, }), - line({ + dotted({ metric: blocks.mining.hashValuePhsMin, name: "PH/s Min", color: colors.red, unit: Unit.satsPerPhsPerDay, - options: { lineStyle: 1 }, }), ], }, diff --git a/website/scripts/options/full.js b/website/scripts/options/full.js index bcd2289c3..f69192844 100644 --- a/website/scripts/options/full.js +++ b/website/scripts/options/full.js @@ -108,6 +108,11 @@ export function initOptions(brk) { for (let i = 0; i < arr.length; i++) { const blueprint = arr[i]; + // Check for undefined metric + if (!blueprint.metric) { + throw new Error(`Blueprint has undefined metric: ${blueprint.title}`); + } + // Check for price pattern blueprint (has dollars/sats sub-metrics) // Use unknown cast for safe property access check const maybePriceMetric = /** @type {{ dollars?: AnyMetricPattern, sats?: AnyMetricPattern }} */ ( diff --git a/website/scripts/options/investing.js b/website/scripts/options/investing.js new file mode 100644 index 000000000..805a0013a --- /dev/null +++ b/website/scripts/options/investing.js @@ -0,0 +1,749 @@ +/** Investing section - Investment strategy tools and analysis */ + +import { Unit } from "../utils/units.js"; +import { priceLine } from "./constants.js"; +import { line, baseline, price, dotted } from "./series.js"; +import { satsBtcUsd } from "./shared.js"; +import { periodIdToName } from "./utils.js"; + +/** + * Create Investing section + * @param {PartialContext} ctx + * @returns {PartialOptionsGroup} + */ +export function createInvestingSection(ctx) { + const { brk } = ctx; + const { market } = brk.metrics; + const { dca, lookback, returns } = market; + + return { + name: "Investing", + tree: [ + createDcaVsLumpSumSection(ctx, { dca, lookback, returns }), + createDcaByPeriodSection(ctx, { dca }), + createLumpSumByPeriodSection(ctx, { dca, lookback }), + createDcaByStartYearSection(ctx, { dca }), + ], + }; +} + +/** Period configuration by term group */ +const PERIODS = { + short: [ + { id: "1w", key: /** @type {const} */ ("_1w") }, + { id: "1m", key: /** @type {const} */ ("_1m") }, + { id: "3m", key: /** @type {const} */ ("_3m") }, + { id: "6m", key: /** @type {const} */ ("_6m") }, + ], + medium: [ + { id: "1y", key: /** @type {const} */ ("_1y") }, + { id: "2y", key: /** @type {const} */ ("_2y") }, + { id: "3y", key: /** @type {const} */ ("_3y") }, + ], + long: [ + { id: "4y", key: /** @type {const} */ ("_4y") }, + { id: "5y", key: /** @type {const} */ ("_5y") }, + { id: "6y", key: /** @type {const} */ ("_6y") }, + { id: "8y", key: /** @type {const} */ ("_8y") }, + { id: "10y", key: /** @type {const} */ ("_10y") }, + ], +}; + +const ALL_PERIODS = [...PERIODS.short, ...PERIODS.medium, ...PERIODS.long]; + +/** DCA year classes by decade */ +const YEAR_GROUPS = { + _2020s: /** @type {const} */ ([2026, 2025, 2024, 2023, 2022, 2021, 2020]), + _2010s: /** @type {const} */ ([2019, 2018, 2017, 2016, 2015]), +}; + +const ALL_YEARS = [...YEAR_GROUPS._2020s, ...YEAR_GROUPS._2010s]; + +/** @typedef {ReturnType} YearClass */ + +/** + * Build DCA class data from year + * @param {Colors} colors + * @param {MarketDca} dca + * @param {number} year + */ +function buildYearClass(colors, dca, year) { + const key = /** @type {keyof Colors["dcaYears"]} */ (`_${year}`); + return { + year, + color: colors.dcaYears[key], + costBasis: dca.classAveragePrice[key], + returns: dca.classReturns[key], + stack: dca.classStack[key], + daysInProfit: dca.classDaysInProfit[key], + daysInLoss: dca.classDaysInLoss[key], + minReturn: dca.classMinReturn[key], + maxReturn: dca.classMaxReturn[key], + }; +} + +/** + * Pattern for creating a single entry (period or year) + * @typedef {Object} SingleEntryPattern + * @property {string} name - Display name + * @property {string} [titlePrefix] - Prefix for chart titles (defaults to name) + * @property {Color} color - Primary color + * @property {AnyPricePattern} costBasis - Cost basis metric + * @property {AnyMetricPattern} returns - Returns metric + * @property {AnyMetricPattern} minReturn - Min return metric + * @property {AnyMetricPattern} maxReturn - Max return metric + * @property {AnyMetricPattern} daysInProfit - Days in profit metric + * @property {AnyMetricPattern} daysInLoss - Days in loss metric + * @property {AnyValuePattern} stack - Stack pattern + */ + +/** + * Item for compare charts + * @typedef {Object} CompareItem + * @property {string} name - Display name + * @property {Color} color - Item color + * @property {AnyPricePattern} costBasis - Cost basis metric + * @property {AnyMetricPattern} returns - Returns metric + * @property {AnyMetricPattern} daysInProfit - Days in profit metric + * @property {AnyMetricPattern} daysInLoss - Days in loss metric + * @property {AnyValuePattern} stack - Stack pattern + */ + +/** + * Create profitability folder for compare charts + * @param {string} context + * @param {CompareItem[]} items + */ +function createProfitabilityFolder(context, items) { + const top = items.map(({ name, color, costBasis }) => + price({ metric: costBasis, name, color }), + ); + return { + name: "Profitability", + tree: [ + { + name: "Days in Profit", + title: `Days in Profit: ${context}`, + top, + bottom: items.map(({ name, color, daysInProfit }) => + line({ metric: daysInProfit, name, color, unit: Unit.days }), + ), + }, + { + name: "Days in Loss", + title: `Days in Loss: ${context}`, + top, + bottom: items.map(({ name, color, daysInLoss }) => + line({ metric: daysInLoss, name, color, unit: Unit.days }), + ), + }, + ], + }; +} + +/** + * Create compare folder from items + * @param {string} context + * @param {CompareItem[]} items + */ +function createCompareFolder(context, items) { + const topPane = items.map(({ name, color, costBasis }) => + price({ metric: costBasis, name, color }), + ); + return { + name: "Compare", + tree: [ + { + name: "Cost Basis", + title: `Cost Basis: ${context}`, + top: topPane, + }, + { + name: "Returns", + title: `Returns: ${context}`, + top: topPane, + bottom: items.map(({ name, color, returns }) => + baseline({ + metric: returns, + name, + color: [color, color], + unit: Unit.percentage, + }), + ), + }, + createProfitabilityFolder(context, items), + { + name: "Accumulated", + title: `Accumulated Value: ${context}`, + top: topPane, + bottom: items.flatMap(({ name, color, stack }) => + satsBtcUsd({ pattern: stack, name, color }), + ), + }, + ], + }; +} + +/** + * Create a single entry from a pattern + * @param {Colors} colors + * @param {SingleEntryPattern} pattern + */ +function createSingleEntry(colors, pattern) { + const { + name, + titlePrefix = name, + color, + costBasis, + returns, + minReturn, + maxReturn, + daysInProfit, + daysInLoss, + stack, + } = pattern; + const top = [price({ metric: costBasis, name: "Cost Basis", color })]; + return { + name, + tree: [ + { name: "Cost Basis", title: `Cost Basis: ${titlePrefix}`, top }, + { + name: "Returns", + title: `Returns: ${titlePrefix}`, + top, + bottom: [ + baseline({ metric: returns, name: "Current", unit: Unit.percentage }), + dotted({ + metric: maxReturn, + name: "Max", + color: colors.green, + unit: Unit.percentage, + defaultActive: false, + }), + dotted({ + metric: minReturn, + name: "Min", + color: colors.red, + unit: Unit.percentage, + defaultActive: false, + }), + ], + }, + { + name: "Profitability", + title: `Profitability: ${titlePrefix}`, + top, + bottom: [ + line({ + metric: daysInProfit, + name: "Days in Profit", + color: colors.green, + unit: Unit.days, + }), + line({ + metric: daysInLoss, + name: "Days in Loss", + color: colors.red, + unit: Unit.days, + }), + ], + }, + { + name: "Accumulated", + title: `Accumulated Value: ${titlePrefix}`, + top, + bottom: satsBtcUsd({ pattern: stack, name: "Value" }), + }, + ], + }; +} + +/** + * Create DCA vs Lump Sum section + * @param {PartialContext} ctx + * @param {Object} args + * @param {Market["dca"]} args.dca + * @param {Market["lookback"]} args.lookback + * @param {Market["returns"]} args.returns + */ +export function createDcaVsLumpSumSection(ctx, { dca, lookback, returns }) { + const { colors } = ctx; + + // Chart builders + /** @param {AllPeriodKey} key */ + const topPane = (key) => [ + price({ + metric: dca.periodAveragePrice[key], + name: "DCA", + color: colors.green, + }), + price({ metric: lookback[key], name: "Lump Sum", color: colors.orange }), + ]; + + /** @param {string} name @param {AllPeriodKey} key */ + const costBasisChart = (name, key) => ({ + name: "Cost Basis", + title: `Cost Basis: ${name} DCA vs Lump Sum`, + top: topPane(key), + }); + + /** @param {string} name @param {AllPeriodKey} key */ + const returnsFolder = (name, key) => ({ + name: "Returns", + tree: [ + { + name: "Current", + title: `Returns: ${name} DCA vs Lump Sum`, + top: topPane(key), + bottom: [ + baseline({ + metric: dca.periodReturns[key], + name: "DCA", + unit: Unit.percentage, + }), + baseline({ + metric: dca.periodLumpSumReturns[key], + name: "Lump Sum", + color: [colors.cyan, colors.orange], + unit: Unit.percentage, + }), + ], + }, + { + name: "Max", + title: `Max Return: ${name} DCA vs Lump Sum`, + top: topPane(key), + bottom: [ + baseline({ + metric: dca.periodMaxReturn[key], + name: "DCA", + unit: Unit.percentage, + }), + baseline({ + metric: dca.periodLumpSumMaxReturn[key], + name: "Lump Sum", + color: [colors.cyan, colors.orange], + unit: Unit.percentage, + }), + ], + }, + { + name: "Min", + title: `Min Return: ${name} DCA vs Lump Sum`, + top: topPane(key), + bottom: [ + baseline({ + metric: dca.periodMinReturn[key], + name: "DCA", + unit: Unit.percentage, + }), + baseline({ + metric: dca.periodLumpSumMinReturn[key], + name: "Lump Sum", + color: [colors.cyan, colors.orange], + unit: Unit.percentage, + }), + ], + }, + ], + }); + + /** @param {string} name @param {LongPeriodKey} key */ + const returnsFolderWithCagr = (name, key) => ({ + name: "Returns", + tree: [ + { + name: "Current", + title: `Returns: ${name} DCA vs Lump Sum`, + top: topPane(key), + bottom: [ + baseline({ + metric: dca.periodReturns[key], + name: "DCA", + unit: Unit.percentage, + }), + baseline({ + metric: dca.periodLumpSumReturns[key], + name: "Lump Sum", + color: [colors.cyan, colors.orange], + unit: Unit.percentage, + }), + line({ + metric: dca.periodCagr[key], + name: "DCA CAGR", + color: colors.purple, + unit: Unit.percentage, + defaultActive: false, + }), + line({ + metric: returns.cagr[key], + name: "Lump Sum CAGR", + color: colors.indigo, + unit: Unit.percentage, + defaultActive: false, + }), + priceLine({ ctx, unit: Unit.percentage }), + ], + }, + { + name: "Max", + title: `Max Return: ${name} DCA vs Lump Sum`, + top: topPane(key), + bottom: [ + line({ + metric: dca.periodMaxReturn[key], + name: "DCA", + color: colors.green, + unit: Unit.percentage, + }), + line({ + metric: dca.periodLumpSumMaxReturn[key], + name: "Lump Sum", + color: colors.orange, + unit: Unit.percentage, + }), + ], + }, + { + name: "Min", + title: `Min Return: ${name} DCA vs Lump Sum`, + top: topPane(key), + bottom: [ + line({ + metric: dca.periodMinReturn[key], + name: "DCA", + color: colors.green, + unit: Unit.percentage, + }), + line({ + metric: dca.periodLumpSumMinReturn[key], + name: "Lump Sum", + color: colors.orange, + unit: Unit.percentage, + }), + ], + }, + ], + }); + + /** @param {string} name @param {AllPeriodKey} key */ + const profitabilityFolder = (name, key) => ({ + name: "Profitability", + tree: [ + { + name: "Days in Profit", + title: `Days in Profit: ${name} DCA vs Lump Sum`, + top: topPane(key), + bottom: [ + line({ + metric: dca.periodDaysInProfit[key], + name: "DCA", + color: colors.green, + unit: Unit.days, + }), + line({ + metric: dca.periodLumpSumDaysInProfit[key], + name: "Lump Sum", + color: colors.orange, + unit: Unit.days, + }), + ], + }, + { + name: "Days in Loss", + title: `Days in Loss: ${name} DCA vs Lump Sum`, + top: topPane(key), + bottom: [ + line({ + metric: dca.periodDaysInLoss[key], + name: "DCA", + color: colors.green, + unit: Unit.days, + }), + line({ + metric: dca.periodLumpSumDaysInLoss[key], + name: "Lump Sum", + color: colors.orange, + unit: Unit.days, + }), + ], + }, + ], + }); + + /** @param {string} name @param {AllPeriodKey} key */ + const stackChart = (name, key) => ({ + name: "Accumulated", + title: `Accumulated Value: ${name} DCA vs Lump Sum`, + top: topPane(key), + bottom: [ + ...satsBtcUsd({ + pattern: dca.periodStack[key], + name: "DCA", + color: colors.green, + }), + ...satsBtcUsd({ + pattern: dca.periodLumpSumStack[key], + name: "Lump Sum", + color: colors.orange, + }), + ], + }); + + /** + * Check if a period key has CAGR data + * @param {AllPeriodKey} key + * @returns {key is LongPeriodKey} + */ + const hasCagr = (key) => key in dca.periodCagr; + + /** + * Create individual period entry + * @param {{ id: string, key: AllPeriodKey }} period + */ + const createPeriodEntry = ({ id, key }) => { + const name = periodIdToName(id, true); + return { + name, + tree: [ + costBasisChart(name, key), + hasCagr(key) + ? returnsFolderWithCagr(name, key) + : returnsFolder(name, key), + profitabilityFolder(name, key), + stackChart(name, key), + ], + }; + }; + + /** + * Create term group + * @param {string} name + * @param {string} title + * @param {{ id: string, key: AllPeriodKey }[]} periods + */ + const createTermGroup = (name, title, periods) => ({ + name, + title, + tree: periods.map(createPeriodEntry), + }); + + return { + name: "DCA vs Lump Sum", + title: "Compare Investment Strategies", + tree: [ + createTermGroup("Short Term", "Under 1 Year", PERIODS.short), + createTermGroup("Medium Term", "1-3 Years", PERIODS.medium), + createTermGroup("Long Term", "4+ Years", PERIODS.long), + ], + }; +} + +/** + * Create DCA by Period section (DCA only, no Lump Sum comparison) + * @param {PartialContext} ctx + * @param {Object} args + * @param {Market["dca"]} args.dca + */ +export function createDcaByPeriodSection(ctx, { dca }) { + const { colors } = ctx; + + /** + * Create compare charts for a set of periods + * @param {string} context + * @param {{ id: string, key: AllPeriodKey }[]} periods + */ + const createCompare = (context, periods) => + createCompareFolder( + context, + periods.map(({ id, key }) => ({ + name: id, + color: colors.dcaPeriods[key], + costBasis: dca.periodAveragePrice[key], + returns: dca.periodReturns[key], + daysInProfit: dca.periodDaysInProfit[key], + daysInLoss: dca.periodDaysInLoss[key], + stack: dca.periodStack[key], + })), + ); + + /** + * Create individual period entry (DCA only) + * @param {{ id: string, key: AllPeriodKey }} period + */ + const createPeriodEntry = ({ id, key }) => { + const name = periodIdToName(id, true); + return createSingleEntry(colors, { + name, + titlePrefix: `${name} DCA`, + color: colors.dcaPeriods[key], + costBasis: dca.periodAveragePrice[key], + returns: dca.periodReturns[key], + maxReturn: dca.periodMaxReturn[key], + minReturn: dca.periodMinReturn[key], + daysInProfit: dca.periodDaysInProfit[key], + daysInLoss: dca.periodDaysInLoss[key], + stack: dca.periodStack[key], + }); + }; + + /** @param {string} name @param {string} title @param {{ id: string, key: AllPeriodKey }[]} periods */ + const createTermGroup = (name, title, periods) => ({ + name, + title, + tree: [ + createCompare(`${name} DCA`, periods), + ...periods.map(createPeriodEntry), + ], + }); + + return { + name: "DCA by Period", + title: "DCA Performance by Investment Period", + tree: [ + createCompare("All Periods DCA", ALL_PERIODS), + createTermGroup("Short Term", "Under 1 Year", PERIODS.short), + createTermGroup("Medium Term", "1-3 Years", PERIODS.medium), + createTermGroup("Long Term", "4+ Years", PERIODS.long), + ], + }; +} + +/** + * Create Lump Sum by Period section + * @param {PartialContext} ctx + * @param {Object} args + * @param {Market["dca"]} args.dca + * @param {Market["lookback"]} args.lookback + */ +export function createLumpSumByPeriodSection(ctx, { dca, lookback }) { + const { colors } = ctx; + + /** + * Create compare charts for a set of periods + * @param {string} context + * @param {{ id: string, key: AllPeriodKey }[]} periods + */ + const createCompare = (context, periods) => + createCompareFolder( + context, + periods.map(({ id, key }) => ({ + name: id, + color: colors.dcaPeriods[key], + costBasis: lookback[key], + returns: dca.periodLumpSumReturns[key], + daysInProfit: dca.periodLumpSumDaysInProfit[key], + daysInLoss: dca.periodLumpSumDaysInLoss[key], + stack: dca.periodLumpSumStack[key], + })), + ); + + /** + * Create individual period entry (Lump Sum only) + * @param {{ id: string, key: AllPeriodKey }} period + */ + const createPeriodEntry = ({ id, key }) => { + const name = periodIdToName(id, true); + return createSingleEntry(colors, { + name, + titlePrefix: `${name} Lump Sum`, + color: colors.dcaPeriods[key], + costBasis: lookback[key], + returns: dca.periodLumpSumReturns[key], + maxReturn: dca.periodLumpSumMaxReturn[key], + minReturn: dca.periodLumpSumMinReturn[key], + daysInProfit: dca.periodLumpSumDaysInProfit[key], + daysInLoss: dca.periodLumpSumDaysInLoss[key], + stack: dca.periodLumpSumStack[key], + }); + }; + + /** @param {string} name @param {string} title @param {{ id: string, key: AllPeriodKey }[]} periods */ + const createTermGroup = (name, title, periods) => ({ + name, + title, + tree: [ + createCompare(`${name} Lump Sum`, periods), + ...periods.map(createPeriodEntry), + ], + }); + + return { + name: "Lump Sum by Period", + title: "Lump Sum Performance by Investment Period", + tree: [ + createCompare("All Periods Lump Sum", ALL_PERIODS), + createTermGroup("Short Term", "Under 1 Year", PERIODS.short), + createTermGroup("Medium Term", "1-3 Years", PERIODS.medium), + createTermGroup("Long Term", "4+ Years", PERIODS.long), + ], + }; +} + +/** + * Create DCA by Start Year section + * @param {PartialContext} ctx + * @param {Object} args + * @param {Market["dca"]} args.dca + */ +export function createDcaByStartYearSection(ctx, { dca }) { + const { colors } = ctx; + + /** + * Convert YearClass to CompareItem + * @param {YearClass} c + * @returns {CompareItem} + */ + const toCompareItem = (c) => ({ + name: `${c.year}`, + color: c.color, + costBasis: c.costBasis, + returns: c.returns, + daysInProfit: c.daysInProfit, + daysInLoss: c.daysInLoss, + stack: c.stack, + }); + + /** + * Create individual year entry + * @param {YearClass} yearClass + */ + const createYearEntry = (yearClass) => + createSingleEntry(colors, { + name: `${yearClass.year}`, + titlePrefix: `${yearClass.year} DCA`, + color: yearClass.color, + costBasis: yearClass.costBasis, + returns: yearClass.returns, + maxReturn: yearClass.maxReturn, + minReturn: yearClass.minReturn, + daysInProfit: yearClass.daysInProfit, + daysInLoss: yearClass.daysInLoss, + stack: yearClass.stack, + }); + + /** @param {string} name @param {string} title @param {YearClass[]} classes */ + const createDecadeGroup = (name, title, classes) => ({ + name, + title, + tree: [ + createCompareFolder(`${name} DCA`, classes.map(toCompareItem)), + ...classes.map(createYearEntry), + ], + }); + + // Build all classes once, then filter by decade + const allClasses = ALL_YEARS.map((year) => buildYearClass(colors, dca, year)); + const classes2020s = allClasses.filter((c) => c.year >= 2020); + const classes2010s = allClasses.filter((c) => c.year < 2020); + + return { + name: "DCA by Start Year", + title: "DCA Performance by When You Started", + tree: [ + createCompareFolder("All Years DCA", allClasses.map(toCompareItem)), + createDecadeGroup("2020s", "2020-2026", classes2020s), + createDecadeGroup("2010s", "2015-2019", classes2010s), + ], + }; +} diff --git a/website/scripts/options/market/averages.js b/website/scripts/options/market/averages.js index b76a29da0..b319feb84 100644 --- a/website/scripts/options/market/averages.js +++ b/website/scripts/options/market/averages.js @@ -2,7 +2,7 @@ import { price } from "../series.js"; import { createPriceRatioCharts } from "../shared.js"; -import { periodIdToName } from "./utils.js"; +import { periodIdToName } from "../utils.js"; /** * @param {Colors} colors @@ -79,12 +79,16 @@ const COMPARISON_PERIODS = ["1w", "1m", "200d", "1y", "200w", "4y"]; */ function createCompareSection(smaAverages, emaAverages) { // Find matching SMA/EMA pairs - const pairs = COMPARISON_PERIODS.map(id => { - const sma = smaAverages.find(a => a.id === id); - const ema = emaAverages.find(a => a.id === id); + const pairs = COMPARISON_PERIODS.map((id) => { + const sma = smaAverages.find((a) => a.id === id); + const ema = emaAverages.find((a) => a.id === id); if (!sma || !ema) return null; return { id, sma, ema }; - }).filter(/** @type {(p: any) => p is { id: string, sma: ReturnType[number], ema: ReturnType[number] }} */ (p) => p !== null); + }).filter( + /** @type {(p: any) => p is { id: string, sma: ReturnType[number], ema: ReturnType[number] }} */ ( + p, + ) => p !== null, + ); return { name: "Compare", @@ -93,8 +97,17 @@ function createCompareSection(smaAverages, emaAverages) { name: "All Periods", title: "SMA vs EMA Comparison", top: pairs.flatMap(({ sma, ema }) => [ - price({ metric: sma.ratio.price, name: `${sma.id} SMA`, color: sma.color }), - price({ metric: ema.ratio.price, name: `${ema.id} EMA`, color: ema.color, options: { lineStyle: 1 } }), + price({ + metric: sma.ratio.price, + name: `${sma.id} SMA`, + color: sma.color, + }), + price({ + metric: ema.ratio.price, + name: `${ema.id} EMA`, + color: ema.color, + style: 1, + }), ]), }, ...pairs.map(({ id, sma, ema }) => ({ @@ -102,7 +115,12 @@ function createCompareSection(smaAverages, emaAverages) { title: `${periodIdToName(id, true)} SMA vs EMA`, top: [ price({ metric: sma.ratio.price, name: "SMA", color: sma.color }), - price({ metric: ema.ratio.price, name: "EMA", color: ema.color, options: { lineStyle: 1 } }), + price({ + metric: ema.ratio.price, + name: "EMA", + color: ema.color, + style: 1, + }), ], })), ], @@ -123,8 +141,12 @@ export function createAveragesSection(ctx, movingAverage) { * @param {ReturnType | ReturnType} averages */ const createSubSection = (label, averages) => { - const commonAverages = averages.filter(({ id }) => COMMON_PERIODS.includes(id)); - const moreAverages = averages.filter(({ id }) => !COMMON_PERIODS.includes(id)); + const commonAverages = averages.filter(({ id }) => + COMMON_PERIODS.includes(id), + ); + const moreAverages = averages.filter( + ({ id }) => !COMMON_PERIODS.includes(id), + ); return { name: label, diff --git a/website/scripts/options/market/index.js b/website/scripts/options/market/index.js index 1fd51d61f..49d42d20a 100644 --- a/website/scripts/options/market/index.js +++ b/website/scripts/options/market/index.js @@ -8,10 +8,6 @@ import { createMomentumSection } from "./momentum.js"; import { createVolatilitySection } from "./volatility.js"; import { createBandsSection } from "./bands.js"; import { createValuationSection } from "./onchain.js"; -import { - createDcaVsLumpSumSection, - createDcaByYearSection, -} from "./investing.js"; /** * Create Market section @@ -21,16 +17,7 @@ import { export function createMarketSection(ctx) { const { colors, brk } = ctx; const { market, supply } = brk.metrics; - const { - movingAverage, - ath, - returns, - volatility, - range, - dca, - lookback, - indicators, - } = market; + const { movingAverage, ath, returns, volatility, range, indicators } = market; return { name: "Market", @@ -183,12 +170,6 @@ export function createMarketSection(ctx) { // Valuation createValuationSection(ctx, { indicators, movingAverage }), - - // DCA vs Lump Sum - createDcaVsLumpSumSection(ctx, { dca, lookback, returns }), - - // DCA by Year - createDcaByYearSection(ctx, { dca }), ], }; } diff --git a/website/scripts/options/market/investing.js b/website/scripts/options/market/investing.js deleted file mode 100644 index 271b594d0..000000000 --- a/website/scripts/options/market/investing.js +++ /dev/null @@ -1,425 +0,0 @@ -/** Investing section (DCA) */ - -import { Unit } from "../../utils/units.js"; -import { priceLine } from "../constants.js"; -import { line, baseline, price } from "../series.js"; -import { satsBtcUsd } from "../shared.js"; -import { periodIdToName } from "./utils.js"; - -/** - * Build DCA classes data array - * @param {Colors} colors - * @param {MarketDca} dca - */ -export function buildDcaClasses(colors, dca) { - return /** @type {const} */ ([ - [2026, "rose", true], - [2025, "pink", true], - [2024, "fuchsia", true], - [2023, "purple", true], - [2022, "blue", true], - [2021, "sky", true], - [2020, "teal", true], - [2019, "green", true], - [2018, "yellow", true], - [2017, "orange", true], - [2016, "red", false], - [2015, "pink", false], - ]).map(([year, colorKey, defaultActive]) => ({ - year, - color: colors[colorKey], - defaultActive, - costBasis: dca.classAveragePrice[`_${year}`], - returns: dca.classReturns[`_${year}`], - stack: dca.classStack[`_${year}`], - daysInProfit: dca.classDaysInProfit[`_${year}`], - daysInLoss: dca.classDaysInLoss[`_${year}`], - maxDrawdown: dca.classMaxDrawdown[`_${year}`], - maxReturn: dca.classMaxReturn[`_${year}`], - })); -} - -/** - * Create DCA vs Lump Sum section - * @param {PartialContext} ctx - * @param {Object} args - * @param {Market["dca"]} args.dca - * @param {Market["lookback"]} args.lookback - * @param {Market["returns"]} args.returns - */ -export function createDcaVsLumpSumSection(ctx, { dca, lookback, returns }) { - const { colors } = ctx; - - /** - * @param {string} name - * @param {AllPeriodKey} key - */ - const costBasisChart = (name, key) => ({ - name: "Cost Basis", - title: `${name} Cost Basis`, - top: [ - price({ - metric: dca.periodAveragePrice[key], - name: "DCA", - color: colors.green, - }), - price({ - metric: lookback[key], - name: "Lump sum", - color: colors.orange, - }), - ], - }); - - /** - * @param {string} name - * @param {AllPeriodKey} key - */ - const daysInProfitChart = (name, key) => ({ - name: "Days in Profit", - title: `${name} Days in Profit`, - top: [ - price({ metric: dca.periodAveragePrice[key], name: "DCA", color: colors.green }), - price({ metric: lookback[key], name: "Lump sum", color: colors.orange }), - ], - bottom: [ - line({ metric: dca.periodDaysInProfit[key], name: "DCA", color: colors.green, unit: Unit.days }), - line({ metric: dca.periodLumpSumDaysInProfit[key], name: "Lump sum", color: colors.orange, unit: Unit.days }), - ], - }); - - /** - * @param {string} name - * @param {AllPeriodKey} key - */ - const daysInLossChart = (name, key) => ({ - name: "Days in Loss", - title: `${name} Days in Loss`, - top: [ - price({ metric: dca.periodAveragePrice[key], name: "DCA", color: colors.green }), - price({ metric: lookback[key], name: "Lump sum", color: colors.orange }), - ], - bottom: [ - line({ metric: dca.periodDaysInLoss[key], name: "DCA", color: colors.red, unit: Unit.days }), - line({ metric: dca.periodLumpSumDaysInLoss[key], name: "Lump sum", color: colors.orange, unit: Unit.days }), - ], - }); - - /** - * @param {string} name - * @param {AllPeriodKey} key - */ - const maxDrawdownChart = (name, key) => ({ - name: "Max Drawdown", - title: `${name} Max Drawdown`, - top: [ - price({ metric: dca.periodAveragePrice[key], name: "DCA", color: colors.green }), - price({ metric: lookback[key], name: "Lump sum", color: colors.orange }), - ], - bottom: [ - line({ metric: dca.periodMaxDrawdown[key], name: "DCA", color: colors.green, unit: Unit.percentage }), - line({ metric: dca.periodLumpSumMaxDrawdown[key], name: "Lump sum", color: colors.orange, unit: Unit.percentage }), - ], - }); - - /** - * @param {string} name - * @param {AllPeriodKey} key - */ - const maxReturnChart = (name, key) => ({ - name: "Max Return", - title: `${name} Max Return`, - top: [ - price({ metric: dca.periodAveragePrice[key], name: "DCA", color: colors.green }), - price({ metric: lookback[key], name: "Lump sum", color: colors.orange }), - ], - bottom: [ - line({ metric: dca.periodMaxReturn[key], name: "DCA", color: colors.green, unit: Unit.percentage }), - line({ metric: dca.periodLumpSumMaxReturn[key], name: "Lump sum", color: colors.orange, unit: Unit.percentage }), - ], - }); - - /** - * @param {string} name - * @param {AllPeriodKey} key - */ - const stackChart = (name, key) => ({ - name: "Stack", - title: `${name} Stack`, - bottom: [ - ...satsBtcUsd({ pattern: dca.periodStack[key], name: "DCA", color: colors.green }), - ...satsBtcUsd({ pattern: dca.periodLumpSumStack[key], name: "Lump sum", color: colors.orange }), - ], - }); - - /** - * @param {string} id - * @param {ShortPeriodKey} key - */ - const createPeriodTree = (id, key) => { - const name = periodIdToName(id, true); - return { - name, - tree: [ - costBasisChart(name, key), - { - name: "Returns", - title: `${name} Returns`, - bottom: [ - baseline({ - metric: dca.periodReturns[key], - name: "DCA", - unit: Unit.percentage, - }), - baseline({ - metric: dca.periodLumpSumReturns[key], - name: "Lump sum", - color: [colors.cyan, colors.orange], - unit: Unit.percentage, - }), - ], - }, - { - name: "Profitability", - tree: [ - daysInProfitChart(name, key), - daysInLossChart(name, key), - maxDrawdownChart(name, key), - maxReturnChart(name, key), - ], - }, - stackChart(name, key), - ], - }; - }; - - /** - * @param {string} id - * @param {LongPeriodKey} key - */ - const createPeriodTreeWithCagr = (id, key) => { - const name = periodIdToName(id, true); - return { - name, - tree: [ - costBasisChart(name, key), - { - name: "Returns", - title: `${name} Returns`, - bottom: [ - baseline({ - metric: dca.periodReturns[key], - name: "DCA", - unit: Unit.percentage, - }), - baseline({ - metric: dca.periodLumpSumReturns[key], - name: "Lump sum", - color: [colors.cyan, colors.orange], - unit: Unit.percentage, - }), - line({ - metric: dca.periodCagr[key], - name: "DCA CAGR", - color: colors.purple, - unit: Unit.percentage, - defaultActive: false, - }), - line({ - metric: returns.cagr[key], - name: "Lump sum CAGR", - color: colors.indigo, - unit: Unit.percentage, - defaultActive: false, - }), - priceLine({ ctx, unit: Unit.percentage }), - ], - }, - { - name: "Profitability", - tree: [ - daysInProfitChart(name, key), - daysInLossChart(name, key), - maxDrawdownChart(name, key), - maxReturnChart(name, key), - ], - }, - stackChart(name, key), - ], - }; - }; - - return { - name: "DCA vs Lump Sum", - tree: [ - createPeriodTree("1w", "_1w"), - createPeriodTree("1m", "_1m"), - createPeriodTree("3m", "_3m"), - createPeriodTree("6m", "_6m"), - createPeriodTree("1y", "_1y"), - createPeriodTreeWithCagr("2y", "_2y"), - createPeriodTreeWithCagr("3y", "_3y"), - createPeriodTreeWithCagr("4y", "_4y"), - createPeriodTreeWithCagr("5y", "_5y"), - createPeriodTreeWithCagr("6y", "_6y"), - createPeriodTreeWithCagr("8y", "_8y"), - createPeriodTreeWithCagr("10y", "_10y"), - ], - }; -} - -/** - * Create DCA by Year section - * @param {PartialContext} ctx - * @param {Object} args - * @param {Market["dca"]} args.dca - */ -export function createDcaByYearSection(ctx, { dca }) { - const { colors } = ctx; - const dcaClasses = buildDcaClasses(colors, dca); - - return { - name: "DCA by Year", - tree: [ - // Comparison charts (all years overlaid) - { - name: "Compare", - tree: [ - { - name: "Cost basis", - title: "DCA Cost Basis", - top: dcaClasses.map(({ year, color, defaultActive, costBasis }) => - price({ - metric: costBasis, - name: `${year}`, - color, - defaultActive, - }), - ), - }, - { - name: "Returns", - title: "DCA Returns", - bottom: dcaClasses.map(({ year, defaultActive, returns }) => - baseline({ - metric: returns, - name: `${year}`, - defaultActive, - unit: Unit.percentage, - }), - ), - }, - { - name: "Profitability", - title: "DCA Profitability", - bottom: [ - ...dcaClasses.map(({ year, color, defaultActive, daysInProfit }) => - line({ - metric: daysInProfit, - name: `${year} Days in Profit`, - color, - defaultActive, - unit: Unit.days, - }), - ), - ...dcaClasses.map(({ year, color, daysInLoss }) => - line({ - metric: daysInLoss, - name: `${year} Days in Loss`, - color, - defaultActive: false, - unit: Unit.days, - }), - ), - ], - }, - { - name: "Stack", - title: "DCA Stack", - bottom: dcaClasses.flatMap( - ({ year, color, defaultActive, stack }) => - satsBtcUsd({ pattern: stack, name: `${year}`, color, defaultActive }), - ), - }, - ], - }, - // Individual year charts - ...dcaClasses.map( - ({ - year, - color, - costBasis, - returns, - stack, - daysInProfit, - daysInLoss, - maxDrawdown, - maxReturn, - }) => ({ - name: `${year}`, - tree: [ - { - name: "Cost Basis", - title: `${year} Cost Basis`, - top: [ - price({ - metric: costBasis, - name: "Cost Basis", - color, - }), - ], - }, - { - name: "Returns", - title: `${year} Returns`, - bottom: [ - baseline({ - metric: returns, - name: "Returns", - unit: Unit.percentage, - }), - ], - }, - { - name: "Profitability", - title: `${year} Profitability`, - bottom: [ - line({ - metric: daysInProfit, - name: "Days in Profit", - color: colors.green, - unit: Unit.days, - }), - line({ - metric: daysInLoss, - name: "Days in Loss", - color: colors.red, - unit: Unit.days, - }), - line({ - metric: maxDrawdown, - name: "Max Drawdown", - color: colors.purple, - unit: Unit.percentage, - defaultActive: false, - }), - line({ - metric: maxReturn, - name: "Max Return", - color: colors.cyan, - unit: Unit.percentage, - defaultActive: false, - }), - ], - }, - { - name: "Stack", - title: `${year} Stack`, - bottom: satsBtcUsd({ pattern: stack, name: "Stack", color }), - }, - ], - }), - ), - ], - }; -} diff --git a/website/scripts/options/market/performance.js b/website/scripts/options/market/performance.js index 6370a4449..5ce122444 100644 --- a/website/scripts/options/market/performance.js +++ b/website/scripts/options/market/performance.js @@ -3,7 +3,7 @@ import { Unit } from "../../utils/units.js"; import { priceLine } from "../constants.js"; import { baseline } from "../series.js"; -import { periodIdToName } from "./utils.js"; +import { periodIdToName } from "../utils.js"; /** * Create Returns section @@ -41,7 +41,9 @@ export function createReturnsSection(ctx, returns) { */ const createPeriodChart = ([id, returnKey, cagrKey]) => { const priceReturns = returns.priceReturns[/** @type {K} */ (returnKey)]; - const cagr = cagrKey ? returns.cagr[/** @type {keyof typeof returns.cagr} */ (cagrKey)] : undefined; + const cagr = cagrKey + ? returns.cagr[/** @type {keyof typeof returns.cagr} */ (cagrKey)] + : undefined; const name = periodIdToName(id, true); return { name, @@ -75,13 +77,50 @@ export function createReturnsSection(ctx, returns) { name: "Compare", title: "Returns Comparison", bottom: [ - baseline({ metric: returns.priceReturns._1d, name: "1d", color: colors.red, unit: Unit.percentage }), - baseline({ metric: returns.priceReturns._1w, name: "1w", color: colors.orange, unit: Unit.percentage }), - baseline({ metric: returns.priceReturns._1m, name: "1m", color: colors.yellow, unit: Unit.percentage }), - baseline({ metric: returns.priceReturns._3m, name: "3m", color: colors.lime, unit: Unit.percentage, defaultActive: false }), - baseline({ metric: returns.priceReturns._6m, name: "6m", color: colors.green, unit: Unit.percentage, defaultActive: false }), - baseline({ metric: returns.priceReturns._1y, name: "1y", color: colors.teal, unit: Unit.percentage }), - baseline({ metric: returns.priceReturns._4y, name: "4y", color: colors.blue, unit: Unit.percentage }), + baseline({ + metric: returns.priceReturns._1d, + name: "1d", + color: colors.red, + unit: Unit.percentage, + }), + baseline({ + metric: returns.priceReturns._1w, + name: "1w", + color: colors.orange, + unit: Unit.percentage, + }), + baseline({ + metric: returns.priceReturns._1m, + name: "1m", + color: colors.yellow, + unit: Unit.percentage, + }), + baseline({ + metric: returns.priceReturns._3m, + name: "3m", + color: colors.lime, + unit: Unit.percentage, + defaultActive: false, + }), + baseline({ + metric: returns.priceReturns._6m, + name: "6m", + color: colors.green, + unit: Unit.percentage, + defaultActive: false, + }), + baseline({ + metric: returns.priceReturns._1y, + name: "1y", + color: colors.teal, + unit: Unit.percentage, + }), + baseline({ + metric: returns.priceReturns._4y, + name: "4y", + color: colors.blue, + unit: Unit.percentage, + }), priceLine({ ctx, unit: Unit.percentage }), ], }, diff --git a/website/scripts/options/mining.js b/website/scripts/options/mining.js new file mode 100644 index 000000000..655baaa4b --- /dev/null +++ b/website/scripts/options/mining.js @@ -0,0 +1,592 @@ +/** Mining section - Network security and miner economics */ + +import { Unit } from "../utils/units.js"; +import { priceLine } from "./constants.js"; +import { line, baseline, dots, dotted } from "./series.js"; +import { satsBtcUsd } from "./shared.js"; + +/** Major pools to show in Compare section (by current hashrate dominance) */ +const MAJOR_POOL_IDS = [ + "foundryusa", // ~32% - largest pool + "antpool", // ~18% - Bitmain-owned + "viabtc", // ~14% - independent + "f2pool", // ~10% - one of the oldest pools + "marapool", // MARA Holdings + "braiinspool", // formerly Slush Pool + "spiderpool", // growing Asian pool + "ocean", // decentralization-focused +]; + +/** + * AntPool & friends - pools sharing AntPool's block templates + * Based on b10c's research: https://b10c.me/blog/015-bitcoin-mining-centralization/ + * Collectively ~35-40% of network hashrate + */ +const ANTPOOL_AND_FRIENDS_IDS = [ + "antpool", // Bitmain-owned, template source + "poolin", // shares AntPool templates + "btccom", // CloverPool (formerly BTC.com) + "braiinspool", // shares AntPool templates + "ultimuspool", // shares AntPool templates + "binancepool", // shares AntPool templates + "secpool", // shares AntPool templates + "sigmapoolcom", // SigmaPool + "rawpool", // shares AntPool templates + "luxor", // shares AntPool templates +]; + +/** + * Create Mining section + * @param {PartialContext} ctx + * @returns {PartialOptionsGroup} + */ +export function createMiningSection(ctx) { + const { + colors, + brk, + fromSumStatsPattern, + fromCoinbasePattern, + fromValuePattern, + } = ctx; + const { blocks, transactions, pools } = brk.metrics; + + // Build pools tree dynamically + const poolEntries = Object.entries(pools.vecs); + const poolsTree = poolEntries.map(([key, pool]) => { + const poolName = + brk.POOL_ID_TO_POOL_NAME[ + /** @type {keyof typeof brk.POOL_ID_TO_POOL_NAME} */ (key.toLowerCase()) + ] || key; + return { + name: poolName, + tree: [ + { + name: "Dominance", + title: `Dominance: ${poolName}`, + bottom: [ + dots({ + metric: pool._24hDominance, + name: "24h", + color: colors.pink, + unit: Unit.percentage, + defaultActive: false, + }), + line({ + metric: pool._1wDominance, + name: "1w", + color: colors.red, + unit: Unit.percentage, + defaultActive: false, + }), + line({ + metric: pool._1mDominance, + name: "1m", + unit: Unit.percentage, + }), + line({ + metric: pool._1yDominance, + name: "1y", + color: colors.lime, + unit: Unit.percentage, + defaultActive: false, + }), + line({ + metric: pool.dominance, + name: "All Time", + color: colors.teal, + unit: Unit.percentage, + defaultActive: false, + }), + ], + }, + { + name: "Blocks Mined", + title: `Blocks Mined: ${poolName}`, + bottom: [ + dots({ + metric: pool.blocksMined.sum, + name: "Sum", + unit: Unit.count, + }), + line({ + metric: pool.blocksMined.cumulative, + name: "Cumulative", + color: colors.blue, + unit: Unit.count, + defaultActive: false, + }), + line({ + metric: pool._24hBlocksMined, + name: "24h sum", + color: colors.pink, + unit: Unit.count, + defaultActive: false, + }), + line({ + metric: pool._1wBlocksMined, + name: "1w sum", + color: colors.red, + unit: Unit.count, + defaultActive: false, + }), + line({ + metric: pool._1mBlocksMined, + name: "1m sum", + color: colors.pink, + unit: Unit.count, + defaultActive: false, + }), + line({ + metric: pool._1yBlocksMined, + name: "1y sum", + color: colors.purple, + unit: Unit.count, + defaultActive: false, + }), + ], + }, + { + name: "Rewards", + title: `Rewards: ${poolName}`, + bottom: [ + ...fromValuePattern({ + pattern: pool.coinbase, + title: "coinbase", + sumColor: colors.orange, + cumulativeColor: colors.red, + }), + ...fromValuePattern({ + pattern: pool.subsidy, + title: "subsidy", + sumColor: colors.lime, + cumulativeColor: colors.emerald, + }), + ...fromValuePattern({ + pattern: pool.fee, + title: "fee", + sumColor: colors.cyan, + cumulativeColor: colors.indigo, + }), + ], + }, + { + name: "Since Last Block", + title: `Since Last Block: ${poolName}`, + bottom: [ + line({ + metric: pool.blocksSinceBlock, + name: "Elapsed", + unit: Unit.blocks, + }), + line({ + metric: pool.daysSinceBlock, + name: "Elapsed", + unit: Unit.days, + }), + ], + }, + ], + }; + }); + + return { + name: "Mining", + tree: [ + // Hashrate + { + name: "Hashrate", + title: "Network Hashrate", + bottom: [ + dots({ + metric: blocks.mining.hashRate, + name: "Hashrate", + unit: Unit.hashRate, + }), + line({ + metric: blocks.mining.hashRate1wSma, + name: "1w SMA", + color: colors.red, + unit: Unit.hashRate, + defaultActive: false, + }), + line({ + metric: blocks.mining.hashRate1mSma, + name: "1m SMA", + color: colors.orange, + unit: Unit.hashRate, + defaultActive: false, + }), + line({ + metric: blocks.mining.hashRate2mSma, + name: "2m SMA", + color: colors.yellow, + unit: Unit.hashRate, + defaultActive: false, + }), + line({ + metric: blocks.mining.hashRate1ySma, + name: "1y SMA", + color: colors.lime, + unit: Unit.hashRate, + defaultActive: false, + }), + dotted({ + metric: blocks.difficulty.asHash, + name: "Difficulty", + color: colors.default, + unit: Unit.hashRate, + }), + ], + }, + + // Difficulty + { + name: "Difficulty", + tree: [ + { + name: "Current", + title: "Mining Difficulty", + bottom: [ + line({ + metric: blocks.difficulty.raw, + name: "Difficulty", + unit: Unit.difficulty, + }), + ], + }, + { + name: "Epoch", + title: "Difficulty Epoch", + bottom: [ + line({ + metric: blocks.difficulty.epoch, + name: "Epoch", + unit: Unit.epoch, + }), + ], + }, + { + name: "Adjustment", + title: "Difficulty Adjustment", + bottom: [ + baseline({ + metric: blocks.difficulty.adjustment, + name: "Change", + unit: Unit.percentage, + }), + priceLine({ ctx, number: 0, unit: Unit.percentage }), + ], + }, + { + name: "Countdown", + title: "Next Difficulty Adjustment", + bottom: [ + line({ + metric: blocks.difficulty.blocksBeforeNextAdjustment, + name: "Remaining", + color: colors.indigo, + unit: Unit.blocks, + }), + line({ + metric: blocks.difficulty.daysBeforeNextAdjustment, + name: "Remaining", + color: colors.purple, + unit: Unit.days, + }), + ], + }, + ], + }, + + // Revenue + { + name: "Revenue", + tree: [ + { + name: "Coinbase", + title: "Coinbase Rewards", + bottom: [ + ...fromCoinbasePattern({ pattern: blocks.rewards.coinbase }), + ...satsBtcUsd({ + pattern: blocks.rewards._24hCoinbaseSum, + name: "24h sum", + color: colors.pink, + defaultActive: false, + }), + ], + }, + { + name: "Subsidy", + title: "Block Subsidy", + bottom: [ + ...fromCoinbasePattern({ pattern: blocks.rewards.subsidy }), + line({ + metric: blocks.rewards.subsidyDominance, + name: "Dominance", + color: colors.purple, + unit: Unit.percentage, + }), + line({ + metric: blocks.rewards.subsidyUsd1ySma, + name: "1y SMA", + color: colors.lime, + unit: Unit.usd, + defaultActive: false, + }), + ], + }, + { + name: "Fees", + title: "Transaction Fee Revenue", + bottom: [ + ...fromSumStatsPattern({ + pattern: transactions.fees.fee.bitcoin, + unit: Unit.btc, + }), + ...fromSumStatsPattern({ + pattern: transactions.fees.fee.sats, + unit: Unit.sats, + }), + ...fromSumStatsPattern({ + pattern: transactions.fees.fee.dollars, + unit: Unit.usd, + }), + line({ + metric: blocks.rewards.feeDominance, + name: "Dominance", + color: colors.purple, + unit: Unit.percentage, + }), + ], + }, + { + name: "Unclaimed", + title: "Unclaimed Rewards", + bottom: fromValuePattern({ + pattern: blocks.rewards.unclaimedRewards, + title: "Unclaimed", + }), + }, + ], + }, + + // Economics + { + name: "Economics", + tree: [ + { + name: "Hash Price", + title: "Hash Price", + bottom: [ + line({ + metric: blocks.mining.hashPriceThs, + name: "TH/s", + color: colors.emerald, + unit: Unit.usdPerThsPerDay, + }), + line({ + metric: blocks.mining.hashPricePhs, + name: "PH/s", + color: colors.emerald, + unit: Unit.usdPerPhsPerDay, + }), + line({ + metric: blocks.mining.hashPriceRebound, + name: "Rebound", + color: colors.yellow, + unit: Unit.percentage, + }), + dotted({ + metric: blocks.mining.hashPriceThsMin, + name: "TH/s Min", + color: colors.red, + unit: Unit.usdPerThsPerDay, + }), + dotted({ + metric: blocks.mining.hashPricePhsMin, + name: "PH/s Min", + color: colors.red, + unit: Unit.usdPerPhsPerDay, + }), + ], + }, + { + name: "Hash Value", + title: "Hash Value", + bottom: [ + line({ + metric: blocks.mining.hashValueThs, + name: "TH/s", + color: colors.orange, + unit: Unit.satsPerThsPerDay, + }), + line({ + metric: blocks.mining.hashValuePhs, + name: "PH/s", + color: colors.orange, + unit: Unit.satsPerPhsPerDay, + }), + line({ + metric: blocks.mining.hashValueRebound, + name: "Rebound", + color: colors.yellow, + unit: Unit.percentage, + }), + dotted({ + metric: blocks.mining.hashValueThsMin, + name: "TH/s Min", + color: colors.red, + unit: Unit.satsPerThsPerDay, + }), + dotted({ + metric: blocks.mining.hashValuePhsMin, + name: "PH/s Min", + color: colors.red, + unit: Unit.satsPerPhsPerDay, + }), + ], + }, + ], + }, + + // Halving + { + name: "Halving", + tree: [ + { + name: "Countdown", + title: "Next Halving", + bottom: [ + line({ + metric: blocks.halving.blocksBeforeNextHalving, + name: "Remaining", + unit: Unit.blocks, + }), + line({ + metric: blocks.halving.daysBeforeNextHalving, + name: "Remaining", + color: colors.blue, + unit: Unit.days, + }), + ], + }, + { + name: "Epoch", + title: "Halving Epoch", + bottom: [ + line({ + metric: blocks.halving.epoch, + name: "Epoch", + unit: Unit.epoch, + }), + ], + }, + ], + }, + + // Pools + { + name: "Pools", + tree: [ + // Compare section (major pools only) + { + name: "Compare", + tree: [ + { + name: "Dominance", + title: "Dominance: Major Pools", + bottom: poolEntries + .filter(([key]) => MAJOR_POOL_IDS.includes(key.toLowerCase())) + .map(([key, pool]) => { + const poolName = + brk.POOL_ID_TO_POOL_NAME[ + /** @type {keyof typeof brk.POOL_ID_TO_POOL_NAME} */ ( + key.toLowerCase() + ) + ] || key; + return line({ + metric: pool._1mDominance, + name: poolName, + unit: Unit.percentage, + }); + }), + }, + { + name: "Blocks Mined", + title: "Blocks Mined: Major Pools (1m)", + bottom: poolEntries + .filter(([key]) => MAJOR_POOL_IDS.includes(key.toLowerCase())) + .map(([key, pool]) => { + const poolName = + brk.POOL_ID_TO_POOL_NAME[ + /** @type {keyof typeof brk.POOL_ID_TO_POOL_NAME} */ ( + key.toLowerCase() + ) + ] || key; + return line({ + metric: pool._1mBlocksMined, + name: poolName, + unit: Unit.count, + }); + }), + }, + ], + }, + // AntPool & friends - pools sharing block templates + { + name: "AntPool & Friends", + tree: [ + { + name: "Dominance", + title: "Dominance: AntPool & Friends", + bottom: poolEntries + .filter(([key]) => + ANTPOOL_AND_FRIENDS_IDS.includes(key.toLowerCase()), + ) + .map(([key, pool]) => { + const poolName = + brk.POOL_ID_TO_POOL_NAME[ + /** @type {keyof typeof brk.POOL_ID_TO_POOL_NAME} */ ( + key.toLowerCase() + ) + ] || key; + return line({ + metric: pool._1mDominance, + name: poolName, + unit: Unit.percentage, + }); + }), + }, + { + name: "Blocks Mined", + title: "Blocks Mined: AntPool & Friends (1m)", + bottom: poolEntries + .filter(([key]) => + ANTPOOL_AND_FRIENDS_IDS.includes(key.toLowerCase()), + ) + .map(([key, pool]) => { + const poolName = + brk.POOL_ID_TO_POOL_NAME[ + /** @type {keyof typeof brk.POOL_ID_TO_POOL_NAME} */ ( + key.toLowerCase() + ) + ] || key; + return line({ + metric: pool._1mBlocksMined, + name: poolName, + unit: Unit.count, + }); + }), + }, + ], + }, + // Individual pools + { + name: "Individual", + tree: poolsTree, + }, + ], + }, + ], + }; +} diff --git a/website/scripts/options/network.js b/website/scripts/options/network.js new file mode 100644 index 000000000..ec722266b --- /dev/null +++ b/website/scripts/options/network.js @@ -0,0 +1,696 @@ +/** Network section - On-chain activity and health */ + +import { Unit } from "../utils/units.js"; +import { priceLine } from "./constants.js"; +import { line, dots } from "./series.js"; +import { satsBtcUsd } from "./shared.js"; +import { spendableTypeColors } from "./colors/index.js"; + +/** + * Create Network section + * @param {PartialContext} ctx + * @returns {PartialOptionsGroup} + */ +export function createNetworkSection(ctx) { + const { + colors, + brk, + fromSumStatsPattern, + fromBaseStatsPattern, + fromFullStatsPattern, + fromStatsPattern, + fromCoinbasePattern, + fromValuePattern, + fromCountPattern, + fromSupplyPattern, + } = ctx; + const { + blocks, + transactions, + inputs, + outputs, + scripts, + supply, + distribution, + } = brk.metrics; + + // Address types for mapping (using spendableTypeColors for consistency) + /** @type {ReadonlyArray<{key: AddressableType, name: string, color: Color, defaultActive?: boolean}>} */ + const addressTypes = [ + { key: "p2pkh", name: "P2PKH", color: colors[spendableTypeColors.p2pkh] }, + { key: "p2sh", name: "P2SH", color: colors[spendableTypeColors.p2sh] }, + { key: "p2wpkh", name: "P2WPKH", color: colors[spendableTypeColors.p2wpkh] }, + { key: "p2wsh", name: "P2WSH", color: colors[spendableTypeColors.p2wsh] }, + { key: "p2tr", name: "P2TR", color: colors[spendableTypeColors.p2tr] }, + { key: "p2pk65", name: "P2PK65", color: colors[spendableTypeColors.p2pk65], defaultActive: false }, + { key: "p2pk33", name: "P2PK33", color: colors[spendableTypeColors.p2pk33], defaultActive: false }, + { key: "p2a", name: "P2A", color: colors[spendableTypeColors.p2a], defaultActive: false }, + ]; + + // Activity types for mapping + /** @type {ReadonlyArray<{key: "sending" | "receiving" | "both" | "reactivated" | "balanceIncreased" | "balanceDecreased", name: string, title: string, compareTitle: string}>} */ + const activityTypes = [ + { key: "sending", name: "Sending", title: "Sending Address Count", compareTitle: "Sending Address Count by Type" }, + { key: "receiving", name: "Receiving", title: "Receiving Address Count", compareTitle: "Receiving Address Count by Type" }, + { key: "both", name: "Both", title: "Addresses Sending & Receiving (Same Block)", compareTitle: "Addresses Sending & Receiving by Type" }, + { key: "reactivated", name: "Reactivated", title: "Reactivated Address Count (Was Empty)", compareTitle: "Reactivated Address Count by Type" }, + { key: "balanceIncreased", name: "Balance Increased", title: "Addresses with Increased Balance", compareTitle: "Addresses with Increased Balance by Type" }, + { key: "balanceDecreased", name: "Balance Decreased", title: "Addresses with Decreased Balance", compareTitle: "Addresses with Decreased Balance by Type" }, + ]; + + // Count types for comparison charts + /** @type {ReadonlyArray<{key: "addrCount" | "emptyAddrCount" | "totalAddrCount", name: string, title: string}>} */ + const countTypes = [ + { key: "addrCount", name: "Loaded", title: "Address Count by Type" }, + { key: "emptyAddrCount", name: "Empty", title: "Empty Address Count by Type" }, + { key: "totalAddrCount", name: "Total", title: "Total Address Count by Type" }, + ]; + + /** + * Create address metrics tree for a given type key + * @param {AddressableType | "all"} key + * @param {string} titlePrefix + */ + const createAddressMetricsTree = (key, titlePrefix) => [ + { + name: "Count", + title: `${titlePrefix}Address Count`, + bottom: [ + line({ + metric: distribution.addrCount[key], + name: "Loaded", + unit: Unit.count, + }), + line({ + metric: distribution.totalAddrCount[key], + name: "Total", + color: colors.default, + unit: Unit.count, + defaultActive: false, + }), + line({ + metric: distribution.emptyAddrCount[key], + name: "Empty", + color: colors.gray, + unit: Unit.count, + defaultActive: false, + }), + ], + }, + { + name: "New", + title: `${titlePrefix}New Address Count`, + bottom: fromFullStatsPattern({ pattern: distribution.newAddrCount[key], unit: Unit.count }), + }, + { + name: "Growth Rate", + title: `${titlePrefix}Address Growth Rate`, + bottom: fromBaseStatsPattern({ pattern: distribution.growthRate[key], unit: Unit.ratio }), + }, + { + name: "Activity", + tree: activityTypes.map((a) => ({ + name: a.name, + title: `${titlePrefix}${a.name} Address Count`, + bottom: fromBaseStatsPattern({ + pattern: distribution.addressActivity[key][a.key], + unit: Unit.count, + }), + })), + }, + ]; + + return { + name: "Network", + tree: [ + // Transactions + { + name: "Transactions", + tree: [ + { + name: "Count", + title: "Transaction Count", + bottom: fromFullStatsPattern({ pattern: transactions.count.txCount, unit: Unit.count }), + }, + { + name: "Per Second", + title: "Transactions Per Second", + bottom: [ + dots({ + metric: transactions.volume.txPerSec, + name: "TPS", + unit: Unit.perSec, + }), + ], + }, + { + name: "Volume", + title: "Transaction Volume", + bottom: [ + ...satsBtcUsd({ pattern: transactions.volume.sentSum, name: "Sent" }), + ...satsBtcUsd({ + pattern: transactions.volume.receivedSum, + name: "Received", + color: colors.cyan, + defaultActive: false, + }), + ...satsBtcUsd({ + pattern: transactions.volume.annualizedVolume, + name: "Annualized", + color: colors.red, + defaultActive: false, + }), + ], + }, + { + name: "Size", + title: "Transaction Size", + bottom: [ + ...fromStatsPattern({ pattern: transactions.size.weight, unit: Unit.wu }), + ...fromStatsPattern({ pattern: transactions.size.vsize, unit: Unit.vb }), + ], + }, + { + name: "Versions", + title: "Transaction Versions", + bottom: [ + ...fromCountPattern({ + pattern: transactions.versions.v1, + unit: Unit.count, + title: "v1", + sumColor: colors.orange, + cumulativeColor: colors.red, + }), + ...fromCountPattern({ + pattern: transactions.versions.v2, + unit: Unit.count, + title: "v2", + sumColor: colors.cyan, + cumulativeColor: colors.blue, + }), + ...fromCountPattern({ + pattern: transactions.versions.v3, + unit: Unit.count, + title: "v3", + sumColor: colors.lime, + cumulativeColor: colors.green, + }), + ], + }, + { + name: "Velocity", + title: "Transaction Velocity", + bottom: [ + line({ + metric: supply.velocity.btc, + name: "Bitcoin", + unit: Unit.ratio, + }), + line({ + metric: supply.velocity.usd, + name: "Dollars", + color: colors.emerald, + unit: Unit.ratio, + }), + ], + }, + ], + }, + + // Fees + { + name: "Fees", + tree: [ + { + name: "Fee Rate", + title: "Fee Rate", + bottom: fromStatsPattern({ pattern: transactions.fees.feeRate, unit: Unit.feeRate }), + }, + { + name: "Total", + title: "Total Fees", + bottom: [ + ...fromSumStatsPattern({ pattern: transactions.fees.fee.bitcoin, unit: Unit.btc }), + ...fromSumStatsPattern({ pattern: transactions.fees.fee.sats, unit: Unit.sats }), + ...fromSumStatsPattern({ pattern: transactions.fees.fee.dollars, unit: Unit.usd }), + ], + }, + ], + }, + + // Blocks + { + name: "Blocks", + tree: [ + { + name: "Count", + title: "Block Count", + bottom: [ + ...fromCountPattern({ pattern: blocks.count.blockCount, unit: Unit.count }), + line({ + metric: blocks.count.blockCountTarget, + name: "Target", + color: colors.gray, + unit: Unit.count, + options: { lineStyle: 4 }, + }), + line({ + metric: blocks.count._24hBlockCount, + name: "24h sum", + color: colors.pink, + unit: Unit.count, + defaultActive: false, + }), + line({ + metric: blocks.count._1wBlockCount, + name: "1w sum", + color: colors.red, + unit: Unit.count, + defaultActive: false, + }), + line({ + metric: blocks.count._1mBlockCount, + name: "1m sum", + color: colors.orange, + unit: Unit.count, + defaultActive: false, + }), + line({ + metric: blocks.count._1yBlockCount, + name: "1y sum", + color: colors.purple, + unit: Unit.count, + defaultActive: false, + }), + ], + }, + { + name: "Interval", + title: "Block Interval", + bottom: [ + ...fromBaseStatsPattern({ pattern: blocks.interval, unit: Unit.secs, avgActive: false }), + priceLine({ ctx, unit: Unit.secs, name: "Target", number: 600 }), + ], + }, + { + name: "Size", + title: "Block Size", + bottom: [ + ...fromSumStatsPattern({ pattern: blocks.size, unit: Unit.bytes }), + line({ + metric: blocks.totalSize, + name: "Total", + color: colors.purple, + unit: Unit.bytes, + defaultActive: false, + }), + ...fromBaseStatsPattern({ pattern: blocks.vbytes, unit: Unit.vb }), + ...fromBaseStatsPattern({ pattern: blocks.weight, unit: Unit.wu }), + line({ + metric: blocks.weight.sum, + name: "Sum", + color: colors.stat.sum, + unit: Unit.wu, + defaultActive: false, + }), + line({ + metric: blocks.weight.cumulative, + name: "Cumulative", + color: colors.stat.cumulative, + unit: Unit.wu, + defaultActive: false, + }), + ], + }, + { + name: "Fullness", + title: "Block Fullness", + bottom: fromBaseStatsPattern({ pattern: blocks.fullness, unit: Unit.percentage }), + }, + ], + }, + + // UTXO Set + { + name: "UTXO Set", + tree: [ + { + name: "UTXO Count", + title: "UTXO Count", + bottom: [ + line({ + metric: outputs.count.utxoCount, + name: "Count", + unit: Unit.count, + }), + ], + }, + { + name: "Inputs", + tree: [ + { + name: "Count", + title: "Input Count", + bottom: [...fromSumStatsPattern({ pattern: inputs.count, unit: Unit.count })], + }, + { + name: "Rate", + title: "Inputs Per Second", + bottom: [ + dots({ + metric: transactions.volume.inputsPerSec, + name: "Inputs/sec", + unit: Unit.perSec, + }), + ], + }, + ], + }, + { + name: "Outputs", + tree: [ + { + name: "Count", + title: "Output Count", + bottom: [...fromSumStatsPattern({ pattern: outputs.count.totalCount, unit: Unit.count })], + }, + { + name: "Rate", + title: "Outputs Per Second", + bottom: [ + dots({ + metric: transactions.volume.outputsPerSec, + name: "Outputs/sec", + unit: Unit.perSec, + }), + ], + }, + ], + }, + ], + }, + + // Addresses + { + name: "Addresses", + tree: [ + // Overview - global metrics for all addresses + { name: "Overview", tree: createAddressMetricsTree("all", "") }, + + // Compare - cross-type comparisons + { + name: "Compare", + tree: [ + { + name: "Count", + tree: countTypes.map((c) => ({ + name: c.name, + title: c.title, + bottom: addressTypes.map((t) => + line({ + metric: distribution[c.key][t.key], + name: t.name, + color: t.color, + unit: Unit.count, + defaultActive: t.defaultActive, + }), + ), + })), + }, + { + name: "New", + title: "New Address Count by Type", + bottom: addressTypes.flatMap((t) => [ + dots({ + metric: distribution.newAddrCount[t.key].base, + name: t.name, + color: t.color, + unit: Unit.count, + defaultActive: t.defaultActive, + }), + line({ + metric: distribution.newAddrCount[t.key].average, + name: `${t.name} Avg`, + color: t.color, + unit: Unit.count, + defaultActive: false, + }), + ]), + }, + { + name: "Growth Rate", + title: "Address Growth Rate by Type", + bottom: addressTypes.flatMap((t) => [ + dots({ + metric: distribution.growthRate[t.key].base, + name: t.name, + color: t.color, + unit: Unit.ratio, + defaultActive: t.defaultActive, + }), + line({ + metric: distribution.growthRate[t.key].average, + name: `${t.name} Avg`, + color: t.color, + unit: Unit.ratio, + defaultActive: false, + }), + ]), + }, + { + name: "Activity", + tree: activityTypes.map((a) => ({ + name: a.name, + title: a.compareTitle, + bottom: addressTypes.flatMap((t) => [ + dots({ + metric: distribution.addressActivity[t.key][a.key].base, + name: t.name, + color: t.color, + unit: Unit.count, + defaultActive: t.defaultActive, + }), + line({ + metric: distribution.addressActivity[t.key][a.key].average, + name: `${t.name} Avg`, + color: t.color, + unit: Unit.count, + defaultActive: false, + }), + ]), + })), + }, + ], + }, + + // Individual address types + ...addressTypes.map((t) => ({ + name: t.name, + tree: createAddressMetricsTree(t.key, `${t.name} `), + })), + ], + }, + + // Scripts + { + name: "Scripts", + tree: [ + { + name: "Output Counts", + tree: [ + // Legacy scripts + { + name: "Legacy", + tree: [ + { + name: "P2PKH", + title: "P2PKH Output Count", + bottom: fromFullStatsPattern({ pattern: scripts.count.p2pkh, unit: Unit.count }), + }, + { + name: "P2PK33", + title: "P2PK33 Output Count", + bottom: fromFullStatsPattern({ pattern: scripts.count.p2pk33, unit: Unit.count }), + }, + { + name: "P2PK65", + title: "P2PK65 Output Count", + bottom: fromFullStatsPattern({ pattern: scripts.count.p2pk65, unit: Unit.count }), + }, + ], + }, + // Script Hash + { + name: "Script Hash", + tree: [ + { + name: "P2SH", + title: "P2SH Output Count", + bottom: fromFullStatsPattern({ pattern: scripts.count.p2sh, unit: Unit.count }), + }, + { + name: "P2MS", + title: "P2MS Output Count", + bottom: fromFullStatsPattern({ pattern: scripts.count.p2ms, unit: Unit.count }), + }, + ], + }, + // SegWit scripts + { + name: "SegWit", + tree: [ + { + name: "All SegWit", + title: "SegWit Output Count", + bottom: fromFullStatsPattern({ pattern: scripts.count.segwit, unit: Unit.count }), + }, + { + name: "P2WPKH", + title: "P2WPKH Output Count", + bottom: fromFullStatsPattern({ pattern: scripts.count.p2wpkh, unit: Unit.count }), + }, + { + name: "P2WSH", + title: "P2WSH Output Count", + bottom: fromFullStatsPattern({ pattern: scripts.count.p2wsh, unit: Unit.count }), + }, + ], + }, + // Taproot scripts + { + name: "Taproot", + tree: [ + { + name: "P2TR", + title: "P2TR Output Count", + bottom: fromFullStatsPattern({ pattern: scripts.count.p2tr, unit: Unit.count }), + }, + { + name: "P2A", + title: "P2A Output Count", + bottom: fromFullStatsPattern({ pattern: scripts.count.p2a, unit: Unit.count }), + }, + ], + }, + // Other scripts + { + name: "Other", + tree: [ + { + name: "OP_RETURN", + title: "OP_RETURN Output Count", + bottom: fromFullStatsPattern({ pattern: scripts.count.opreturn, unit: Unit.count }), + }, + { + name: "Empty", + title: "Empty Output Count", + bottom: fromFullStatsPattern({ pattern: scripts.count.emptyoutput, unit: Unit.count }), + }, + { + name: "Unknown", + title: "Unknown Output Count", + bottom: fromFullStatsPattern({ pattern: scripts.count.unknownoutput, unit: Unit.count }), + }, + ], + }, + ], + }, + { + name: "Adoption", + tree: [ + { + name: "SegWit", + title: "SegWit Adoption", + bottom: [ + line({ + metric: scripts.count.segwitAdoption.base, + name: "Base", + unit: Unit.percentage, + }), + line({ + metric: scripts.count.segwitAdoption.sum, + name: "Sum", + color: colors.stat.sum, + unit: Unit.percentage, + }), + line({ + metric: scripts.count.segwitAdoption.cumulative, + name: "Cumulative", + color: colors.stat.cumulative, + unit: Unit.percentage, + defaultActive: false, + }), + ], + }, + { + name: "Taproot", + title: "Taproot Adoption", + bottom: [ + line({ + metric: scripts.count.taprootAdoption.base, + name: "Base", + unit: Unit.percentage, + }), + line({ + metric: scripts.count.taprootAdoption.sum, + name: "Sum", + color: colors.stat.sum, + unit: Unit.percentage, + }), + line({ + metric: scripts.count.taprootAdoption.cumulative, + name: "Cumulative", + color: colors.stat.cumulative, + unit: Unit.percentage, + defaultActive: false, + }), + ], + }, + ], + }, + ], + }, + + // Supply + { + name: "Supply", + tree: [ + { + name: "Circulating", + title: "Circulating Supply", + bottom: fromSupplyPattern({ pattern: supply.circulating, title: "Supply" }), + }, + { + name: "Inflation", + title: "Inflation Rate", + bottom: [ + dots({ + metric: supply.inflation, + name: "Rate", + unit: Unit.percentage, + }), + ], + }, + { + name: "Burned", + tree: [ + { + name: "Unspendable", + title: "Unspendable Supply", + bottom: fromValuePattern({ pattern: supply.burned.unspendable }), + }, + { + name: "OP_RETURN", + title: "OP_RETURN Burned", + bottom: [ + ...fromValuePattern({ pattern: supply.burned.opreturn }), + ...fromCoinbasePattern({ pattern: scripts.value.opreturn }), + ], + }, + ], + }, + ], + }, + ], + }; +} diff --git a/website/scripts/options/partial.js b/website/scripts/options/partial.js index 39dde3e77..3a9118fb7 100644 --- a/website/scripts/options/partial.js +++ b/website/scripts/options/partial.js @@ -16,8 +16,10 @@ import { createAddressCohortFolder, } from "./distribution/index.js"; import { createMarketSection } from "./market/index.js"; -import { createChainSection } from "./chain.js"; +import { createNetworkSection } from "./network.js"; +import { createMiningSection } from "./mining.js"; import { createCointimeSection } from "./cointime.js"; +import { createInvestingSection } from "./investing.js"; import { colors } from "../chart/colors.js"; // Re-export types for external consumers @@ -95,8 +97,11 @@ export function createPartialOptions({ brk }) { // Market section createMarketSection(ctx), - // Chain section - createChainSection(ctx), + // Network section (on-chain activity) + createNetworkSection(ctx), + + // Mining section (security & economics) + createMiningSection(ctx), // Cohorts section { @@ -294,6 +299,9 @@ export function createPartialOptions({ brk }) { name: "Frameworks", tree: [createCointimeSection(ctx)], }, + + // Investing section + createInvestingSection(ctx), ], }, diff --git a/website/scripts/options/series.js b/website/scripts/options/series.js index 73178874c..9c2512598 100644 --- a/website/scripts/options/series.js +++ b/website/scripts/options/series.js @@ -146,6 +146,24 @@ export function line({ }; } +/** + * @param {Omit[0], 'style'>} args + */ +export function dotted(args) { + const _args = /** @type {Parameters[0]} */ (args); + _args.style = 1; + return line(_args); +} + +/** + * @param {Omit[0], 'style'>} args + */ +export function sparseDotted(args) { + const _args = /** @type {Parameters[0]} */ (args); + _args.style = 4; + return line(_args); +} + /** * Create a Dots series (line with only point markers visible) * @param {Object} args @@ -329,7 +347,10 @@ export function fromSumStatsPattern(colors, { pattern, unit, title = "" }) { * @param {boolean} [args.avgActive] * @returns {AnyFetchedSeriesBlueprint[]} */ -export function fromBaseStatsPattern(colors, { pattern, unit, title = "", baseColor, avgActive = true }) { +export function fromBaseStatsPattern( + colors, + { pattern, unit, title = "", baseColor, avgActive = true }, +) { const { stat } = colors; return [ { metric: pattern.base, title: title || "base", color: baseColor, unit }, @@ -441,9 +462,21 @@ export function fromAnyFullStatsPattern(colors, { pattern, unit, title = "" }) { */ export function fromCoinbasePattern(colors, { pattern, title = "" }) { return [ - ...fromAnyFullStatsPattern(colors, { pattern: pattern.bitcoin, unit: Unit.btc, title }), - ...fromAnyFullStatsPattern(colors, { pattern: pattern.sats, unit: Unit.sats, title }), - ...fromAnyFullStatsPattern(colors, { pattern: pattern.dollars, unit: Unit.usd, title }), + ...fromAnyFullStatsPattern(colors, { + pattern: pattern.bitcoin, + unit: Unit.btc, + title, + }), + ...fromAnyFullStatsPattern(colors, { + pattern: pattern.sats, + unit: Unit.sats, + title, + }), + ...fromAnyFullStatsPattern(colors, { + pattern: pattern.dollars, + unit: Unit.usd, + title, + }), ]; } @@ -457,7 +490,10 @@ export function fromCoinbasePattern(colors, { pattern, title = "" }) { * @param {Color} [args.cumulativeColor] * @returns {AnyFetchedSeriesBlueprint[]} */ -export function fromValuePattern(colors, { pattern, title = "", sumColor, cumulativeColor }) { +export function fromValuePattern( + colors, + { pattern, title = "", sumColor, cumulativeColor }, +) { return [ { metric: pattern.bitcoin.sum, @@ -469,7 +505,7 @@ export function fromValuePattern(colors, { pattern, title = "", sumColor, cumula metric: pattern.bitcoin.cumulative, title: `${title} cumulative`.trim(), color: cumulativeColor ?? colors.stat.cumulative, - unit: Unit.btc, + unit: Unit.btcCumulative, defaultActive: false, }, { @@ -482,7 +518,7 @@ export function fromValuePattern(colors, { pattern, title = "", sumColor, cumula metric: pattern.sats.cumulative, title: `${title} cumulative`.trim(), color: cumulativeColor ?? colors.stat.cumulative, - unit: Unit.sats, + unit: Unit.satsCumulative, defaultActive: false, }, { @@ -495,7 +531,7 @@ export function fromValuePattern(colors, { pattern, title = "", sumColor, cumula metric: pattern.dollars.cumulative, title: `${title} cumulative`.trim(), color: cumulativeColor ?? colors.stat.cumulative, - unit: Unit.usd, + unit: Unit.usdCumulative, defaultActive: false, }, ]; @@ -513,7 +549,10 @@ export function fromValuePattern(colors, { pattern, title = "", sumColor, cumula * @param {boolean} [args.defaultActive] * @returns {AnyFetchedSeriesBlueprint[]} */ -export function fromBitcoinPatternWithUnit(colors, { pattern, unit, title = "", sumColor, cumulativeColor, defaultActive }) { +export function fromBitcoinPatternWithUnit( + colors, + { pattern, unit, title = "", sumColor, cumulativeColor, defaultActive }, +) { return [ { metric: pattern.sum, @@ -543,7 +582,10 @@ export function fromBitcoinPatternWithUnit(colors, { pattern, unit, title = "", * @param {Color} [args.cumulativeColor] * @returns {AnyFetchedSeriesBlueprint[]} */ -export function fromCountPattern(colors, { pattern, unit, title = "", sumColor, cumulativeColor }) { +export function fromCountPattern( + colors, + { pattern, unit, title = "", sumColor, cumulativeColor }, +) { return [ { metric: pattern.sum, diff --git a/website/scripts/options/shared.js b/website/scripts/options/shared.js index 84afeafa7..db5cc0ba4 100644 --- a/website/scripts/options/shared.js +++ b/website/scripts/options/shared.js @@ -15,7 +15,7 @@ export const formatCohortTitle = (cohortTitle) => /** * Create sats/btc/usd line series from a pattern with .sats/.bitcoin/.dollars * @param {Object} args - * @param {{ sats: AnyMetricPattern, bitcoin: AnyMetricPattern, dollars: AnyMetricPattern }} args.pattern + * @param {AnyValuePattern} args.pattern * @param {string} args.name * @param {Color} [args.color] * @param {boolean} [args.defaultActive] diff --git a/website/scripts/options/types.js b/website/scripts/options/types.js index 1ef3eb117..0200623d1 100644 --- a/website/scripts/options/types.js +++ b/website/scripts/options/types.js @@ -51,6 +51,9 @@ * Any pattern with dollars and sats sub-metrics (auto-expands to USD + sats) * @typedef {{ dollars: AnyMetricPattern, sats: AnyMetricPattern }} AnyPricePattern * + * Any pattern with sats, bitcoin, and dollars sub-metrics (value patterns like stack) + * @typedef {{ sats: AnyMetricPattern, bitcoin: AnyMetricPattern, dollars: AnyMetricPattern }} AnyValuePattern + * * Top pane price series - requires a price pattern with dollars/sats, auto-expands to USD + sats * @typedef {{ metric: AnyPricePattern }} FetchedPriceSeriesOptions * @typedef {LineSeriesBlueprint & FetchedPriceSeriesOptions} FetchedPriceSeriesBlueprint diff --git a/website/scripts/options/market/utils.js b/website/scripts/options/utils.js similarity index 100% rename from website/scripts/options/market/utils.js rename to website/scripts/options/utils.js diff --git a/website/scripts/types.js b/website/scripts/types.js index 019b42c14..0cb303d07 100644 --- a/website/scripts/types.js +++ b/website/scripts/types.js @@ -14,7 +14,7 @@ * * @import { WebSockets } from "./utils/ws.js" * - * @import { Option, PartialChartOption, ChartOption, AnyPartialOption, ProcessedOptionAddons, OptionsTree, SimulationOption, AnySeriesBlueprint, SeriesType, AnyFetchedSeriesBlueprint, TableOption, ExplorerOption, UrlOption, PartialOptionsGroup, OptionsGroup, PartialOptionsTree, UtxoCohortObject, AddressCohortObject, CohortObject, CohortGroupObject, FetchedLineSeriesBlueprint, FetchedBaselineSeriesBlueprint, FetchedHistogramSeriesBlueprint, PartialContext, PatternAll, PatternFull, PatternWithAdjusted, PatternWithPercentiles, PatternBasic, PatternBasicWithMarketCap, PatternBasicWithoutMarketCap, PatternWithoutRelative, CohortAll, CohortFull, CohortWithAdjusted, CohortWithPercentiles, CohortBasic, CohortBasicWithMarketCap, CohortBasicWithoutMarketCap, CohortWithoutRelative, CohortAddress, CohortLongTerm, CohortAgeRange, CohortMinAge, CohortGroupFull, CohortGroupWithAdjusted, CohortGroupWithPercentiles, CohortGroupLongTerm, CohortGroupAgeRange, CohortGroupBasic, CohortGroupBasicWithMarketCap, CohortGroupBasicWithoutMarketCap, CohortGroupWithoutRelative, CohortGroupMinAge, CohortGroupAddress, UtxoCohortGroupObject, AddressCohortGroupObject, FetchedDotsSeriesBlueprint, FetchedCandlestickSeriesBlueprint, FetchedPriceSeriesBlueprint, AnyPricePattern } 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, PartialContext, PatternAll, PatternFull, PatternWithAdjusted, PatternWithPercentiles, PatternBasic, PatternBasicWithMarketCap, PatternBasicWithoutMarketCap, PatternWithoutRelative, CohortAll, CohortFull, CohortWithAdjusted, CohortWithPercentiles, CohortBasic, CohortBasicWithMarketCap, CohortBasicWithoutMarketCap, CohortWithoutRelative, CohortAddress, CohortLongTerm, CohortAgeRange, CohortMinAge, CohortGroupFull, CohortGroupWithAdjusted, CohortGroupWithPercentiles, CohortGroupLongTerm, CohortGroupAgeRange, CohortGroupBasic, CohortGroupBasicWithMarketCap, CohortGroupBasicWithoutMarketCap, CohortGroupWithoutRelative, CohortGroupMinAge, CohortGroupAddress, UtxoCohortGroupObject, AddressCohortGroupObject, FetchedDotsSeriesBlueprint, FetchedCandlestickSeriesBlueprint, FetchedPriceSeriesBlueprint, AnyPricePattern, AnyValuePattern } from "./options/partial.js" * * * @import { UnitObject as Unit } from "./utils/units.js" diff --git a/website/scripts/utils/units.js b/website/scripts/utils/units.js index 4a9718faa..e455695a8 100644 --- a/website/scripts/utils/units.js +++ b/website/scripts/utils/units.js @@ -9,6 +9,11 @@ export const Unit = /** @type {const} */ ({ btc: { id: "btc", name: "Bitcoin" }, usd: { id: "usd", name: "US Dollars" }, + // Cumulative value units (running totals) + satsCumulative: { id: "sats-total", name: "Satoshis (Total)" }, + btcCumulative: { id: "btc-total", name: "Bitcoin (Total)" }, + usdCumulative: { id: "usd-total", name: "US Dollars (Total)" }, + // Ratios & percentages percentage: { id: "percentage", name: "Percentage" }, ratio: { id: "ratio", name: "Ratio" }, @@ -30,6 +35,7 @@ export const Unit = /** @type {const} */ ({ // Counts count: { id: "count", name: "Count" }, + countCumulative: { id: "count-total", name: "Count (Total)" }, blocks: { id: "blocks", name: "Blocks" }, // Size