Files
brk/website/scripts/options/distribution/shared.js
2026-02-03 11:03:51 +01:00

1413 lines
37 KiB
JavaScript

/** Shared cohort chart section builders */
import { colors } from "../../utils/colors.js";
import { Unit } from "../../utils/units.js";
import { priceLine } from "../constants.js";
import { baseline, dots, line, price } from "../series.js";
import {
satsBtcUsd,
createPriceRatioCharts,
formatCohortTitle,
} from "../shared.js";
// ============================================================================
// Generic Price Helpers
// ============================================================================
/**
* Create price folder (price + ratio + z-scores wrapped in folder)
* For cohorts with full extended ratio metrics (ActivePriceRatioPattern)
* @param {{ name: string, cohortTitle?: string, priceMetric: ActivePricePattern, ratioPattern: AnyRatioPattern, color: Color }} args
* @returns {PartialOptionsGroup}
*/
export function createPriceFolder({
name,
cohortTitle,
priceMetric,
ratioPattern,
color,
}) {
const context = cohortTitle ? `${cohortTitle} ${name}` : name;
return {
name,
tree: createPriceRatioCharts({
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 {CohortObject | CohortWithoutRelative} cohort
* @returns {AnyFetchedSeriesBlueprint[]}
*/
function createSingleSupplySeriesBase(cohort) {
const { tree } = cohort;
return [
...satsBtcUsd({
pattern: tree.supply.total,
name: "Supply",
color: colors.default,
}),
...satsBtcUsd({
pattern: tree.supply._30dChange,
name: "30d Change",
color: colors.orange,
}),
...satsBtcUsd({
pattern: tree.unrealized.supplyInProfit,
name: "In Profit",
color: colors.green,
}),
...satsBtcUsd({
pattern: tree.unrealized.supplyInLoss,
name: "In Loss",
color: colors.red,
}),
...satsBtcUsd({
pattern: tree.supply.halved,
name: "half",
color: colors.gray,
}).map((s) => ({
...s,
options: { lineStyle: 4 },
})),
];
}
/**
* Create supply relative to own supply metrics
* @param {UtxoCohortObject | AddressCohortObject} cohort
* @returns {AnyFetchedSeriesBlueprint[]}
*/
function createSingleSupplyRelativeToOwnMetrics(cohort) {
const { tree } = cohort;
return [
line({
metric: tree.relative.supplyInProfitRelToOwnSupply,
name: "In Profit",
color: colors.green,
unit: Unit.pctOwn,
}),
line({
metric: tree.relative.supplyInLossRelToOwnSupply,
name: "In Loss",
color: colors.red,
unit: Unit.pctOwn,
}),
priceLine({
unit: Unit.pctOwn,
number: 100,
style: 0,
color: colors.default,
}),
priceLine({ unit: Unit.pctOwn, number: 50 }),
];
}
/**
* Create supply section for a single cohort (with relative metrics)
* @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(
cohort,
{ supplyRelative = [], pnlRelative = [] } = {},
) {
return [
...createSingleSupplySeriesBase(cohort),
...supplyRelative,
...pnlRelative,
...createSingleSupplyRelativeToOwnMetrics(cohort),
];
}
/**
* Create supply series for cohorts WITHOUT relative metrics
* @param {CohortWithoutRelative} cohort
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createSingleSupplySeriesWithoutRelative(cohort) {
return createSingleSupplySeriesBase(cohort);
}
/**
* Create supply total series for grouped cohorts
* @template {readonly CohortObject[]} T
* @param {T} list
* @param {Object} [options]
* @param {(cohort: T[number]) => AnyFetchedSeriesBlueprint[]} [options.relativeMetrics] - Generator for relative supply metrics
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createGroupedSupplyTotalSeries(list, { relativeMetrics } = {}) {
return list.flatMap((cohort) => [
...satsBtcUsd({
pattern: cohort.tree.supply.total,
name: cohort.name,
color: cohort.color,
}),
...(relativeMetrics ? relativeMetrics(cohort) : []),
]);
}
/**
* Create supply in profit series for grouped cohorts
* @template {readonly CohortObject[]} T
* @param {T} list
* @param {Object} [options]
* @param {(cohort: T[number]) => AnyFetchedSeriesBlueprint[]} [options.relativeMetrics] - Generator for relative supply metrics
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createGroupedSupplyInProfitSeries(
list,
{ relativeMetrics } = {},
) {
return list.flatMap((cohort) => [
...satsBtcUsd({
pattern: cohort.tree.unrealized.supplyInProfit,
name: cohort.name,
color: cohort.color,
}),
...(relativeMetrics ? relativeMetrics(cohort) : []),
]);
}
/**
* Create supply in loss series for grouped cohorts
* @template {readonly CohortObject[]} T
* @param {T} list
* @param {Object} [options]
* @param {(cohort: T[number]) => AnyFetchedSeriesBlueprint[]} [options.relativeMetrics] - Generator for relative supply metrics
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createGroupedSupplyInLossSeries(
list,
{ relativeMetrics } = {},
) {
return list.flatMap((cohort) => [
...satsBtcUsd({
pattern: cohort.tree.unrealized.supplyInLoss,
name: cohort.name,
color: cohort.color,
}),
...(relativeMetrics ? relativeMetrics(cohort) : []),
]);
}
/**
* Create supply section for grouped cohorts
* @template {readonly (CohortObject | CohortWithoutRelative)[]} T
* @param {T} list
* @param {(metric: string) => string} title
* @param {Object} [options]
* @param {(cohort: T[number]) => AnyFetchedSeriesBlueprint[]} [options.supplyRelativeMetrics] - Generator for supply relative metrics
* @param {(cohort: T[number]) => AnyFetchedSeriesBlueprint[]} [options.profitRelativeMetrics] - Generator for supply in profit relative metrics
* @param {(cohort: T[number]) => AnyFetchedSeriesBlueprint[]} [options.lossRelativeMetrics] - Generator for supply in loss relative metrics
* @returns {PartialOptionsGroup}
*/
export function createGroupedSupplySection(
list,
title,
{ supplyRelativeMetrics, profitRelativeMetrics, lossRelativeMetrics } = {},
) {
return {
name: "Supply",
tree: [
{
name: "Total",
title: title("Supply"),
bottom: createGroupedSupplyTotalSeries(list, {
relativeMetrics: supplyRelativeMetrics,
}),
},
{
name: "30d Change",
title: title("Supply 30d Change"),
bottom: list.flatMap(({ color, name, tree }) =>
satsBtcUsd({ pattern: tree.supply._30dChange, name, color }),
),
},
{
name: "In Profit",
title: title("Supply In Profit"),
bottom: createGroupedSupplyInProfitSeries(list, {
relativeMetrics: profitRelativeMetrics,
}),
},
{
name: "In Loss",
title: title("Supply In Loss"),
bottom: createGroupedSupplyInLossSeries(list, {
relativeMetrics: lossRelativeMetrics,
}),
},
],
};
}
// ============================================================================
// Circulating Supply Relative Metrics Generators
// ============================================================================
/**
* Create supply relative to circulating supply series for single cohort
* @param {CohortWithCirculatingSupplyRelative} cohort
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createSupplyRelativeToCirculatingSeries(cohort) {
return [
line({
metric: cohort.tree.relative.supplyRelToCirculatingSupply,
name: "Supply",
color: colors.default,
unit: Unit.pctSupply,
}),
];
}
/**
* Create supply in profit/loss relative to circulating supply series for single cohort
* @param {CohortWithCirculatingSupplyRelative} cohort
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createSupplyPnlRelativeToCirculatingSeries(cohort) {
return [
line({
metric: cohort.tree.relative.supplyInProfitRelToCirculatingSupply,
name: "In Profit",
color: colors.green,
unit: Unit.pctSupply,
}),
line({
metric: cohort.tree.relative.supplyInLossRelToCirculatingSupply,
name: "In Loss",
color: colors.red,
unit: Unit.pctSupply,
}),
];
}
/**
* Create supply relative to circulating supply metrics generator for grouped cohorts
* @param {CohortWithCirculatingSupplyRelative} cohort
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createGroupedSupplyRelativeMetrics(cohort) {
return [
line({
metric: cohort.tree.relative.supplyRelToCirculatingSupply,
name: cohort.name,
color: cohort.color,
unit: Unit.pctSupply,
}),
];
}
/**
* Create supply in profit relative to circulating supply metrics generator for grouped cohorts
* @param {CohortWithCirculatingSupplyRelative} cohort
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createGroupedSupplyInProfitRelativeMetrics(cohort) {
return [
line({
metric: cohort.tree.relative.supplyInProfitRelToCirculatingSupply,
name: cohort.name,
color: cohort.color,
unit: Unit.pctSupply,
}),
];
}
/**
* Create supply in loss relative to circulating supply metrics generator for grouped cohorts
* @param {CohortWithCirculatingSupplyRelative} cohort
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createGroupedSupplyInLossRelativeMetrics(cohort) {
return [
line({
metric: cohort.tree.relative.supplyInLossRelToCirculatingSupply,
name: cohort.name,
color: cohort.color,
unit: Unit.pctSupply,
}),
];
}
/**
* Grouped supply relative generators object for cohorts with circulating supply relative
* @type {{ supplyRelativeMetrics: typeof createGroupedSupplyRelativeMetrics, profitRelativeMetrics: typeof createGroupedSupplyInProfitRelativeMetrics, lossRelativeMetrics: typeof createGroupedSupplyInLossRelativeMetrics }}
*/
export const groupedSupplyRelativeGenerators = {
supplyRelativeMetrics: createGroupedSupplyRelativeMetrics,
profitRelativeMetrics: createGroupedSupplyInProfitRelativeMetrics,
lossRelativeMetrics: createGroupedSupplyInLossRelativeMetrics,
};
/**
* Create single cohort supply relative options for cohorts with circulating supply relative
* @param {CohortWithCirculatingSupplyRelative} cohort
* @returns {{ supplyRelative: AnyFetchedSeriesBlueprint[], pnlRelative: AnyFetchedSeriesBlueprint[] }}
*/
export function createSingleSupplyRelativeOptions(cohort) {
return {
supplyRelative: createSupplyRelativeToCirculatingSeries(cohort),
pnlRelative: createSupplyPnlRelativeToCirculatingSeries(cohort),
};
}
/**
* Create UTXO count series
* @param {readonly CohortObject[]} list
* @param {boolean} useGroupName
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createUtxoCountSeries(list, useGroupName) {
return list.flatMap(({ color, name, tree }) => [
line({
metric: tree.outputs.utxoCount,
name: useGroupName ? name : "Count",
color,
unit: Unit.count,
}),
]);
}
/**
* Create address count series (for address cohorts only)
* @param {readonly AddressCohortObject[]} list
* @param {boolean} useGroupName
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createAddressCountSeries(list, useGroupName) {
return list.flatMap(({ color, name, tree }) => [
line({
metric: tree.addrCount,
name: useGroupName ? name : "Count",
color: useGroupName ? color : colors.orange,
unit: Unit.count,
}),
]);
}
/**
* Create realized price series for grouped cohorts
* @param {readonly CohortObject[]} list
* @returns {FetchedPriceSeriesBlueprint[]}
*/
export function createRealizedPriceSeries(list) {
return list.map(({ color, name, tree }) =>
price({ metric: tree.realized.realizedPrice, name, color }),
);
}
/**
* Create realized price ratio series for grouped cohorts
* @param {readonly CohortObject[]} list
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createRealizedPriceRatioSeries(list) {
return list.map(({ name, tree }) =>
baseline({
metric: tree.realized.realizedPriceExtra.ratio,
name,
unit: Unit.ratio,
base: 1,
}),
);
}
/**
* Create realized capitalization series
* @param {readonly CohortObject[]} list
* @param {boolean} useGroupName
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createRealizedCapSeries(list, useGroupName) {
return list.flatMap(({ color, name, tree }) => [
line({
metric: tree.realized.realizedCap,
name: useGroupName ? name : "Capitalization",
color,
unit: Unit.usd,
}),
]);
}
/**
* Create cost basis percentile series (only for cohorts with CostBasisPattern2)
* Includes min (p0) and max (p100) with full rainbow coloring
* @param {Colors} colors
* @param {readonly CohortWithCostBasisPercentiles[]} list
* @param {boolean} useGroupName
* @returns {FetchedPriceSeriesBlueprint[]}
*/
export function createCostBasisPercentilesSeries(colors, list, useGroupName) {
return list.flatMap(({ name, tree }) => {
const cb = tree.costBasis;
const p = cb.percentiles;
const n = (/** @type {number} */ pct) =>
useGroupName ? `${name} p${pct}` : `p${pct}`;
return [
price({
metric: cb.max,
name: n(100),
color: colors.purple,
defaultActive: false,
}),
price({
metric: p.pct95,
name: n(95),
color: colors.fuchsia,
defaultActive: false,
}),
price({
metric: p.pct90,
name: n(90),
color: colors.pink,
defaultActive: false,
}),
price({
metric: p.pct85,
name: n(85),
color: colors.pink,
defaultActive: false,
}),
price({
metric: p.pct80,
name: n(80),
color: colors.rose,
defaultActive: false,
}),
price({
metric: p.pct75,
name: n(75),
color: colors.red,
defaultActive: false,
}),
price({
metric: p.pct70,
name: n(70),
color: colors.orange,
defaultActive: false,
}),
price({
metric: p.pct65,
name: n(65),
color: colors.amber,
defaultActive: false,
}),
price({
metric: p.pct60,
name: n(60),
color: colors.yellow,
defaultActive: false,
}),
price({
metric: p.pct55,
name: n(55),
color: colors.yellow,
defaultActive: false,
}),
price({ metric: p.pct50, name: n(50), color: colors.avocado }),
price({
metric: p.pct45,
name: n(45),
color: colors.lime,
defaultActive: false,
}),
price({
metric: p.pct40,
name: n(40),
color: colors.green,
defaultActive: false,
}),
price({
metric: p.pct35,
name: n(35),
color: colors.emerald,
defaultActive: false,
}),
price({
metric: p.pct30,
name: n(30),
color: colors.teal,
defaultActive: false,
}),
price({
metric: p.pct25,
name: n(25),
color: colors.teal,
defaultActive: false,
}),
price({
metric: p.pct20,
name: n(20),
color: colors.cyan,
defaultActive: false,
}),
price({
metric: p.pct15,
name: n(15),
color: colors.sky,
defaultActive: false,
}),
price({
metric: p.pct10,
name: n(10),
color: colors.blue,
defaultActive: false,
}),
price({
metric: p.pct05,
name: n(5),
color: colors.indigo,
defaultActive: false,
}),
price({
metric: cb.min,
name: n(0),
color: colors.violet,
defaultActive: false,
}),
];
});
}
/**
* 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
// ============================================================================
/**
* Create coins destroyed series (coinblocks, coindays, satblocks, satdays) for single cohort
* All metrics on one chart
* @param {CohortObject} cohort
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createSingleCoinsDestroyedSeries(cohort) {
const { tree, color } = cohort;
return [
line({
metric: tree.activity.coinblocksDestroyed.sum,
name: "Coinblocks",
color,
unit: Unit.coinblocks,
}),
line({
metric: tree.activity.coinblocksDestroyed.cumulative,
name: "Coinblocks Cumulative",
color,
unit: Unit.coinblocks,
defaultActive: false,
}),
line({
metric: tree.activity.coindaysDestroyed.sum,
name: "Coindays",
color,
unit: Unit.coindays,
}),
line({
metric: tree.activity.coindaysDestroyed.cumulative,
name: "Coindays Cumulative",
color,
unit: Unit.coindays,
defaultActive: false,
}),
];
}
/**
* Create coinblocks destroyed series for grouped cohorts (comparison)
* @param {readonly CohortObject[]} list
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createGroupedCoinblocksDestroyedSeries(list) {
return list.flatMap(({ color, name, tree }) => [
line({
metric: tree.activity.coinblocksDestroyed.sum,
name,
color,
unit: Unit.coinblocks,
}),
]);
}
/**
* Create coindays destroyed series for grouped cohorts (comparison)
* @param {readonly CohortObject[]} list
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createGroupedCoindaysDestroyedSeries(list) {
return list.flatMap(({ color, name, tree }) => [
line({
metric: tree.activity.coindaysDestroyed.sum,
name,
color,
unit: Unit.coindays,
}),
]);
}
/**
* Create sent series (sats, btc, usd) for single cohort - all on one chart
* @param {CohortObject} cohort
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createSingleSentSeries(cohort) {
const { tree, color } = cohort;
return [
line({
metric: tree.activity.sent.sats.sum,
name: "Sent",
color,
unit: Unit.sats,
}),
line({
metric: tree.activity.sent.sats.cumulative,
name: "Cumulative",
color,
unit: Unit.sats,
defaultActive: false,
}),
line({
metric: tree.activity.sent.bitcoin.sum,
name: "Sent",
color,
unit: Unit.btc,
}),
line({
metric: tree.activity.sent.bitcoin.cumulative,
name: "Cumulative",
color,
unit: Unit.btc,
defaultActive: false,
}),
line({
metric: tree.activity.sent.dollars.sum,
name: "Sent",
color,
unit: Unit.usd,
}),
line({
metric: tree.activity.sent.dollars.cumulative,
name: "Cumulative",
color,
unit: Unit.usd,
defaultActive: false,
}),
...satsBtcUsd({ pattern: tree.activity.sent14dEma, name: "14d EMA" }),
];
}
// ============================================================================
// Sell Side Risk Ratio Helpers
// ============================================================================
/**
* Create sell side risk ratio series for single cohort
* @param {Colors} colors
* @param {{ realized: AnyRealizedPattern }} tree
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createSingleSellSideRiskSeries(colors, tree) {
return [
dots({
metric: tree.realized.sellSideRiskRatio,
name: "Raw",
color: colors.orange,
unit: Unit.ratio,
}),
line({
metric: tree.realized.sellSideRiskRatio7dEma,
name: "7d EMA",
color: colors.red,
unit: Unit.ratio,
}),
line({
metric: tree.realized.sellSideRiskRatio30dEma,
name: "30d EMA",
color: colors.pink,
unit: Unit.ratio,
}),
];
}
/**
* Create sell side risk ratio series for grouped cohorts
* @param {readonly CohortObject[]} list
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createGroupedSellSideRiskSeries(list) {
return list.flatMap(({ color, name, tree }) => [
line({
metric: tree.realized.sellSideRiskRatio,
name,
color,
unit: Unit.ratio,
}),
]);
}
// ============================================================================
// Value Created & Destroyed Helpers
// ============================================================================
/**
* Create value created & destroyed series for single cohort
* @param {Colors} colors
* @param {{ realized: AnyRealizedPattern }} tree
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createSingleValueCreatedDestroyedSeries(colors, tree) {
return [
line({
metric: tree.realized.valueCreated,
name: "Created",
color: colors.emerald,
unit: Unit.usd,
}),
line({
metric: tree.realized.valueDestroyed,
name: "Destroyed",
color: colors.red,
unit: Unit.usd,
}),
];
}
/**
* 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
// ============================================================================
/**
* Create base SOPR series for single cohort (all cohorts have base SOPR)
* @param {Colors} colors
* @param {{ realized: AnyRealizedPattern }} tree
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createSingleSoprSeries(colors, tree) {
return [
baseline({
metric: tree.realized.sopr,
name: "SOPR",
unit: Unit.ratio,
base: 1,
}),
baseline({
metric: tree.realized.sopr7dEma,
name: "7d EMA",
color: [colors.lime, colors.rose],
unit: Unit.ratio,
defaultActive: false,
base: 1,
}),
baseline({
metric: tree.realized.sopr30dEma,
name: "30d EMA",
color: [colors.avocado, colors.pink],
unit: Unit.ratio,
defaultActive: false,
base: 1,
}),
];
}
// ============================================================================
// 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 {{ tree: { realized: RealizedWithExtras }, color: Color }} cohort
* @param {string} [cohortTitle] - Cohort title (e.g., "STH")
* @returns {PartialOptionsGroup}
*/
export function createInvestorPriceFolderFull(cohort, cohortTitle) {
const { tree, color } = cohort;
return createPriceFolder({
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,
});
}
// ============================================================================
// Peak Regret Helpers
// ============================================================================
/**
* Create realized peak regret series for single cohort
* @param {{ realized: AnyRealizedPattern }} tree
* @param {Color} color
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createSingleRealizedAthRegretSeries(tree, color) {
return [
line({
metric: tree.realized.peakRegret.sum,
name: "Peak Regret",
color,
unit: Unit.usd,
}),
line({
metric: tree.realized.peakRegret.cumulative,
name: "Cumulative",
color,
unit: Unit.usd,
defaultActive: false,
}),
baseline({
metric: tree.realized.peakRegretRelToRealizedCap,
name: "Rel. to Realized Cap",
color,
unit: Unit.pctRcap,
}),
];
}
/**
* 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.peakRegret.sum,
name,
color,
unit: Unit.usd,
}),
baseline({
metric: tree.realized.peakRegretRelToRealizedCap,
name,
color,
unit: Unit.pctRcap,
}),
]);
}
// ============================================================================
// 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,
}),
]);
}