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
+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"))
)