website: snapshot

This commit is contained in:
nym21
2026-02-02 12:44:16 +01:00
parent f7d7c5704a
commit da923e409a
23 changed files with 2537 additions and 619 deletions
+20 -20
View File
@@ -4352,20 +4352,20 @@ pub struct MetricsTree_Market_Dca {
pub period_cagr: _10y2y3y4y5y6y8yPattern,
pub period_days_in_profit: _10y1m1w1y2y3m3y4y5y6m6y8yPattern2<StoredU32>,
pub period_days_in_loss: _10y1m1w1y2y3m3y4y5y6m6y8yPattern2<StoredU32>,
pub period_max_drawdown: _10y1m1w1y2y3m3y4y5y6m6y8yPattern2<StoredF32>,
pub period_min_return: _10y1m1w1y2y3m3y4y5y6m6y8yPattern2<StoredF32>,
pub period_max_return: _10y1m1w1y2y3m3y4y5y6m6y8yPattern2<StoredF32>,
pub period_lump_sum_stack: _10y1m1w1y2y3m3y4y5y6m6y8yPattern3,
pub period_lump_sum_returns: _10y1m1w1y2y3m3y4y5y6m6y8yPattern2<StoredF32>,
pub period_lump_sum_days_in_profit: _10y1m1w1y2y3m3y4y5y6m6y8yPattern2<StoredU32>,
pub period_lump_sum_days_in_loss: _10y1m1w1y2y3m3y4y5y6m6y8yPattern2<StoredU32>,
pub period_lump_sum_max_drawdown: _10y1m1w1y2y3m3y4y5y6m6y8yPattern2<StoredF32>,
pub period_lump_sum_min_return: _10y1m1w1y2y3m3y4y5y6m6y8yPattern2<StoredF32>,
pub period_lump_sum_max_return: _10y1m1w1y2y3m3y4y5y6m6y8yPattern2<StoredF32>,
pub class_stack: MetricsTree_Market_Dca_ClassStack,
pub class_average_price: MetricsTree_Market_Dca_ClassAveragePrice,
pub class_returns: _201520162017201820192020202120222023202420252026Pattern2<StoredF32>,
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<StoredF32>,
pub _2016: MetricPattern4<StoredF32>,
pub _2017: MetricPattern4<StoredF32>,
@@ -4588,21 +4588,21 @@ pub struct MetricsTree_Market_Dca_ClassMaxDrawdown {
pub _2026: MetricPattern4<StoredF32>,
}
impl MetricsTree_Market_Dca_ClassMaxDrawdown {
impl MetricsTree_Market_Dca_ClassMinReturn {
pub fn new(client: Arc<BrkClientBase>, 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()),
}
}
}
+13 -13
View File
@@ -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<ComputedFromDateLast<StoredU32>>,
days_in_loss: &mut ByDcaPeriod<ComputedFromDateLast<StoredU32>>,
max_drawdown: &mut ByDcaPeriod<ComputedFromDateLast<StoredF32>>,
min_return: &mut ByDcaPeriod<ComputedFromDateLast<StoredF32>>,
max_return: &mut ByDcaPeriod<ComputedFromDateLast<StoredF32>>,
returns: &ByDcaPeriod<LazyBinaryFromDateLast<StoredF32, Close<Dollars>, 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<ComputedFromDateLast<StoredU32>>,
days_in_loss: &mut ByDcaClass<ComputedFromDateLast<StoredU32>>,
max_drawdown: &mut ByDcaClass<ComputedFromDateLast<StoredF32>>,
min_return: &mut ByDcaClass<ComputedFromDateLast<StoredF32>>,
max_return: &mut ByDcaClass<ComputedFromDateLast<StoredF32>>,
returns: &ByDcaClass<LazyBinaryFromDateLast<StoredF32, Close<Dollars>, 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,
+9 -9
View File
@@ -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,
})
}
+3 -3
View File
@@ -16,7 +16,7 @@ pub struct Vecs {
// DCA by period - profitability
pub period_days_in_profit: ByDcaPeriod<ComputedFromDateLast<StoredU32>>,
pub period_days_in_loss: ByDcaPeriod<ComputedFromDateLast<StoredU32>>,
pub period_max_drawdown: ByDcaPeriod<ComputedFromDateLast<StoredF32>>,
pub period_min_return: ByDcaPeriod<ComputedFromDateLast<StoredF32>>,
pub period_max_return: ByDcaPeriod<ComputedFromDateLast<StoredF32>>,
// 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<ComputedFromDateLast<StoredU32>>,
pub period_lump_sum_days_in_loss: ByDcaPeriod<ComputedFromDateLast<StoredU32>>,
pub period_lump_sum_max_drawdown: ByDcaPeriod<ComputedFromDateLast<StoredF32>>,
pub period_lump_sum_min_return: ByDcaPeriod<ComputedFromDateLast<StoredF32>>,
pub period_lump_sum_max_return: ByDcaPeriod<ComputedFromDateLast<StoredF32>>,
// DCA by year class - KISS types
@@ -37,6 +37,6 @@ pub struct Vecs {
// DCA by year class - profitability
pub class_days_in_profit: ByDcaClass<ComputedFromDateLast<StoredU32>>,
pub class_days_in_loss: ByDcaClass<ComputedFromDateLast<StoredU32>>,
pub class_max_drawdown: ByDcaClass<ComputedFromDateLast<StoredF32>>,
pub class_min_return: ByDcaClass<ComputedFromDateLast<StoredF32>>,
pub class_max_return: ByDcaClass<ComputedFromDateLast<StoredF32>>,
}
+19 -19
View File
@@ -4121,20 +4121,20 @@ function createUtxoPattern(client, acc) {
* @property {_10y2y3y4y5y6y8yPattern} periodCagr
* @property {_10y1m1w1y2y3m3y4y5y6m6y8yPattern2<StoredU32>} periodDaysInProfit
* @property {_10y1m1w1y2y3m3y4y5y6m6y8yPattern2<StoredU32>} periodDaysInLoss
* @property {_10y1m1w1y2y3m3y4y5y6m6y8yPattern2<StoredF32>} periodMaxDrawdown
* @property {_10y1m1w1y2y3m3y4y5y6m6y8yPattern2<StoredF32>} periodMinReturn
* @property {_10y1m1w1y2y3m3y4y5y6m6y8yPattern2<StoredF32>} periodMaxReturn
* @property {_10y1m1w1y2y3m3y4y5y6m6y8yPattern3} periodLumpSumStack
* @property {_10y1m1w1y2y3m3y4y5y6m6y8yPattern2<StoredF32>} periodLumpSumReturns
* @property {_10y1m1w1y2y3m3y4y5y6m6y8yPattern2<StoredU32>} periodLumpSumDaysInProfit
* @property {_10y1m1w1y2y3m3y4y5y6m6y8yPattern2<StoredU32>} periodLumpSumDaysInLoss
* @property {_10y1m1w1y2y3m3y4y5y6m6y8yPattern2<StoredF32>} periodLumpSumMaxDrawdown
* @property {_10y1m1w1y2y3m3y4y5y6m6y8yPattern2<StoredF32>} periodLumpSumMinReturn
* @property {_10y1m1w1y2y3m3y4y5y6m6y8yPattern2<StoredF32>} periodLumpSumMaxReturn
* @property {MetricsTree_Market_Dca_ClassStack} classStack
* @property {MetricsTree_Market_Dca_ClassAveragePrice} classAveragePrice
* @property {_201520162017201820192020202120222023202420252026Pattern2<StoredF32>} 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<StoredF32>} _2015
* @property {MetricPattern4<StoredF32>} _2016
* @property {MetricPattern4<StoredF32>} _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'),
+16 -16
View File
@@ -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:
+44 -3
View File
@@ -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<keyof Colors, "stat">} ColorName
* @typedef {keyof typeof baseColors} ColorName
*/
+216 -57
View File
@@ -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 },
}),
],
},
+5
View File
@@ -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 }} */ (
+749
View File
@@ -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<typeof buildYearClass>} 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),
],
};
}
+32 -10
View File
@@ -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<typeof buildSmaAverages>[number], ema: ReturnType<typeof buildEmaAverages>[number] }} */ (p) => p !== null);
}).filter(
/** @type {(p: any) => p is { id: string, sma: ReturnType<typeof buildSmaAverages>[number], ema: ReturnType<typeof buildEmaAverages>[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<typeof buildSmaAverages> | ReturnType<typeof buildEmaAverages>} 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,
+1 -20
View File
@@ -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 }),
],
};
}
-425
View File
@@ -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 }),
},
],
}),
),
],
};
}
+48 -9
View File
@@ -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 }),
],
},
+592
View File
@@ -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,
},
],
},
],
};
}
+696
View File
@@ -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 }),
],
},
],
},
],
},
],
};
}
+11 -3
View File
@@ -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),
],
},
+52 -10
View File
@@ -146,6 +146,24 @@ export function line({
};
}
/**
* @param {Omit<Parameters<typeof line>[0], 'style'>} args
*/
export function dotted(args) {
const _args = /** @type {Parameters<typeof line>[0]} */ (args);
_args.style = 1;
return line(_args);
}
/**
* @param {Omit<Parameters<typeof line>[0], 'style'>} args
*/
export function sparseDotted(args) {
const _args = /** @type {Parameters<typeof line>[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,
+1 -1
View File
@@ -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]
+3
View File
@@ -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
+1 -1
View File
@@ -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"
+6
View File
@@ -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