global: snapshot

This commit is contained in:
nym21
2026-01-31 17:39:48 +01:00
parent 8dd350264a
commit ff5bb770d7
116 changed files with 13312 additions and 9530 deletions
+25 -63
View File
@@ -33,7 +33,6 @@ import { style } from "../utils/elements.js";
/**
* @template T
* @typedef {Object} Series
* @property {string} key
* @property {string} id
* @property {number} paneIndex
* @property {PersistedValue<boolean>} active
@@ -448,16 +447,12 @@ export function createChart({ parent, id: chartId, brk, fitContent }) {
};
const serieses = {
/** @type {Map<string, PersistedValue<boolean>>} */
activeStates: new Map(),
/** @type {Map<string, Set<AnySeries>>} */
byKey: new Map(),
/** @type {Set<AnySeries>} */
all: new Set(),
refreshAll() {
serieses.byKey.forEach((set) => {
set.forEach((s) => {
if (s.active.value) s.fetch?.();
});
serieses.all.forEach((s) => {
if (s.active.value) s.fetch?.();
});
},
@@ -503,17 +498,12 @@ export function createChart({ parent, id: chartId, brk, fitContent }) {
const key = customKey ?? stringToId(name);
const id = `${unit.id}-${key}`;
// Reuse existing state if same name (links legends across panes, regardless of unit)
const existingActive = serieses.activeStates.get(key);
const active =
existingActive ??
createPersistedValue({
defaultValue: defaultActive ?? true,
storageKey: key,
urlKey: key,
...serdeBool,
});
if (!existingActive) serieses.activeStates.set(key, active);
const active = createPersistedValue({
defaultValue: defaultActive ?? true,
storageKey: `${chartId}-p${paneIndex}-${key}`,
urlKey: `${paneIndex === 0 ? "t" : "b"}-${key}`,
...serdeBool,
});
setOrder(-order);
@@ -533,18 +523,9 @@ export function createChart({ parent, id: chartId, brk, fitContent }) {
setActive(value) {
const wasActive = active.value;
active.set(value);
const linkedSeries = serieses.byKey.get(key);
linkedSeries?.forEach((s) => {
value ? s.show() : s.hide();
});
document.querySelectorAll(`[data-series="${key}"]`).forEach((el) => {
if (el instanceof HTMLInputElement && el.type === "checkbox") {
el.checked = value;
}
});
// Fetch data for ALL linked series, not just this one
value ? show() : hide();
if (value && !wasActive) {
linkedSeries?.forEach((s) => s.fetch?.());
_fetch?.();
}
panes.updateVisibility();
},
@@ -555,7 +536,6 @@ export function createChart({ parent, id: chartId, brk, fitContent }) {
tame,
hasData: () => hasData,
fetch: () => _fetch?.(),
key,
id,
paneIndex,
url: null,
@@ -563,18 +543,12 @@ export function createChart({ parent, id: chartId, brk, fitContent }) {
update,
remove() {
onRemove();
serieses.byKey.get(key)?.delete(series);
serieses.all.delete(series);
panes.seriesByHome.get(paneIndex)?.delete(series);
},
};
// Register series for cross-pane linking (by name only)
let keySet = serieses.byKey.get(key);
if (!keySet) {
keySet = new Set();
serieses.byKey.set(key, keySet);
}
keySet.add(series);
serieses.all.add(series);
/** @param {ChartableIndex} idx */
function setupIndexEffect(idx) {
@@ -1312,35 +1286,28 @@ export function createChart({ parent, id: chartId, brk, fitContent }) {
deserialize: (s) => /** @type {"lin" | "log"} */ (s),
});
/** @param {"lin" | "log"} value */
const applyScale = (value) => {
panes.whenReady(paneIndex, () => {
try {
ichart
.panes()
.at(paneIndex)
?.priceScale("right")
.applyOptions({
mode: value === "lin" ? 0 : 1,
});
} catch {}
});
/** @param {IPaneApi<Time>} pane @param {"lin" | "log"} value */
const applyScale = (pane, value) => {
try {
pane.priceScale("right").applyOptions({
mode: value === "lin" ? 0 : 1,
});
} catch {}
};
applyScale(persisted.value);
fieldsets.addIfNeeded({
id,
paneIndex,
position: "sw",
createChild() {
createChild(pane) {
applyScale(pane, persisted.value);
return createRadios({
choices: /** @type {const} */ (["lin", "log"]),
id: stringToId(`${id} ${paneIndex}`),
initialValue: persisted.value,
onChange(value) {
persisted.set(value);
applyScale(value);
applyScale(pane, value);
},
});
},
@@ -1472,12 +1439,7 @@ export function createChart({ parent, id: chartId, brk, fitContent }) {
// Remove old series AFTER adding new ones to prevent pane collapse
oldSeries.forEach((s) => s.remove());
// Ensure other pane's series are in their correct pane before applying scale
// (they may have been collapsed when this pane was empty)
const otherPaneIndex = paneIndex === 0 ? 1 : 0;
panes.moveTo(otherPaneIndex, otherPaneIndex);
// Apply scale after series are created and panes are properly separated
// Store scale config - it will be applied when createForPane runs after updateVisibility
applyScaleForUnit(paneIndex, unit);
},
+3 -5
View File
@@ -50,19 +50,17 @@ export function createLegend() {
}
legends[order] = div;
const { input, label } = createLabeledInput({
const { label } = createLabeledInput({
inputId: stringToId(`legend-${series.id}`),
inputName: stringToId(`selected-${series.id}`),
inputValue: "value",
title: "Click to toggle",
inputChecked: series.active.value,
onClick: (event) => {
event.preventDefault();
onClick: () => {
series.setActive(!series.active.value);
},
type: "checkbox",
});
input.dataset.series = series.key;
const spanMain = window.document.createElement("span");
spanMain.classList.add("main");
@@ -93,7 +91,7 @@ export function createLegend() {
anchor.href = series.url;
anchor.target = "_blank";
anchor.rel = "noopener noreferrer";
anchor.title = "Click to view data";
anchor.title = "Open the metric data in a new tab";
div.append(anchor);
}
},
+39 -40
View File
@@ -45,14 +45,13 @@ export function createChainSection(ctx) {
const {
colors,
brk,
fromSizePattern,
fromFullnessPattern,
fromDollarsPattern,
fromFeeRatePattern,
fromSumStatsPattern,
fromBaseStatsPattern,
fromFullStatsPattern,
fromStatsPattern,
fromCoinbasePattern,
fromValuePattern,
fromBlockCountWithUnit,
fromIntervalPattern,
fromCountPattern,
fromSupplyPattern,
} = ctx;
const {
@@ -132,19 +131,19 @@ export function createChainSection(ctx) {
{
name: "New",
title: `${titlePrefix}New Address Count`,
bottom: fromDollarsPattern(distribution.newAddrCount[key], Unit.count),
bottom: fromFullStatsPattern(distribution.newAddrCount[key], Unit.count),
},
{
name: "Growth Rate",
title: `${titlePrefix}Address Growth Rate`,
bottom: fromFullnessPattern(distribution.growthRate[key], Unit.ratio),
bottom: fromBaseStatsPattern(distribution.growthRate[key], Unit.ratio),
},
{
name: "Activity",
tree: activityTypes.map((a) => ({
name: a.name,
title: `${titlePrefix}${a.name} Address Count`,
bottom: fromFullnessPattern(
bottom: fromBaseStatsPattern(
distribution.addressActivity[key][a.key],
Unit.count,
),
@@ -297,7 +296,7 @@ export function createChainSection(ctx) {
name: "Count",
title: "Block Count",
bottom: [
...fromBlockCountWithUnit(blocks.count.blockCount, Unit.count),
...fromCountPattern(blocks.count.blockCount, Unit.count),
line({
metric: blocks.count.blockCountTarget,
name: "Target",
@@ -339,7 +338,7 @@ export function createChainSection(ctx) {
name: "Interval",
title: "Block Interval",
bottom: [
...fromIntervalPattern(blocks.interval, Unit.secs),
...fromBaseStatsPattern(blocks.interval, Unit.secs, "", { avgActive: false }),
priceLine({ ctx, unit: Unit.secs, name: "Target", number: 600 }),
],
},
@@ -347,7 +346,7 @@ export function createChainSection(ctx) {
name: "Size",
title: "Block Size",
bottom: [
...fromSizePattern(blocks.size, Unit.bytes),
...fromSumStatsPattern(blocks.size, Unit.bytes),
line({
metric: blocks.totalSize,
name: "Total",
@@ -355,8 +354,8 @@ export function createChainSection(ctx) {
unit: Unit.bytes,
defaultActive: false,
}),
...fromFullnessPattern(blocks.vbytes, Unit.vb),
...fromFullnessPattern(blocks.weight, Unit.wu),
...fromBaseStatsPattern(blocks.vbytes, Unit.vb),
...fromBaseStatsPattern(blocks.weight, Unit.wu),
line({
metric: blocks.weight.sum,
name: "Sum",
@@ -376,7 +375,7 @@ export function createChainSection(ctx) {
{
name: "Fullness",
title: "Block Fullness",
bottom: fromFullnessPattern(blocks.fullness, Unit.percentage),
bottom: fromBaseStatsPattern(blocks.fullness, Unit.percentage),
},
],
},
@@ -388,7 +387,7 @@ export function createChainSection(ctx) {
{
name: "Count",
title: "Transaction Count",
bottom: fromDollarsPattern(transactions.count.txCount, Unit.count),
bottom: fromFullStatsPattern(transactions.count.txCount, Unit.count),
},
{
name: "Speed",
@@ -426,34 +425,34 @@ export function createChainSection(ctx) {
name: "Size",
title: "Transaction Size",
bottom: [
...fromFeeRatePattern(transactions.size.weight, Unit.wu),
...fromFeeRatePattern(transactions.size.vsize, Unit.vb),
...fromStatsPattern(transactions.size.weight, Unit.wu),
...fromStatsPattern(transactions.size.vsize, Unit.vb),
],
},
{
name: "Fee Rate",
title: "Fee Rate",
bottom: fromFeeRatePattern(transactions.fees.feeRate, Unit.feeRate),
bottom: fromStatsPattern(transactions.fees.feeRate, Unit.feeRate),
},
{
name: "Versions",
title: "Transaction Versions",
bottom: [
...fromBlockCountWithUnit(
...fromCountPattern(
transactions.versions.v1,
Unit.count,
"v1",
colors.orange,
colors.red,
),
...fromBlockCountWithUnit(
...fromCountPattern(
transactions.versions.v2,
Unit.count,
"v2",
colors.cyan,
colors.blue,
),
...fromBlockCountWithUnit(
...fromCountPattern(
transactions.versions.v3,
Unit.count,
"v3",
@@ -489,12 +488,12 @@ export function createChainSection(ctx) {
{
name: "Input Count",
title: "Input Count",
bottom: [...fromSizePattern(inputs.count, Unit.count)],
bottom: [...fromSumStatsPattern(inputs.count, Unit.count)],
},
{
name: "Output Count",
title: "Output Count",
bottom: [...fromSizePattern(outputs.count.totalCount, Unit.count)],
bottom: [...fromSumStatsPattern(outputs.count.totalCount, Unit.count)],
},
{
name: "Inputs/sec",
@@ -546,12 +545,12 @@ export function createChainSection(ctx) {
{
name: "P2PKH",
title: "P2PKH Output Count",
bottom: fromDollarsPattern(scripts.count.p2pkh, Unit.count),
bottom: fromFullStatsPattern(scripts.count.p2pkh, Unit.count),
},
{
name: "P2PK33",
title: "P2PK33 Output Count",
bottom: fromDollarsPattern(
bottom: fromFullStatsPattern(
scripts.count.p2pk33,
Unit.count,
),
@@ -559,7 +558,7 @@ export function createChainSection(ctx) {
{
name: "P2PK65",
title: "P2PK65 Output Count",
bottom: fromDollarsPattern(
bottom: fromFullStatsPattern(
scripts.count.p2pk65,
Unit.count,
),
@@ -573,12 +572,12 @@ export function createChainSection(ctx) {
{
name: "P2SH",
title: "P2SH Output Count",
bottom: fromDollarsPattern(scripts.count.p2sh, Unit.count),
bottom: fromFullStatsPattern(scripts.count.p2sh, Unit.count),
},
{
name: "P2MS",
title: "P2MS Output Count",
bottom: fromDollarsPattern(scripts.count.p2ms, Unit.count),
bottom: fromFullStatsPattern(scripts.count.p2ms, Unit.count),
},
],
},
@@ -589,7 +588,7 @@ export function createChainSection(ctx) {
{
name: "All SegWit",
title: "SegWit Output Count",
bottom: fromDollarsPattern(
bottom: fromFullStatsPattern(
scripts.count.segwit,
Unit.count,
),
@@ -597,7 +596,7 @@ export function createChainSection(ctx) {
{
name: "P2WPKH",
title: "P2WPKH Output Count",
bottom: fromDollarsPattern(
bottom: fromFullStatsPattern(
scripts.count.p2wpkh,
Unit.count,
),
@@ -605,7 +604,7 @@ export function createChainSection(ctx) {
{
name: "P2WSH",
title: "P2WSH Output Count",
bottom: fromDollarsPattern(scripts.count.p2wsh, Unit.count),
bottom: fromFullStatsPattern(scripts.count.p2wsh, Unit.count),
},
],
},
@@ -616,12 +615,12 @@ export function createChainSection(ctx) {
{
name: "P2TR",
title: "P2TR Output Count",
bottom: fromDollarsPattern(scripts.count.p2tr, Unit.count),
bottom: fromFullStatsPattern(scripts.count.p2tr, Unit.count),
},
{
name: "P2A",
title: "P2A Output Count",
bottom: fromDollarsPattern(scripts.count.p2a, Unit.count),
bottom: fromFullStatsPattern(scripts.count.p2a, Unit.count),
},
],
},
@@ -632,7 +631,7 @@ export function createChainSection(ctx) {
{
name: "OP_RETURN",
title: "OP_RETURN Output Count",
bottom: fromDollarsPattern(
bottom: fromFullStatsPattern(
scripts.count.opreturn,
Unit.count,
),
@@ -640,7 +639,7 @@ export function createChainSection(ctx) {
{
name: "Empty",
title: "Empty Output Count",
bottom: fromDollarsPattern(
bottom: fromFullStatsPattern(
scripts.count.emptyoutput,
Unit.count,
),
@@ -648,7 +647,7 @@ export function createChainSection(ctx) {
{
name: "Unknown",
title: "Unknown Output Count",
bottom: fromDollarsPattern(
bottom: fromFullStatsPattern(
scripts.count.unknownoutput,
Unit.count,
),
@@ -793,9 +792,9 @@ export function createChainSection(ctx) {
name: "Fee",
title: "Transaction Fees",
bottom: [
...fromSizePattern(transactions.fees.fee.bitcoin, Unit.btc),
...fromSizePattern(transactions.fees.fee.sats, Unit.sats),
...fromSizePattern(transactions.fees.fee.dollars, Unit.usd),
...fromSumStatsPattern(transactions.fees.fee.bitcoin, Unit.btc),
...fromSumStatsPattern(transactions.fees.fee.sats, Unit.sats),
...fromSumStatsPattern(transactions.fees.fee.dollars, Unit.usd),
line({
metric: blocks.rewards.feeDominance,
name: "Dominance",
+4 -35
View File
@@ -1,37 +1,6 @@
import { Unit } from "../utils/units.js";
import { line, price } from "./series.js";
import {
satsBtcUsd,
createRatioChart,
createZScoresFolder,
formatCohortTitle,
} from "./shared.js";
/**
* Create price with ratio options for cointime prices
* @param {PartialContext} ctx
* @param {Object} args
* @param {string} args.title
* @param {string} args.legend
* @param {AnyPricePattern} args.pricePattern
* @param {ActivePriceRatioPattern} args.ratio
* @param {Color} args.color
* @returns {PartialOptionsTree}
*/
function createCointimePriceWithRatioOptions(
ctx,
{ title, legend, pricePattern, ratio, color },
) {
return [
{
name: "Price",
title,
top: [price({ metric: pricePattern, name: legend, color })],
},
createRatioChart(ctx, { title: formatCohortTitle(title), pricePattern, ratio, color }),
createZScoresFolder(ctx, { title, legend, pricePattern, ratio, color }),
];
}
import { satsBtcUsd, createPriceRatioCharts } from "./shared.js";
/**
* Create Cointime section
@@ -134,12 +103,12 @@ export function createCointimeSection(ctx) {
},
...cointimePrices.map(({ pricePattern, ratio, name, color, title }) => ({
name,
tree: createCointimePriceWithRatioOptions(ctx, {
tree: createPriceRatioCharts(ctx, {
context: title,
legend: name,
pricePattern,
ratio,
legend: name,
color,
title,
}),
})),
],
+10 -12
View File
@@ -1,13 +1,12 @@
import {
fromSizePattern,
fromFullnessPattern,
fromDollarsPattern,
fromFeeRatePattern,
fromSumStatsPattern,
fromBaseStatsPattern,
fromFullStatsPattern,
fromStatsPattern,
fromCoinbasePattern,
fromValuePattern,
fromBitcoinPatternWithUnit,
fromBlockCountWithUnit,
fromIntervalPattern,
fromCountPattern,
fromSupplyPattern,
} from "./series.js";
import { colors } from "../chart/colors.js";
@@ -39,15 +38,14 @@ export function createContext({ brk }) {
return {
colors,
brk,
fromSizePattern: bind(fromSizePattern),
fromFullnessPattern: bind(fromFullnessPattern),
fromDollarsPattern: bind(fromDollarsPattern),
fromFeeRatePattern: bind(fromFeeRatePattern),
fromSumStatsPattern: bind(fromSumStatsPattern),
fromBaseStatsPattern: bind(fromBaseStatsPattern),
fromFullStatsPattern: bind(fromFullStatsPattern),
fromStatsPattern: bind(fromStatsPattern),
fromCoinbasePattern: bind(fromCoinbasePattern),
fromValuePattern: bind(fromValuePattern),
fromBitcoinPatternWithUnit: bind(fromBitcoinPatternWithUnit),
fromBlockCountWithUnit: bind(fromBlockCountWithUnit),
fromIntervalPattern: bind(fromIntervalPattern),
fromCountPattern: bind(fromCountPattern),
fromSupplyPattern,
};
}
+107 -3
View File
@@ -26,7 +26,13 @@ import {
createSingleSupplyRelativeOptions,
createSingleSellSideRiskSeries,
createSingleValueCreatedDestroyedSeries,
createSingleValueFlowBreakdownSeries,
createSingleCapitulationProfitFlowSeries,
createSingleSoprSeries,
createSingleInvestorPriceSeries,
createSingleInvestorPriceRatioSeries,
createInvestorPriceSeries,
createInvestorPriceRatioSeries,
} from "./shared.js";
/**
@@ -41,7 +47,7 @@ export function createAddressCohortFolder(ctx, args) {
const useGroupName = "list" in args;
const isSingle = !("list" in args);
const title = formatCohortTitle(args.title);
const title = formatCohortTitle(args.name);
return {
name: args.name || "all",
@@ -96,6 +102,21 @@ export function createAddressCohortFolder(ctx, args) {
title: title("Realized Price Ratio"),
bottom: createRealizedPriceRatioSeries(list),
},
{
name: "Investor Price",
tree: [
{
name: "Price",
title: title("Investor Price"),
top: createInvestorPriceSeries(list),
},
{
name: "Ratio",
title: title("Investor Price Ratio"),
bottom: createInvestorPriceRatioSeries(list),
},
],
},
]
: createRealizedPriceOptions(
/** @type {AddressCohortObject} */ (args),
@@ -161,6 +182,21 @@ function createRealizedPriceOptions(args, title) {
}),
],
},
{
name: "Investor Price",
tree: [
{
name: "Price",
title: title("Investor Price"),
top: createSingleInvestorPriceSeries(tree, color),
},
{
name: "Ratio",
title: title("Investor Price Ratio"),
bottom: createSingleInvestorPriceRatioSeries(tree, color),
},
],
},
];
}
@@ -365,8 +401,23 @@ function createRealizedPnlSection(ctx, args, title) {
},
{
name: "Value",
title: title("Value Created & Destroyed"),
bottom: createSingleValueCreatedDestroyedSeries(colors, args.tree),
tree: [
{
name: "Created & Destroyed",
title: title("Value Created & Destroyed"),
bottom: createSingleValueCreatedDestroyedSeries(colors, args.tree),
},
{
name: "Breakdown",
title: title("Value Flow Breakdown"),
bottom: createSingleValueFlowBreakdownSeries(colors, args.tree),
},
{
name: "Flow",
title: title("Capitulation & Profit Flow"),
bottom: createSingleCapitulationProfitFlowSeries(colors, args.tree),
},
],
},
];
}
@@ -434,6 +485,35 @@ function createUnrealizedSection(ctx, list, useGroupName, title) {
}),
]),
},
{
name: "Invested Capital",
tree: [
{
name: "In Profit",
title: title("Invested Capital In Profit"),
bottom: list.flatMap(({ color, name, tree }) => [
line({
metric: tree.unrealized.investedCapitalInProfit,
name: useGroupName ? name : "In Profit",
color: useGroupName ? color : colors.green,
unit: Unit.usd,
}),
]),
},
{
name: "In Loss",
title: title("Invested Capital In Loss"),
bottom: list.flatMap(({ color, name, tree }) => [
line({
metric: tree.unrealized.investedCapitalInLoss,
name: useGroupName ? name : "In Loss",
color: useGroupName ? color : colors.red,
unit: Unit.usd,
}),
]),
},
],
},
{
name: "Relative",
tree: [
@@ -498,6 +578,30 @@ function createUnrealizedSection(ctx, list, useGroupName, title) {
}),
]),
},
{
name: "Invested Capital In Profit",
title: title("Invested Capital In Profit"),
bottom: list.flatMap(({ color, name, tree }) => [
baseline({
metric: tree.relative.investedCapitalInProfitPct,
name: useGroupName ? name : "In Profit",
color: useGroupName ? color : colors.green,
unit: Unit.pctRcap,
}),
]),
},
{
name: "Invested Capital In Loss",
title: title("Invested Capital In Loss"),
bottom: list.flatMap(({ color, name, tree }) => [
baseline({
metric: tree.relative.investedCapitalInLossPct,
name: useGroupName ? name : "In Loss",
color: useGroupName ? color : colors.red,
unit: Unit.pctRcap,
}),
]),
},
],
},
{
@@ -14,6 +14,7 @@ export {
createCohortFolderAgeRange,
createCohortFolderBasicWithMarketCap,
createCohortFolderBasicWithoutMarketCap,
createCohortFolderWithoutRelative,
createCohortFolderAddress,
} from "./utxo.js";
export { createAddressCohortFolder } from "./address.js";
+552 -10
View File
@@ -3,31 +3,148 @@
import { Unit } from "../../utils/units.js";
import { priceLine } from "../constants.js";
import { baseline, dots, line, price } from "../series.js";
import { satsBtcUsd } from "../shared.js";
import { satsBtcUsd, createPriceRatioCharts, formatCohortTitle } from "../shared.js";
// ============================================================================
// Generic Price Helpers
// ============================================================================
/**
* Create supply section for a single cohort
* Create price folder (price + ratio + z-scores wrapped in folder)
* For cohorts with full extended ratio metrics (ActivePriceRatioPattern)
* @param {PartialContext} ctx
* @param {CohortObject} cohort
* @param {Object} [options]
* @param {AnyFetchedSeriesBlueprint[]} [options.supplyRelative] - Supply relative to circulating supply metrics
* @param {AnyFetchedSeriesBlueprint[]} [options.pnlRelative] - Supply in profit/loss relative to circulating supply metrics
* @param {{ name: string, cohortTitle?: string, priceMetric: ActivePricePattern, ratioPattern: AnyRatioPattern, color: Color }} args
* @returns {PartialOptionsGroup}
*/
export function createPriceFolder(ctx, { name, cohortTitle, priceMetric, ratioPattern, color }) {
const context = cohortTitle ? `${cohortTitle} ${name}` : name;
return {
name,
tree: createPriceRatioCharts(ctx, {
context,
legend: name,
pricePattern: priceMetric,
ratio: ratioPattern,
color,
}),
};
}
/**
* Create basic price charts (price + ratio only, no z-scores) - flat array
* For cohorts with basic ratio metrics (only .ratio field)
* @template {AnyMetricPattern} R
* @param {{ name: string, context: string, priceMetric: ActivePricePattern, ratioMetric: R, color: Color }} args
* @returns {PartialOptionsTree}
*/
export function createBasicPriceCharts({ name, context, priceMetric, ratioMetric, color }) {
return [
{
name: "Price",
title: context,
top: [price({ metric: priceMetric, name, color })],
},
{
name: "Ratio",
title: formatCohortTitle(context)("Ratio"),
bottom: [
baseline({
metric: ratioMetric,
name: "Ratio",
color,
unit: Unit.ratio,
base: 1,
}),
],
},
];
}
/**
* Create basic price folder (price + ratio wrapped in folder, no z-scores)
* For cohorts with basic ratio metrics (only .ratio field)
* @template {AnyMetricPattern} R
* @param {{ name: string, cohortTitle?: string, priceMetric: ActivePricePattern, ratioMetric: R, color: Color }} args
* @returns {PartialOptionsGroup}
*/
export function createBasicPriceFolder({ name, cohortTitle, priceMetric, ratioMetric, color }) {
const context = cohortTitle ? `${cohortTitle} ${name}` : name;
return {
name,
tree: createBasicPriceCharts({ name, context, priceMetric, ratioMetric, color }),
};
}
/**
* Create grouped price charts (price + ratio) - flat array, no z-scores
* @template {{ color: Color, name: string, tree: { realized: AnyRealizedPattern } }} T
* @param {{ name: string, title: (metric: string) => string, list: readonly T[], getPrice: (tree: T['tree']) => ActivePricePattern, getRatio: (tree: T['tree']) => AnyMetricPattern }} args
* @returns {PartialOptionsTree}
*/
export function createGroupedPriceCharts({ name, title, list, getPrice, getRatio }) {
return [
{
name: "Price",
title: title(name),
top: list.map(({ color, name: cohortName, tree }) =>
price({ metric: getPrice(tree), name: cohortName, color }),
),
},
{
name: "Ratio",
title: title(`${name} Ratio`),
bottom: list.map(({ color, name: cohortName, tree }) =>
baseline({ metric: getRatio(tree), name: cohortName, color, unit: Unit.ratio, base: 1 }),
),
},
];
}
/**
* Create grouped price folder (price + ratio wrapped in folder)
* @template {{ color: Color, name: string, tree: { realized: AnyRealizedPattern } }} T
* @param {{ name: string, title: (metric: string) => string, list: readonly T[], getPrice: (tree: T['tree']) => ActivePricePattern, getRatio: (tree: T['tree']) => AnyMetricPattern }} args
* @returns {PartialOptionsGroup}
*/
export function createGroupedPriceFolder({ name, title, list, getPrice, getRatio }) {
return {
name,
tree: createGroupedPriceCharts({ name, title, list, getPrice, getRatio }),
};
}
/**
* Create base supply series (without relative metrics)
* @param {PartialContext} ctx
* @param {CohortObject | CohortWithoutRelative} cohort
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createSingleSupplySeries(ctx, cohort, { supplyRelative = [], pnlRelative = [] } = {}) {
function createSingleSupplySeriesBase(ctx, cohort) {
const { colors } = ctx;
const { tree } = cohort;
return [
...satsBtcUsd(tree.supply.total, "Supply", colors.default),
...supplyRelative,
...satsBtcUsd(tree.unrealized.supplyInProfit, "In Profit", colors.green),
...satsBtcUsd(tree.unrealized.supplyInLoss, "In Loss", colors.red),
...satsBtcUsd(tree.supply.halved, "half", colors.gray).map((s) => ({
...s,
options: { lineStyle: 4 },
})),
...pnlRelative,
];
}
/**
* Create supply relative to own supply metrics
* @param {PartialContext} ctx
* @param {UtxoCohortObject | AddressCohortObject} cohort
* @returns {AnyFetchedSeriesBlueprint[]}
*/
function createSingleSupplyRelativeToOwnMetrics(ctx, cohort) {
const { colors } = ctx;
const { tree } = cohort;
return [
line({
metric: tree.relative.supplyInProfitRelToOwnSupply,
name: "In Profit",
@@ -51,6 +168,34 @@ export function createSingleSupplySeries(ctx, cohort, { supplyRelative = [], pnl
];
}
/**
* Create supply section for a single cohort (with relative metrics)
* @param {PartialContext} ctx
* @param {UtxoCohortObject | AddressCohortObject} cohort
* @param {Object} [options]
* @param {AnyFetchedSeriesBlueprint[]} [options.supplyRelative] - Supply relative to circulating supply metrics
* @param {AnyFetchedSeriesBlueprint[]} [options.pnlRelative] - Supply in profit/loss relative to circulating supply metrics
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createSingleSupplySeries(ctx, cohort, { supplyRelative = [], pnlRelative = [] } = {}) {
return [
...createSingleSupplySeriesBase(ctx, cohort),
...supplyRelative,
...pnlRelative,
...createSingleSupplyRelativeToOwnMetrics(ctx, cohort),
];
}
/**
* Create supply series for cohorts WITHOUT relative metrics
* @param {PartialContext} ctx
* @param {CohortWithoutRelative} cohort
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createSingleSupplySeriesWithoutRelative(ctx, cohort) {
return createSingleSupplySeriesBase(ctx, cohort);
}
/**
* Create supply total series for grouped cohorts
* @template {readonly CohortObject[]} T
@@ -98,7 +243,7 @@ export function createGroupedSupplyInLossSeries(list, { relativeMetrics } = {})
/**
* Create supply section for grouped cohorts
* @template {readonly CohortObject[]} T
* @template {readonly (CohortObject | CohortWithoutRelative)[]} T
* @param {T} list
* @param {(metric: string) => string} title
* @param {Object} [options]
@@ -365,6 +510,67 @@ export function createCostBasisPercentilesSeries(colors, list, useGroupName) {
});
}
/**
* Create invested capital percentile series (only for cohorts with CostBasisPattern2)
* Shows invested capital at each percentile level
* @param {Colors} colors
* @param {readonly CohortWithCostBasisPercentiles[]} list
* @param {boolean} useGroupName
* @returns {FetchedPriceSeriesBlueprint[]}
*/
export function createInvestedCapitalPercentilesSeries(colors, list, useGroupName) {
return list.flatMap(({ name, tree }) => {
const ic = tree.costBasis.investedCapital;
const n = (/** @type {number} */ pct) => (useGroupName ? `${name} p${pct}` : `p${pct}`);
return [
price({ metric: ic.pct95, name: n(95), color: colors.fuchsia, defaultActive: false }),
price({ metric: ic.pct90, name: n(90), color: colors.pink, defaultActive: false }),
price({ metric: ic.pct85, name: n(85), color: colors.pink, defaultActive: false }),
price({ metric: ic.pct80, name: n(80), color: colors.rose, defaultActive: false }),
price({ metric: ic.pct75, name: n(75), color: colors.red, defaultActive: false }),
price({ metric: ic.pct70, name: n(70), color: colors.orange, defaultActive: false }),
price({ metric: ic.pct65, name: n(65), color: colors.amber, defaultActive: false }),
price({ metric: ic.pct60, name: n(60), color: colors.yellow, defaultActive: false }),
price({ metric: ic.pct55, name: n(55), color: colors.yellow, defaultActive: false }),
price({ metric: ic.pct50, name: n(50), color: colors.avocado }),
price({ metric: ic.pct45, name: n(45), color: colors.lime, defaultActive: false }),
price({ metric: ic.pct40, name: n(40), color: colors.green, defaultActive: false }),
price({ metric: ic.pct35, name: n(35), color: colors.emerald, defaultActive: false }),
price({ metric: ic.pct30, name: n(30), color: colors.teal, defaultActive: false }),
price({ metric: ic.pct25, name: n(25), color: colors.teal, defaultActive: false }),
price({ metric: ic.pct20, name: n(20), color: colors.cyan, defaultActive: false }),
price({ metric: ic.pct15, name: n(15), color: colors.sky, defaultActive: false }),
price({ metric: ic.pct10, name: n(10), color: colors.blue, defaultActive: false }),
price({ metric: ic.pct05, name: n(5), color: colors.indigo, defaultActive: false }),
];
});
}
/**
* Create spot percentile series (shows current percentile of price relative to cost basis/invested capital)
* @param {Colors} colors
* @param {readonly CohortWithCostBasisPercentiles[]} list
* @param {boolean} useGroupName
* @returns {FetchedBaselineSeriesBlueprint[]}
*/
export function createSpotPercentileSeries(colors, list, useGroupName) {
return list.flatMap(({ name, color, tree }) => [
baseline({
metric: tree.costBasis.spotCostBasisPercentile,
name: useGroupName ? `${name} Cost Basis` : "Cost Basis",
color: useGroupName ? color : colors.default,
unit: Unit.ratio,
}),
baseline({
metric: tree.costBasis.spotInvestedCapitalPercentile,
name: useGroupName ? `${name} Invested Capital` : "Invested Capital",
color: useGroupName ? color : colors.orange,
unit: Unit.ratio,
defaultActive: false,
}),
]);
}
// ============================================================================
// Activity Section Helpers
// ============================================================================
@@ -613,6 +819,67 @@ export function createSingleValueCreatedDestroyedSeries(colors, tree) {
];
}
/**
* Create profit/loss value breakdown series for single cohort
* Shows profit value created/destroyed and loss value created/destroyed
* @param {Colors} colors
* @param {{ realized: AnyRealizedPattern }} tree
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createSingleValueFlowBreakdownSeries(colors, tree) {
return [
line({
metric: tree.realized.profitValueCreated,
name: "Profit Created",
color: colors.green,
unit: Unit.usd,
}),
line({
metric: tree.realized.profitValueDestroyed,
name: "Profit Destroyed",
color: colors.lime,
unit: Unit.usd,
defaultActive: false,
}),
line({
metric: tree.realized.lossValueCreated,
name: "Loss Created",
color: colors.orange,
unit: Unit.usd,
defaultActive: false,
}),
line({
metric: tree.realized.lossValueDestroyed,
name: "Loss Destroyed",
color: colors.red,
unit: Unit.usd,
}),
];
}
/**
* Create capitulation & profit flow series for single cohort
* @param {Colors} colors
* @param {{ realized: AnyRealizedPattern }} tree
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createSingleCapitulationProfitFlowSeries(colors, tree) {
return [
line({
metric: tree.realized.profitFlow,
name: "Profit Flow",
color: colors.green,
unit: Unit.usd,
}),
line({
metric: tree.realized.capitulationFlow,
name: "Capitulation Flow",
color: colors.red,
unit: Unit.usd,
}),
];
}
// ============================================================================
// SOPR Helpers
// ============================================================================
@@ -649,3 +916,278 @@ export function createSingleSoprSeries(colors, tree) {
}),
];
}
// ============================================================================
// Investor Price Helpers
// ============================================================================
/**
* Create investor price series for single cohort
* @param {{ realized: AnyRealizedPattern }} tree
* @param {Color} color
* @returns {FetchedPriceSeriesBlueprint[]}
*/
export function createSingleInvestorPriceSeries(tree, color) {
return [
price({
metric: tree.realized.investorPrice,
name: "Investor",
color,
}),
];
}
/**
* Create investor price ratio series for single cohort
* @param {{ realized: AnyRealizedPattern }} tree
* @param {Color} color
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createSingleInvestorPriceRatioSeries(tree, color) {
return [
baseline({
metric: tree.realized.investorPriceExtra.ratio,
name: "Investor Ratio",
color,
unit: Unit.ratio,
base: 1,
}),
];
}
/**
* Create investor price series for grouped cohorts
* @param {readonly CohortObject[]} list
* @returns {FetchedPriceSeriesBlueprint[]}
*/
export function createInvestorPriceSeries(list) {
return list.map(({ color, name, tree }) =>
price({ metric: tree.realized.investorPrice, name, color }),
);
}
/**
* Create investor price ratio series for grouped cohorts
* @param {readonly CohortObject[]} list
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createInvestorPriceRatioSeries(list) {
return list.map(({ name, tree }) =>
baseline({
metric: tree.realized.investorPriceExtra.ratio,
name,
unit: Unit.ratio,
base: 1,
}),
);
}
/**
* Create investor price folder for extended cohorts (with full Z-scores)
* For cohorts with ActivePriceRatioPattern (all, term.*, ageRange.* UTXO cohorts)
* @param {PartialContext} ctx
* @param {{ tree: { realized: RealizedWithExtras }, color: Color }} cohort
* @param {string} [cohortTitle] - Cohort title (e.g., "STH")
* @returns {PartialOptionsGroup}
*/
export function createInvestorPriceFolderFull(ctx, cohort, cohortTitle) {
const { tree, color } = cohort;
return createPriceFolder(ctx, {
name: "Investor Price",
cohortTitle,
priceMetric: tree.realized.investorPrice,
ratioPattern: tree.realized.investorPriceExtra,
color,
});
}
/**
* Create investor price folder for basic cohorts (price + ratio only)
* For cohorts with InvestorPriceExtraPattern (only .ratio field)
* @param {{ tree: { realized: AnyRealizedPattern }, color: Color }} cohort
* @param {string} [cohortTitle] - Cohort title (e.g., "STH")
* @returns {PartialOptionsGroup}
*/
export function createInvestorPriceFolderBasic(cohort, cohortTitle) {
const { tree, color } = cohort;
return createBasicPriceFolder({
name: "Investor Price",
cohortTitle,
priceMetric: tree.realized.investorPrice,
ratioMetric: tree.realized.investorPriceExtra.ratio,
color,
});
}
/**
* Create investor price folder for grouped cohorts
* @param {readonly CohortObject[]} list
* @param {(metric: string) => string} title
* @returns {PartialOptionsGroup}
*/
export function createGroupedInvestorPriceFolder(list, title) {
return createGroupedPriceFolder({
name: "Investor Price",
title,
list,
getPrice: (tree) => tree.realized.investorPrice,
getRatio: (tree) => tree.realized.investorPriceExtra.ratio,
});
}
// ============================================================================
// ATH Regret Helpers
// ============================================================================
/**
* Create realized ATH regret series for single cohort
* @param {{ realized: AnyRealizedPattern }} tree
* @param {Color} color
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createSingleRealizedAthRegretSeries(tree, color) {
return [
line({
metric: tree.realized.athRegret.sum,
name: "ATH Regret",
color,
unit: Unit.usd,
}),
line({
metric: tree.realized.athRegret.cumulative,
name: "Cumulative",
color,
unit: Unit.usd,
defaultActive: false,
}),
];
}
/**
* Create unrealized ATH regret series for single cohort
* @param {{ unrealized: UnrealizedPattern }} tree
* @param {Color} color
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createSingleUnrealizedAthRegretSeries(tree, color) {
return [
line({
metric: tree.unrealized.athRegret,
name: "ATH Regret",
color,
unit: Unit.usd,
}),
];
}
/**
* Create realized ATH regret series for grouped cohorts
* @param {readonly CohortObject[]} list
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createGroupedRealizedAthRegretSeries(list) {
return list.flatMap(({ color, name, tree }) => [
line({
metric: tree.realized.athRegret.sum,
name,
color,
unit: Unit.usd,
}),
]);
}
/**
* Create unrealized ATH regret series for grouped cohorts
* @param {readonly { color: Color, name: string, tree: { unrealized: UnrealizedPattern } }[]} list
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createGroupedUnrealizedAthRegretSeries(list) {
return list.flatMap(({ color, name, tree }) => [
line({
metric: tree.unrealized.athRegret,
name,
color,
unit: Unit.usd,
}),
]);
}
// ============================================================================
// Sentiment Helpers (greedIndex, painIndex, netSentiment)
// ============================================================================
/**
* Create sentiment series for single cohort
* @param {Colors} colors
* @param {{ unrealized: UnrealizedPattern }} tree
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createSingleSentimentSeries(colors, tree) {
return [
baseline({
metric: tree.unrealized.netSentiment,
name: "Net Sentiment",
unit: Unit.usd,
}),
line({
metric: tree.unrealized.greedIndex,
name: "Greed Index",
color: colors.green,
unit: Unit.usd,
}),
line({
metric: tree.unrealized.painIndex,
name: "Pain Index",
color: colors.red,
unit: Unit.usd,
}),
];
}
/**
* Create net sentiment series for grouped cohorts
* @param {readonly { color: Color, name: string, tree: { unrealized: UnrealizedPattern } }[]} list
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createGroupedNetSentimentSeries(list) {
return list.flatMap(({ color, name, tree }) => [
baseline({
metric: tree.unrealized.netSentiment,
name,
color,
unit: Unit.usd,
}),
]);
}
/**
* Create greed index series for grouped cohorts
* @param {readonly { color: Color, name: string, tree: { unrealized: UnrealizedPattern } }[]} list
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createGroupedGreedIndexSeries(list) {
return list.flatMap(({ color, name, tree }) => [
line({
metric: tree.unrealized.greedIndex,
name,
color,
unit: Unit.usd,
}),
]);
}
/**
* Create pain index series for grouped cohorts
* @param {readonly { color: Color, name: string, tree: { unrealized: UnrealizedPattern } }[]} list
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createGroupedPainIndexSeries(list) {
return list.flatMap(({ color, name, tree }) => [
line({
metric: tree.unrealized.painIndex,
name,
color,
unit: Unit.usd,
}),
]);
}
File diff suppressed because it is too large Load Diff
+81 -115
View File
@@ -24,7 +24,6 @@ export function initOptions(brk) {
const savedPath = /** @type {string[]} */ (
JSON.parse(readStored(LS_SELECTED_KEY) || "[]") || []
).filter((v) => v);
console.log(savedPath);
const partialOptions = createPartialOptions({
brk,
@@ -83,91 +82,80 @@ export function initOptions(brk) {
);
}
/**
* Check if a metric is an ActivePricePattern (has dollars and sats sub-metrics)
* @param {any} metric
* @returns {metric is ActivePricePattern}
*/
function isActivePricePattern(metric) {
return (
metric &&
typeof metric === "object" &&
"dollars" in metric &&
"sats" in metric &&
metric.dollars?.by &&
metric.sats?.by
);
}
/**
* @param {(AnyFetchedSeriesBlueprint | FetchedPriceSeriesBlueprint)[]} [arr]
*/
function arrayToMap(arr = []) {
function arrayToMap(arr) {
/** @type {Map<Unit, AnyFetchedSeriesBlueprint[]>} */
const map = new Map();
/** @type {Map<Unit, Set<number>>} */
const priceLines = new Map();
for (const blueprint of arr || []) {
if (!blueprint.metric) {
throw new Error(
`Blueprint missing metric: ${JSON.stringify(blueprint)}`,
);
}
if (!arr) return map;
// Auto-expand ActivePricePattern into USD and sats versions
if (isActivePricePattern(blueprint.metric)) {
const pricePattern = /** @type {AnyPricePattern} */ (blueprint.metric);
// Cache arrays for common units outside loop
/** @type {AnyFetchedSeriesBlueprint[] | undefined} */
let usdArr;
/** @type {AnyFetchedSeriesBlueprint[] | undefined} */
let satsArr;
// USD version
markUsed(pricePattern.dollars);
if (!map.has(Unit.usd)) map.set(Unit.usd, []);
map.get(Unit.usd)?.push({ ...blueprint, metric: pricePattern.dollars, unit: Unit.usd });
for (let i = 0; i < arr.length; i++) {
const blueprint = arr[i];
// Sats version
markUsed(pricePattern.sats);
if (!map.has(Unit.sats)) map.set(Unit.sats, []);
map.get(Unit.sats)?.push({ ...blueprint, metric: pricePattern.sats, unit: Unit.sats });
// Check for price pattern blueprint (has dollars/sats sub-metrics)
// Use unknown cast for safe property access check
const maybePriceMetric = /** @type {{ dollars?: AnyMetricPattern, sats?: AnyMetricPattern }} */ (
/** @type {unknown} */ (blueprint.metric)
);
if (maybePriceMetric.dollars?.by && maybePriceMetric.sats?.by) {
const { dollars, sats } = maybePriceMetric;
markUsed(dollars);
if (!usdArr) map.set(Unit.usd, (usdArr = []));
usdArr.push({ ...blueprint, metric: dollars, unit: Unit.usd });
markUsed(sats);
if (!satsArr) map.set(Unit.sats, (satsArr = []));
satsArr.push({ ...blueprint, metric: sats, unit: Unit.sats });
continue;
}
// At this point, blueprint is definitely an AnyFetchedSeriesBlueprint (not a price pattern)
// After continue, we know this is a regular metric blueprint
const regularBlueprint = /** @type {AnyFetchedSeriesBlueprint} */ (blueprint);
if (!regularBlueprint.unit) {
throw new Error(`Blueprint missing unit: ${regularBlueprint.title}`);
}
markUsed(regularBlueprint.metric);
const metric = regularBlueprint.metric;
const unit = regularBlueprint.unit;
if (!map.has(unit)) {
map.set(unit, []);
}
map.get(unit)?.push(regularBlueprint);
if (!unit) continue;
markUsed(metric);
let unitArr = map.get(unit);
if (!unitArr) map.set(unit, (unitArr = []));
unitArr.push(regularBlueprint);
// Track baseline base values for auto price lines
if (regularBlueprint.type === "Baseline") {
const baseValue = regularBlueprint.options?.baseValue?.price ?? 0;
if (!priceLines.has(unit)) priceLines.set(unit, new Set());
priceLines.get(unit)?.add(baseValue);
}
// Remove from set if manual price line already exists
// Note: line() doesn't set type, so undefined means Line
if (regularBlueprint.type === "Line" || regularBlueprint.type === undefined) {
const path = Object.values(regularBlueprint.metric.by)[0]?.path ?? "";
if (path.includes("constant_")) {
priceLines.get(unit)?.delete(parseFloat(regularBlueprint.title));
const type = regularBlueprint.type;
if (type === "Baseline") {
let priceSet = priceLines.get(unit);
if (!priceSet) priceLines.set(unit, (priceSet = new Set()));
priceSet.add(regularBlueprint.options?.baseValue?.price ?? 0);
} else if (!type || type === "Line") {
// Check if manual price line - avoid Object.values() array allocation
const by = metric.by;
for (const k in by) {
if (by[/** @type {Index} */ (k)]?.path?.includes("constant_")) {
priceLines.get(unit)?.delete(parseFloat(regularBlueprint.title));
}
break;
}
}
}
// Add price lines at end for remaining values
for (const [unit, values] of priceLines) {
const arr = map.get(unit);
if (!arr) continue;
for (const baseValue of values) {
const metric = getConstant(brk.metrics.constants, baseValue);
markUsed(metric);
map.get(unit)?.push({
arr.push({
metric,
title: `${baseValue}`,
color: colors.gray,
@@ -238,30 +226,33 @@ export function initOptions(brk) {
* @typedef {ProcessedGroup | ProcessedOption} ProcessedNode
*/
// Pre-compute path strings for faster comparison
const urlPathStr = urlPath?.join("/");
const savedPathStr = savedPath?.join("/");
/**
* @param {PartialOptionsTree} partialTree
* @param {string[]} parentPath
* @returns {ProcessedNode[]}
* @param {string} parentPathStr
* @returns {{ nodes: ProcessedNode[], count: number }}
*/
function processPartialTree(partialTree, parentPath = []) {
function processPartialTree(partialTree, parentPath = [], parentPathStr = "") {
/** @type {ProcessedNode[]} */
const nodes = [];
let totalCount = 0;
for (const anyPartial of partialTree) {
for (let i = 0; i < partialTree.length; i++) {
const anyPartial = partialTree[i];
if ("tree" in anyPartial) {
const serName = stringToId(anyPartial.name);
const path = [...parentPath, serName];
const children = processPartialTree(anyPartial.tree, path);
// Compute count from children
const count = children.reduce(
(sum, child) => sum + (child.type === "group" ? child.count : 1),
0,
);
const pathStr = parentPathStr ? `${parentPathStr}/${serName}` : serName;
const path = parentPath.concat(serName);
const { nodes: children, count } = processPartialTree(anyPartial.tree, path, pathStr);
// Skip groups with no children
if (count === 0) continue;
totalCount += count;
nodes.push({
type: "group",
name: anyPartial.name,
@@ -273,39 +264,23 @@ export function initOptions(brk) {
} else {
const option = /** @type {Option} */ (anyPartial);
const name = option.name;
const path = [...parentPath, stringToId(option.name)];
const serName = stringToId(name);
const pathStr = parentPathStr ? `${parentPathStr}/${serName}` : serName;
const path = parentPath.concat(serName);
// Transform partial to full option
if ("kind" in anyPartial && anyPartial.kind === "explorer") {
Object.assign(
option,
/** @satisfies {ExplorerOption} */ ({
kind: anyPartial.kind,
path,
name,
title: option.title,
}),
);
option.kind = anyPartial.kind;
option.path = path;
option.name = name;
} else if ("kind" in anyPartial && anyPartial.kind === "table") {
Object.assign(
option,
/** @satisfies {TableOption} */ ({
kind: anyPartial.kind,
path,
name,
title: option.title,
}),
);
option.kind = anyPartial.kind;
option.path = path;
option.name = name;
} else if ("kind" in anyPartial && anyPartial.kind === "simulation") {
Object.assign(
option,
/** @satisfies {SimulationOption} */ ({
kind: anyPartial.kind,
path,
name,
title: anyPartial.title,
}),
);
option.kind = anyPartial.kind;
option.path = path;
option.name = name;
} else if ("url" in anyPartial) {
Object.assign(
option,
@@ -319,7 +294,7 @@ export function initOptions(brk) {
}),
);
} else {
const title = option.title || option.name;
const title = option.title || name;
Object.assign(
option,
/** @satisfies {ChartOption} */ ({
@@ -334,22 +309,13 @@ export function initOptions(brk) {
}
list.push(option);
totalCount++;
// Check if this matches URL or saved path
if (urlPath) {
const sameAsURLPath =
urlPath.length === path.length &&
urlPath.every((val, i) => val === path[i]);
if (sameAsURLPath) {
selected.set(option);
}
} else if (savedPath) {
const sameAsSavedPath =
savedPath.length === path.length &&
savedPath.every((val, i) => val === path[i]);
if (sameAsSavedPath) {
savedOption = option;
}
// Check if this matches URL or saved path (string comparison is faster)
if (urlPathStr && pathStr === urlPathStr) {
selected.set(option);
} else if (savedPathStr && pathStr === savedPathStr) {
savedOption = option;
}
nodes.push({
@@ -360,10 +326,10 @@ export function initOptions(brk) {
}
}
return nodes;
return { nodes, count: totalCount };
}
const processedTree = processPartialTree(partialOptions);
const { nodes: processedTree } = processPartialTree(partialOptions);
logUnused();
/**
+9 -40
View File
@@ -1,7 +1,7 @@
/** Moving averages section */
import { price } from "../series.js";
import { createRatioChart, createZScoresFolder, formatCohortTitle } from "../shared.js";
import { createPriceRatioCharts } from "../shared.js";
import { periodIdToName } from "./utils.js";
/**
@@ -66,39 +66,6 @@ function buildEmaAverages(colors, ma) {
}));
}
/**
* 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 pricePattern = ratio.price;
return [
{
name: "Price",
title,
top: [price({ metric: pricePattern, name: legend, color })],
},
createRatioChart(ctx, { title: formatCohortTitle(title), pricePattern, ratio, color }),
createZScoresFolder(ctx, {
title,
legend,
pricePattern,
ratio,
color,
}),
];
}
/** Common period IDs to show at top level */
const COMMON_PERIODS = ["1w", "1m", "200d", "1y", "200w", "4y"];
@@ -176,10 +143,11 @@ export function createAveragesSection(ctx, movingAverage) {
// Common periods at top level
...commonAverages.map(({ name, color, ratio }) => ({
name,
tree: createPriceWithRatioOptions(ctx, {
ratio,
title: `${name} ${label}`,
tree: createPriceRatioCharts(ctx, {
context: `${name} ${label}`,
legend: "average",
pricePattern: ratio.price,
ratio,
color,
}),
})),
@@ -188,10 +156,11 @@ export function createAveragesSection(ctx, movingAverage) {
name: "More...",
tree: moreAverages.map(({ name, color, ratio }) => ({
name,
tree: createPriceWithRatioOptions(ctx, {
ratio,
title: `${name} ${label}`,
tree: createPriceRatioCharts(ctx, {
context: `${name} ${label}`,
legend: "average",
pricePattern: ratio.price,
ratio,
color,
}),
})),
+2 -3
View File
@@ -1,8 +1,7 @@
/** Market section - Main entry point */
import { localhost } from "../../utils/env.js";
import { Unit } from "../../utils/units.js";
import { candlestick, line, price } from "../series.js";
import { line, price } from "../series.js";
import { createAveragesSection } from "./averages.js";
import { createReturnsSection } from "./performance.js";
import { createMomentumSection } from "./momentum.js";
@@ -21,7 +20,7 @@ import {
*/
export function createMarketSection(ctx) {
const { colors, brk } = ctx;
const { market, supply, price: priceMetrics } = brk.metrics;
const { market, supply } = brk.metrics;
const {
movingAverage,
ath,
+18 -6
View File
@@ -52,7 +52,7 @@ export function createDcaVsLumpSumSection(ctx, { dca, lookback, returns }) {
/**
* @param {string} name
* @param {ShortPeriodKey | LongPeriodKey} key
* @param {AllPeriodKey} key
*/
const costBasisChart = (name, key) => ({
name: "Cost Basis",
@@ -71,7 +71,10 @@ export function createDcaVsLumpSumSection(ctx, { dca, lookback, returns }) {
],
});
/** @param {string} name @param {ShortPeriodKey | LongPeriodKey} key */
/**
* @param {string} name
* @param {AllPeriodKey} key
*/
const daysInProfitChart = (name, key) => ({
name: "Days in Profit",
title: `${name} Days in Profit`,
@@ -85,7 +88,10 @@ export function createDcaVsLumpSumSection(ctx, { dca, lookback, returns }) {
],
});
/** @param {string} name @param {ShortPeriodKey | LongPeriodKey} key */
/**
* @param {string} name
* @param {AllPeriodKey} key
*/
const daysInLossChart = (name, key) => ({
name: "Days in Loss",
title: `${name} Days in Loss`,
@@ -99,7 +105,10 @@ export function createDcaVsLumpSumSection(ctx, { dca, lookback, returns }) {
],
});
/** @param {string} name @param {ShortPeriodKey | LongPeriodKey} key */
/**
* @param {string} name
* @param {AllPeriodKey} key
*/
const maxDrawdownChart = (name, key) => ({
name: "Max Drawdown",
title: `${name} Max Drawdown`,
@@ -113,7 +122,10 @@ export function createDcaVsLumpSumSection(ctx, { dca, lookback, returns }) {
],
});
/** @param {string} name @param {ShortPeriodKey | LongPeriodKey} key */
/**
* @param {string} name
* @param {AllPeriodKey} key
*/
const maxReturnChart = (name, key) => ({
name: "Max Return",
title: `${name} Max Return`,
@@ -129,7 +141,7 @@ export function createDcaVsLumpSumSection(ctx, { dca, lookback, returns }) {
/**
* @param {string} name
* @param {ShortPeriodKey | LongPeriodKey} key
* @param {AllPeriodKey} key
*/
const stackChart = (name, key) => ({
name: "Stack",
+18 -14
View File
@@ -10,6 +10,7 @@ import {
createCohortFolderAgeRange,
createCohortFolderBasicWithMarketCap,
createCohortFolderBasicWithoutMarketCap,
createCohortFolderWithoutRelative,
createCohortFolderAddress,
createAddressCohortFolder,
} from "./distribution/index.js";
@@ -64,6 +65,9 @@ export function createPartialOptions({ brk }) {
/** @param {CohortBasicWithoutMarketCap} cohort */
const mapBasicWithoutMarketCap = (cohort) =>
createCohortFolderBasicWithoutMarketCap(ctx, cohort);
/** @param {CohortWithoutRelative} cohort */
const mapWithoutRelative = (cohort) =>
createCohortFolderWithoutRelative(ctx, cohort);
/** @param {CohortAddress} cohort */
const mapAddress = (cohort) => createCohortFolderAddress(ctx, cohort);
/** @param {AddressCohortObject} cohort */
@@ -107,7 +111,7 @@ export function createPartialOptions({ brk }) {
// STH vs LTH - Direct comparison
createCohortFolderWithNupl(ctx, {
name: "STH vs LTH",
title: "Holders",
title: "STH vs LTH",
list: [termShort, termLong],
}),
@@ -121,7 +125,7 @@ export function createPartialOptions({ brk }) {
tree: [
createCohortFolderWithAdjusted(ctx, {
name: "Compare",
title: "Age Younger Than",
title: "Max Age",
list: upToDate,
}),
...upToDate.map(mapWithAdjusted),
@@ -133,7 +137,7 @@ export function createPartialOptions({ brk }) {
tree: [
createCohortFolderBasicWithMarketCap(ctx, {
name: "Compare",
title: "Age Older Than",
title: "Min Age",
list: fromDate,
}),
...fromDate.map(mapBasicWithMarketCap),
@@ -145,7 +149,7 @@ export function createPartialOptions({ brk }) {
tree: [
createCohortFolderAgeRange(ctx, {
name: "Compare",
title: "Age Range",
title: "Age Ranges",
list: dateRange,
}),
...dateRange.map(mapAgeRange),
@@ -164,7 +168,7 @@ export function createPartialOptions({ brk }) {
tree: [
createCohortFolderBasicWithMarketCap(ctx, {
name: "Compare",
title: "Size Less Than",
title: "Max Size",
list: utxosUnderAmount,
}),
...utxosUnderAmount.map(mapBasicWithMarketCap),
@@ -176,7 +180,7 @@ export function createPartialOptions({ brk }) {
tree: [
createCohortFolderBasicWithMarketCap(ctx, {
name: "Compare",
title: "Size More Than",
title: "Min Size",
list: utxosAboveAmount,
}),
...utxosAboveAmount.map(mapBasicWithMarketCap),
@@ -188,7 +192,7 @@ export function createPartialOptions({ brk }) {
tree: [
createCohortFolderBasicWithoutMarketCap(ctx, {
name: "Compare",
title: "Size Range",
title: "Size Ranges",
list: utxosAmountRanges,
}),
...utxosAmountRanges.map(mapBasicWithoutMarketCap),
@@ -207,7 +211,7 @@ export function createPartialOptions({ brk }) {
tree: [
createAddressCohortFolder(ctx, {
name: "Compare",
title: "Balance Less Than",
title: "Max Balance",
list: addressesUnderAmount,
}),
...addressesUnderAmount.map(mapAddressCohorts),
@@ -219,7 +223,7 @@ export function createPartialOptions({ brk }) {
tree: [
createAddressCohortFolder(ctx, {
name: "Compare",
title: "Balance More Than",
title: "Min Balance",
list: addressesAboveAmount,
}),
...addressesAboveAmount.map(mapAddressCohorts),
@@ -231,7 +235,7 @@ export function createPartialOptions({ brk }) {
tree: [
createAddressCohortFolder(ctx, {
name: "Compare",
title: "Balance Range",
title: "Balance Ranges",
list: addressesAmountRanges,
}),
...addressesAmountRanges.map(mapAddressCohorts),
@@ -246,11 +250,11 @@ export function createPartialOptions({ brk }) {
tree: [
createCohortFolderAddress(ctx, {
name: "Compare",
title: "Script Type",
title: "Script Types",
list: typeAddressable,
}),
...typeAddressable.map(mapAddress),
...typeOther.map(mapBasicWithoutMarketCap),
...typeOther.map(mapWithoutRelative),
],
},
@@ -260,7 +264,7 @@ export function createPartialOptions({ brk }) {
tree: [
createCohortFolderBasicWithoutMarketCap(ctx, {
name: "Compare",
title: "Epoch",
title: "Epochs",
list: epoch,
}),
...epoch.map(mapBasicWithoutMarketCap),
@@ -273,7 +277,7 @@ export function createPartialOptions({ brk }) {
tree: [
createCohortFolderBasicWithoutMarketCap(ctx, {
name: "Compare",
title: "Year",
title: "Years",
list: year,
}),
...year.map(mapBasicWithoutMarketCap),
+27 -49
View File
@@ -46,9 +46,8 @@ export function price({
/**
* Create percentile series (max/min/median/pct75/pct25/pct90/pct10) from any stats pattern
* Works with FullnessPattern, FeeRatePattern, AnyStatsPattern, DollarsPattern, etc.
* @param {Colors} colors
* @param {FullnessPattern<any> | FeeRatePattern<any> | AnyStatsPattern | DollarsPattern<any>} pattern
* @param {StatsPattern<any> | BaseStatsPattern<any> | FullStatsPattern<any> | AnyStatsPattern} pattern
* @param {Unit} unit
* @param {string} title
* @param {{ type?: "Dots" }} [options]
@@ -289,14 +288,14 @@ export function histogram({
}
/**
* Create series from a SizePattern ({ average, sum, cumulative, min, max, percentiles })
* Create series from patterns with sum + cumulative + percentiles (NO base)
* @param {Colors} colors
* @param {AnyStatsPattern} pattern
* @param {Unit} unit
* @param {string} [title]
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function fromSizePattern(colors, pattern, unit, title = "") {
export function fromSumStatsPattern(colors, pattern, unit, title = "") {
const { stat } = colors;
return [
{ metric: pattern.average, title: `${title} avg`.trim(), unit },
@@ -319,36 +318,39 @@ export function fromSizePattern(colors, pattern, unit, title = "") {
}
/**
* Create series from a FullnessPattern ({ base, average, sum, cumulative, min, max, percentiles })
* Create series from a BaseStatsPattern (base + avg + percentiles, NO sum)
* @param {Colors} colors
* @param {FullnessPattern<any>} pattern
* @param {BaseStatsPattern<any>} pattern
* @param {Unit} unit
* @param {string} [title]
* @param {{ baseColor?: Color, avgActive?: boolean }} [options]
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function fromFullnessPattern(colors, pattern, unit, title = "") {
export function fromBaseStatsPattern(colors, pattern, unit, title = "", options) {
const { stat } = colors;
const { baseColor, avgActive = true } = options || {};
return [
{ metric: pattern.base, title: title || "base", unit },
{ metric: pattern.base, title: title || "base", color: baseColor, unit },
{
metric: pattern.average,
title: `${title} avg`.trim(),
color: stat.avg,
unit,
defaultActive: avgActive,
},
...percentileSeries(colors, pattern, unit, title),
];
}
/**
* Create series from a DollarsPattern ({ base, sum, cumulative, average, min, max, percentiles })
* Create series from a FullStatsPattern (base + sum + cumulative + avg + percentiles)
* @param {Colors} colors
* @param {DollarsPattern<any>} pattern
* @param {FullStatsPattern<any>} pattern
* @param {Unit} unit
* @param {string} [title]
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function fromDollarsPattern(colors, pattern, unit, title = "") {
export function fromFullStatsPattern(colors, pattern, unit, title = "") {
const { stat } = colors;
return [
{ metric: pattern.base, title: title || "base", unit },
@@ -377,14 +379,14 @@ export function fromDollarsPattern(colors, pattern, unit, title = "") {
}
/**
* Create series from a FeeRatePattern ({ average, min, max, percentiles })
* Create series from a StatsPattern ({ average, min, max, percentiles })
* @param {Colors} colors
* @param {FeeRatePattern<any>} pattern
* @param {StatsPattern<any>} pattern
* @param {Unit} unit
* @param {string} [title]
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function fromFeeRatePattern(colors, pattern, unit, title = "") {
export function fromStatsPattern(colors, pattern, unit, title = "") {
return [
{
type: "Dots",
@@ -397,14 +399,14 @@ export function fromFeeRatePattern(colors, pattern, unit, title = "") {
}
/**
* Create series from a pattern with sum and cumulative (fullness stats + sum + cumulative)
* Create series from AnyFullStatsPattern (base + sum + cumulative + avg + percentiles)
* @param {Colors} colors
* @param {FullnessPatternWithSumCumulative} pattern
* @param {AnyFullStatsPattern} pattern
* @param {Unit} unit
* @param {string} [title]
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function fromFullnessPatternWithSumCumulative(
export function fromAnyFullStatsPattern(
colors,
pattern,
unit,
@@ -412,7 +414,7 @@ export function fromFullnessPatternWithSumCumulative(
) {
const { stat } = colors;
return [
...fromFullnessPattern(colors, pattern, unit, title),
...fromBaseStatsPattern(colors, pattern, unit, title),
{
metric: pattern.sum,
title: `${title} sum`.trim(),
@@ -438,19 +440,19 @@ export function fromFullnessPatternWithSumCumulative(
*/
export function fromCoinbasePattern(colors, pattern, title = "") {
return [
...fromFullnessPatternWithSumCumulative(
...fromAnyFullStatsPattern(
colors,
pattern.bitcoin,
Unit.btc,
title,
),
...fromFullnessPatternWithSumCumulative(
...fromAnyFullStatsPattern(
colors,
pattern.sats,
Unit.sats,
title,
),
...fromFullnessPatternWithSumCumulative(
...fromAnyFullStatsPattern(
colors,
pattern.dollars,
Unit.usd,
@@ -460,7 +462,7 @@ export function fromCoinbasePattern(colors, pattern, title = "") {
}
/**
* Create series from a ValuePattern ({ sats, bitcoin, dollars } each as BlockCountPattern with sum + cumulative)
* Create series from a ValuePattern ({ sats, bitcoin, dollars } each as CountPattern with sum + cumulative)
* @param {Colors} colors
* @param {ValuePattern} pattern
* @param {string} [title]
@@ -557,16 +559,16 @@ export function fromBitcoinPatternWithUnit(
}
/**
* Create sum/cumulative series from a BlockCountPattern with explicit unit and colors
* Create sum/cumulative series from a CountPattern with explicit unit and colors
* @param {Colors} colors
* @param {BlockCountPattern<any>} pattern
* @param {CountPattern<any>} pattern
* @param {Unit} unit
* @param {string} [title]
* @param {Color} [sumColor]
* @param {Color} [cumulativeColor]
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function fromBlockCountWithUnit(
export function fromCountPattern(
colors,
pattern,
unit,
@@ -591,30 +593,6 @@ export function fromBlockCountWithUnit(
];
}
/**
* Create series from an IntervalPattern (base + average/min/max/median/percentiles, no sum/cumulative)
* @param {Colors} colors
* @param {IntervalPattern} pattern
* @param {Unit} unit
* @param {string} [title]
* @param {Color} [color]
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function fromIntervalPattern(colors, pattern, unit, title = "", color) {
const { stat } = colors;
return [
{ metric: pattern.base, title: title ?? "base", color, unit },
{
metric: pattern.average,
title: `${title} avg`.trim(),
color: stat.avg,
unit,
defaultActive: false,
},
...percentileSeries(colors, pattern, unit, title),
];
}
/**
* Create series from a SupplyPattern (sats/bitcoin/dollars, no sum/cumulative)
* @param {SupplyPattern} pattern
+51 -10
View File
@@ -44,7 +44,7 @@ export function satsBtcUsd(pattern, name, color, options) {
/**
* Build percentile USD mappings from a ratio pattern
* @param {Colors} colors
* @param {ActivePriceRatioPattern} ratio
* @param {AnyRatioPattern} ratio
*/
export function percentileUsdMap(colors, ratio) {
return /** @type {const} */ ([
@@ -60,7 +60,7 @@ export function percentileUsdMap(colors, ratio) {
/**
* Build percentile ratio mappings from a ratio pattern
* @param {Colors} colors
* @param {ActivePriceRatioPattern} ratio
* @param {AnyRatioPattern} ratio
*/
export function percentileMap(colors, ratio) {
return /** @type {const} */ ([
@@ -75,7 +75,7 @@ export function percentileMap(colors, ratio) {
/**
* Build SD patterns from a ratio pattern
* @param {ActivePriceRatioPattern} ratio
* @param {AnyRatioPattern} ratio
*/
export function sdPatterns(ratio) {
return /** @type {const} */ ([
@@ -135,7 +135,7 @@ export function sdBandsRatio(colors, sd) {
/**
* Build ratio SMA series from a ratio pattern
* @param {Colors} colors
* @param {ActivePriceRatioPattern} ratio
* @param {AnyRatioPattern} ratio
*/
export function ratioSmas(colors, ratio) {
return /** @type {const} */ ([
@@ -154,7 +154,7 @@ export function ratioSmas(colors, ratio) {
* @param {Object} args
* @param {(metric: string) => string} args.title
* @param {AnyPricePattern} args.pricePattern - The price pattern to show in top pane
* @param {ActivePriceRatioPattern} args.ratio - The ratio pattern
* @param {AnyRatioPattern} args.ratio - The ratio pattern
* @param {Color} args.color
* @param {string} [args.name] - Optional name override (default: "ratio")
* @returns {PartialChartOption}
@@ -205,16 +205,16 @@ export function createRatioChart(ctx, { title, pricePattern, ratio, color, name
* Create ZScores folder from ActivePriceRatioPattern
* @param {PartialContext} ctx
* @param {Object} args
* @param {string} args.title
* @param {(suffix: string) => string} args.formatTitle - Function that takes metric suffix and returns full title
* @param {string} args.legend
* @param {AnyPricePattern} args.pricePattern - The price pattern to show in top pane
* @param {ActivePriceRatioPattern} args.ratio - The ratio pattern
* @param {AnyRatioPattern} args.ratio - The ratio pattern
* @param {Color} args.color
* @returns {PartialOptionsGroup}
*/
export function createZScoresFolder(
ctx,
{ title, legend, pricePattern, ratio, color },
{ formatTitle, legend, pricePattern, ratio, color },
) {
const { colors } = ctx;
const sdPats = sdPatterns(ratio);
@@ -224,7 +224,7 @@ export function createZScoresFolder(
tree: [
{
name: "Compare",
title: `${title} Z-Scores`,
title: formatTitle("Z-Scores"),
top: [
price({ metric: pricePattern, name: legend, color }),
price({
@@ -287,7 +287,7 @@ export function createZScoresFolder(
},
...sdPats.map(({ nameAddon, titleAddon, sd }) => ({
name: nameAddon,
title: `${title} ${titleAddon} Z-Score`,
title: formatTitle(`${titleAddon ? `${titleAddon} ` : ""}Z-Score`),
top: [
price({ metric: pricePattern, name: legend, color }),
...sdBandsUsd(colors, sd).map(
@@ -343,3 +343,44 @@ export function createZScoresFolder(
],
};
}
/**
* Create price + ratio + z-scores charts - flat array
* Unified helper for averages, distribution, and other price-based metrics
* @param {PartialContext} ctx
* @param {Object} args
* @param {string} args.context - Context string for ratio/z-scores titles (e.g., "1 Week SMA", "STH")
* @param {string} args.legend - Legend name for the price series
* @param {AnyPricePattern} args.pricePattern - The price pattern
* @param {AnyRatioPattern} args.ratio - The ratio pattern
* @param {Color} args.color
* @param {string} [args.ratioName] - Optional custom name for ratio chart (default: "ratio")
* @param {string} [args.priceTitle] - Optional override for price chart title (default: context)
* @param {string} [args.zScoresSuffix] - Optional suffix appended to context for z-scores (e.g., "MVRV" gives "2y Z-Score: STH MVRV")
* @returns {PartialOptionsTree}
*/
export function createPriceRatioCharts(ctx, { context, legend, pricePattern, ratio, color, ratioName, priceTitle, zScoresSuffix }) {
const titleFn = formatCohortTitle(context);
const zScoresTitleFn = zScoresSuffix ? formatCohortTitle(`${context} ${zScoresSuffix}`) : titleFn;
return [
{
name: "Price",
title: priceTitle ?? context,
top: [price({ metric: pricePattern, name: legend, color })],
},
createRatioChart(ctx, {
title: titleFn,
pricePattern,
ratio,
color,
name: ratioName,
}),
createZScoresFolder(ctx, {
formatTitle: zScoresTitleFn,
legend,
pricePattern,
ratio,
color,
}),
];
}
+17 -1
View File
@@ -160,6 +160,10 @@
* - EpochPattern (epoch.*, amountRange.*, year.*, type.*)
* @typedef {EpochPattern} PatternBasicWithoutMarketCap
*
* Patterns without relative section entirely (edge case output types):
* - EmptyPattern (type.empty, type.p2ms, type.unknown)
* @typedef {EmptyPattern} PatternWithoutRelative
*
* Union of basic patterns (for backwards compat)
* @typedef {PatternBasicWithMarketCap | PatternBasicWithoutMarketCap} PatternBasic
*
@@ -224,6 +228,13 @@
* @property {Color} color
* @property {PatternBasicWithoutMarketCap} tree
*
* Cohort without relative section (edge case types: empty, p2ms, unknown)
* @typedef {Object} CohortWithoutRelative
* @property {string} name
* @property {string} title
* @property {Color} color
* @property {PatternWithoutRelative} tree
*
* Union of basic cohort types
* @typedef {CohortBasicWithMarketCap | CohortBasicWithoutMarketCap} CohortBasic
*
@@ -273,6 +284,11 @@
* @property {string} title
* @property {readonly CohortBasicWithoutMarketCap[]} list
*
* @typedef {Object} CohortGroupWithoutRelative
* @property {string} name
* @property {string} title
* @property {readonly CohortWithoutRelative[]} list
*
* Union of basic cohort group types
* @typedef {CohortGroupBasicWithMarketCap | CohortGroupBasicWithoutMarketCap} CohortGroupBasic
*
@@ -287,7 +303,7 @@
* @property {Color} color
* @property {AddressCohortPattern} tree
*
* @typedef {UtxoCohortObject | AddressCohortObject} CohortObject
* @typedef {UtxoCohortObject | AddressCohortObject | CohortWithoutRelative} CohortObject
*
*
* @typedef {Object} AddressCohortGroupObject
+2
View File
@@ -44,8 +44,10 @@ function walk(node, map, path) {
kn.startsWith("satblocks") ||
kn.startsWith("satdays") ||
kn.endsWith("state") ||
kn.endsWith("cents") ||
kn.endsWith("index") ||
kn.endsWith("indexes") ||
kn.endsWith("raw") ||
kn.endsWith("bytes") ||
(kn.startsWith("_") && kn.endsWith("start"))
)
+72 -51
View File
@@ -14,7 +14,7 @@
*
* @import { WebSockets } from "./utils/ws.js"
*
* @import { Option, PartialChartOption, ChartOption, AnyPartialOption, ProcessedOptionAddons, OptionsTree, SimulationOption, AnySeriesBlueprint, SeriesType, AnyFetchedSeriesBlueprint, TableOption, ExplorerOption, UrlOption, PartialOptionsGroup, OptionsGroup, PartialOptionsTree, UtxoCohortObject, AddressCohortObject, CohortObject, CohortGroupObject, FetchedLineSeriesBlueprint, FetchedBaselineSeriesBlueprint, FetchedHistogramSeriesBlueprint, PartialContext, PatternAll, PatternFull, PatternWithAdjusted, PatternWithPercentiles, PatternBasic, PatternBasicWithMarketCap, PatternBasicWithoutMarketCap, CohortAll, CohortFull, CohortWithAdjusted, CohortWithPercentiles, CohortBasic, CohortBasicWithMarketCap, CohortBasicWithoutMarketCap, CohortAddress, CohortLongTerm, CohortAgeRange, CohortGroupFull, CohortGroupWithAdjusted, CohortGroupWithPercentiles, CohortGroupLongTerm, CohortGroupAgeRange, CohortGroupBasic, CohortGroupBasicWithMarketCap, CohortGroupBasicWithoutMarketCap, CohortGroupAddress, UtxoCohortGroupObject, AddressCohortGroupObject, FetchedDotsSeriesBlueprint, FetchedCandlestickSeriesBlueprint, FetchedPriceSeriesBlueprint, AnyPricePattern } from "./options/partial.js"
* @import { Option, PartialChartOption, ChartOption, AnyPartialOption, ProcessedOptionAddons, OptionsTree, SimulationOption, AnySeriesBlueprint, SeriesType, AnyFetchedSeriesBlueprint, TableOption, ExplorerOption, UrlOption, PartialOptionsGroup, OptionsGroup, PartialOptionsTree, UtxoCohortObject, AddressCohortObject, CohortObject, CohortGroupObject, FetchedLineSeriesBlueprint, FetchedBaselineSeriesBlueprint, FetchedHistogramSeriesBlueprint, PartialContext, PatternAll, PatternFull, PatternWithAdjusted, PatternWithPercentiles, PatternBasic, PatternBasicWithMarketCap, PatternBasicWithoutMarketCap, PatternWithoutRelative, CohortAll, CohortFull, CohortWithAdjusted, CohortWithPercentiles, CohortBasic, CohortBasicWithMarketCap, CohortBasicWithoutMarketCap, CohortWithoutRelative, CohortAddress, CohortLongTerm, CohortAgeRange, CohortGroupFull, CohortGroupWithAdjusted, CohortGroupWithPercentiles, CohortGroupLongTerm, CohortGroupAgeRange, CohortGroupBasic, CohortGroupBasicWithMarketCap, CohortGroupBasicWithoutMarketCap, CohortGroupWithoutRelative, CohortGroupAddress, UtxoCohortGroupObject, AddressCohortGroupObject, FetchedDotsSeriesBlueprint, FetchedCandlestickSeriesBlueprint, FetchedPriceSeriesBlueprint, AnyPricePattern } from "./options/partial.js"
*
*
* @import { UnitObject as Unit } from "./utils/units.js"
@@ -31,74 +31,91 @@
* @typedef {ISeriesMarkersPluginApi<Time>} SeriesMarkersPlugin
* @typedef {SeriesMarker<Time>} TimeSeriesMarker
*
* Brk type aliases
* Brk tree types (stable across regenerations)
* @typedef {Brk.MetricsTree_Distribution_UtxoCohorts} UtxoCohortTree
* @typedef {Brk.MetricsTree_Distribution_AddressCohorts} AddressCohortTree
* @typedef {Brk.MetricsTree_Distribution_UtxoCohorts_All} AllUtxoPattern
* @typedef {Brk.MetricsTree_Distribution_UtxoCohorts_Term_Short} ShortTermPattern
* @typedef {Brk.MetricsTree_Distribution_UtxoCohorts_Term_Long} LongTermPattern
* @typedef {Brk._10yPattern} MaxAgePattern
* @typedef {Brk._10yTo12yPattern} AgeRangePattern
* @typedef {Brk._0satsPattern2} UtxoAmountPattern
* @typedef {Brk._0satsPattern} AddressAmountPattern
* @typedef {Brk._100btcPattern} BasicUtxoPattern
* @typedef {Brk._0satsPattern2} EpochPattern
* @typedef {Brk.Ratio1ySdPattern} Ratio1ySdPattern
* @typedef {Brk.Dollars} Dollars
* @typedef {Brk.Price111dSmaPattern} EmaRatioPattern
* @typedef {Brk.CoinbasePattern} CoinbasePattern
* @typedef {Brk.ActivePriceRatioPattern} ActivePriceRatioPattern
* @typedef {Brk.UnclaimedRewardsPattern} ValuePattern
* @typedef {Brk.AnyMetricPattern} AnyMetricPattern
* @typedef {Brk.ActivePricePattern} ActivePricePattern
* @typedef {Brk.AnyMetricEndpointBuilder} AnyMetricEndpoint
* @typedef {Brk.AnyMetricData} AnyMetricData
* @typedef {Brk.AddrCountPattern} AddrCountPattern
* @typedef {Brk.MetricsTree_Distribution_UtxoCohorts_All_Relative} AllRelativePattern
* @typedef {Brk.MetricsTree_Supply_Circulating} SupplyPattern
* @typedef {Brk.MetricsTree_Blocks_Size} BlockSizePattern
* @typedef {keyof Brk.MetricsTree_Distribution_UtxoCohorts_Type} SpendableType
* @typedef {keyof Brk.MetricsTree_Distribution_AnyAddressIndexes} AddressableType
* @typedef {FullnessPattern<any>} IntervalPattern
* @typedef {Brk.MetricsTree_Supply_Circulating} SupplyPattern
* @typedef {Brk.RelativePattern} GlobalRelativePattern
* @typedef {Brk.RelativePattern2} OwnRelativePattern
* @typedef {Brk.RelativePattern5} FullRelativePattern
* @typedef {Brk.MetricsTree_Distribution_UtxoCohorts_All_Relative} AllRelativePattern
* @typedef {Brk.UnrealizedPattern} UnrealizedPattern
*
* Brk pattern types (using new pattern names)
* @typedef {Brk.ActivityCostOutputsRealizedRelativeSupplyUnrealizedPattern5} MaxAgePattern
* @typedef {Brk.ActivityCostOutputsRealizedRelativeSupplyUnrealizedPattern} AgeRangePattern
* @typedef {Brk.ActivityCostOutputsRealizedRelativeSupplyUnrealizedPattern3} UtxoAmountPattern
* @typedef {Brk.ActivityAddrCostOutputsRealizedRelativeSupplyUnrealizedPattern} AddressAmountPattern
* @typedef {Brk.ActivityCostOutputsRealizedRelativeSupplyUnrealizedPattern4} BasicUtxoPattern
* @typedef {Brk.ActivityCostOutputsRealizedRelativeSupplyUnrealizedPattern3} EpochPattern
* @typedef {Brk.ActivityCostOutputsRealizedSupplyUnrealizedPattern} EmptyPattern
* @typedef {Brk._0sdM0M1M1sdM2M2sdM3sdP0P1P1sdP2P2sdP3sdSdSmaZscorePattern} Ratio1ySdPattern
* @typedef {Brk.Dollars} Dollars
* CoinbasePattern: patterns with bitcoin/sats/dollars each having fullness + sum + cumulative
* @typedef {Brk.BitcoinDollarsSatsPattern2} CoinbasePattern
* ActivePriceRatioPattern: ratio pattern with price (extended)
* @typedef {Brk.PriceRatioPattern} ActivePriceRatioPattern
* AnyRatioPattern: full ratio patterns (with or without price) - has ratio, percentiles, z-scores
* @typedef {Brk.RatioPattern | Brk.PriceRatioPattern} AnyRatioPattern
* ValuePattern: patterns with {bitcoin.sum, bitcoin.cumulative, sats.sum, sats.cumulative, dollars.sum, dollars.cumulative}
* @typedef {Brk.BitcoinDollarsSatsPattern6 | Brk.BitcoinDollarsSatsPattern3 | Brk.BitcoinDollarsSatsPattern2} ValuePattern
* @typedef {Brk.AnyMetricPattern} AnyMetricPattern
* @typedef {Brk.DollarsSatsPattern} ActivePricePattern
* @typedef {Brk.AnyMetricEndpointBuilder} AnyMetricEndpoint
* @typedef {Brk.AnyMetricData} AnyMetricData
* @typedef {Brk.AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern} AddrCountPattern
* Relative patterns by capability:
* - BasicRelativePattern: minimal relative (investedCapitalIn*Pct, supplyIn*RelToOwnSupply only)
* - GlobalRelativePattern: has RelToMarketCap metrics (netUnrealizedPnlRelToMarketCap, etc)
* - OwnRelativePattern: has RelToOwnMarketCap metrics (netUnrealizedPnlRelToOwnMarketCap, etc)
* - FullRelativePattern: has BOTH RelToMarketCap AND RelToOwnMarketCap
* @typedef {Brk.InvestedSupplyPattern} BasicRelativePattern
* @typedef {Brk.InvestedNegNetNuplSupplyUnrealizedPattern} GlobalRelativePattern
* @typedef {Brk.InvestedNegNetSupplyUnrealizedPattern} OwnRelativePattern
* @typedef {Brk.InvestedNegNetNuplSupplyUnrealizedPattern2} FullRelativePattern
* @typedef {Brk.AthGreedInvestedInvestorNegNetPainSupplyTotalUnrealizedPattern} UnrealizedPattern
*
* Realized patterns
* @typedef {Brk.AthCapCapitulationInvestorLossMvrvNegNetProfitRealizedSellSoprTotalValuePattern} RealizedPattern
* @typedef {Brk.AthCapCapitulationInvestorLossMvrvNegNetProfitRealizedSellSoprTotalValuePattern2} RealizedPattern2
* @typedef {Brk.AdjustedAthCapCapitulationInvestorLossMvrvNegNetProfitRealizedSellSoprTotalValuePattern} RealizedPattern3
* @typedef {Brk.AdjustedAthCapCapitulationInvestorLossMvrvNegNetProfitRealizedSellSoprTotalValuePattern2} RealizedPattern4
*/
/**
* @template T
* @typedef {Brk.BlockCountPattern<T>} BlockCountPattern
*/
/**
* @template T
* @typedef {Brk.FullnessPattern<T>} FullnessPattern
*/
/**
* @template T
* @typedef {Brk.FeeRatePattern<T>} FeeRatePattern
*/
/**
* @template T
* @typedef {Brk.MetricEndpointBuilder<T>} MetricEndpoint
*/
/**
* Stats pattern: average, min, max, percentiles (NO base)
* @template T
* @typedef {Brk.DollarsPattern<T>} SizePattern
* @typedef {Brk.AverageMaxMedianMinPct10Pct25Pct75Pct90TxindexPattern<T>} StatsPattern
*/
/**
* Base stats pattern: base, average, min, max, percentiles (NO sum/cumulative)
* @template T
* @typedef {Brk.DollarsPattern<T>} DollarsPattern
* @typedef {Brk.AverageBaseMaxMedianMinPct10Pct25Pct75Pct90Pattern<T>} BaseStatsPattern
*/
/**
* Full stats pattern: base, average, sum, cumulative, min, max, percentiles
* @template T
* @typedef {Brk.CountPattern2<T>} CountStatsPattern
* @typedef {Brk.AverageBaseCumulativeMaxMedianMinPct10Pct25Pct75Pct90SumPattern2<T>} FullStatsPattern
*/
/**
* @typedef {Brk.MetricsTree_Blocks_Size} BlockSizePattern
* Sum stats pattern: average, sum, cumulative, percentiles (NO base)
* @template T
* @typedef {Brk.AverageCumulativeMaxMedianMinPct10Pct25Pct75Pct90SumPattern2<T>} SumStatsPattern
*/
/**
* Stats pattern union - accepts both CountStatsPattern and BlockSizePattern
* @typedef {CountStatsPattern<any> | BlockSizePattern} AnyStatsPattern
* Count pattern: sum and cumulative only
* @template T
* @typedef {Brk.CumulativeSumPattern<T>} CountPattern
*/
/**
* Any stats pattern union - patterns with sum/cumulative + percentiles
* @typedef {SumStatsPattern<any> | FullStatsPattern<any> | BlockSizePattern} AnyStatsPattern
*/
/**
@@ -112,12 +129,14 @@
* @typedef {Brk.MetricsTree_Market} Market
* @typedef {Brk.MetricsTree_Market_MovingAverage} MarketMovingAverage
* @typedef {Brk.MetricsTree_Market_Dca} MarketDca
* @typedef {Brk.PeriodCagrPattern} PeriodCagrPattern
* @typedef {Brk.BitcoinPattern | Brk.DollarsPattern<any>} FullnessPatternWithSumCumulative
* @typedef {Brk._10y2y3y4y5y6y8yPattern} PeriodCagrPattern
* Full stats pattern union (both generic and non-generic variants)
* @typedef {Brk.AverageBaseCumulativeMaxMedianMinPct10Pct25Pct75Pct90SumPattern | FullStatsPattern<any>} AnyFullStatsPattern
*
* DCA period keys
* DCA period keys - derived from pattern types
* @typedef {keyof Brk._10y2y3y4y5y6y8yPattern} LongPeriodKey
* @typedef {"_1w" | "_1m" | "_3m" | "_6m" | "_1y"} ShortPeriodKey
* @typedef {keyof PeriodCagrPattern} LongPeriodKey
* @typedef {ShortPeriodKey | LongPeriodKey} AllPeriodKey
*
* Pattern unions by cohort type
* @typedef {AllUtxoPattern | AgeRangePattern | UtxoAmountPattern} UtxoCohortPattern
@@ -129,12 +148,14 @@
* @typedef {OwnRelativePattern | FullRelativePattern} RelativeWithOwnMarketCap
* @typedef {OwnRelativePattern | FullRelativePattern | AllRelativePattern} RelativeWithOwnPnl
* @typedef {GlobalRelativePattern | FullRelativePattern} RelativeWithNupl
* @typedef {BasicRelativePattern | GlobalRelativePattern | OwnRelativePattern | FullRelativePattern | AllRelativePattern} RelativeWithInvestedCapitalPct
*
* Realized pattern capability types (RealizedPattern2 and RealizedPattern3 have extra metrics)
* @typedef {Brk.RealizedPattern2 | Brk.RealizedPattern3} RealizedWithExtras
* Realized pattern capability types
* RealizedWithExtras: patterns with realizedCapRelToOwnMarketCap + realizedProfitToLossRatio
* @typedef {RealizedPattern2 | RealizedPattern3} RealizedWithExtras
*
* Any realized pattern (all have sellSideRiskRatio, valueCreated, valueDestroyed, etc.)
* @typedef {Brk.RealizedPattern | Brk.RealizedPattern2 | Brk.RealizedPattern3 | Brk.RealizedPattern4} AnyRealizedPattern
* @typedef {RealizedPattern | RealizedPattern2 | RealizedPattern3 | RealizedPattern4} AnyRealizedPattern
*
* Capability-based pattern groupings (patterns that have specific properties)
* @typedef {AllUtxoPattern | AgeRangePattern | UtxoAmountPattern} PatternWithRealizedPrice
+3 -2
View File
@@ -153,10 +153,11 @@ export function createLabeledInput({
if (title) {
label.title = title;
}
label.htmlFor = inputId;
if (onClick) {
label.addEventListener("click", onClick);
input.addEventListener("click", onClick);
} else {
label.htmlFor = inputId;
}
return {