Files
brk/website/scripts/options/distribution/profitability.js
2026-03-21 17:15:53 +01:00

1299 lines
36 KiB
JavaScript

/**
* Profitability section builders
*
* Capability tiers:
* - Full (All/STH/LTH): full unrealized with rel series, invested capital, sentiment;
* full realized with relToRcap, peakRegret, profitToLossRatio, grossPnl
* - Mid (AgeRange/MaxAge): unrealized profit/loss/netPnl/nupl (no rel, no invested, no sentiment);
* realized with netPnl + delta (no relToRcap, no peakRegret)
* - Basic (UtxoAmount, Empty, Address): nupl only unrealized;
* basic realized profit/loss (no netPnl, no relToRcap)
*/
import { Unit } from "../../utils/units.js";
import {
ROLLING_WINDOWS,
line,
dotted,
baseline,
percentRatio,
percentRatioBaseline,
sumsTreeBaseline,
rollingPercentRatioTree,
mapWindows,
} from "../series.js";
import { colors } from "../../utils/colors.js";
import { priceLine } from "../constants.js";
import {
mapCohortsWithAll,
flatMapCohortsWithAll,
groupedWindowsCumulativeUsd,
} from "../shared.js";
// ============================================================================
// Core Series Builders
// ============================================================================
// ============================================================================
// Unrealized P&L Builders
// ============================================================================
/**
* Overview chart: net + profit + loss inverted (active), loss raw (hidden)
* @param {{ usd: AnySeriesPattern }} profit
* @param {{ usd: AnySeriesPattern, negative: AnySeriesPattern }} loss
* @param {AnySeriesPattern} netPnlUsd
* @param {(name: string) => string} title
* @returns {PartialChartOption}
*/
function unrealizedOverview(profit, loss, netPnlUsd, title) {
return {
name: "Overview",
title: title("Unrealized P&L"),
bottom: [
baseline({ series: netPnlUsd, name: "Net", unit: Unit.usd }),
dotted({
series: profit.usd,
name: "Profit",
color: colors.profit,
unit: Unit.usd,
}),
dotted({
series: loss.negative,
name: "Neg. Loss",
color: colors.loss,
unit: Unit.usd,
}),
dotted({
series: loss.usd,
name: "Loss",
color: colors.loss,
unit: Unit.usd,
defaultActive: false,
}),
priceLine({ unit: Unit.usd }),
],
};
}
/**
* Relative P&L chart: profit + loss as % of some denominator
* @param {{ percent: AnySeriesPattern, ratio: AnySeriesPattern }} profit
* @param {{ percent: AnySeriesPattern, ratio: AnySeriesPattern }} loss
* @param {string} name
* @param {(name: string) => string} title
* @returns {PartialChartOption}
*/
function relPnlChart(profit, loss, name, title) {
return {
name,
title: title(`Unrealized P&L (${name})`),
bottom: [
...percentRatio({
pattern: profit,
name: "Profit",
color: colors.profit,
}),
...percentRatio({ pattern: loss, name: "Loss", color: colors.loss }),
],
};
}
/** @param {{ percent: AnySeriesPattern, ratio: AnySeriesPattern }} net @param {{ percent: AnySeriesPattern, ratio: AnySeriesPattern }} profit @param {{ percent: AnySeriesPattern, ratio: AnySeriesPattern }} loss @param {string} name @param {(name: string) => string} title */
function relPnlChartWithNet(net, profit, loss, name, title) {
return {
name,
title: title(`Unrealized P&L (${name})`),
bottom: [
...percentRatioBaseline({ pattern: net, name: "Net" }),
...percentRatio({ pattern: profit, name: "Profit", color: colors.profit }),
...percentRatio({ pattern: loss, name: "Loss", color: colors.loss }),
],
};
}
/**
* Core unrealized items: Overview + Net + NUPL + Profit + Loss
* @param {{ profit: { usd: AnySeriesPattern }, loss: { usd: AnySeriesPattern, negative: AnySeriesPattern }, netPnl: { usd: AnySeriesPattern }, nupl: NuplPattern }} u
* @param {(name: string) => string} title
* @returns {PartialOptionsTree}
*/
function unrealizedCore(u, title) {
return [
unrealizedOverview(u.profit, u.loss, u.netPnl.usd, title),
{
name: "Net",
title: title("Net Unrealized P&L"),
bottom: [baseline({ series: u.netPnl.usd, name: "Net", unit: Unit.usd })],
},
{ name: "NUPL", title: title("NUPL"), bottom: nuplSeries(u.nupl) },
{
name: "Profit",
title: title("Unrealized Profit"),
bottom: [
line({
series: u.profit.usd,
name: "Profit",
color: colors.profit,
unit: Unit.usd,
}),
],
},
{
name: "Loss",
title: title("Unrealized Loss"),
bottom: [
line({
series: u.loss.usd,
name: "Loss",
color: colors.loss,
unit: Unit.usd,
}),
],
},
];
}
/**
* Core unrealized items + Gross
* @param {{ profit: { usd: AnySeriesPattern }, loss: { usd: AnySeriesPattern, negative: AnySeriesPattern }, netPnl: { usd: AnySeriesPattern }, grossPnl: { usd: AnySeriesPattern }, nupl: NuplPattern }} u
* @param {(name: string) => string} title
* @returns {PartialOptionsTree}
*/
function unrealizedCoreWithGross(u, title) {
return [
...unrealizedCore(u, title),
{
name: "Gross",
title: title("Gross Unrealized P&L"),
bottom: [
line({
series: u.grossPnl.usd,
name: "Gross",
color: colors.gross,
unit: Unit.usd,
}),
],
},
];
}
/**
* % of Own P&L chart
* @param {AllRelativePattern | FullRelativePattern} u
* @param {(name: string) => string} title
* @returns {PartialChartOption}
*/
function ownPnlChart(u, title) {
return {
name: "% of Own P&L",
title: title("Unrealized P&L (% of Own P&L)"),
bottom: [
...percentRatioBaseline({ pattern: u.netPnl.toOwnGrossPnl, name: "Net" }),
...percentRatio({
pattern: u.profit.toOwnGrossPnl,
name: "Profit",
color: colors.profit,
defaultActive: false,
}),
...percentRatio({
pattern: u.loss.toOwnGrossPnl,
name: "Loss",
color: colors.loss,
defaultActive: false,
}),
],
};
}
/** @param {AllRelativePattern} u @param {(name: string) => string} title */
function unrealizedTreeAll(u, title) {
return [
...unrealizedCoreWithGross(u, title),
ownPnlChart(u, title),
relPnlChart(u.profit.toMcap, u.loss.toMcap, "% of Market Cap", title),
];
}
/** @param {FullRelativePattern} u @param {(name: string) => string} title */
function unrealizedTreeFull(u, title) {
return [
...unrealizedCoreWithGross(u, title),
ownPnlChart(u, title),
relPnlChart(u.profit.toMcap, u.loss.toMcap, "% of Market Cap", title),
relPnlChartWithNet(u.netPnl.toOwnMcap, u.profit.toOwnMcap, u.loss.toOwnMcap, "% of Own Market Cap", title),
];
}
/** @param {FullRelativePattern} u @param {(name: string) => string} title */
function unrealizedTreeLongTerm(u, title) {
return [
...unrealizedCoreWithGross(u, title),
ownPnlChart(u, title),
{
name: "% of Market Cap",
title: title("Unrealized Loss (% of Market Cap)"),
bottom: percentRatio({
pattern: u.loss.toMcap,
name: "Loss",
color: colors.loss,
}),
},
relPnlChartWithNet(u.netPnl.toOwnMcap, u.profit.toOwnMcap, u.loss.toOwnMcap, "% of Own Market Cap", title),
];
}
// ============================================================================
// Invested Capital, Sentiment, NUPL
// ============================================================================
/**
* Sentiment (Full unrealized only)
* @param {FullRelativePattern | AllRelativePattern} u
* @returns {AnyFetchedSeriesBlueprint[]}
*/
function sentimentSeries(u) {
return [
baseline({
series: u.sentiment.net.usd,
name: "Net Sentiment",
unit: Unit.usd,
}),
line({
series: u.sentiment.greedIndex.usd,
name: "Greed Index",
color: colors.profit,
unit: Unit.usd,
defaultActive: false,
}),
line({
series: u.sentiment.painIndex.usd,
name: "Pain Index",
color: colors.loss,
unit: Unit.usd,
defaultActive: false,
}),
];
}
/**
* NUPL series
* @param {NuplPattern} nupl
* @returns {AnyFetchedSeriesBlueprint[]}
*/
function nuplSeries(nupl) {
return [baseline({ series: nupl.ratio, name: "NUPL", unit: Unit.ratio })];
}
// ============================================================================
// Realized P&L Helpers
// ============================================================================
/**
* Flat metric folder: Compare + windows + Cumulative + optional % of Realized Cap
* @param {Object} args
* @param {{ sum: Record<string, { usd: AnySeriesPattern }>, cumulative: { usd: AnySeriesPattern } }} args.pattern
* @param {string} args.metricTitle
* @param {Color} args.color
* @param {(name: string) => string} args.title
* @returns {PartialOptionsTree}
*/
function realizedMetricFolder({ pattern, metricTitle, color, title }) {
return [
{
name: "Compare",
title: title(`Realized ${metricTitle}`),
bottom: ROLLING_WINDOWS.map((w) =>
line({
series: pattern.sum[w.key].usd,
name: w.name,
color: w.color,
unit: Unit.usd,
}),
),
},
...ROLLING_WINDOWS.map((w) => ({
name: w.name,
title: title(`Realized ${metricTitle} (${w.title})`),
bottom: [
line({
series: pattern.sum[w.key].usd,
name: metricTitle,
color,
unit: Unit.usd,
}),
],
})),
{
name: "Cumulative",
title: title(`Cumulative Realized ${metricTitle}`),
bottom: [
line({
series: pattern.cumulative.usd,
name: metricTitle,
color,
unit: Unit.usd,
}),
],
},
];
}
/**
* Net P&L folder: Compare + windows + Cumulative + optional % of Rcap + Change/
* @param {Object} args
* @param {NetPnlFullPattern | NetPnlBasicPattern} args.netPnl
* @param {(name: string) => string} args.title
* @param {PartialOptionsTree} [args.extraChange] - Additional change items (% of Mcap, % of Rcap)
* @returns {PartialOptionsGroup}
*/
function realizedNetFolder({ netPnl, title, extraChange = [] }) {
return {
name: "Net",
tree: [
{
name: "Compare",
title: title("Net Realized P&L"),
bottom: ROLLING_WINDOWS.map((w) =>
baseline({
series: netPnl.sum[w.key].usd,
name: w.name,
color: w.color,
unit: Unit.usd,
}),
),
},
...ROLLING_WINDOWS.map((w) => ({
name: w.name,
title: title(`Net Realized P&L (${w.title})`),
bottom: [
baseline({
series: netPnl.sum[w.key].usd,
name: "Net",
unit: Unit.usd,
}),
],
})),
{
name: "Cumulative",
title: title("Cumulative Net Realized P&L"),
bottom: [
baseline({
series: netPnl.cumulative.usd,
name: "Net",
unit: Unit.usd,
}),
],
},
{
...sumsTreeBaseline({
windows: mapWindows(netPnl.delta.absolute, (c) => c.usd),
title: title("Net Realized P&L Change"),
unit: Unit.usd,
}),
name: "Change",
},
{
name: "Growth Rate",
tree: [
...rollingPercentRatioTree({
windows: netPnl.delta.rate,
title: title("Net Realized P&L Growth Rate"),
}).tree,
...extraChange,
],
},
],
};
}
/**
* Realized overview folder: one chart per window showing net + profit (dotted) + neg. loss (dotted) + loss (hidden) + gross (hidden)
* @param {Object} args
* @param {{ sum: Record<string, { usd: AnySeriesPattern }> }} args.profit
* @param {{ sum: Record<string, { usd: AnySeriesPattern }>, negative: { sum: Record<string, AnySeriesPattern> } }} args.loss
* @param {{ sum: Record<string, { usd: AnySeriesPattern }> }} args.netPnl
* @param {{ sum: Record<string, { usd: AnySeriesPattern }> }} [args.grossPnl]
* @param {{ sum: Record<string, { usd: AnySeriesPattern }> }} [args.peakRegret]
* @param {(name: string) => string} args.title
* @returns {PartialOptionsGroup}
*/
function realizedOverviewFolder({
profit,
loss,
netPnl,
grossPnl,
peakRegret,
title,
}) {
return {
name: "Overview",
tree: ROLLING_WINDOWS.map((w) => ({
name: w.name,
title: title(`Realized P&L (${w.title})`),
bottom: [
baseline({
series: netPnl.sum[w.key].usd,
name: "Net",
unit: Unit.usd,
}),
dotted({
series: profit.sum[w.key].usd,
name: "Profit",
color: colors.profit,
unit: Unit.usd,
}),
dotted({
series: loss.negative.sum[w.key],
name: "Neg. Loss",
color: colors.loss,
unit: Unit.usd,
}),
dotted({
series: loss.sum[w.key].usd,
name: "Loss",
color: colors.loss,
unit: Unit.usd,
defaultActive: false,
}),
...(grossPnl
? [
dotted({
series: grossPnl.sum[w.key].usd,
name: "Gross",
color: colors.gross,
unit: Unit.usd,
defaultActive: false,
}),
]
: []),
...(peakRegret
? [
dotted({
series: peakRegret.sum[w.key].usd,
name: "Peak Regret",
color: colors.regret,
unit: Unit.usd,
defaultActive: false,
}),
]
: []),
priceLine({ unit: Unit.usd }),
],
})),
};
}
// ============================================================================
// Realized Subfolder Builders
// ============================================================================
/**
* Full realized subfolder (All/STH/LTH)
* @param {RealizedPattern | LthRealizedPattern} r
* @param {(name: string) => string} title
* @returns {PartialOptionsGroup}
*/
function realizedSubfolderFull(r, title) {
return {
name: "Realized",
tree: [
realizedOverviewFolder({
profit: r.profit,
loss: r.loss,
netPnl: r.netPnl,
grossPnl: r.grossPnl,
peakRegret: r.peakRegret,
title,
}),
realizedNetFolder({
netPnl: r.netPnl,
title,
extraChange: [
{
name: "% of Market Cap",
title: title("Net Realized P&L Change (% of Market Cap)"),
bottom: percentRatioBaseline({
pattern: r.netPnl.change1m.toMcap,
name: "30d Change",
}),
},
{
name: "% of Realized Cap",
title: title("Net Realized P&L Change (% of Realized Cap)"),
bottom: percentRatioBaseline({
pattern: r.netPnl.change1m.toRcap,
name: "30d Change",
}),
},
],
}),
{
name: "Profit",
tree: realizedMetricFolder({
pattern: r.profit,
metricTitle: "Profit",
color: colors.profit,
title,
}),
},
{
name: "Loss",
tree: realizedMetricFolder({
pattern: r.loss,
metricTitle: "Loss",
color: colors.loss,
title,
}),
},
{
name: "Gross",
tree: realizedMetricFolder({
pattern: r.grossPnl,
metricTitle: "Gross P&L",
color: colors.gross,
title,
}),
},
{
name: "P/L Ratio",
tree: [
{
name: "Compare",
title: title("Realized P/L Ratio"),
bottom: ROLLING_WINDOWS.map((w) =>
baseline({
series: r.profitToLossRatio[w.key],
name: w.name,
color: w.color,
unit: Unit.ratio,
base: 1,
}),
),
},
...ROLLING_WINDOWS.map((w) => ({
name: w.name,
title: title(`Realized P/L Ratio (${w.title})`),
bottom: [
baseline({
series: r.profitToLossRatio[w.key],
name: "P/L Ratio",
unit: Unit.ratio,
base: 1,
}),
],
})),
],
},
{
name: "Peak Regret",
tree: [
{
name: "Compare",
title: title("Peak Regret"),
bottom: ROLLING_WINDOWS.map((w) =>
line({
series: r.peakRegret.sum[w.key].usd,
name: w.name,
color: w.color,
unit: Unit.usd,
}),
),
},
...ROLLING_WINDOWS.map((w) => ({
name: w.name,
title: title(`Peak Regret (${w.title})`),
bottom: [
line({
series: r.peakRegret.sum[w.key].usd,
name: "Peak Regret",
unit: Unit.usd,
}),
],
})),
{
name: "Cumulative",
title: title("Cumulative Peak Regret"),
bottom: [
line({
series: r.peakRegret.cumulative.usd,
name: "Peak Regret",
unit: Unit.usd,
}),
],
},
],
},
],
};
}
/**
* Mid realized subfolder (AgeRange/MaxAge — has netPnl + delta, no relToRcap/peakRegret)
* @param {MidRealizedPattern} r
* @param {(name: string) => string} title
* @returns {PartialOptionsGroup}
*/
function realizedSubfolderMid(r, title) {
return {
name: "Realized",
tree: [
realizedOverviewFolder({
profit: r.profit,
loss: r.loss,
netPnl: r.netPnl,
title,
}),
realizedNetFolder({ netPnl: r.netPnl, title }),
{
name: "Profit",
tree: realizedMetricFolder({
pattern: r.profit,
metricTitle: "Profit",
color: colors.profit,
title,
}),
},
{
name: "Loss",
tree: realizedMetricFolder({
pattern: r.loss,
metricTitle: "Loss",
color: colors.loss,
title,
}),
},
],
};
}
/**
* Basic realized subfolder (no netPnl, no relToRcap)
* @param {BasicRealizedPattern} r
* @param {(name: string) => string} title
* @returns {PartialOptionsGroup}
*/
function realizedSubfolderBasic(r, title) {
return {
name: "Realized",
tree: [
{
name: "Profit",
tree: realizedMetricFolder({
pattern: r.profit,
metricTitle: "Profit",
color: colors.profit,
title,
}),
},
{
name: "Loss",
tree: realizedMetricFolder({
pattern: r.loss,
metricTitle: "Loss",
color: colors.loss,
title,
}),
},
],
};
}
// ============================================================================
// Single Cohort Section Builders
// ============================================================================
/**
* Basic profitability section (NUPL only unrealized, basic realized)
* @param {{ cohort: UtxoCohortObject, title: (name: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createProfitabilitySection({ cohort, title }) {
return {
name: "Profitability",
tree: [
{ name: "Unrealized", tree: [{ name: "NUPL", title: title("NUPL"), bottom: nuplSeries(cohort.tree.unrealized.nupl) }] },
realizedSubfolderBasic(cohort.tree.realized, title),
],
};
}
/**
* Profitability section with unrealized P&L + NUPL (no netPnl, no rel)
* For: CohortWithoutRelative (p2ms, unknown, empty)
* @param {{ cohort: CohortWithoutRelative, title: (name: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createProfitabilitySectionWithProfitLoss({ cohort, title }) {
const u = cohort.tree.unrealized;
return {
name: "Profitability",
tree: [
{
name: "Unrealized",
tree: [
{
name: "Overview",
title: title("Unrealized P&L"),
bottom: [
line({ series: u.profit.usd, name: "Profit", color: colors.profit, unit: Unit.usd }),
line({ series: u.loss.negative, name: "Neg. Loss", color: colors.loss, unit: Unit.usd }),
line({ series: u.loss.usd, name: "Loss", color: colors.loss, unit: Unit.usd, defaultActive: false }),
priceLine({ unit: Unit.usd }),
],
},
{ name: "NUPL", title: title("NUPL"), bottom: nuplSeries(u.nupl) },
{
name: "Profit",
title: title("Unrealized Profit"),
bottom: [
line({
series: u.profit.usd,
name: "Profit",
color: colors.profit,
unit: Unit.usd,
}),
],
},
{
name: "Loss",
title: title("Unrealized Loss"),
bottom: [
line({
series: u.loss.usd,
name: "Loss",
color: colors.loss,
unit: Unit.usd,
}),
],
},
],
},
realizedSubfolderBasic(cohort.tree.realized, title),
],
};
}
/**
* Section for All cohort
* @param {{ cohort: CohortAll, title: (name: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createProfitabilitySectionAll({ cohort, title }) {
const u = cohort.tree.unrealized;
const r = cohort.tree.realized;
return {
name: "Profitability",
tree: [
{ name: "Unrealized", tree: unrealizedTreeAll(u, title) },
realizedSubfolderFull(r, title),
{
name: "Sentiment",
title: title("Market Sentiment"),
bottom: sentimentSeries(u),
},
],
};
}
/**
* Section for Full cohorts (STH)
* @param {{ cohort: CohortFull, title: (name: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createProfitabilitySectionFull({ cohort, title }) {
const u = cohort.tree.unrealized;
const r = cohort.tree.realized;
return {
name: "Profitability",
tree: [
{ name: "Unrealized", tree: unrealizedTreeFull(u, title) },
realizedSubfolderFull(r, title),
{
name: "Sentiment",
title: title("Market Sentiment"),
bottom: sentimentSeries(u),
},
],
};
}
/**
* Section for LongTerm cohort
* @param {{ cohort: CohortLongTerm, title: (name: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createProfitabilitySectionLongTerm({ cohort, title }) {
const u = cohort.tree.unrealized;
const r = cohort.tree.realized;
return {
name: "Profitability",
tree: [
{ name: "Unrealized", tree: unrealizedTreeLongTerm(u, title) },
realizedSubfolderFull(r, title),
{
name: "Sentiment",
title: title("Market Sentiment"),
bottom: sentimentSeries(u),
},
],
};
}
/**
* Section for AgeRange cohorts (mid-tier: has unrealized profit/loss/netPnl, mid realized)
* @param {{ cohort: CohortAgeRange, title: (name: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createProfitabilitySectionWithInvestedCapitalPct({
cohort,
title,
}) {
const u = cohort.tree.unrealized;
const r = cohort.tree.realized;
return {
name: "Profitability",
tree: [
{ name: "Unrealized", tree: unrealizedCore(u, title) },
realizedSubfolderMid(r, title),
],
};
}
// ============================================================================
// Grouped Cohort Helpers
// ============================================================================
/**
* Grouped realized subfolder (basic)
* @template {{ name: string, color: Color, tree: { realized: { profit: RealizedProfitLossPattern, loss: RealizedProfitLossPattern } } }} T
* @template {{ name: string, color: Color, tree: { realized: { profit: RealizedProfitLossPattern, loss: RealizedProfitLossPattern } } }} A
* @param {readonly T[]} list
* @param {A} all
* @param {(name: string) => string} title
* @returns {PartialOptionsGroup}
*/
/**
* Grouped realized profit + loss items
* @param {readonly UtxoCohortObject[]} list
* @param {CohortAll} all
* @param {(name: string) => string} title
* @returns {PartialOptionsTree}
*/
function groupedRealizedProfitLossItems(list, all, title) {
return [
{ name: "Profit", tree: groupedWindowsCumulativeUsd({ list, all, title, metricTitle: "Realized Profit", getMetric: (c) => c.tree.realized.profit }) },
{ name: "Loss", tree: groupedWindowsCumulativeUsd({ list, all, title, metricTitle: "Realized Loss", getMetric: (c) => c.tree.realized.loss }) },
];
}
/**
* Grouped realized net item
* @param {readonly (CohortAgeRange | CohortWithAdjusted | CohortAll | CohortFull | CohortLongTerm)[]} list
* @param {CohortAll} all
* @param {(name: string) => string} title
* @returns {PartialOptionsGroup}
*/
function groupedRealizedNetItem(list, all, title) {
return { name: "Net", tree: groupedWindowsCumulativeUsd({ list, all, title, metricTitle: "Net Realized P&L", getMetric: (c) => c.tree.realized.netPnl, seriesFn: baseline }) };
}
/**
* @param {readonly UtxoCohortObject[]} list
* @param {CohortAll} all
* @param {(name: string) => string} title
* @returns {PartialOptionsGroup}
*/
function groupedRealizedSubfolder(list, all, title) {
return { name: "Realized", tree: groupedRealizedProfitLossItems(list, all, title) };
}
/**
* @param {readonly (CohortAgeRange | CohortWithAdjusted)[]} list
* @param {CohortAll} all
* @param {(name: string) => string} title
* @returns {PartialOptionsGroup}
*/
function groupedRealizedSubfolderMid(list, all, title) {
return { name: "Realized", tree: [groupedRealizedNetItem(list, all, title), ...groupedRealizedProfitLossItems(list, all, title)] };
}
/**
* Grouped net realized P&L delta (Absolute + Rate with all rolling windows)
* @param {readonly (CohortAll | CohortFull | CohortLongTerm)[]} list
* @param {CohortAll} all
* @param {(name: string) => string} title
* @returns {PartialOptionsTree}
*/
function groupedRealizedNetPnlDeltaItems(list, all, title) {
return [
{
name: "Change",
tree: ROLLING_WINDOWS.map((w) => ({
name: w.name,
title: title(`Net Realized P&L Change (${w.title})`),
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
baseline({
series: tree.realized.netPnl.delta.absolute[w.key].usd,
name,
color,
unit: Unit.usd,
}),
),
})),
},
{
name: "Growth Rate",
tree: ROLLING_WINDOWS.map((w) => ({
name: w.name,
title: title(`Net Realized P&L Growth Rate (${w.title})`),
bottom: flatMapCohortsWithAll(list, all, ({ name, color, tree }) =>
percentRatioBaseline({
pattern: tree.realized.netPnl.delta.rate[w.key],
name,
color,
}),
),
})),
},
];
}
/**
* Grouped realized subfolder for full cohorts
* @param {readonly (CohortAll | CohortFull | CohortLongTerm)[]} list
* @param {CohortAll} all
* @param {(name: string) => string} title
* @returns {PartialOptionsGroup}
*/
function groupedRealizedSubfolderFull(list, all, title) {
return {
name: "Realized",
tree: [
{
name: "Net",
tree: [
...groupedWindowsCumulativeUsd({
list,
all,
title,
metricTitle: "Net Realized P&L",
getMetric: (c) => c.tree.realized.netPnl,
seriesFn: baseline,
}),
...groupedRealizedNetPnlDeltaItems(list, all, title),
],
},
...groupedRealizedProfitLossItems(list, all, title),
{
name: "Gross",
tree: groupedWindowsCumulativeUsd({
list,
all,
title,
metricTitle: "Gross Realized P&L",
getMetric: (c) => c.tree.realized.grossPnl,
}),
},
{
name: "P/L Ratio",
tree: ROLLING_WINDOWS.map((w) => ({
name: w.name,
title: title(`Realized P/L Ratio (${w.title})`),
bottom: mapCohortsWithAll(list, all, (c) =>
baseline({
series: c.tree.realized.profitToLossRatio[w.key],
name: c.name,
color: c.color,
unit: Unit.ratio,
base: 1,
}),
),
})),
},
{
name: "Peak Regret",
tree: groupedWindowsCumulativeUsd({
list,
all,
title,
metricTitle: "Peak Regret",
getMetric: (c) => c.tree.realized.peakRegret,
}),
},
],
};
}
/**
* Grouped NUPL chart
* @template {{ name: string, color: Color, tree: { unrealized: { nupl: NuplPattern } } }} T
* @template {{ name: string, color: Color, tree: { unrealized: { nupl: NuplPattern } } }} A
* @param {readonly T[]} list
* @param {A} all
* @param {(name: string) => string} title
* @returns {PartialOptionsTree}
*/
function groupedNuplCharts(list, all, title) {
return [
{
name: "NUPL",
title: title("NUPL"),
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
baseline({
series: tree.unrealized.nupl.ratio,
name,
color,
unit: Unit.ratio,
}),
),
},
];
}
/**
* Grouped unrealized: Net → NUPL → Profit → Loss (no relative)
* @param {readonly (CohortAgeRange | CohortWithAdjusted)[]} list
* @param {CohortAll} all
* @param {(name: string) => string} title
* @returns {PartialOptionsTree}
*/
function groupedUnrealizedMid(list, all, title) {
return [
{
name: "Net",
title: title("Net Unrealized P&L"),
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
baseline({
series: tree.unrealized.netPnl.usd,
name,
color,
unit: Unit.usd,
}),
),
},
...groupedNuplCharts(list, all, title),
{
name: "Profit",
title: title("Unrealized Profit"),
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
line({
series: tree.unrealized.profit.usd,
name,
color,
unit: Unit.usd,
}),
),
},
{
name: "Loss",
title: title("Unrealized Loss"),
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
line({ series: tree.unrealized.loss.usd, name, color, unit: Unit.usd }),
),
},
];
}
/**
* Grouped unrealized: Net → NUPL → Profit → Loss → Relative(Market Cap)
* @param {readonly (CohortFull | CohortLongTerm)[]} list
* @param {CohortAll} all
* @param {(name: string) => string} title
* @returns {PartialOptionsTree}
*/
function groupedUnrealizedWithMarketCap(list, all, title) {
return [
...groupedUnrealizedMid(list, all, title),
{
name: "% of Market Cap",
tree: [
{
name: "Profit",
title: title("Unrealized Profit (% of Market Cap)"),
bottom: flatMapCohortsWithAll(list, all, ({ name, color, tree }) =>
percentRatio({
pattern: tree.unrealized.profit.toMcap,
name,
color,
}),
),
},
{
name: "Loss",
title: title("Unrealized Loss (% of Market Cap)"),
bottom: flatMapCohortsWithAll(list, all, ({ name, color, tree }) =>
percentRatio({ pattern: tree.unrealized.loss.toMcap, name, color }),
),
},
],
},
];
}
/**
* Grouped sentiment (full unrealized only)
* @param {readonly (CohortAll | CohortFull | CohortLongTerm)[]} list
* @param {CohortAll} all
* @param {(name: string) => string} title
* @returns {PartialOptionsGroup}
*/
function groupedSentiment(list, all, title) {
return {
name: "Sentiment",
tree: [
{
name: "Net",
title: title("Net Sentiment"),
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
baseline({
series: tree.unrealized.sentiment.net.usd,
name,
color,
unit: Unit.usd,
}),
),
},
{
name: "Greed",
title: title("Greed Index"),
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
line({
series: tree.unrealized.sentiment.greedIndex.usd,
name,
color,
unit: Unit.usd,
}),
),
},
{
name: "Pain",
title: title("Pain Index"),
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
line({
series: tree.unrealized.sentiment.painIndex.usd,
name,
color,
unit: Unit.usd,
}),
),
},
],
};
}
// ============================================================================
// Grouped Section Builders
// ============================================================================
/**
* Grouped profitability section (basic — NUPL only)
* @param {{ list: readonly (UtxoCohortObject | CohortWithoutRelative)[], all: CohortAll, title: (name: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createGroupedProfitabilitySection({ list, all, title }) {
return {
name: "Profitability",
tree: [
{ name: "Unrealized", tree: groupedNuplCharts(list, all, title) },
groupedRealizedSubfolder(list, all, title),
],
};
}
/**
* Grouped profitability with unrealized profit/loss + NUPL
* For: CohortWithoutRelative (p2ms, unknown, empty)
* @param {{ list: readonly CohortWithoutRelative[], all: CohortAll, title: (name: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createGroupedProfitabilitySectionWithProfitLoss({
list,
all,
title,
}) {
return {
name: "Profitability",
tree: [
{
name: "Unrealized",
tree: [
...groupedNuplCharts(list, all, title),
{
name: "Profit",
title: title("Unrealized Profit"),
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
line({
series: tree.unrealized.profit.usd,
name,
color,
unit: Unit.usd,
}),
),
},
{
name: "Loss",
title: title("Unrealized Loss"),
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
line({
series: tree.unrealized.loss.usd,
name,
color,
unit: Unit.usd,
}),
),
},
],
},
groupedRealizedSubfolder(list, all, title),
],
};
}
/**
* Grouped section for ageRange/maxAge cohorts
* @param {{ list: readonly (CohortAgeRange | CohortWithAdjusted)[], all: CohortAll, title: (name: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createGroupedProfitabilitySectionWithInvestedCapitalPct({
list,
all,
title,
}) {
return {
name: "Profitability",
tree: [
{ name: "Unrealized", tree: groupedUnrealizedMid(list, all, title) },
groupedRealizedSubfolderMid(list, all, title),
],
};
}
/**
* Grouped section with NUPL + relToMcap
* @param {{ list: readonly (CohortFull | CohortLongTerm)[], all: CohortAll, title: (name: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createGroupedProfitabilitySectionWithNupl({
list,
all,
title,
}) {
return {
name: "Profitability",
tree: [
{
name: "Unrealized",
tree: groupedUnrealizedWithMarketCap(list, all, title),
},
groupedRealizedSubfolderFull(list, all, title),
groupedSentiment(list, all, title),
],
};
}