global: snapshot

This commit is contained in:
nym21
2026-03-26 15:57:22 +01:00
parent 6d3307c0df
commit 18bb4186a8
72 changed files with 2013 additions and 1150 deletions

View File

@@ -69,19 +69,19 @@ const ALL_YEARS = /** @type {const} */ ([...YEARS_2020S, ...YEARS_2010S]);
/**
* Build DCA class entry from year
* @param {MarketDca} dca
* @param {Investing} investing
* @param {DcaYear} year
* @param {number} i
* @returns {BaseEntryItem}
*/
function buildYearEntry(dca, year, i) {
function buildYearEntry(investing, year, i) {
const key = /** @type {DcaYearKey} */ (`from${year}`);
return {
name: `${year}`,
color: colors.at(i, ALL_YEARS.length),
costBasis: dca.class.costBasis[key],
returns: dca.class.return[key],
stack: dca.class.stack[key],
costBasis: investing.class.costBasis[key],
returns: investing.class.return[key],
stack: investing.class.stack[key],
};
}
@@ -90,16 +90,16 @@ function buildYearEntry(dca, year, i) {
* @returns {PartialOptionsGroup}
*/
export function createInvestingSection() {
const { market } = brk.series;
const { dca, lookback, returns } = market;
const { market, investing } = brk.series;
const { lookback, returns } = market;
return {
name: "Investing",
tree: [
createDcaVsLumpSumSection({ dca, lookback, returns }),
createDcaByPeriodSection({ dca, returns }),
createLumpSumByPeriodSection({ dca, lookback, returns }),
createDcaByStartYearSection({ dca }),
createDcaVsLumpSumSection({ investing, lookback, returns }),
createDcaByPeriodSection({ investing, returns }),
createLumpSumByPeriodSection({ investing, lookback, returns }),
createDcaByStartYearSection({ investing }),
],
};
}
@@ -270,15 +270,15 @@ function createLongSingleEntry(item) {
/**
* Create DCA vs Lump Sum section
* @param {Object} args
* @param {Market["dca"]} args.dca
* @param {Investing} args.investing
* @param {Market["lookback"]} args.lookback
* @param {Market["returns"]} args.returns
*/
export function createDcaVsLumpSumSection({ dca, lookback, returns }) {
export function createDcaVsLumpSumSection({ investing, lookback, returns }) {
/** @param {AllPeriodKey} key */
const topPane = (key) => [
price({
series: dca.period.costBasis[key],
series: investing.period.costBasis[key],
name: "DCA",
color: colors.profit,
}),
@@ -299,11 +299,11 @@ export function createDcaVsLumpSumSection({ dca, lookback, returns }) {
top: topPane(key),
bottom: [
...percentRatioBaseline({
pattern: dca.period.return[key],
pattern: investing.period.return[key],
name: "DCA",
}),
...percentRatioBaseline({
pattern: dca.period.lumpSumReturn[key],
pattern: investing.period.lumpSumReturn[key],
name: "Lump Sum",
color: colors.bi.p2,
}),
@@ -317,7 +317,7 @@ export function createDcaVsLumpSumSection({ dca, lookback, returns }) {
top: topPane(key),
bottom: [
...percentRatioBaseline({
pattern: dca.period.cagr[key],
pattern: investing.period.cagr[key],
name: "DCA",
}),
...percentRatioBaseline({
@@ -335,12 +335,12 @@ export function createDcaVsLumpSumSection({ dca, lookback, returns }) {
top: topPane(key),
bottom: [
...satsBtcUsd({
pattern: dca.period.stack[key],
pattern: investing.period.stack[key],
name: "DCA",
color: colors.profit,
}),
...satsBtcUsd({
pattern: dca.period.lumpSumStack[key],
pattern: investing.period.lumpSumStack[key],
name: "Lump Sum",
color: colors.bitcoin,
}),
@@ -395,11 +395,11 @@ export function createDcaVsLumpSumSection({ dca, lookback, returns }) {
/**
* Create period-based section (DCA or Lump Sum)
* @param {Object} args
* @param {Market["dca"]} args.dca
* @param {Investing} args.investing
* @param {Market["lookback"]} [args.lookback]
* @param {Market["returns"]} args.returns
*/
function createPeriodSection({ dca, lookback, returns }) {
function createPeriodSection({ investing, lookback, returns }) {
const isLumpSum = !!lookback;
const suffix = isLumpSum ? "Lump Sum" : "DCA";
@@ -409,20 +409,20 @@ function createPeriodSection({ dca, lookback, returns }) {
const buildBaseEntry = (key, i) => ({
name: periodName(key),
color: colors.at(i, allPeriods.length),
costBasis: isLumpSum ? lookback[key] : dca.period.costBasis[key],
costBasis: isLumpSum ? lookback[key] : investing.period.costBasis[key],
returns: isLumpSum
? dca.period.lumpSumReturn[key]
: dca.period.return[key],
? investing.period.lumpSumReturn[key]
: investing.period.return[key],
stack: isLumpSum
? dca.period.lumpSumStack[key]
: dca.period.stack[key],
? investing.period.lumpSumStack[key]
: investing.period.stack[key],
});
/** @param {LongPeriodKey} key @param {number} i @returns {LongEntryItem} */
const buildLongEntry = (key, i) =>
withCagr(
buildBaseEntry(key, i),
isLumpSum ? returns.cagr[key] : dca.period.cagr[key],
isLumpSum ? returns.cagr[key] : investing.period.cagr[key],
);
/** @param {BaseEntryItem} entry */
@@ -471,30 +471,30 @@ function createPeriodSection({ dca, lookback, returns }) {
/**
* Create DCA by Period section
* @param {Object} args
* @param {Market["dca"]} args.dca
* @param {Investing} args.investing
* @param {Market["returns"]} args.returns
*/
export function createDcaByPeriodSection({ dca, returns }) {
return createPeriodSection({ dca, returns });
export function createDcaByPeriodSection({ investing, returns }) {
return createPeriodSection({ investing, returns });
}
/**
* Create Lump Sum by Period section
* @param {Object} args
* @param {Market["dca"]} args.dca
* @param {Investing} args.investing
* @param {Market["lookback"]} args.lookback
* @param {Market["returns"]} args.returns
*/
export function createLumpSumByPeriodSection({ dca, lookback, returns }) {
return createPeriodSection({ dca, lookback, returns });
export function createLumpSumByPeriodSection({ investing, lookback, returns }) {
return createPeriodSection({ investing, lookback, returns });
}
/**
* Create DCA by Start Year section
* @param {Object} args
* @param {Market["dca"]} args.dca
* @param {Investing} args.investing
*/
export function createDcaByStartYearSection({ dca }) {
export function createDcaByStartYearSection({ investing }) {
/** @param {string} name @param {string} title @param {BaseEntryItem[]} entries */
const createDecadeGroup = (name, title, entries) => ({
name,
@@ -511,10 +511,10 @@ export function createDcaByStartYearSection({ dca }) {
});
const entries2020s = YEARS_2020S.map((year, i) =>
buildYearEntry(dca, year, i),
buildYearEntry(investing, year, i),
);
const entries2010s = YEARS_2010S.map((year, i) =>
buildYearEntry(dca, year, YEARS_2020S.length + i),
buildYearEntry(investing, year, YEARS_2020S.length + i),
);
return {

View File

@@ -16,7 +16,7 @@ import {
ROLLING_WINDOWS,
ROLLING_WINDOWS_TO_1M,
} from "./series.js";
import { simplePriceRatioTree } from "./shared.js";
import { simplePriceRatioTree, percentileBands, priceBands } from "./shared.js";
import { periodIdToName } from "./utils.js";
/**
@@ -165,7 +165,10 @@ function returnsSubSectionWithCagr(name, periods) {
...periods.map((p) => ({
name: periodIdToName(p.id, true),
title: `${periodIdToName(p.id, true)} Total Price Returns`,
bottom: percentRatioBaseline({ pattern: p.returns, name: "Return" }),
bottom: percentRatioBaseline({
pattern: p.returns,
name: "Return",
}),
})),
],
},
@@ -1084,6 +1087,46 @@ export function createMarketSection() {
color: colors.loss,
}),
},
{
name: "Thermometer",
tree: [
{
name: "Bands",
title: "Thermometer",
top: priceBands(percentileBands(indicators.thermometer), { defaultActive: true }),
},
{
name: "Score",
title: "Thermometer",
top: priceBands(percentileBands(indicators.thermometer)),
bottom: [
histogram({
series: indicators.thermometer.zone,
name: "Zone",
unit: Unit.count,
colorFn: (v) => /** @type {const} */ ([
colors.ratioPct._0_5,
colors.ratioPct._1,
colors.ratioPct._2,
colors.ratioPct._5,
colors.transparent,
colors.ratioPct._95,
colors.ratioPct._98,
colors.ratioPct._99,
colors.ratioPct._99_5,
])[v + 4],
}),
baseline({
series: indicators.thermometer.score,
name: "Score",
unit: Unit.count,
color: [colors.ratioPct._99, colors.ratioPct._1],
defaultActive: false,
}),
],
},
],
},
],
},
],

View File

@@ -147,6 +147,7 @@ function percentileSeries({ pattern, unit, title = "" }) {
* @param {string} [args.key] - Optional key for persistence (derived from name if not provided)
* @param {LineStyle} [args.style]
* @param {Color} [args.color]
* @param {(value: number) => Color} [args.colorFn]
* @param {boolean} [args.defaultActive]
* @param {LineSeriesPartialOptions} [args.options]
* @returns {FetchedLineSeriesBlueprint}
@@ -157,6 +158,7 @@ export function line({
key,
style,
color,
colorFn,
defaultActive,
unit,
options,
@@ -166,6 +168,7 @@ export function line({
title: name,
key,
color,
colorFn,
unit,
defaultActive,
options: {
@@ -370,6 +373,7 @@ export function dotsBaseline({
* @param {Unit} args.unit
* @param {string} [args.key] - Optional key for persistence (derived from name if not provided)
* @param {Color | [Color, Color]} [args.color]
* @param {(value: number) => Color} [args.colorFn]
* @param {boolean} [args.defaultActive]
* @param {HistogramSeriesPartialOptions} [args.options]
* @returns {FetchedHistogramSeriesBlueprint}
@@ -379,6 +383,7 @@ export function histogram({
name,
key,
color,
colorFn,
defaultActive,
unit,
options,
@@ -389,6 +394,7 @@ export function histogram({
title: name,
key,
color,
colorFn,
unit,
defaultActive,
options,

View File

@@ -1,7 +1,13 @@
/** Shared helpers for options */
import { Unit } from "../utils/units.js";
import { ROLLING_WINDOWS, line, baseline, price, sumsAndAveragesCumulativeWith } from "./series.js";
import {
ROLLING_WINDOWS,
line,
baseline,
price,
sumsAndAveragesCumulativeWith,
} from "./series.js";
import { priceLine, priceLines } from "./constants.js";
import { colors } from "../utils/colors.js";
@@ -270,11 +276,19 @@ export function simplePriceRatioTree({ pattern, title, legend, color }) {
}
/**
* @template T
* @param {InvestorPercentilesPattern} p
* @param {(entry: InvestorPercentileEntry) => T} extract
* @param {{ pct95: AnyPricePattern, pct5: AnyPricePattern, pct98: AnyPricePattern, pct2: AnyPricePattern, pct99: AnyPricePattern, pct1: AnyPricePattern, pct995: AnyPricePattern, pct05: AnyPricePattern }} p
*/
function percentileBands(p, extract) {
export function percentileBands(p) {
return percentileBandsWith(p, (e) => e);
}
/**
* @template E
* @template T
* @param {{ pct95: E, pct5: E, pct98: E, pct2: E, pct99: E, pct1: E, pct995: E, pct05: E }} p
* @param {(entry: E) => T} extract
*/
export function percentileBandsWith(p, extract) {
return [
{ name: "P95", prop: extract(p.pct95), color: colors.ratioPct._95 },
{ name: "P5", prop: extract(p.pct5), color: colors.ratioPct._5 },
@@ -282,20 +296,38 @@ function percentileBands(p, extract) {
{ name: "P2", prop: extract(p.pct2), color: colors.ratioPct._2 },
{ name: "P99", prop: extract(p.pct99), color: colors.ratioPct._99 },
{ name: "P1", prop: extract(p.pct1), color: colors.ratioPct._1 },
{ name: "P99.5", prop: extract(p.pct995), color: colors.ratioPct._99_5 },
{ name: "P0.5", prop: extract(p.pct05), color: colors.ratioPct._0_5 },
];
}
/** @param {{ name: string, prop: AnyPricePattern, color: Color }[]} bands */
function priceBands(bands) {
/**
* @param {{ name: string, prop: AnyPricePattern, color: Color }[]} bands
* @param {{ defaultActive?: boolean }} [opts]
*/
export function priceBands(bands, opts) {
return bands.map(({ name, prop, color }) =>
price({ series: prop, name, color, defaultActive: false, options: { lineStyle: 1 } }),
price({
series: prop,
name,
color,
defaultActive: opts?.defaultActive ?? false,
options: { lineStyle: 1 },
}),
);
}
/** @param {{ name: string, prop: AnySeriesPattern, color: Color }[]} bands */
function ratioBands(bands) {
return bands.map(({ name, prop, color }) =>
line({ series: prop, name, color, defaultActive: false, unit: Unit.ratio, options: { lineStyle: 1 } }),
line({
series: prop,
name,
color,
defaultActive: false,
unit: Unit.ratio,
options: { lineStyle: 1 },
}),
);
}
@@ -319,8 +351,8 @@ export function priceRatioPercentilesTree({
priceReferences,
}) {
const p = pattern.percentiles;
const pctUsd = percentileBands(p, (e) => e.price);
const pctRatio = percentileBands(p, (e) => e.ratio);
const pctUsd = percentileBandsWith(p, (e) => e.price);
const pctRatio = percentileBandsWith(p, (e) => e.ratio);
return [
{
name: "Price",
@@ -500,7 +532,11 @@ export function ratioSmas(ratio) {
{ name: "1y SMA", series: ratio.sma._1y.ratio },
{ name: "2y SMA", series: ratio.sma._2y.ratio },
{ name: "4y SMA", series: ratio.sma._4y.ratio },
{ name: "All Time SMA", series: ratio.sma.all.ratio, color: colors.time.all },
{
name: "All Time SMA",
series: ratio.sma.all.ratio,
color: colors.time.all,
},
].map((s, i, arr) => ({ color: colors.at(i, arr.length), ...s }));
}
@@ -543,7 +579,14 @@ export function ratioBottomSeries(ratio) {
* @param {string} [args.legend]
* @returns {PartialChartOption}
*/
export function createRatioChart({ title, pricePattern, ratio, color, name, legend }) {
export function createRatioChart({
title,
pricePattern,
ratio,
color,
name,
legend,
}) {
return {
name: name ?? "Ratio",
title: title(name ?? "Ratio"),
@@ -727,7 +770,7 @@ export function createPriceRatioCharts({
priceReferences,
}) {
const titleFn = formatCohortTitle(context);
const pctUsd = percentileBands(ratio.percentiles, (e) => e.price);
const pctUsd = percentileBandsWith(ratio.percentiles, (e) => e.price);
return [
{
name: "Price",
@@ -775,20 +818,39 @@ export function createPriceRatioCharts({
* @param {Unit} args.unit
* @returns {PartialOptionsTree}
*/
export function groupedWindowsCumulative({ list, all, title, metricTitle, getWindowSeries, getCumulativeSeries, seriesFn, unit }) {
export function groupedWindowsCumulative({
list,
all,
title,
metricTitle,
getWindowSeries,
getCumulativeSeries,
seriesFn,
unit,
}) {
return [
...ROLLING_WINDOWS.map((w) => ({
name: w.name,
title: title(`${w.title} ${metricTitle}`),
bottom: mapCohortsWithAll(list, all, (c) =>
seriesFn({ series: getWindowSeries(c, w.key), name: c.name, color: c.color, unit }),
seriesFn({
series: getWindowSeries(c, w.key),
name: c.name,
color: c.color,
unit,
}),
),
})),
{
name: "Cumulative",
title: title(`Cumulative ${metricTitle}`),
bottom: mapCohortsWithAll(list, all, (c) =>
seriesFn({ series: getCumulativeSeries(c), name: c.name, color: c.color, unit }),
seriesFn({
series: getCumulativeSeries(c),
name: c.name,
color: c.color,
unit,
}),
),
},
];
@@ -807,9 +869,21 @@ export function groupedWindowsCumulative({ list, all, title, metricTitle, getWin
* @param {(args: { series: AnySeriesPattern, name: string, color: Color, unit: Unit }) => AnyFetchedSeriesBlueprint} [args.seriesFn]
* @returns {PartialOptionsTree}
*/
export function groupedWindowsCumulativeUsd({ list, all, title, metricTitle, getMetric, seriesFn = line }) {
export function groupedWindowsCumulativeUsd({
list,
all,
title,
metricTitle,
getMetric,
seriesFn = line,
}) {
return groupedWindowsCumulative({
list, all, title, metricTitle, seriesFn, unit: Unit.usd,
list,
all,
title,
metricTitle,
seriesFn,
unit: Unit.usd,
getWindowSeries: (c, key) => getMetric(c).sum[key].usd,
getCumulativeSeries: (c) => getMetric(c).cumulative.usd,
});
@@ -827,20 +901,34 @@ export function groupedWindowsCumulativeUsd({ list, all, title, metricTitle, get
* @param {(c: T | A) => { sum: Record<string, AnyValuePattern>, cumulative: AnyValuePattern }} args.getMetric
* @returns {PartialOptionsTree}
*/
export function groupedWindowsCumulativeSatsBtcUsd({ list, all, title, metricTitle, getMetric }) {
export function groupedWindowsCumulativeSatsBtcUsd({
list,
all,
title,
metricTitle,
getMetric,
}) {
return [
...ROLLING_WINDOWS.map((w) => ({
name: w.name,
title: title(`${w.title} ${metricTitle}`),
bottom: flatMapCohortsWithAll(list, all, (c) =>
satsBtcUsd({ pattern: getMetric(c).sum[w.key], name: c.name, color: c.color }),
satsBtcUsd({
pattern: getMetric(c).sum[w.key],
name: c.name,
color: c.color,
}),
),
})),
{
name: "Cumulative",
title: title(`Cumulative ${metricTitle}`),
bottom: flatMapCohortsWithAll(list, all, (c) =>
satsBtcUsd({ pattern: getMetric(c).cumulative, name: c.name, color: c.color }),
satsBtcUsd({
pattern: getMetric(c).cumulative,
name: c.name,
color: c.color,
}),
),
},
];

View File

@@ -20,12 +20,14 @@
* @typedef {Object} LineSeriesBlueprintSpecific
* @property {"Line"} [type]
* @property {Color} [color]
* @property {(value: number) => Color} [colorFn]
* @property {LineSeriesPartialOptions} [options]
* @typedef {BaseSeriesBlueprint & LineSeriesBlueprintSpecific} LineSeriesBlueprint
*
* @typedef {Object} HistogramSeriesBlueprintSpecific
* @property {"Histogram"} type
* @property {Color | [Color, Color]} [color] - Single color or [positive, negative] colors (defaults to green/red)
* @property {(value: number) => Color} [colorFn]
* @property {HistogramSeriesPartialOptions} [options]
* @typedef {BaseSeriesBlueprint & HistogramSeriesBlueprintSpecific} HistogramSeriesBlueprint
*