global: snapshot

This commit is contained in:
nym21
2026-01-19 16:52:17 +01:00
parent c90953adbe
commit 371ff86287
23 changed files with 1043 additions and 908 deletions
+2 -15
View File
@@ -1,6 +1,7 @@
/** Chain section builder - typed tree-based patterns */
import { Unit } from "../utils/units.js";
import { satsBtcUsd } from "./shared.js";
/**
* Create Chain section
@@ -239,21 +240,7 @@ export function createChainSection(ctx) {
name: "Volume",
title: "Transaction Volume",
bottom: [
line({
metric: transactions.volume.sentSum.sats,
name: "Sent",
unit: Unit.sats,
}),
line({
metric: transactions.volume.sentSum.bitcoin,
name: "Sent",
unit: Unit.btc,
}),
line({
metric: transactions.volume.sentSum.dollars,
name: "Sent",
unit: Unit.usd,
}),
...satsBtcUsd(ctx, transactions.volume.sentSum, "Sent"),
line({
metric: transactions.volume.annualizedVolume.sats,
name: "annualized",
+19 -121
View File
@@ -1,6 +1,7 @@
/** Shared cohort chart section builders */
import { Unit } from "../../utils/units.js";
import { satsBtcUsd } from "../shared.js";
/**
* Create supply section for a single cohort
@@ -13,24 +14,7 @@ export function createSingleSupplySeries(ctx, cohort) {
const { tree } = cohort;
return [
line({
metric: tree.supply.total.sats,
name: "Supply",
color: colors.default,
unit: Unit.sats,
}),
line({
metric: tree.supply.total.bitcoin,
name: "Supply",
color: colors.default,
unit: Unit.btc,
}),
line({
metric: tree.supply.total.dollars,
name: "Supply",
color: colors.default,
unit: Unit.usd,
}),
...satsBtcUsd(ctx, tree.supply.total, "Supply", colors.default),
...("supplyRelToCirculatingSupply" in tree.relative
? [
line({
@@ -41,63 +25,12 @@ export function createSingleSupplySeries(ctx, cohort) {
}),
]
: []),
line({
metric: tree.unrealized.supplyInProfit.sats,
name: "In Profit",
color: colors.green,
unit: Unit.sats,
}),
line({
metric: tree.unrealized.supplyInProfit.bitcoin,
name: "In Profit",
color: colors.green,
unit: Unit.btc,
}),
line({
metric: tree.unrealized.supplyInProfit.dollars,
name: "In Profit",
color: colors.green,
unit: Unit.usd,
}),
line({
metric: tree.unrealized.supplyInLoss.sats,
name: "In Loss",
color: colors.red,
unit: Unit.sats,
}),
line({
metric: tree.unrealized.supplyInLoss.bitcoin,
name: "In Loss",
color: colors.red,
unit: Unit.btc,
}),
line({
metric: tree.unrealized.supplyInLoss.dollars,
name: "In Loss",
color: colors.red,
unit: Unit.usd,
}),
line({
metric: tree.supply.halved.sats,
name: "half",
color: colors.gray,
unit: Unit.sats,
...satsBtcUsd(ctx, tree.unrealized.supplyInProfit, "In Profit", colors.green),
...satsBtcUsd(ctx, tree.unrealized.supplyInLoss, "In Loss", colors.red),
...satsBtcUsd(ctx, tree.supply.halved, "half", colors.gray).map((s) => ({
...s,
options: { lineStyle: 4 },
}),
line({
metric: tree.supply.halved.bitcoin,
name: "half",
color: colors.gray,
unit: Unit.btc,
options: { lineStyle: 4 },
}),
line({
metric: tree.supply.halved.dollars,
name: "half",
color: colors.gray,
unit: Unit.usd,
options: { lineStyle: 4 },
}),
})),
...("supplyInProfitRelToCirculatingSupply" in tree.relative
? [
line({
@@ -147,17 +80,16 @@ export function createGroupedSupplyTotalSeries(ctx, list) {
const constant100 = brk.metrics.constants.constant100;
return list.flatMap(({ color, name, tree }) => [
line({ metric: tree.supply.total.sats, name, color, unit: Unit.sats }),
line({ metric: tree.supply.total.bitcoin, name, color, unit: Unit.btc }),
line({ metric: tree.supply.total.dollars, name, color, unit: Unit.usd }),
"supplyRelToCirculatingSupply" in tree.relative
? line({
metric: tree.relative.supplyRelToCirculatingSupply,
name,
color,
unit: Unit.pctSupply,
})
: line({ metric: constant100, name, color, unit: Unit.pctSupply }),
...satsBtcUsd(ctx, tree.supply.total, name, color),
line({
metric:
"supplyRelToCirculatingSupply" in tree.relative
? tree.relative.supplyRelToCirculatingSupply
: constant100,
name,
color,
unit: Unit.pctSupply,
}),
]);
}
@@ -171,24 +103,7 @@ export function createGroupedSupplyInProfitSeries(ctx, list) {
const { line } = ctx;
return list.flatMap(({ color, name, tree }) => [
line({
metric: tree.unrealized.supplyInProfit.sats,
name,
color,
unit: Unit.sats,
}),
line({
metric: tree.unrealized.supplyInProfit.bitcoin,
name,
color,
unit: Unit.btc,
}),
line({
metric: tree.unrealized.supplyInProfit.dollars,
name,
color,
unit: Unit.usd,
}),
...satsBtcUsd(ctx, tree.unrealized.supplyInProfit, name, color),
...("supplyInProfitRelToCirculatingSupply" in tree.relative
? [
line({
@@ -212,24 +127,7 @@ export function createGroupedSupplyInLossSeries(ctx, list) {
const { line } = ctx;
return list.flatMap(({ color, name, tree }) => [
line({
metric: tree.unrealized.supplyInLoss.sats,
name,
color,
unit: Unit.sats,
}),
line({
metric: tree.unrealized.supplyInLoss.bitcoin,
name,
color,
unit: Unit.btc,
}),
line({
metric: tree.unrealized.supplyInLoss.dollars,
name,
color,
unit: Unit.usd,
}),
...satsBtcUsd(ctx, tree.unrealized.supplyInLoss, name, color),
...("supplyInLossRelToCirculatingSupply" in tree.relative
? [
line({
+20 -92
View File
@@ -1,6 +1,14 @@
/** Cointime section builder - typed tree-based patterns */
import { Unit } from "../utils/units.js";
import {
satsBtcUsd,
priceLines,
percentileUsdMap,
percentileMap,
sdPatterns,
sdBands,
} from "./shared.js";
/**
* Create price with ratio options for cointime prices
@@ -19,50 +27,9 @@ function createCointimePriceWithRatioOptions(
) {
const { line, colors, createPriceLine } = ctx;
// 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 },
];
const pctUsdMap = percentileUsdMap(colors, ratio);
const pctMap = percentileMap(colors, ratio);
const sdPats = sdPatterns(ratio);
return [
{
@@ -75,7 +42,7 @@ function createCointimePriceWithRatioOptions(
title: `${title} Ratio`,
top: [
line({ metric: price, name: legend, color, unit: Unit.usd }),
...percentileUsdMap.map(({ name: pctName, prop, color: pctColor }) =>
...pctUsdMap.map(({ name: pctName, prop, color: pctColor }) =>
line({
metric: prop,
name: pctName,
@@ -124,7 +91,7 @@ function createCointimePriceWithRatioOptions(
color: colors.rose,
unit: Unit.ratio,
}),
...percentileMap.map(({ name: pctName, prop, color: pctColor }) =>
...pctMap.map(({ name: pctName, prop, color: pctColor }) =>
line({
metric: prop,
name: pctName,
@@ -200,22 +167,14 @@ function createCointimePriceWithRatioOptions(
color: colors.yellow,
unit: Unit.sd,
}),
createPriceLine({ unit: Unit.sd, number: 4 }),
createPriceLine({ unit: Unit.sd, number: 3 }),
createPriceLine({ unit: Unit.sd, number: 2 }),
createPriceLine({ unit: Unit.sd, number: 1 }),
createPriceLine({ unit: Unit.sd, number: 0 }),
createPriceLine({ unit: Unit.sd, number: -1 }),
createPriceLine({ unit: Unit.sd, number: -2 }),
createPriceLine({ unit: Unit.sd, number: -3 }),
createPriceLine({ unit: Unit.sd, number: -4 }),
...priceLines(ctx, Unit.sd, [0, 1, -1, 2, -2, 3, -3, 4, -4]),
],
},
// Individual Z-Score charts
...sdPatterns.map(({ nameAddon, titleAddon, sd }) => ({
...sdPats.map(({ nameAddon, titleAddon, sd }) => ({
name: nameAddon,
title: `${title} ${titleAddon} Z-Score`,
top: getSdBands(sd).map(({ name: bandName, prop, color: bandColor }) =>
top: sdBands(colors, sd).map(({ name: bandName, prop, color: bandColor }) =>
line({
metric: prop,
name: bandName,
@@ -225,13 +184,7 @@ function createCointimePriceWithRatioOptions(
),
bottom: [
line({ metric: sd.zscore, name: "Z-Score", color, unit: Unit.sd }),
createPriceLine({ unit: Unit.sd, number: 3 }),
createPriceLine({ unit: Unit.sd, number: 2 }),
createPriceLine({ unit: Unit.sd, number: 1 }),
createPriceLine({ unit: Unit.sd, number: 0 }),
createPriceLine({ unit: Unit.sd, number: -1 }),
createPriceLine({ unit: Unit.sd, number: -2 }),
createPriceLine({ unit: Unit.sd, number: -3 }),
...priceLines(ctx, Unit.sd, [0, 1, -1, 2, -2, 3, -3]),
],
})),
],
@@ -395,34 +348,9 @@ export function createCointimeSection(ctx) {
name: "Supply",
title: "Cointime Supply",
bottom: [
// All supply (different pattern structure)
line({
metric: all.supply.total.sats,
name: "All",
color: colors.orange,
unit: Unit.sats,
}),
line({
metric: all.supply.total.bitcoin,
name: "All",
color: colors.orange,
unit: Unit.btc,
}),
line({
metric: all.supply.total.dollars,
name: "All",
color: colors.orange,
unit: Unit.usd,
}),
// Cointime supplies (ActiveSupplyPattern)
.../** @type {const} */ ([
[cointimeSupply.vaultedSupply, "Vaulted", colors.lime],
[cointimeSupply.activeSupply, "Active", colors.rose],
]).flatMap(([supplyItem, name, color]) => [
line({ metric: supplyItem.sats, name, color, unit: Unit.sats }),
line({ metric: supplyItem.bitcoin, name, color, unit: Unit.btc }),
line({ metric: supplyItem.dollars, name, color, unit: Unit.usd }),
]),
...satsBtcUsd(ctx, all.supply.total, "All", colors.orange),
...satsBtcUsd(ctx, cointimeSupply.vaultedSupply, "Vaulted", colors.lime),
...satsBtcUsd(ctx, cointimeSupply.activeSupply, "Active", colors.rose),
],
},
+140 -142
View File
@@ -2,7 +2,6 @@ import { createPartialOptions } from "./partial.js";
import {
createButtonElement,
createAnchorElement,
insertElementAtIndex,
} from "../utils/dom.js";
import { pushHistory, resetParams } from "../utils/url.js";
import { readStored, writeToStorage } from "../utils/storage.js";
@@ -43,6 +42,21 @@ export function initOptions({ colors, signals, brk, qrcode }) {
const parent = signals.createSignal(/** @type {HTMLElement | null} */ (null));
/** @type {Map<string, HTMLLIElement>} */
const liByPath = new Map();
/**
* @param {string[]} nodePath
*/
function isOnSelectedPath(nodePath) {
const selectedPath = selected()?.path;
return (
selectedPath &&
nodePath.length <= selectedPath.length &&
nodePath.every((v, i) => v === selectedPath[i])
);
}
/**
* @param {AnyFetchedSeriesBlueprint[]} [arr]
*/
@@ -120,137 +134,55 @@ export function initOptions({ colors, signals, brk, qrcode }) {
/** @type {Option | undefined} */
let savedOption;
// ============================================
// Phase 1: Process partial tree (non-reactive)
// Transforms options, computes counts, populates list
// ============================================
/**
* @typedef {{ type: "group"; name: string; serName: string; path: string[]; count: number; children: ProcessedNode[] }} ProcessedGroup
* @typedef {{ type: "option"; option: Option; path: string[] }} ProcessedOption
* @typedef {ProcessedGroup | ProcessedOption} ProcessedNode
*/
/**
* @param {PartialOptionsTree} partialTree
* @param {Accessor<HTMLElement | null>} parent
* @param {string[] | undefined} parentPath
* @returns {Accessor<number>}
* @param {string[]} parentPath
* @returns {ProcessedNode[]}
*/
function recursiveProcessPartialTree(
partialTree,
parent,
parentPath = [],
depth = 0,
) {
/** @type {Accessor<number>[]} */
const listForSum = [];
const ul = signals.createMemo(
// @ts_ignore
(_previous) => {
const previous = /** @type {HTMLUListElement | null} */ (_previous);
previous?.remove();
const _parent = parent();
if (_parent) {
if ("open" in _parent && !_parent.open) {
throw "Set accesor to null instead";
}
const ul = window.document.createElement("ul");
_parent.append(ul);
return ul;
} else {
return null;
}
},
null,
);
partialTree.forEach((anyPartial, partialIndex) => {
const renderLi = signals.createSignal(true);
const li = signals.createMemo((_previous) => {
const previous = _previous;
previous?.remove();
const _ul = ul();
if (renderLi() && _ul) {
const li = window.document.createElement("li");
insertElementAtIndex(_ul, li, partialIndex);
return li;
} else {
return null;
}
}, /** @type {HTMLLIElement | null} */ (null));
function processPartialTree(partialTree, parentPath = []) {
/** @type {ProcessedNode[]} */
const nodes = [];
for (const anyPartial of partialTree) {
if ("tree" in anyPartial) {
/** @type {Omit<OptionsGroup, keyof PartialOptionsGroup>} */
const groupAddons = {};
Object.assign(anyPartial, groupAddons);
const passedDetails = signals.createSignal(
/** @type {HTMLDivElement | HTMLDetailsElement | null} */ (null),
);
const serName = stringToId(anyPartial.name);
const path = [...parentPath, serName];
const childOptionsCount = recursiveProcessPartialTree(
anyPartial.tree,
passedDetails,
path,
depth + 1,
const children = processPartialTree(anyPartial.tree, path);
// Compute count from children
const count = children.reduce(
(sum, child) => sum + (child.type === "group" ? child.count : 1),
0,
);
listForSum.push(childOptionsCount);
// Skip groups with no children
if (count === 0) continue;
signals.createEffect(li, (li) => {
if (!li) {
passedDetails.set(null);
return;
}
signals.createEffect(selected, (selected) => {
if (
path.length <= selected.path.length &&
path.every((v, i) => selected.path.at(i) === v)
) {
li.dataset.highlight = "";
} else {
delete li.dataset.highlight;
}
});
const details = window.document.createElement("details");
details.dataset.name = serName;
li.appendChild(details);
const summary = window.document.createElement("summary");
details.append(summary);
summary.append(anyPartial.name);
const supCount = window.document.createElement("sup");
summary.append(supCount);
signals.createEffect(childOptionsCount, (childOptionsCount) => {
supCount.innerHTML = childOptionsCount.toLocaleString("en-us");
});
details.addEventListener("toggle", () => {
const open = details.open;
if (open) {
passedDetails.set(details);
} else {
passedDetails.set(null);
}
});
nodes.push({
type: "group",
name: anyPartial.name,
serName,
path,
count,
children,
});
function createRenderLiEffect() {
signals.createEffect(childOptionsCount, (count) => {
renderLi.set(!!count);
});
}
createRenderLiEffect();
} else {
const option = /** @type {Option} */ (anyPartial);
const name = option.name;
const path = [...parentPath, stringToId(option.name)];
// Transform partial to full option
if ("kind" in anyPartial && anyPartial.kind === "explorer") {
Object.assign(
option,
@@ -310,6 +242,7 @@ export function initOptions({ colors, signals, brk, qrcode }) {
list.push(option);
// Check if this matches URL or saved path
if (urlPath) {
const sameAsURLPath =
urlPath.length === path.length &&
@@ -326,38 +259,103 @@ export function initOptions({ colors, signals, brk, qrcode }) {
}
}
signals.createEffect(li, (li) => {
if (!li) {
return;
}
signals.createEffect(selected, (selected) => {
if (selected === option) {
li.dataset.highlight = "";
} else {
delete li.dataset.highlight;
}
});
const element = createOptionElement({
option,
qrcode,
});
li.append(element);
nodes.push({
type: "option",
option,
path,
});
listForSum.push(() => 1);
}
});
}
return signals.createMemo(() =>
listForSum.reduce((acc, s) => acc + s(), 0),
);
return nodes;
}
recursiveProcessPartialTree(partialOptions, parent);
const processedTree = processPartialTree(partialOptions);
logUnused();
// ============================================
// Phase 2: Build DOM lazily (imperative)
// Uses native toggle events for lazy loading
// ============================================
/**
* @param {ProcessedNode[]} nodes
* @param {HTMLElement} parentEl
*/
function buildTreeDOM(nodes, parentEl) {
const ul = window.document.createElement("ul");
parentEl.append(ul);
for (const node of nodes) {
const li = window.document.createElement("li");
ul.append(li);
const pathKey = node.path.join("/");
liByPath.set(pathKey, li);
if (isOnSelectedPath(node.path)) {
li.dataset.highlight = "";
}
if (node.type === "group") {
const details = window.document.createElement("details");
details.dataset.name = node.serName;
li.appendChild(details);
const summary = window.document.createElement("summary");
details.append(summary);
summary.append(node.name);
const supCount = window.document.createElement("sup");
supCount.innerHTML = node.count.toLocaleString("en-us");
summary.append(supCount);
let built = false;
details.addEventListener("toggle", () => {
if (details.open && !built) {
built = true;
buildTreeDOM(node.children, details);
}
});
} else {
const element = createOptionElement({
option: node.option,
qrcode,
});
li.append(element);
}
}
}
// Single effect to kick off DOM building when parent is set
signals.createEffect(
() => parent(),
(_parent) => {
if (!_parent) return;
buildTreeDOM(processedTree, _parent);
},
);
// Single effect for highlighting on selection change
signals.createEffect(
() => selected(),
(selected) => {
if (!selected) return;
// Clear all existing highlights
liByPath.forEach((li) => {
delete li.dataset.highlight;
});
// Highlight selected option and parent groups
for (let i = 1; i <= selected.path.length; i++) {
const pathKey = selected.path.slice(0, i).join("/");
const li = liByPath.get(pathKey);
if (li) li.dataset.highlight = "";
}
},
);
if (!selected()) {
const option =
savedOption || list.find((option) => option.kind === "chart");
+15 -52
View File
@@ -1,6 +1,13 @@
/** Moving averages section */
import { Unit } from "../../utils/units.js";
import {
priceLines,
percentileUsdMap,
percentileMap,
sdPatterns,
sdBands,
} from "../shared.js";
import { periodIdToName } from "./utils.js";
/**
@@ -51,47 +58,9 @@ export function createPriceWithRatioOptions(
const { line, 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 },
];
const pctUsdMap = percentileUsdMap(colors, ratio);
const pctMap = percentileMap(colors, ratio);
const sdPats = sdPatterns(ratio);
return [
{
@@ -104,7 +73,7 @@ export function createPriceWithRatioOptions(
title: `${title} Ratio`,
top: [
line({ metric: priceMetric, name: legend, color, unit: Unit.usd }),
...percentileUsdMap.map(({ name: pctName, prop, color: pctColor }) =>
...pctUsdMap.map(({ name: pctName, prop, color: pctColor }) =>
line({
metric: prop,
name: pctName,
@@ -153,7 +122,7 @@ export function createPriceWithRatioOptions(
color: colors.rose,
unit: Unit.ratio,
}),
...percentileMap.map(({ name: pctName, prop, color: pctColor }) =>
...pctMap.map(({ name: pctName, prop, color: pctColor }) =>
line({
metric: prop,
name: pctName,
@@ -168,10 +137,10 @@ export function createPriceWithRatioOptions(
},
{
name: "ZScores",
tree: sdPatterns.map(({ nameAddon, titleAddon, sd }) => ({
tree: sdPats.map(({ nameAddon, titleAddon, sd }) => ({
name: nameAddon,
title: `${title} ${titleAddon} Z-Score`,
top: getSdBands(sd).map(({ name: bandName, prop, color: bandColor }) =>
top: sdBands(colors, sd).map(({ name: bandName, prop, color: bandColor }) =>
line({
metric: prop,
name: bandName,
@@ -181,13 +150,7 @@ export function createPriceWithRatioOptions(
),
bottom: [
line({ metric: sd.zscore, name: "Z-Score", color, unit: Unit.sd }),
createPriceLine({ unit: Unit.sd, number: 3 }),
createPriceLine({ unit: Unit.sd, number: 2 }),
createPriceLine({ unit: Unit.sd, number: 1 }),
createPriceLine({ unit: Unit.sd, number: 0 }),
createPriceLine({ unit: Unit.sd, number: -1 }),
createPriceLine({ unit: Unit.sd, number: -2 }),
createPriceLine({ unit: Unit.sd, number: -3 }),
...priceLines(ctx, Unit.sd, [0, 1, -1, 2, -2, 3, -3]),
],
})),
},
+6 -79
View File
@@ -1,6 +1,7 @@
/** Investing section (DCA) */
import { Unit } from "../../utils/units.js";
import { satsBtcUsd } from "../shared.js";
import { periodIdToName } from "./utils.js";
/**
@@ -113,42 +114,8 @@ export function createInvestingSection(ctx, { dca, lookback, returns }) {
name: "Stack",
title: `${name} DCA vs Lump Sum Stack ($100/day)`,
bottom: [
line({
metric: dcaStack.sats,
name: "DCA",
color: colors.green,
unit: Unit.sats,
}),
line({
metric: dcaStack.bitcoin,
name: "DCA",
color: colors.green,
unit: Unit.btc,
}),
line({
metric: dcaStack.dollars,
name: "DCA",
color: colors.green,
unit: Unit.usd,
}),
line({
metric: lumpSumStack.sats,
name: "Lump sum",
color: colors.orange,
unit: Unit.sats,
}),
line({
metric: lumpSumStack.bitcoin,
name: "Lump sum",
color: colors.orange,
unit: Unit.btc,
}),
line({
metric: lumpSumStack.dollars,
name: "Lump sum",
color: colors.orange,
unit: Unit.usd,
}),
...satsBtcUsd(ctx, dcaStack, "DCA", colors.green),
...satsBtcUsd(ctx, lumpSumStack, "Lump sum", colors.orange),
],
},
],
@@ -196,29 +163,8 @@ export function createInvestingSection(ctx, { dca, lookback, returns }) {
name: "Stack",
title: "DCA Stack by Year ($100/day)",
bottom: dcaClasses.flatMap(
({ year, color, defaultActive, stack }) => [
line({
metric: stack.sats,
name: `${year}`,
color,
defaultActive,
unit: Unit.sats,
}),
line({
metric: stack.bitcoin,
name: `${year}`,
color,
defaultActive,
unit: Unit.btc,
}),
line({
metric: stack.dollars,
name: `${year}`,
color,
defaultActive,
unit: Unit.usd,
}),
],
({ year, color, defaultActive, stack }) =>
satsBtcUsd(ctx, stack, `${year}`, color, { defaultActive }),
),
},
],
@@ -254,26 +200,7 @@ export function createInvestingSection(ctx, { dca, lookback, returns }) {
{
name: "Stack",
title: `DCA Class ${year} Stack ($100/day)`,
bottom: [
line({
metric: stack.sats,
name: "Stack",
color,
unit: Unit.sats,
}),
line({
metric: stack.bitcoin,
name: "Stack",
color,
unit: Unit.btc,
}),
line({
metric: stack.dollars,
name: "Stack",
color,
unit: Unit.usd,
}),
],
bottom: satsBtcUsd(ctx, stack, "Stack", color),
},
],
})),
+99
View File
@@ -0,0 +1,99 @@
/** Shared helpers for options */
import { Unit } from "../utils/units.js";
/**
* Create sats/btc/usd line series from a pattern with .sats/.bitcoin/.dollars
* @param {PartialContext} ctx
* @param {{ sats: AnyMetricPattern, bitcoin: AnyMetricPattern, dollars: AnyMetricPattern }} pattern
* @param {string} name
* @param {Color} [color]
* @param {{ defaultActive?: boolean }} [options]
* @returns {FetchedLineSeriesBlueprint[]}
*/
export function satsBtcUsd(ctx, pattern, name, color, options) {
const { defaultActive } = options || {};
return [
ctx.line({ metric: pattern.sats, name, color, unit: Unit.sats, defaultActive }),
ctx.line({ metric: pattern.bitcoin, name, color, unit: Unit.btc, defaultActive }),
ctx.line({ metric: pattern.dollars, name, color, unit: Unit.usd, defaultActive }),
];
}
/**
* Create multiple price lines with the same unit
* @param {PartialContext} ctx
* @param {Unit} unit
* @param {number[]} numbers
*/
export function priceLines(ctx, unit, numbers) {
return numbers.map((n) => ctx.createPriceLine({ unit, number: n }));
}
/**
* Build percentile USD mappings from a ratio pattern
* @param {Colors} colors
* @param {ActivePriceRatioPattern} ratio
*/
export function percentileUsdMap(colors, ratio) {
return /** @type {const} */ ([
{ 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 },
]);
}
/**
* Build percentile ratio mappings from a ratio pattern
* @param {Colors} colors
* @param {ActivePriceRatioPattern} ratio
*/
export function percentileMap(colors, ratio) {
return /** @type {const} */ ([
{ 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 },
]);
}
/**
* Build SD patterns from a ratio pattern
* @param {ActivePriceRatioPattern} ratio
*/
export function sdPatterns(ratio) {
return /** @type {const} */ ([
{ 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 },
]);
}
/**
* Build SD band mappings from an SD pattern
* @param {Colors} colors
* @param {Ratio1ySdPattern} sd
*/
export function sdBands(colors, sd) {
return /** @type {const} */ ([
{ 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 },
]);
}