global: BIG snapshot

This commit is contained in:
nym21
2026-01-04 01:47:03 +01:00
parent c33444a92e
commit 3cae817915
127 changed files with 4750 additions and 9474 deletions

View File

@@ -48,10 +48,16 @@ export function initOptions({ colors, signals, brk, qrcode }) {
* @param {AnyFetchedSeriesBlueprint[]} [arr]
*/
function arrayToRecord(arr = []) {
return (arr || []).reduce((record, blueprint) => {
return [...(arr || [])].reduce((record, blueprint) => {
if (!blueprint.metric) {
throw new Error(
`Blueprint missing metric: ${JSON.stringify(blueprint)}`,
);
}
markUsed(blueprint.metric);
// Use any index's path - unit is the same regardless of index (e.g., supply is "sats" for both height and dateindex)
const unit = blueprint.unit ?? serdeUnit.deserialize(blueprint.metric.name);
const unit =
blueprint.unit ?? serdeUnit.deserialize(blueprint.metric.name);
record[unit] ??= [];
record[unit].push(blueprint);
return record;

File diff suppressed because it is too large Load Diff

View File

@@ -47,7 +47,8 @@ export function createSingleSupplySeries(ctx, cohort, title) {
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createGroupedSupplyTotalSeries(ctx, list) {
const { s, constant100 } = ctx;
const { s, brk } = ctx;
const constant100 = brk.tree.computed.constants.constant100;
return list.flatMap(({ color, name, tree }) => [
s({ metric: tree.supply.supply.sats, name, color }),
@@ -206,11 +207,11 @@ export function createCostBasisPercentilesSeries(ctx, list, useGroupName) {
return list.flatMap(({ color, name, tree }) => {
const percentiles = tree.costBasis.percentiles;
return [
s({ metric: percentiles.pct10, name: useGroupName ? `${name} p10` : "p10", color, defaultActive: false }),
s({ metric: percentiles.pct25, name: useGroupName ? `${name} p25` : "p25", color, defaultActive: false }),
s({ metric: percentiles.pct50, name: useGroupName ? `${name} p50` : "p50", color }),
s({ metric: percentiles.pct75, name: useGroupName ? `${name} p75` : "p75", color, defaultActive: false }),
s({ metric: percentiles.pct90, name: useGroupName ? `${name} p90` : "p90", color, defaultActive: false }),
s({ metric: percentiles.costBasisPct10, name: useGroupName ? `${name} p10` : "p10", color, defaultActive: false }),
s({ metric: percentiles.costBasisPct25, name: useGroupName ? `${name} p25` : "p25", color, defaultActive: false }),
s({ metric: percentiles.costBasisPct50, name: useGroupName ? `${name} p50` : "p50", color }),
s({ metric: percentiles.costBasisPct75, name: useGroupName ? `${name} p75` : "p75", color, defaultActive: false }),
s({ metric: percentiles.costBasisPct90, name: useGroupName ? `${name} p90` : "p90", color, defaultActive: false }),
];
});
}

View File

@@ -15,8 +15,6 @@ import {
createUtxoCountSeries,
createRealizedPriceSeries,
createRealizedPriceRatioSeries,
createRealizedCapSeries,
createCostBasisMinMaxSeries,
createCostBasisPercentilesSeries,
} from "./shared.js";
@@ -97,7 +95,12 @@ export function createUtxoCohortFolder(ctx, args) {
createRealizedSection(ctx, list, args, useGroupName, isSingle, title),
...createUnrealizedSection(ctx, list, useGroupName, title),
...(hasPercentiles
? createCostBasisSectionWithPercentiles(ctx, /** @type {readonly AgeCohortObject[]} */ (list), useGroupName, title)
? createCostBasisSectionWithPercentiles(
ctx,
/** @type {readonly AgeCohortObject[]} */ (list),
useGroupName,
title,
)
: createCostBasisSectionBasic(ctx, list, useGroupName, title)),
...createActivitySection(ctx, list, useGroupName, title),
],
@@ -120,7 +123,11 @@ function createSupplySection(ctx, list, args, useGroupName, isSingle, title) {
? {
name: "supply",
title: `Supply ${title}`,
bottom: createSingleSupplySeries(ctx, /** @type {UtxoCohortObject} */ (args), title),
bottom: createSingleSupplySeries(
ctx,
/** @type {UtxoCohortObject} */ (args),
title,
),
}
: {
name: "supply",
@@ -188,13 +195,29 @@ function createRealizedSection(ctx, list, args, useGroupName, isSingle, title) {
bottom: createRealizedPriceRatioSeries(ctx, list),
},
]
: createRealizedPriceOptions(ctx, /** @type {UtxoCohortObject} */ (args), title)),
: createRealizedPriceOptions(
ctx,
/** @type {UtxoCohortObject} */ (args),
title,
)),
{
name: "capitalization",
title: `Realized Capitalization ${title}`,
bottom: createRealizedCapWithExtras(ctx, list, args, useGroupName, title),
bottom: createRealizedCapWithExtras(
ctx,
list,
args,
useGroupName,
title,
),
},
...(!useGroupName ? createRealizedPnlSection(ctx, /** @type {UtxoCohortObject} */ (args), title) : []),
...(!useGroupName
? createRealizedPnlSection(
ctx,
/** @type {UtxoCohortObject} */ (args),
title,
)
: []),
],
};
}
@@ -214,7 +237,9 @@ function createRealizedPriceOptions(ctx, args, title) {
{
name: "price",
title: `Realized Price ${title}`,
top: [s({ metric: tree.realized.realizedPrice, name: "realized", color })],
top: [
s({ metric: tree.realized.realizedPrice, name: "realized", color }),
],
},
];
}
@@ -390,14 +415,22 @@ function createCostBasisSectionWithPercentiles(ctx, list, useGroupName, title) {
name: "min",
title: `Min Cost Basis ${title}`,
top: list.map(({ color, name, tree }) =>
s({ metric: tree.costBasis.minCostBasis, name: useGroupName ? name : "Min", color }),
s({
metric: tree.costBasis.minCostBasis,
name: useGroupName ? name : "Min",
color,
}),
),
},
{
name: "max",
title: `Max Cost Basis ${title}`,
top: list.map(({ color, name, tree }) =>
s({ metric: tree.costBasis.maxCostBasis, name: useGroupName ? name : "Max", color }),
s({
metric: tree.costBasis.maxCostBasis,
name: useGroupName ? name : "Max",
color,
}),
),
},
{
@@ -429,14 +462,22 @@ function createCostBasisSectionBasic(ctx, list, useGroupName, title) {
name: "min",
title: `Min Cost Basis ${title}`,
top: list.map(({ color, name, tree }) =>
s({ metric: tree.costBasis.minCostBasis, name: useGroupName ? name : "Min", color }),
s({
metric: tree.costBasis.minCostBasis,
name: useGroupName ? name : "Min",
color,
}),
),
},
{
name: "max",
title: `Max Cost Basis ${title}`,
top: list.map(({ color, name, tree }) =>
s({ metric: tree.costBasis.maxCostBasis, name: useGroupName ? name : "Max", color }),
s({
metric: tree.costBasis.maxCostBasis,
name: useGroupName ? name : "Max",
color,
}),
),
},
],

View File

@@ -7,7 +7,7 @@
* @param {string} args.name
* @param {string} args.title
* @param {string} args.legend
* @param {MetricAccessor<any>} args.price
* @param {AnyMetricPattern} args.price
* @param {ActivePriceRatioPattern} args.ratio
* @param {Color} [args.color]
* @returns {PartialOptionsTree}

View File

@@ -5,45 +5,20 @@
* Examples: 0 → constant0, 38.2 → constant382, -1 → constantMinus1
* @param {BrkClient["tree"]["computed"]["constants"]} constants
* @param {number} num
* @returns {Constant0Pattern<any>}
* @returns {AnyMetricPattern}
*/
export function getConstant(constants, num) {
const key =
num >= 0
? `constant${String(num).replace(".", "")}`
: `constantMinus${Math.abs(num)}`;
const constant = /** @type {Constant0Pattern<any> | undefined} */ (
/** @type {Record<string, Constant0Pattern<any>>} */ (constants)[key]
const constant = /** @type {AnyMetricPattern | undefined} */ (
/** @type {Record<string, AnyMetricPattern>} */ (constants)[key]
);
if (!constant) throw new Error(`Unknown constant: ${num} (key: ${key})`);
return constant;
}
/**
* Flatten a Constant0Pattern into a simple MetricAccessor
* Constant0Pattern has { dateindex: { by: {...} }, height: { by: {...} }, ... }
* This flattens it to { by: { dateindex: MetricNode, height: MetricNode, ... } }
* @param {Constant0Pattern<any>} pattern
* @returns {MetricAccessor<any>}
*/
export function flattenConstant(pattern) {
return {
by: {
dateindex: pattern.dateindex.by.dateindex,
decadeindex: pattern.decadeindex.by.decadeindex,
height: pattern.height.by.height,
monthindex: pattern.monthindex.by.monthindex,
quarterindex: pattern.quarterindex.by.quarterindex,
semesterindex: pattern.semesterindex.by.semesterindex,
weekindex: pattern.weekindex.by.weekindex,
yearindex: pattern.yearindex.by.yearindex,
},
indexes() {
return /** @type {Index[]} */ (Object.keys(this.by));
},
};
}
/**
* Create a price line series (horizontal reference line)
* @param {Object} args
@@ -68,7 +43,7 @@ export function createPriceLine({
lineStyle,
}) {
return {
metric: flattenConstant(getConstant(constants, number)),
metric: getConstant(constants, number),
title: name ?? `${number}`,
unit,
defaultActive,
@@ -92,7 +67,7 @@ export function createPriceLine({
*/
export function createPriceLines({ constants, colors, numbers, unit }) {
return numbers.map((number) => ({
metric: flattenConstant(getConstant(constants, number)),
metric: getConstant(constants, number),
title: `${number}`,
unit,
defaultActive: !number,
@@ -109,7 +84,7 @@ export function createPriceLines({ constants, colors, numbers, unit }) {
* Create a constant line series
* @param {Object} args
* @param {Colors} args.colors
* @param {Constant0Pattern<any>} args.constant
* @param {AnyMetricPattern} args.constant
* @param {string} args.name
* @param {Unit} args.unit
* @param {Color} [args.color]
@@ -127,7 +102,7 @@ export function line({
defaultActive,
}) {
return {
metric: flattenConstant(constant),
metric: constant,
title: name,
unit,
defaultActive,

View File

@@ -1,11 +1,5 @@
import { s, fromBlockCount, fromBitcoin, fromBlockSize } from "./series.js";
import {
getConstant,
flattenConstant,
createPriceLine,
createPriceLines,
line,
} from "./constants.js";
import { createPriceLine, createPriceLines, line } from "./constants.js";
/**
* Create a context object with all dependencies for building partial options
@@ -16,13 +10,10 @@ import {
*/
export function createContext({ colors, brk }) {
const constants = brk.tree.computed.constants;
const constant100 = flattenConstant(constants.constant100);
return {
colors,
brk,
constants,
constant100,
// Series helpers
s,
@@ -33,9 +24,6 @@ export function createContext({ colors, brk }) {
fromBlockSize: (pattern, title, color) =>
fromBlockSize(colors, pattern, title, color),
// Constant helpers
getConstant: (num) => getConstant(constants, num),
flattenConstant,
createPriceLine: (args) => createPriceLine({ constants, colors, ...args }),
createPriceLines: (args) =>
createPriceLines({ constants, colors, ...args }),

View File

@@ -7,7 +7,7 @@ import {
createUtxoCohortFolder,
createAddressCohortFolder,
} from "./cohorts/index.js";
import { createMarketSection } from "./market.js";
import { createMarketSection } from "./market/index.js";
import { createChainSection } from "./chain.js";
import { createCointimeSection } from "./cointime.js";

View File

@@ -1,480 +0,0 @@
/** Market section builder - typed tree-based patterns */
/**
* Convert period ID to readable name
* @param {string} id
* @param {boolean} [compoundAdjective]
*/
function periodIdToName(id, compoundAdjective) {
const suffix = compoundAdjective || parseInt(id) === 1 ? "" : "s";
return id
.replace("d", ` day${suffix}`)
.replace("w", ` week${suffix}`)
.replace("m", ` month${suffix}`)
.replace("y", ` year${suffix}`);
}
/**
* Create price with ratio options (for moving averages)
* @param {PartialContext} ctx
* @param {Object} args
* @param {string} args.name
* @param {string} args.title
* @param {string} args.legend
* @param {EmaRatioPattern} args.ratio
* @param {Color} [args.color]
* @returns {PartialOptionsTree}
*/
function createPriceWithRatioOptions(ctx, { name, title, legend, ratio, color }) {
const { s, colors, createPriceLine } = ctx;
const priceMetric = ratio.price;
// Percentile USD mappings
const percentileUsdMap = [
{ name: "pct99", prop: ratio.ratioPct99Usd, color: colors.rose },
{ name: "pct98", prop: ratio.ratioPct98Usd, color: colors.pink },
{ name: "pct95", prop: ratio.ratioPct95Usd, color: colors.fuchsia },
{ name: "pct5", prop: ratio.ratioPct5Usd, color: colors.cyan },
{ name: "pct2", prop: ratio.ratioPct2Usd, color: colors.sky },
{ name: "pct1", prop: ratio.ratioPct1Usd, color: colors.blue },
];
// Percentile ratio mappings
const percentileMap = [
{ name: "pct99", prop: ratio.ratioPct99, color: colors.rose },
{ name: "pct98", prop: ratio.ratioPct98, color: colors.pink },
{ name: "pct95", prop: ratio.ratioPct95, color: colors.fuchsia },
{ name: "pct5", prop: ratio.ratioPct5, color: colors.cyan },
{ name: "pct2", prop: ratio.ratioPct2, color: colors.sky },
{ name: "pct1", prop: ratio.ratioPct1, color: colors.blue },
];
// SD patterns by window
const sdPatterns = [
{ nameAddon: "all", titleAddon: "", sd: ratio.ratioSd },
{ nameAddon: "4y", titleAddon: "4y", sd: ratio.ratio4ySd },
{ nameAddon: "2y", titleAddon: "2y", sd: ratio.ratio2ySd },
{ nameAddon: "1y", titleAddon: "1y", sd: ratio.ratio1ySd },
];
/** @param {Ratio1ySdPattern} sd */
const getSdBands = (sd) => [
{ name: "0σ", prop: sd._0sdUsd, color: colors.lime },
{ name: "+0.5σ", prop: sd.p05sdUsd, color: colors.yellow },
{ name: "+1σ", prop: sd.p1sdUsd, color: colors.amber },
{ name: "+1.5σ", prop: sd.p15sdUsd, color: colors.orange },
{ name: "+2σ", prop: sd.p2sdUsd, color: colors.red },
{ name: "+2.5σ", prop: sd.p25sdUsd, color: colors.rose },
{ name: "+3σ", prop: sd.p3sd, color: colors.pink },
{ name: "0.5σ", prop: sd.m05sdUsd, color: colors.teal },
{ name: "1σ", prop: sd.m1sdUsd, color: colors.cyan },
{ name: "1.5σ", prop: sd.m15sdUsd, color: colors.sky },
{ name: "2σ", prop: sd.m2sdUsd, color: colors.blue },
{ name: "2.5σ", prop: sd.m25sdUsd, color: colors.indigo },
{ name: "3σ", prop: sd.m3sd, color: colors.violet },
];
return [
{
name: "price",
title,
top: [s({ metric: priceMetric, name: legend, color, unit: "usd" })],
},
{
name: "Ratio",
title: `${title} Ratio`,
top: [
s({ metric: priceMetric, name: legend, color, unit: "usd" }),
...percentileUsdMap.map(({ name: pctName, prop, color: pctColor }) =>
s({
metric: prop,
name: pctName,
color: pctColor,
defaultActive: false,
unit: "usd",
options: { lineStyle: 1 },
}),
),
],
bottom: [
s({ metric: ratio.ratio, name: "ratio", color, unit: "ratio" }),
s({ metric: ratio.ratio1wSma, name: "1w sma", color: colors.lime, unit: "ratio" }),
s({ metric: ratio.ratio1mSma, name: "1m sma", color: colors.teal, unit: "ratio" }),
s({ metric: ratio.ratio1ySd.sma, name: "1y sma", color: colors.sky, unit: "ratio" }),
s({ metric: ratio.ratio2ySd.sma, name: "2y sma", color: colors.indigo, unit: "ratio" }),
s({ metric: ratio.ratio4ySd.sma, name: "4y sma", color: colors.purple, unit: "ratio" }),
s({ metric: ratio.ratioSd.sma, name: "all sma", color: colors.rose, unit: "ratio" }),
...percentileMap.map(({ name: pctName, prop, color: pctColor }) =>
s({
metric: prop,
name: pctName,
color: pctColor,
defaultActive: false,
unit: "ratio",
options: { lineStyle: 1 },
}),
),
createPriceLine({ unit: "ratio", number: 1 }),
],
},
{
name: "ZScores",
tree: sdPatterns.map(({ nameAddon, titleAddon, sd }) => ({
name: nameAddon,
title: `${title} ${titleAddon} Z-Score`,
top: getSdBands(sd).map(({ name: bandName, prop, color: bandColor }) =>
s({ metric: prop, name: bandName, color: bandColor, unit: "usd" }),
),
bottom: [
s({ metric: sd.zscore, name: "zscore", color, unit: "sd" }),
createPriceLine({ unit: "sd", number: 3 }),
createPriceLine({ unit: "sd", number: 2 }),
createPriceLine({ unit: "sd", number: 1 }),
createPriceLine({ unit: "sd", number: 0 }),
createPriceLine({ unit: "sd", number: -1 }),
createPriceLine({ unit: "sd", number: -2 }),
createPriceLine({ unit: "sd", number: -3 }),
],
})),
},
];
}
/**
* Build averages data array from market patterns
* @param {Colors} colors
* @param {MarketMovingAverage} ma
*/
function buildAverages(colors, ma) {
return /** @type {const} */ ([
["1w", 7, "red", ma.price1wSma, ma.price1wEma],
["8d", 8, "orange", ma.price8dSma, ma.price8dEma],
["13d", 13, "amber", ma.price13dSma, ma.price13dEma],
["21d", 21, "yellow", ma.price21dSma, ma.price21dEma],
["1m", 30, "lime", ma.price1mSma, ma.price1mEma],
["34d", 34, "green", ma.price34dSma, ma.price34dEma],
["55d", 55, "emerald", ma.price55dSma, ma.price55dEma],
["89d", 89, "teal", ma.price89dSma, ma.price89dEma],
["144d", 144, "cyan", ma.price144dSma, ma.price144dEma],
["200d", 200, "sky", ma.price200dSma, ma.price200dEma],
["1y", 365, "blue", ma.price1ySma, ma.price1yEma],
["2y", 730, "indigo", ma.price2ySma, ma.price2yEma],
["200w", 1400, "violet", ma.price200wSma, ma.price200wEma],
["4y", 1460, "purple", ma.price4ySma, ma.price4yEma],
]).map(([id, days, colorKey, sma, ema]) => ({
id,
name: periodIdToName(id, true),
days,
color: colors[colorKey],
sma,
ema,
}));
}
/**
* Build DCA classes data array
* @param {Colors} colors
* @param {MarketDca} dca
*/
function buildDcaClasses(colors, dca) {
return /** @type {const} */ ([
[2015, "pink", false, dca.dcaClass2015AvgPrice, dca.dcaClass2015Returns, dca.dcaClass2015Stack],
[2016, "red", false, dca.dcaClass2016AvgPrice, dca.dcaClass2016Returns, dca.dcaClass2016Stack],
[2017, "orange", true, dca.dcaClass2017AvgPrice, dca.dcaClass2017Returns, dca.dcaClass2017Stack],
[2018, "yellow", true, dca.dcaClass2018AvgPrice, dca.dcaClass2018Returns, dca.dcaClass2018Stack],
[2019, "green", true, dca.dcaClass2019AvgPrice, dca.dcaClass2019Returns, dca.dcaClass2019Stack],
[2020, "teal", true, dca.dcaClass2020AvgPrice, dca.dcaClass2020Returns, dca.dcaClass2020Stack],
[2021, "sky", true, dca.dcaClass2021AvgPrice, dca.dcaClass2021Returns, dca.dcaClass2021Stack],
[2022, "blue", true, dca.dcaClass2022AvgPrice, dca.dcaClass2022Returns, dca.dcaClass2022Stack],
[2023, "purple", true, dca.dcaClass2023AvgPrice, dca.dcaClass2023Returns, dca.dcaClass2023Stack],
[2024, "fuchsia", true, dca.dcaClass2024AvgPrice, dca.dcaClass2024Returns, dca.dcaClass2024Stack],
[2025, "pink", true, dca.dcaClass2025AvgPrice, dca.dcaClass2025Returns, dca.dcaClass2025Stack],
]).map(([year, colorKey, defaultActive, avgPrice, returns, stack]) => ({
year,
color: colors[colorKey],
defaultActive,
avgPrice,
returns,
stack,
}));
}
/**
* Create Market section
* @param {PartialContext} ctx
* @returns {PartialOptionsGroup}
*/
export function createMarketSection(ctx) {
const { colors, brk, s, createPriceLine } = ctx;
const { market, supply } = brk.tree.computed;
const { movingAverage, ath, returns, volatility, range, dca, lookback } = market;
const averages = buildAverages(colors, movingAverage);
const dcaClasses = buildDcaClasses(colors, dca);
return {
name: "Market",
tree: [
// Price (empty chart, shows candlesticks by default)
{
name: "Price",
title: "Bitcoin Price",
},
// Capitalization
{
name: "Capitalization",
title: "Market Capitalization",
bottom: [s({ metric: supply.marketCap.indexes, name: "Capitalization", unit: "usd" })],
},
// All Time High
{
name: "All Time High",
title: "All Time High",
top: [s({ metric: ath.priceAth, name: "ath", unit: "usd" })],
bottom: [
s({ metric: ath.priceDrawdown, name: "Drawdown", color: colors.red, unit: "percentage" }),
s({ metric: ath.daysSincePriceAth, name: "since", unit: "days" }),
s({ metric: ath.maxDaysBetweenPriceAths, name: "Max", color: colors.red, unit: "days" }),
s({ metric: ath.maxYearsBetweenPriceAths, name: "Max", color: colors.red, unit: "years" }),
],
},
// Averages
{
name: "Averages",
tree: [
{ nameAddon: "Simple", metricAddon: /** @type {const} */ ("sma") },
{ nameAddon: "Exponential", metricAddon: /** @type {const} */ ("ema") },
].map(({ nameAddon, metricAddon }) => ({
name: nameAddon,
tree: [
{
name: "Compare",
title: `Market Price ${nameAddon} Moving Averages`,
top: averages.map(({ id, color, sma, ema }) =>
s({
metric: (metricAddon === "sma" ? sma : ema).price,
name: id,
color,
unit: "usd",
}),
),
},
...averages.map(({ name, color, sma, ema }) => ({
name,
tree: createPriceWithRatioOptions(ctx, {
ratio: metricAddon === "sma" ? sma : ema,
name,
title: `${name} Market Price ${nameAddon} Moving Average`,
legend: "average",
color,
}),
})),
],
})),
},
// Performance
{
name: "Performance",
tree: /** @type {const} */ ([
["1d", returns._1dPriceReturns, undefined],
["1w", returns._1wPriceReturns, undefined],
["1m", returns._1mPriceReturns, undefined],
["3m", returns._3mPriceReturns, undefined],
["6m", returns._6mPriceReturns, undefined],
["1y", returns._1yPriceReturns, undefined],
["2y", returns._2yPriceReturns, returns._2yCagr],
["3y", returns._3yPriceReturns, returns._3yCagr],
["4y", returns._4yPriceReturns, returns._4yCagr],
["5y", returns._5yPriceReturns, returns._5yCagr],
["6y", returns._6yPriceReturns, returns._6yCagr],
["8y", returns._8yPriceReturns, returns._8yCagr],
["10y", returns._10yPriceReturns, returns._10yCagr],
]).map(([id, priceReturns, cagr]) => {
const name = periodIdToName(id, true);
return {
name,
title: `${name} Performance`,
bottom: [
/** @type {AnyFetchedSeriesBlueprint} */ ({
metric: priceReturns,
title: "total",
type: "Baseline",
unit: "percentage",
}),
...(cagr
? [
/** @type {AnyFetchedSeriesBlueprint} */ ({
metric: cagr,
title: "cagr",
type: "Baseline",
colors: [colors.lime, colors.pink],
unit: "percentage",
}),
]
: []),
createPriceLine({ unit: "percentage" }),
],
};
}),
},
// Indicators
{
name: "Indicators",
tree: [
// Volatility
{
name: "Volatility",
title: "Bitcoin Price Volatility Index",
bottom: [
s({ metric: volatility.price1wVolatility, name: "1w", color: colors.red, unit: "percentage" }),
s({ metric: volatility.price1mVolatility, name: "1m", color: colors.orange, unit: "percentage" }),
s({ metric: volatility.price1yVolatility, name: "1y", color: colors.lime, unit: "percentage" }),
],
},
// MinMax
{
name: "MinMax",
tree: [
{ id: "1w", title: "1 Week", min: range.price1wMin, max: range.price1wMax },
{ id: "2w", title: "2 Week", min: range.price2wMin, max: range.price2wMax },
{ id: "1m", title: "1 Month", min: range.price1mMin, max: range.price1mMax },
{ id: "1y", title: "1 Year", min: range.price1yMin, max: range.price1yMax },
].map(({ id, title, min, max }) => ({
name: id,
title: `Bitcoin Price ${title} MinMax Bands`,
top: [
s({ metric: min, name: "min", color: colors.red, unit: "usd" }),
s({ metric: max, name: "max", color: colors.green, unit: "usd" }),
],
})),
},
// True range
{
name: "True range",
title: "Bitcoin Price True Range",
bottom: [s({ metric: range.priceTrueRange, name: "value", color: colors.yellow, unit: "usd" })],
},
// Choppiness
{
name: "Choppiness",
title: "Bitcoin Price Choppiness Index",
bottom: [
s({ metric: range.price2wChoppinessIndex, name: "2w", color: colors.red, unit: "index" }),
createPriceLine({ unit: "index", number: 61.8 }),
createPriceLine({ unit: "index", number: 38.2 }),
],
},
// Mayer multiple
{
name: "Mayer multiple",
title: "Mayer multiple",
top: [
s({ metric: movingAverage.price200dSma.price, name: "200d sma", color: colors.yellow, unit: "usd" }),
s({ metric: movingAverage.price200dSmaX24, name: "200d sma x2.4", color: colors.green, unit: "usd" }),
s({ metric: movingAverage.price200dSmaX08, name: "200d sma x0.8", color: colors.red, unit: "usd" }),
],
},
],
},
// Investing
{
name: "Investing",
tree: [
// DCA vs Lump sum
{
name: "DCA vs Lump sum",
tree: [
.../** @type {const} */ ([
["1w", dca._1wDcaAvgPrice, lookback.price1wAgo, dca._1wDcaReturns, returns._1wPriceReturns],
["1m", dca._1mDcaAvgPrice, lookback.price1mAgo, dca._1mDcaReturns, returns._1mPriceReturns],
["3m", dca._3mDcaAvgPrice, lookback.price3mAgo, dca._3mDcaReturns, returns._3mPriceReturns],
["6m", dca._6mDcaAvgPrice, lookback.price6mAgo, dca._6mDcaReturns, returns._6mPriceReturns],
["1y", dca._1yDcaAvgPrice, lookback.price1yAgo, dca._1yDcaReturns, returns._1yPriceReturns],
]).map(([id, dcaAvgPrice, priceAgo, dcaReturns, priceReturns]) => {
const name = periodIdToName(id, true);
return {
name,
tree: [
{
name: "price",
title: `${name} DCA vs Lump Sum (Price)`,
top: [
s({ metric: dcaAvgPrice, name: "DCA avg", color: colors.green, unit: "usd" }),
s({ metric: priceAgo, name: "Lump sum", color: colors.orange, unit: "usd" }),
],
},
{
name: "returns",
title: `${name} DCA vs Lump Sum (Returns)`,
bottom: [
/** @type {AnyFetchedSeriesBlueprint} */ ({
metric: dcaReturns,
title: "DCA",
type: "Baseline",
unit: "percentage",
}),
/** @type {AnyFetchedSeriesBlueprint} */ ({
metric: priceReturns,
title: "Lump sum",
type: "Baseline",
colors: [colors.lime, colors.red],
unit: "percentage",
}),
createPriceLine({ unit: "percentage" }),
],
},
],
};
}),
],
},
// DCA classes
{
name: "DCA classes",
tree: [
{
name: "Average price",
title: "DCA Average Price by Year",
top: dcaClasses.map(({ year, color, defaultActive, avgPrice }) =>
s({ metric: avgPrice, name: `${year}`, color, defaultActive, unit: "usd" }),
),
},
{
name: "Returns",
title: "DCA Returns by Year",
bottom: dcaClasses.map(({ year, color, defaultActive, returns }) =>
/** @type {AnyFetchedSeriesBlueprint} */ ({
metric: returns,
title: `${year}`,
type: "Baseline",
color,
defaultActive,
unit: "percentage",
}),
),
},
{
name: "Stack",
title: "DCA Stack by Year",
bottom: dcaClasses.map(({ year, color, defaultActive, stack }) =>
s({ metric: stack, name: `${year}`, color, defaultActive, unit: "sats" }),
),
},
],
},
],
},
],
};
}

View File

@@ -0,0 +1,179 @@
/** Moving averages section */
import { periodIdToName } from "./utils.js";
/**
* Build averages data array from market patterns
* @param {Colors} colors
* @param {MarketMovingAverage} ma
*/
export function buildAverages(colors, ma) {
return /** @type {const} */ ([
["1w", 7, "red", ma.price1wSma, ma.price1wEma],
["8d", 8, "orange", ma.price8dSma, ma.price8dEma],
["13d", 13, "amber", ma.price13dSma, ma.price13dEma],
["21d", 21, "yellow", ma.price21dSma, ma.price21dEma],
["1m", 30, "lime", ma.price1mSma, ma.price1mEma],
["34d", 34, "green", ma.price34dSma, ma.price34dEma],
["55d", 55, "emerald", ma.price55dSma, ma.price55dEma],
["89d", 89, "teal", ma.price89dSma, ma.price89dEma],
["144d", 144, "cyan", ma.price144dSma, ma.price144dEma],
["200d", 200, "sky", ma.price200dSma, ma.price200dEma],
["1y", 365, "blue", ma.price1ySma, ma.price1yEma],
["2y", 730, "indigo", ma.price2ySma, ma.price2yEma],
["200w", 1400, "violet", ma.price200wSma, ma.price200wEma],
["4y", 1460, "purple", ma.price4ySma, ma.price4yEma],
]).map(([id, days, colorKey, sma, ema]) => ({
id,
name: periodIdToName(id, true),
days,
color: colors[colorKey],
sma,
ema,
}));
}
/**
* Create price with ratio options (for moving averages)
* @param {PartialContext} ctx
* @param {Object} args
* @param {string} args.title
* @param {string} args.legend
* @param {EmaRatioPattern} args.ratio
* @param {Color} [args.color]
* @returns {PartialOptionsTree}
*/
export function createPriceWithRatioOptions(ctx, { title, legend, ratio, color }) {
const { s, colors, createPriceLine } = ctx;
const priceMetric = ratio.price;
const percentileUsdMap = [
{ name: "pct99", prop: ratio.ratioPct99Usd, color: colors.rose },
{ name: "pct98", prop: ratio.ratioPct98Usd, color: colors.pink },
{ name: "pct95", prop: ratio.ratioPct95Usd, color: colors.fuchsia },
{ name: "pct5", prop: ratio.ratioPct5Usd, color: colors.cyan },
{ name: "pct2", prop: ratio.ratioPct2Usd, color: colors.sky },
{ name: "pct1", prop: ratio.ratioPct1Usd, color: colors.blue },
];
const percentileMap = [
{ name: "pct99", prop: ratio.ratioPct99, color: colors.rose },
{ name: "pct98", prop: ratio.ratioPct98, color: colors.pink },
{ name: "pct95", prop: ratio.ratioPct95, color: colors.fuchsia },
{ name: "pct5", prop: ratio.ratioPct5, color: colors.cyan },
{ name: "pct2", prop: ratio.ratioPct2, color: colors.sky },
{ name: "pct1", prop: ratio.ratioPct1, color: colors.blue },
];
const sdPatterns = [
{ nameAddon: "all", titleAddon: "", sd: ratio.ratioSd },
{ nameAddon: "4y", titleAddon: "4y", sd: ratio.ratio4ySd },
{ nameAddon: "2y", titleAddon: "2y", sd: ratio.ratio2ySd },
{ nameAddon: "1y", titleAddon: "1y", sd: ratio.ratio1ySd },
];
/** @param {Ratio1ySdPattern} sd */
const getSdBands = (sd) => [
{ name: "0σ", prop: sd._0sdUsd, color: colors.lime },
{ name: "+0.5σ", prop: sd.p05sdUsd, color: colors.yellow },
{ name: "+1σ", prop: sd.p1sdUsd, color: colors.amber },
{ name: "+1.5σ", prop: sd.p15sdUsd, color: colors.orange },
{ name: "+2σ", prop: sd.p2sdUsd, color: colors.red },
{ name: "+2.5σ", prop: sd.p25sdUsd, color: colors.rose },
{ name: "+3σ", prop: sd.p3sd, color: colors.pink },
{ name: "0.5σ", prop: sd.m05sdUsd, color: colors.teal },
{ name: "1σ", prop: sd.m1sdUsd, color: colors.cyan },
{ name: "1.5σ", prop: sd.m15sdUsd, color: colors.sky },
{ name: "2σ", prop: sd.m2sdUsd, color: colors.blue },
{ name: "2.5σ", prop: sd.m25sdUsd, color: colors.indigo },
{ name: "3σ", prop: sd.m3sd, color: colors.violet },
];
return [
{
name: "price",
title,
top: [s({ metric: priceMetric, name: legend, color, unit: "usd" })],
},
{
name: "Ratio",
title: `${title} Ratio`,
top: [
s({ metric: priceMetric, name: legend, color, unit: "usd" }),
...percentileUsdMap.map(({ name: pctName, prop, color: pctColor }) =>
s({ metric: prop, name: pctName, color: pctColor, defaultActive: false, unit: "usd", options: { lineStyle: 1 } }),
),
],
bottom: [
s({ metric: ratio.ratio, name: "ratio", color, unit: "ratio" }),
s({ metric: ratio.ratio1wSma, name: "1w sma", color: colors.lime, unit: "ratio" }),
s({ metric: ratio.ratio1mSma, name: "1m sma", color: colors.teal, unit: "ratio" }),
s({ metric: ratio.ratio1ySd.sma, name: "1y sma", color: colors.sky, unit: "ratio" }),
s({ metric: ratio.ratio2ySd.sma, name: "2y sma", color: colors.indigo, unit: "ratio" }),
s({ metric: ratio.ratio4ySd.sma, name: "4y sma", color: colors.purple, unit: "ratio" }),
s({ metric: ratio.ratioSd.sma, name: "all sma", color: colors.rose, unit: "ratio" }),
...percentileMap.map(({ name: pctName, prop, color: pctColor }) =>
s({ metric: prop, name: pctName, color: pctColor, defaultActive: false, unit: "ratio", options: { lineStyle: 1 } }),
),
createPriceLine({ unit: "ratio", number: 1 }),
],
},
{
name: "ZScores",
tree: sdPatterns.map(({ nameAddon, titleAddon, sd }) => ({
name: nameAddon,
title: `${title} ${titleAddon} Z-Score`,
top: getSdBands(sd).map(({ name: bandName, prop, color: bandColor }) =>
s({ metric: prop, name: bandName, color: bandColor, unit: "usd" }),
),
bottom: [
s({ metric: sd.zscore, name: "zscore", color, unit: "sd" }),
createPriceLine({ unit: "sd", number: 3 }),
createPriceLine({ unit: "sd", number: 2 }),
createPriceLine({ unit: "sd", number: 1 }),
createPriceLine({ unit: "sd", number: 0 }),
createPriceLine({ unit: "sd", number: -1 }),
createPriceLine({ unit: "sd", number: -2 }),
createPriceLine({ unit: "sd", number: -3 }),
],
})),
},
];
}
/**
* Create Averages section
* @param {PartialContext} ctx
* @param {ReturnType<typeof buildAverages>} averages
*/
export function createAveragesSection(ctx, averages) {
const { s } = ctx;
return {
name: "Averages",
tree: [
{ nameAddon: "Simple", metricAddon: /** @type {const} */ ("sma") },
{ nameAddon: "Exponential", metricAddon: /** @type {const} */ ("ema") },
].map(({ nameAddon, metricAddon }) => ({
name: nameAddon,
tree: [
{
name: "Compare",
title: `Market Price ${nameAddon} Moving Averages`,
top: averages.map(({ id, color, sma, ema }) =>
s({ metric: (metricAddon === "sma" ? sma : ema).price, name: id, color, unit: "usd" }),
),
},
...averages.map(({ name, color, sma, ema }) => ({
name,
tree: createPriceWithRatioOptions(ctx, {
ratio: metricAddon === "sma" ? sma : ema,
title: `${name} Market Price ${nameAddon} Moving Average`,
legend: "average",
color,
}),
})),
],
})),
};
}

View File

@@ -0,0 +1,62 @@
/** Market section - Main entry point */
import { buildAverages, createAveragesSection } from "./averages.js";
import { createPerformanceSection } from "./performance.js";
import { createIndicatorsSection } from "./indicators/index.js";
import { createInvestingSection } from "./investing.js";
/**
* Create Market section
* @param {PartialContext} ctx
* @returns {PartialOptionsGroup}
*/
export function createMarketSection(ctx) {
const { colors, brk, s } = ctx;
const { market, supply } = brk.tree.computed;
const { movingAverage, ath, returns, volatility, range, dca, lookback, indicators } = market;
const averages = buildAverages(colors, movingAverage);
return {
name: "Market",
tree: [
// Price
{
name: "Price",
title: "Bitcoin Price",
},
// Capitalization
{
name: "Capitalization",
title: "Market Capitalization",
bottom: [s({ metric: supply.marketCap.indexes, name: "Capitalization", unit: "usd" })],
},
// All Time High
{
name: "All Time High",
title: "All Time High",
top: [s({ metric: ath.priceAth, name: "ath", unit: "usd" })],
bottom: [
s({ metric: ath.priceDrawdown, name: "Drawdown", color: colors.red, unit: "percentage" }),
s({ metric: ath.daysSincePriceAth, name: "since", unit: "days" }),
s({ metric: ath.maxDaysBetweenPriceAths, name: "Max", color: colors.red, unit: "days" }),
s({ metric: ath.maxYearsBetweenPriceAths, name: "Max", color: colors.red, unit: "years" }),
],
},
// Averages
createAveragesSection(ctx, averages),
// Performance
createPerformanceSection(ctx, returns),
// Indicators
createIndicatorsSection(ctx, { volatility, range, movingAverage, indicators }),
// Investing
createInvestingSection(ctx, { dca, lookback, returns }),
],
};
}

View File

@@ -0,0 +1,43 @@
/** Bands indicators (MinMax, Mayer Multiple) */
/**
* Create Bands section
* @param {PartialContext} ctx
* @param {Object} args
* @param {Market["range"]} args.range
* @param {Market["movingAverage"]} args.movingAverage
*/
export function createBandsSection(ctx, { range, movingAverage }) {
const { s, colors } = ctx;
return {
name: "Bands",
tree: [
{
name: "MinMax",
tree: [
{ id: "1w", title: "1 Week", min: range.price1wMin, max: range.price1wMax },
{ id: "2w", title: "2 Week", min: range.price2wMin, max: range.price2wMax },
{ id: "1m", title: "1 Month", min: range.price1mMin, max: range.price1mMax },
{ id: "1y", title: "1 Year", min: range.price1yMin, max: range.price1yMax },
].map(({ id, title, min, max }) => ({
name: id,
title: `Bitcoin Price ${title} MinMax Bands`,
top: [
s({ metric: min, name: "min", color: colors.red, unit: "usd" }),
s({ metric: max, name: "max", color: colors.green, unit: "usd" }),
],
})),
},
{
name: "Mayer Multiple",
title: "Mayer Multiple",
top: [
s({ metric: movingAverage.price200dSma.price, name: "200d sma", color: colors.yellow, unit: "usd" }),
s({ metric: movingAverage.price200dSmaX24, name: "200d sma x2.4", color: colors.green, unit: "usd" }),
s({ metric: movingAverage.price200dSmaX08, name: "200d sma x0.8", color: colors.red, unit: "usd" }),
],
},
],
};
}

View File

@@ -0,0 +1,27 @@
/** Indicators section - Main entry point */
import { createMomentumSection } from "./momentum.js";
import { createVolatilitySection } from "./volatility.js";
import { createBandsSection } from "./bands.js";
import { createOnchainSection } from "./onchain.js";
/**
* Create Indicators section
* @param {PartialContext} ctx
* @param {Object} args
* @param {Market["volatility"]} args.volatility
* @param {Market["range"]} args.range
* @param {Market["movingAverage"]} args.movingAverage
* @param {Market["indicators"]} args.indicators
*/
export function createIndicatorsSection(ctx, { volatility, range, movingAverage, indicators }) {
return {
name: "Indicators",
tree: [
createMomentumSection(ctx, indicators),
createVolatilitySection(ctx, { volatility, range }),
createBandsSection(ctx, { range, movingAverage }),
createOnchainSection(ctx, { indicators, movingAverage }),
],
};
}

View File

@@ -0,0 +1,106 @@
/** Momentum indicators (RSI, StochRSI, Stochastic, MACD) */
/**
* Create Momentum section
* @param {PartialContext} ctx
* @param {Market["indicators"]} indicators
*/
export function createMomentumSection(ctx, indicators) {
const { s, colors, createPriceLine } = ctx;
return {
name: "Momentum",
tree: [
{
name: "RSI",
title: "Relative Strength Index (14d)",
bottom: [
s({
metric: indicators.rsi14d,
name: "RSI",
color: colors.indigo,
unit: "index",
}),
s({
metric: indicators.rsi14dMin,
name: "Min",
color: colors.red,
defaultActive: false,
unit: "index",
}),
s({
metric: indicators.rsi14dMax,
name: "Max",
color: colors.green,
defaultActive: false,
unit: "index",
}),
createPriceLine({ unit: "index", number: 70 }),
createPriceLine({ unit: "index", number: 50, defaultActive: false }),
createPriceLine({ unit: "index", number: 30 }),
],
},
{
name: "StochRSI",
title: "Stochastic RSI",
bottom: [
// s({
// metric: indicators.stochRsi,
// name: "Stoch RSI",
// color: colors.purple,
// unit: "index",
// }),
s({
metric: indicators.stochRsiK,
name: "K",
color: colors.blue,
unit: "index",
}),
s({
metric: indicators.stochRsiD,
name: "D",
color: colors.orange,
unit: "index",
}),
createPriceLine({ unit: "index", number: 80 }),
createPriceLine({ unit: "index", number: 20 }),
],
},
// {
// name: "Stochastic",
// title: "Stochastic Oscillator",
// bottom: [
// s({ metric: indicators.stochK, name: "K", color: colors.blue, unit: "index" }),
// s({ metric: indicators.stochD, name: "D", color: colors.orange, unit: "index" }),
// createPriceLine({ unit: "index", number: 80 }),
// createPriceLine({ unit: "index", number: 20 }),
// ],
// },
{
name: "MACD",
title: "Moving Average Convergence Divergence",
bottom: [
s({
metric: indicators.macdLine,
name: "MACD",
color: colors.blue,
unit: "usd",
}),
s({
metric: indicators.macdSignal,
name: "Signal",
color: colors.orange,
unit: "usd",
}),
/** @type {FetchedHistogramSeriesBlueprint} */ ({
metric: indicators.macdHistogram,
title: "Histogram",
type: "Histogram",
unit: "usd",
}),
createPriceLine({ unit: "usd" }),
],
},
],
};
}

View File

@@ -0,0 +1,81 @@
/** On-chain indicators (Pi Cycle, Puell, NVT, Gini) */
/**
* Create On-chain section
* @param {PartialContext} ctx
* @param {Object} args
* @param {Market["indicators"]} args.indicators
* @param {Market["movingAverage"]} args.movingAverage
*/
export function createOnchainSection(ctx, { indicators, movingAverage }) {
const { s, colors, createPriceLine } = ctx;
return {
name: "On-chain",
tree: [
{
name: "Pi Cycle",
title: "Pi Cycle Top Indicator",
top: [
s({
metric: movingAverage.price111dSma.price,
name: "111d SMA",
color: colors.green,
unit: "usd",
}),
s({
metric: movingAverage.price350dSmaX2,
name: "350d SMA x2",
color: colors.red,
unit: "usd",
}),
],
bottom: [
s({
metric: indicators.piCycle,
name: "Pi Cycle",
color: colors.purple,
unit: "ratio",
}),
createPriceLine({ unit: "ratio", number: 1 }),
],
},
{
name: "Puell Multiple",
title: "Puell Multiple",
bottom: [
s({
metric: indicators.puellMultiple,
name: "Puell",
color: colors.green,
unit: "ratio",
}),
],
},
{
name: "NVT",
title: "Network Value to Transactions Ratio",
bottom: [
s({
metric: indicators.nvt,
name: "NVT",
color: colors.orange,
unit: "ratio",
}),
],
},
{
name: "Gini",
title: "Gini Coefficient",
bottom: [
s({
metric: indicators.gini,
name: "Gini",
color: colors.red,
unit: "ratio",
}),
],
},
],
};
}

View File

@@ -0,0 +1,61 @@
/** Volatility indicators (Index, True Range, Choppiness, Sharpe, Sortino) */
/**
* Create Volatility section
* @param {PartialContext} ctx
* @param {Object} args
* @param {Market["volatility"]} args.volatility
* @param {Market["range"]} args.range
*/
export function createVolatilitySection(ctx, { volatility, range }) {
const { s, colors, createPriceLine } = ctx;
return {
name: "Volatility",
tree: [
{
name: "Index",
title: "Bitcoin Price Volatility Index",
bottom: [
s({ metric: volatility.price1wVolatility, name: "1w", color: colors.red, unit: "percentage" }),
s({ metric: volatility.price1mVolatility, name: "1m", color: colors.orange, unit: "percentage" }),
s({ metric: volatility.price1yVolatility, name: "1y", color: colors.lime, unit: "percentage" }),
],
},
{
name: "True Range",
title: "Bitcoin Price True Range",
bottom: [s({ metric: range.priceTrueRange, name: "value", color: colors.yellow, unit: "usd" })],
},
{
name: "Choppiness",
title: "Bitcoin Price Choppiness Index",
bottom: [
s({ metric: range.price2wChoppinessIndex, name: "2w", color: colors.red, unit: "index" }),
createPriceLine({ unit: "index", number: 61.8 }),
createPriceLine({ unit: "index", number: 38.2 }),
],
},
{
name: "Sharpe Ratio",
title: "Sharpe Ratio",
bottom: [
s({ metric: volatility.sharpe1w, name: "1w", color: colors.red, unit: "ratio" }),
s({ metric: volatility.sharpe1m, name: "1m", color: colors.orange, unit: "ratio" }),
s({ metric: volatility.sharpe1y, name: "1y", color: colors.lime, unit: "ratio" }),
createPriceLine({ unit: "ratio" }),
],
},
{
name: "Sortino Ratio",
title: "Sortino Ratio",
bottom: [
s({ metric: volatility.sortino1w, name: "1w", color: colors.red, unit: "ratio" }),
s({ metric: volatility.sortino1m, name: "1m", color: colors.orange, unit: "ratio" }),
s({ metric: volatility.sortino1y, name: "1y", color: colors.lime, unit: "ratio" }),
createPriceLine({ unit: "ratio" }),
],
},
],
};
}

View File

@@ -0,0 +1,197 @@
/** Investing section (DCA) */
import { periodIdToName } from "./utils.js";
/**
* Build DCA classes data array
* @param {Colors} colors
* @param {MarketDca} dca
*/
export function buildDcaClasses(colors, dca) {
return /** @type {const} */ ([
[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.classAvgPrice[`_${year}`],
returns: dca.classReturns[`_${year}`],
stack: dca.classStack[`_${year}`],
}));
}
/**
* Create Investing section
* @param {PartialContext} ctx
* @param {Object} args
* @param {Market["dca"]} args.dca
* @param {Market["lookback"]} args.lookback
* @param {Market["returns"]} args.returns
*/
export function createInvestingSection(ctx, { dca, lookback, returns }) {
const { s, colors, createPriceLine } = ctx;
const dcaClasses = buildDcaClasses(colors, dca);
return {
name: "Investing",
tree: [
// DCA vs Lump sum
{
name: "DCA vs Lump sum",
tree: /** @type {const} */ ([
["1w", "_1w"],
["1m", "_1m"],
["3m", "_3m"],
["6m", "_6m"],
["1y", "_1y"],
["2y", "_2y"],
["3y", "_3y"],
["4y", "_4y"],
["5y", "_5y"],
["6y", "_6y"],
["8y", "_8y"],
["10y", "_10y"],
]).map(([id, key]) => {
const name = periodIdToName(id, true);
const priceAgo = lookback.priceAgo[key];
const priceReturns = returns.priceReturns[key];
const dcaCostBasis = dca.periodAvgPrice[key];
const dcaReturns = dca.periodReturns[key];
const dcaStack = dca.periodStack[key];
const lumpSumStack = dca.periodLumpSumStack[key];
return {
name,
tree: [
{
name: "Cost basis",
title: `${name} DCA vs Lump Sum (Cost Basis)`,
top: [
s({ metric: dcaCostBasis, name: "DCA", color: colors.green, unit: "usd" }),
s({ metric: priceAgo, name: "Lump sum", color: colors.orange, unit: "usd" }),
],
},
{
name: "Returns",
title: `${name} DCA vs Lump Sum (Returns)`,
bottom: [
/** @type {AnyFetchedSeriesBlueprint} */ ({
metric: dcaReturns,
title: "DCA",
type: "Baseline",
unit: "percentage",
}),
/** @type {AnyFetchedSeriesBlueprint} */ ({
metric: priceReturns,
title: "Lump sum",
type: "Baseline",
colors: [colors.lime, colors.red],
unit: "percentage",
}),
createPriceLine({ unit: "percentage" }),
],
},
{
name: "Stack",
title: `${name} DCA vs Lump Sum Stack ($100/day)`,
bottom: [
s({ metric: dcaStack.sats, name: "DCA", color: colors.green, unit: "sats" }),
s({ metric: dcaStack.bitcoin, name: "DCA", color: colors.green, unit: "btc" }),
s({ metric: dcaStack.dollars, name: "DCA", color: colors.green, unit: "usd" }),
s({ metric: lumpSumStack.sats, name: "Lump sum", color: colors.orange, unit: "sats" }),
s({ metric: lumpSumStack.bitcoin, name: "Lump sum", color: colors.orange, unit: "btc" }),
s({ metric: lumpSumStack.dollars, name: "Lump sum", color: colors.orange, unit: "usd" }),
],
},
],
};
}),
},
// DCA classes
{
name: "DCA classes",
tree: [
// Comparison charts (all years overlaid)
{
name: "Compare",
tree: [
{
name: "Cost basis",
title: "DCA Cost Basis by Year",
top: dcaClasses.map(({ year, color, defaultActive, costBasis }) =>
s({ metric: costBasis, name: `${year}`, color, defaultActive, unit: "usd" }),
),
},
{
name: "Returns",
title: "DCA Returns by Year",
bottom: dcaClasses.map(({ year, color, defaultActive, returns }) =>
/** @type {AnyFetchedSeriesBlueprint} */ ({
metric: returns,
title: `${year}`,
type: "Baseline",
color,
defaultActive,
unit: "percentage",
}),
),
},
{
name: "Stack",
title: "DCA Stack by Year ($100/day)",
bottom: dcaClasses.flatMap(({ year, color, defaultActive, stack }) => [
s({ metric: stack.sats, name: `${year}`, color, defaultActive, unit: "sats" }),
s({ metric: stack.bitcoin, name: `${year}`, color, defaultActive, unit: "btc" }),
s({ metric: stack.dollars, name: `${year}`, color, defaultActive, unit: "usd" }),
]),
},
],
},
// Individual year charts
...dcaClasses.map(({ year, color, costBasis, returns, stack }) => ({
name: `${year}`,
tree: [
{
name: "Cost basis",
title: `DCA Class ${year} Cost Basis`,
top: [s({ metric: costBasis, name: "Cost basis", color, unit: "usd" })],
},
{
name: "Returns",
title: `DCA Class ${year} Returns`,
bottom: [
/** @type {AnyFetchedSeriesBlueprint} */ ({
metric: returns,
title: "Returns",
type: "Baseline",
color,
unit: "percentage",
}),
],
},
{
name: "Stack",
title: `DCA Class ${year} Stack ($100/day)`,
bottom: [
s({ metric: stack.sats, name: "Stack", color, unit: "sats" }),
s({ metric: stack.bitcoin, name: "Stack", color, unit: "btc" }),
s({ metric: stack.dollars, name: "Stack", color, unit: "usd" }),
],
},
],
})),
],
},
],
};
}

View File

@@ -0,0 +1,59 @@
/** Performance section */
import { periodIdToName } from "./utils.js";
/**
* Create Performance section
* @param {PartialContext} ctx
* @param {Market["returns"]} returns
*/
export function createPerformanceSection(ctx, returns) {
const { colors, createPriceLine } = ctx;
return {
name: "Performance",
tree: /** @type {const} */ ([
["1d", "_1d", undefined],
["1w", "_1w", undefined],
["1m", "_1m", undefined],
["3m", "_3m", undefined],
["6m", "_6m", undefined],
["1y", "_1y", undefined],
["2y", "_2y", "_2y"],
["3y", "_3y", "_3y"],
["4y", "_4y", "_4y"],
["5y", "_5y", "_5y"],
["6y", "_6y", "_6y"],
["8y", "_8y", "_8y"],
["10y", "_10y", "_10y"],
]).map(([id, returnKey, cagrKey]) => {
const priceReturns = returns.priceReturns[returnKey];
const cagr = cagrKey ? returns.cagr[cagrKey] : undefined;
const name = periodIdToName(id, true);
return {
name,
title: `${name} Performance`,
bottom: [
/** @type {AnyFetchedSeriesBlueprint} */ ({
metric: priceReturns,
title: "total",
type: "Baseline",
unit: "percentage",
}),
...(cagr
? [
/** @type {AnyFetchedSeriesBlueprint} */ ({
metric: cagr,
title: "cagr",
type: "Baseline",
colors: [colors.lime, colors.pink],
unit: "percentage",
}),
]
: []),
createPriceLine({ unit: "percentage" }),
],
};
}),
};
}

View File

@@ -0,0 +1,23 @@
/** Market utilities */
/**
* Convert period ID to readable name
* @param {string} id
* @param {boolean} [compoundAdjective]
*/
export function periodIdToName(id, compoundAdjective) {
const num = parseInt(id);
const s = compoundAdjective || num === 1 ? "" : "s";
switch (id.slice(-1)) {
case "d":
return `${num} day${s}`;
case "w":
return `${num} week${s}`;
case "m":
return `${num} month${s}`;
case "y":
return `${num} year${s}`;
default:
return id;
}
}

View File

@@ -3,7 +3,7 @@
/**
* Create a single series from a tree accessor
* @param {Object} args
* @param {MetricAccessor<any>} args.metric - Tree accessor with .by property
* @param {AnyMetricPattern} args.metric - Tree accessor with .by property
* @param {string} args.name - Display name for the series
* @param {Color} [args.color]
* @param {Unit} [args.unit]

View File

@@ -27,7 +27,7 @@
*
* @typedef {Object} HistogramSeriesBlueprintSpecific
* @property {"Histogram"} type
* @property {Color} color
* @property {Color | [Color, Color]} [color] - Single color or [positive, negative] colors (defaults to green/red)
* @property {HistogramSeriesPartialOptions} [options]
* @property {Accessor<HistogramData[]>} [data]
* @typedef {BaseSeriesBlueprint & HistogramSeriesBlueprintSpecific} HistogramSeriesBlueprint
@@ -36,7 +36,7 @@
*
* @typedef {AnySeriesBlueprint["type"]} SeriesType
*
* @typedef {{ metric: MetricAccessor<any>, unit?: Unit }} FetchedAnySeriesOptions
* @typedef {{ metric: AnyMetricPattern, unit?: Unit }} FetchedAnySeriesOptions
*
* @typedef {BaselineSeriesBlueprint & FetchedAnySeriesOptions} FetchedBaselineSeriesBlueprint
* @typedef {CandlestickSeriesBlueprint & FetchedAnySeriesOptions} FetchedCandlestickSeriesBlueprint
@@ -168,17 +168,13 @@
* @typedef {Object} PartialContext
* @property {Colors} colors
* @property {BrkClient} brk
* @property {BrkClient["tree"]["computed"]["constants"]} constants
* @property {(args: { metric: MetricAccessor<any>, name: string, color?: Color, defaultActive?: boolean, unit?: Unit, options?: LineSeriesPartialOptions }) => AnyFetchedSeriesBlueprint} s
* @property {(args: { metric: AnyMetricPattern, name: string, color?: Color, defaultActive?: boolean, unit?: Unit, options?: LineSeriesPartialOptions }) => AnyFetchedSeriesBlueprint} s
* @property {(pattern: BlockCountPattern<any>, title: string, color?: Color) => AnyFetchedSeriesBlueprint[]} fromBlockCount
* @property {(pattern: BitcoinPattern<any>, title: string, color?: Color) => AnyFetchedSeriesBlueprint[]} fromBitcoin
* @property {(pattern: BlockSizePattern<any>, title: string, color?: Color) => AnyFetchedSeriesBlueprint[]} fromBlockSize
* @property {(num: number) => Constant0Pattern<any>} getConstant
* @property {(pattern: Constant0Pattern<any>) => MetricAccessor<any>} flattenConstant
* @property {(args: { number?: number, name?: string, defaultActive?: boolean, lineStyle?: number, color?: Color, unit: Unit }) => FetchedLineSeriesBlueprint} createPriceLine
* @property {(args: { number?: number, name?: string, defaultActive?: boolean, lineStyle?: LineStyle, color?: Color, unit: Unit }) => FetchedLineSeriesBlueprint} createPriceLine
* @property {(args: { numbers: number[], unit: Unit }) => FetchedLineSeriesBlueprint[]} createPriceLines
* @property {(args: { constant: Constant0Pattern<any>, name: string, unit: Unit, color?: Color, lineStyle?: number, defaultActive?: boolean }) => FetchedLineSeriesBlueprint} line
* @property {MetricAccessor<any>} constant100
* @property {(args: { constant: AnyMetricPattern, name: string, unit: Unit, color?: Color, lineStyle?: number, defaultActive?: boolean }) => FetchedLineSeriesBlueprint} line
*/
// Re-export for type consumers

View File

@@ -2,17 +2,17 @@
import { localhost } from "../utils/env.js";
/** @type {Set<MetricAccessor<any>> | null} */
/** @type {Set<AnyMetricPattern> | null} */
export const unused = localhost ? new Set() : null;
/**
* Walk and collect MetricAccessors
* Walk and collect AnyMetricPatterns
* @param {TreeNode | null | undefined} node
* @param {Set<MetricAccessor<unknown>>} set
* @param {Set<AnyMetricPattern>} set
*/
function walk(node, set) {
if (node && "by" in node) {
set.add(/** @type {MetricAccessor<unknown>} */ (node));
set.add(/** @type {AnyMetricPattern} */ (node));
} else if (node && typeof node === "object") {
for (const value of Object.values(node)) {
walk(/** @type {TreeNode | null | undefined} */ (value), set);
@@ -21,7 +21,7 @@ function walk(node, set) {
}
/**
* Collect all MetricAccessors from tree
* Collect all AnyMetricPatterns from tree
* @param {TreeNode} tree
*/
export function collect(tree) {
@@ -30,7 +30,7 @@ export function collect(tree) {
/**
* Mark a metric as used
* @param {MetricAccessor<any>} metric
* @param {AnyMetricPattern} metric
*/
export function markUsed(metric) {
unused?.delete(metric);