Files
brk/website/scripts/options/distribution/profitability.js
2026-03-14 12:36:37 +01:00

1198 lines
46 KiB
JavaScript

/**
* Profitability section builders
*
* Capability tiers:
* - Full (All/STH/LTH): full unrealized with rel metrics, 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 { line, baseline, dots, dotsBaseline } from "../series.js";
import { colors } from "../../utils/colors.js";
import { priceLine, priceLines } from "../constants.js";
import {
mapCohorts,
mapCohortsWithAll,
} from "../shared.js";
// ============================================================================
// Core Series Builders
// ============================================================================
/**
* @typedef {Object} PnlSeriesConfig
* @property {AnyMetricPattern} profit
* @property {AnyMetricPattern} loss
* @property {AnyMetricPattern} negLoss
* @property {AnyMetricPattern} [gross]
*/
/**
* @param {PnlSeriesConfig} m
* @param {Unit} unit
* @returns {AnyFetchedSeriesBlueprint[]}
*/
function pnlLines(m, unit) {
const series = [
line({ metric: m.profit, name: "Profit", color: colors.profit, unit }),
line({ metric: m.loss, name: "Loss", color: colors.loss, unit }),
];
if (m.gross) {
series.push(line({ metric: m.gross, name: "Total", color: colors.default, unit }));
}
series.push(line({ metric: m.negLoss, name: "Negative Loss", color: colors.loss, unit, defaultActive: false }));
return series;
}
/**
* @param {AnyMetricPattern} metric
* @param {Unit} unit
* @returns {AnyFetchedSeriesBlueprint}
*/
function netBaseline(metric, unit) {
return baseline({ metric, name: "Net P&L", unit });
}
// ============================================================================
// Unrealized P&L Builders
// ============================================================================
/**
* Unrealized P&L (USD + relToMcap) for All cohort
* @param {Brk.MetricsTree_Cohorts_Utxo_All_Unrealized} u
* @returns {AnyFetchedSeriesBlueprint[]}
*/
function unrealizedAll(u) {
return [
...pnlLines(
{ profit: u.profit.base.usd, loss: u.loss.base.usd, negLoss: u.loss.negative, gross: u.grossPnl.usd },
Unit.usd,
),
priceLine({ unit: Unit.usd, defaultActive: false }),
line({ metric: u.profit.relToMcap.ratio, name: "Profit", color: colors.profit, unit: Unit.pctMcap }),
line({ metric: u.loss.relToMcap.ratio, name: "Loss", color: colors.loss, unit: Unit.pctMcap }),
priceLine({ unit: Unit.pctMcap, defaultActive: false }),
line({ metric: u.profit.relToOwnGross.ratio, name: "Profit", color: colors.profit, unit: Unit.pctOwnPnl }),
line({ metric: u.loss.relToOwnGross.ratio, name: "Loss", color: colors.loss, unit: Unit.pctOwnPnl }),
...priceLines({ numbers: [100, 50, 0], unit: Unit.pctOwnPnl }),
];
}
/**
* Unrealized P&L (USD + relToMcap + relToOwnMcap + relToOwnGross) for Full cohorts (STH/LTH)
* @param {Brk.GrossInvestedLossNetNuplProfitSentimentPattern2} u
* @returns {AnyFetchedSeriesBlueprint[]}
*/
function unrealizedFull(u) {
return [
...pnlLines(
{ profit: u.profit.base.usd, loss: u.loss.base.usd, negLoss: u.loss.negative, gross: u.grossPnl.usd },
Unit.usd,
),
priceLine({ unit: Unit.usd, defaultActive: false }),
line({ metric: u.profit.relToMcap.ratio, name: "Profit", color: colors.profit, unit: Unit.pctMcap }),
line({ metric: u.loss.relToMcap.ratio, name: "Loss", color: colors.loss, unit: Unit.pctMcap }),
priceLine({ unit: Unit.pctMcap, defaultActive: false }),
line({ metric: u.profit.relToOwnMcap.ratio, name: "Profit", color: colors.profit, unit: Unit.pctOwnMcap }),
line({ metric: u.loss.relToOwnMcap.ratio, name: "Loss", color: colors.loss, unit: Unit.pctOwnMcap }),
priceLine({ unit: Unit.pctOwnMcap, defaultActive: false }),
line({ metric: u.profit.relToOwnGross.ratio, name: "Profit", color: colors.profit, unit: Unit.pctOwnPnl }),
line({ metric: u.loss.relToOwnGross.ratio, name: "Loss", color: colors.loss, unit: Unit.pctOwnPnl }),
...priceLines({ numbers: [100, 50, 0], unit: Unit.pctOwnPnl }),
];
}
/**
* Unrealized P&L for LTH (loss relToMcap only + relToOwnMcap + relToOwnGross)
* @param {Brk.GrossInvestedLossNetNuplProfitSentimentPattern2} u
* @returns {AnyFetchedSeriesBlueprint[]}
*/
function unrealizedLongTerm(u) {
return [
...pnlLines(
{ profit: u.profit.base.usd, loss: u.loss.base.usd, negLoss: u.loss.negative, gross: u.grossPnl.usd },
Unit.usd,
),
priceLine({ unit: Unit.usd, defaultActive: false }),
line({ metric: u.loss.relToMcap.ratio, name: "Loss", color: colors.loss, unit: Unit.pctMcap }),
line({ metric: u.profit.relToOwnMcap.ratio, name: "Profit", color: colors.profit, unit: Unit.pctOwnMcap }),
line({ metric: u.loss.relToOwnMcap.ratio, name: "Loss", color: colors.loss, unit: Unit.pctOwnMcap }),
priceLine({ unit: Unit.pctOwnMcap, defaultActive: false }),
line({ metric: u.profit.relToOwnGross.ratio, name: "Profit", color: colors.profit, unit: Unit.pctOwnPnl }),
line({ metric: u.loss.relToOwnGross.ratio, name: "Loss", color: colors.loss, unit: Unit.pctOwnPnl }),
...priceLines({ numbers: [100, 50, 0], unit: Unit.pctOwnPnl }),
];
}
/**
* Unrealized P&L (USD only) for mid-tier cohorts (AgeRange/MaxAge)
* @param {Brk.LossNetNuplProfitPattern} u
* @returns {AnyFetchedSeriesBlueprint[]}
*/
function unrealizedMid(u) {
return [
...pnlLines(
{ profit: u.profit.base.usd, loss: u.loss.base.usd, negLoss: u.loss.negative },
Unit.usd,
),
priceLine({ unit: Unit.usd, defaultActive: false }),
];
}
// ============================================================================
// Net Unrealized P&L Builders
// ============================================================================
/**
* Net P&L for All cohort
* @param {Brk.MetricsTree_Cohorts_Utxo_All_Unrealized} u
* @returns {AnyFetchedSeriesBlueprint[]}
*/
function netUnrealizedAll(u) {
return [
netBaseline(u.netPnl.usd, Unit.usd),
netBaseline(u.netPnl.relToOwnGross.ratio, Unit.pctOwnPnl),
];
}
/**
* Net P&L for Full cohorts (STH/LTH)
* @param {Brk.GrossInvestedLossNetNuplProfitSentimentPattern2} u
* @returns {AnyFetchedSeriesBlueprint[]}
*/
function netUnrealizedFull(u) {
return [
netBaseline(u.netPnl.usd, Unit.usd),
netBaseline(u.netPnl.relToOwnMcap.ratio, Unit.pctOwnMcap),
netBaseline(u.netPnl.relToOwnGross.ratio, Unit.pctOwnPnl),
];
}
/**
* Net P&L for mid-tier cohorts
* @param {Brk.LossNetNuplProfitPattern} u
* @returns {AnyFetchedSeriesBlueprint[]}
*/
function netUnrealizedMid(u) {
return [netBaseline(u.netPnl.usd, Unit.usd)];
}
// ============================================================================
// Invested Capital, Sentiment, NUPL
// ============================================================================
/**
* Invested capital (Full unrealized only)
* @param {Brk.GrossInvestedLossNetNuplProfitSentimentPattern2 | Brk.MetricsTree_Cohorts_Utxo_All_Unrealized} u
* @returns {AnyFetchedSeriesBlueprint[]}
*/
function investedCapitalSeries(u) {
return [
line({ metric: u.investedCapital.inProfit.usd, name: "In Profit", color: colors.profit, unit: Unit.usd }),
line({ metric: u.investedCapital.inLoss.usd, name: "In Loss", color: colors.loss, unit: Unit.usd }),
];
}
/**
* Sentiment (Full unrealized only)
* @param {Brk.GrossInvestedLossNetNuplProfitSentimentPattern2 | Brk.MetricsTree_Cohorts_Utxo_All_Unrealized} u
* @returns {AnyFetchedSeriesBlueprint[]}
*/
function sentimentSeries(u) {
return [
baseline({ metric: u.sentiment.net.usd, name: "Net Sentiment", unit: Unit.usd }),
line({ metric: u.sentiment.greedIndex.usd, name: "Greed Index", color: colors.profit, unit: Unit.usd, defaultActive: false }),
line({ metric: u.sentiment.painIndex.usd, name: "Pain Index", color: colors.loss, unit: Unit.usd, defaultActive: false }),
];
}
/**
* NUPL series
* @param {Brk.BpsRatioPattern} nupl
* @returns {AnyFetchedSeriesBlueprint[]}
*/
function nuplSeries(nupl) {
return [baseline({ metric: nupl.ratio, name: "NUPL", unit: Unit.ratio })];
}
// ============================================================================
// Realized P&L Builders — Full (All/STH/LTH)
// ============================================================================
/**
* @param {Brk.CapGrossInvestorLossMvrvNetPeakPriceProfitSellSoprPattern | Brk.MetricsTree_Cohorts_Utxo_Lth_Realized} r
* @returns {AnyFetchedSeriesBlueprint[]}
*/
function realizedPnlSumFull(r) {
return [
dots({ metric: r.profit.base.usd, name: "Profit", color: colors.profit, unit: Unit.usd }),
dots({ metric: r.loss.negative, name: "Negative Loss", color: colors.loss, unit: Unit.usd, defaultActive: false }),
dots({ metric: r.loss.base.usd, name: "Loss", color: colors.loss, unit: Unit.usd, defaultActive: false }),
baseline({ metric: r.profit.relToRcap.ratio, name: "Profit", color: colors.profit, unit: Unit.pctRcap }),
baseline({ metric: r.loss.relToRcap.ratio, name: "Loss", color: colors.loss, unit: Unit.pctRcap }),
];
}
/**
* @param {Brk.CapGrossInvestorLossMvrvNetPeakPriceProfitSellSoprPattern | Brk.MetricsTree_Cohorts_Utxo_Lth_Realized} r
* @returns {AnyFetchedSeriesBlueprint[]}
*/
function realizedNetPnlSumFull(r) {
return [
dotsBaseline({ metric: r.netPnl.base.usd, name: "Net", unit: Unit.usd, defaultActive: false }),
baseline({ metric: r.netPnl.relToRcap.ratio, name: "Net", unit: Unit.pctRcap }),
];
}
/**
* @param {Brk.CapGrossInvestorLossMvrvNetPeakPriceProfitSellSoprPattern | Brk.MetricsTree_Cohorts_Utxo_Lth_Realized} r
* @returns {AnyFetchedSeriesBlueprint[]}
*/
function realizedPnlCumulativeFull(r) {
return [
line({ metric: r.profit.cumulative.usd, name: "Profit", color: colors.profit, unit: Unit.usd }),
line({ metric: r.loss.cumulative.usd, name: "Loss", color: colors.loss, unit: Unit.usd }),
line({ metric: r.loss.negative, name: "Negative Loss", color: colors.loss, unit: Unit.usd, defaultActive: false }),
baseline({ metric: r.profit.relToRcap.ratio, name: "Profit", color: colors.profit, unit: Unit.pctRcap }),
baseline({ metric: r.loss.relToRcap.ratio, name: "Loss", color: colors.loss, unit: Unit.pctRcap }),
];
}
/**
* @param {Brk.CapGrossInvestorLossMvrvNetPeakPriceProfitSellSoprPattern | Brk.MetricsTree_Cohorts_Utxo_Lth_Realized} r
* @returns {AnyFetchedSeriesBlueprint[]}
*/
function realized30dChangeFull(r) {
return [
baseline({ metric: r.netPnl.delta.change._1m.usd, name: "30d Change", unit: Unit.usd }),
baseline({ metric: r.netPnl.change1m.relToMcap.ratio, name: "30d Change", unit: Unit.pctMcap }),
baseline({ metric: r.netPnl.change1m.relToRcap.ratio, name: "30d Change", unit: Unit.pctRcap }),
];
}
/**
* Rolling realized with P/L and ratio (full realized only)
* @param {Brk.CapGrossInvestorLossMvrvNetPeakPriceProfitSellSoprPattern | Brk.MetricsTree_Cohorts_Utxo_Lth_Realized} r
* @param {(metric: string) => string} title
* @returns {PartialOptionsTree}
*/
function singleRollingRealizedTreeFull(r, title) {
return [
{
name: "Profit",
tree: [
{
name: "Compare",
title: title("Rolling Realized Profit"),
bottom: [
line({ metric: r.profit.sum._24h.usd, name: "24h", color: colors.time._24h, unit: Unit.usd }),
line({ metric: r.profit.sum._1w.usd, name: "7d", color: colors.time._1w, unit: Unit.usd }),
line({ metric: r.profit.sum._1m.usd, name: "30d", color: colors.time._1m, unit: Unit.usd }),
line({ metric: r.profit.sum._1y.usd, name: "1y", color: colors.time._1y, unit: Unit.usd }),
],
},
{ name: "24h", title: title("Realized Profit (24h)"), bottom: [line({ metric: r.profit.sum._24h.usd, name: "Profit", color: colors.profit, unit: Unit.usd })] },
{ name: "7d", title: title("Realized Profit (7d)"), bottom: [line({ metric: r.profit.sum._1w.usd, name: "Profit", color: colors.profit, unit: Unit.usd })] },
{ name: "30d", title: title("Realized Profit (30d)"), bottom: [line({ metric: r.profit.sum._1m.usd, name: "Profit", color: colors.profit, unit: Unit.usd })] },
{ name: "1y", title: title("Realized Profit (1y)"), bottom: [line({ metric: r.profit.sum._1y.usd, name: "Profit", color: colors.profit, unit: Unit.usd })] },
],
},
{
name: "Loss",
tree: [
{
name: "Compare",
title: title("Rolling Realized Loss"),
bottom: [
line({ metric: r.loss.sum._24h.usd, name: "24h", color: colors.time._24h, unit: Unit.usd }),
line({ metric: r.loss.sum._1w.usd, name: "7d", color: colors.time._1w, unit: Unit.usd }),
line({ metric: r.loss.sum._1m.usd, name: "30d", color: colors.time._1m, unit: Unit.usd }),
line({ metric: r.loss.sum._1y.usd, name: "1y", color: colors.time._1y, unit: Unit.usd }),
],
},
{ name: "24h", title: title("Realized Loss (24h)"), bottom: [line({ metric: r.loss.sum._24h.usd, name: "Loss", color: colors.loss, unit: Unit.usd })] },
{ name: "7d", title: title("Realized Loss (7d)"), bottom: [line({ metric: r.loss.sum._1w.usd, name: "Loss", color: colors.loss, unit: Unit.usd })] },
{ name: "30d", title: title("Realized Loss (30d)"), bottom: [line({ metric: r.loss.sum._1m.usd, name: "Loss", color: colors.loss, unit: Unit.usd })] },
{ name: "1y", title: title("Realized Loss (1y)"), bottom: [line({ metric: r.loss.sum._1y.usd, name: "Loss", color: colors.loss, unit: Unit.usd })] },
],
},
{
name: "P/L Ratio",
tree: [
{
name: "Compare",
title: title("Rolling Realized P/L Ratio"),
bottom: [
baseline({ metric: r.profitToLossRatio._24h, name: "24h", color: colors.time._24h, unit: Unit.ratio }),
baseline({ metric: r.profitToLossRatio._1w, name: "7d", color: colors.time._1w, unit: Unit.ratio }),
baseline({ metric: r.profitToLossRatio._1m, name: "30d", color: colors.time._1m, unit: Unit.ratio }),
baseline({ metric: r.profitToLossRatio._1y, name: "1y", color: colors.time._1y, unit: Unit.ratio }),
],
},
{ name: "24h", title: title("Realized P/L Ratio (24h)"), bottom: [baseline({ metric: r.profitToLossRatio._24h, name: "P/L Ratio", unit: Unit.ratio })] },
{ name: "7d", title: title("Realized P/L Ratio (7d)"), bottom: [baseline({ metric: r.profitToLossRatio._1w, name: "P/L Ratio", unit: Unit.ratio })] },
{ name: "30d", title: title("Realized P/L Ratio (30d)"), bottom: [baseline({ metric: r.profitToLossRatio._1m, name: "P/L Ratio", unit: Unit.ratio })] },
{ name: "1y", title: title("Realized P/L Ratio (1y)"), bottom: [baseline({ metric: r.profitToLossRatio._1y, name: "P/L Ratio", unit: Unit.ratio })] },
],
},
];
}
/**
* Rolling realized profit/loss sums (basic — no P/L ratio)
* @param {Brk.BaseCumulativeSumPattern3} profit
* @param {Brk.BaseCumulativeSumPattern3} loss
* @param {(metric: string) => string} title
* @returns {PartialOptionsTree}
*/
function singleRollingRealizedTreeBasic(profit, loss, title) {
return [
{
name: "Profit",
tree: [
{ name: "24h", title: title("Realized Profit (24h)"), bottom: [line({ metric: profit.sum._24h.usd, name: "Profit", color: colors.profit, unit: Unit.usd })] },
{ name: "7d", title: title("Realized Profit (7d)"), bottom: [line({ metric: profit.sum._1w.usd, name: "Profit", color: colors.profit, unit: Unit.usd })] },
{ name: "30d", title: title("Realized Profit (30d)"), bottom: [line({ metric: profit.sum._1m.usd, name: "Profit", color: colors.profit, unit: Unit.usd })] },
{ name: "1y", title: title("Realized Profit (1y)"), bottom: [line({ metric: profit.sum._1y.usd, name: "Profit", color: colors.profit, unit: Unit.usd })] },
],
},
{
name: "Loss",
tree: [
{ name: "24h", title: title("Realized Loss (24h)"), bottom: [line({ metric: loss.sum._24h.usd, name: "Loss", color: colors.loss, unit: Unit.usd })] },
{ name: "7d", title: title("Realized Loss (7d)"), bottom: [line({ metric: loss.sum._1w.usd, name: "Loss", color: colors.loss, unit: Unit.usd })] },
{ name: "30d", title: title("Realized Loss (30d)"), bottom: [line({ metric: loss.sum._1m.usd, name: "Loss", color: colors.loss, unit: Unit.usd })] },
{ name: "1y", title: title("Realized Loss (1y)"), bottom: [line({ metric: loss.sum._1y.usd, name: "Loss", color: colors.loss, unit: Unit.usd })] },
],
},
];
}
// ============================================================================
// Realized Subfolder Builders
// ============================================================================
/**
* Full realized subfolder (All/STH/LTH)
* @param {Brk.CapGrossInvestorLossMvrvNetPeakPriceProfitSellSoprPattern | Brk.MetricsTree_Cohorts_Utxo_Lth_Realized} r
* @param {(metric: string) => string} title
* @returns {PartialOptionsGroup}
*/
function realizedSubfolderFull(r, title) {
return {
name: "Realized",
tree: [
{ name: "P&L", title: title("Realized P&L"), bottom: realizedPnlSumFull(r) },
{ name: "Net", title: title("Net Realized P&L"), bottom: realizedNetPnlSumFull(r) },
{ name: "30d Change", title: title("Realized P&L 30d Change"), bottom: realized30dChangeFull(r) },
{
name: "Total",
title: title("Total Realized P&L"),
bottom: [line({ metric: r.grossPnl.cumulative.usd, name: "Total", unit: Unit.usd, color: colors.bitcoin })],
},
{
name: "P/L Ratio",
title: title("Realized Profit/Loss Ratio"),
bottom: [baseline({ metric: r.profitToLossRatio._1y, name: "P/L Ratio", unit: Unit.ratio })],
},
{
name: "Peak Regret",
title: title("Realized Peak Regret"),
bottom: [
line({ metric: r.peakRegret.base, name: "Peak Regret", unit: Unit.usd }),
],
},
{ name: "Rolling", tree: singleRollingRealizedTreeFull(r, title) },
{
name: "Cumulative",
tree: [
{ name: "P&L", title: title("Cumulative Realized P&L"), bottom: realizedPnlCumulativeFull(r) },
{
name: "Net",
title: title("Cumulative Net Realized P&L"),
bottom: [
baseline({ metric: r.netPnl.cumulative.usd, name: "Net", unit: Unit.usd }),
baseline({ metric: r.netPnl.relToRcap.ratio, name: "Net", unit: Unit.pctRcap }),
],
},
{
name: "Peak Regret",
title: title("Cumulative Realized Peak Regret"),
bottom: [
line({ metric: r.peakRegret.cumulative, name: "Peak Regret", unit: Unit.usd }),
line({ metric: r.peakRegret.relToRcap.ratio, name: "Peak Regret", unit: Unit.pctRcap }),
],
},
],
},
],
};
}
/**
* Mid realized subfolder (AgeRange/MaxAge — has netPnl + delta, no relToRcap/peakRegret)
* @param {Brk.CapLossMvrvNetPriceProfitSoprPattern} r
* @param {(metric: string) => string} title
* @returns {PartialOptionsGroup}
*/
function realizedSubfolderMid(r, title) {
return {
name: "Realized",
tree: [
{
name: "P&L",
title: title("Realized P&L"),
bottom: [
dots({ metric: r.profit.base.usd, name: "Profit", color: colors.profit, unit: Unit.usd }),
dots({ metric: r.loss.negative, name: "Negative Loss", color: colors.loss, unit: Unit.usd, defaultActive: false }),
dots({ metric: r.loss.base.usd, name: "Loss", color: colors.loss, unit: Unit.usd, defaultActive: false }),
],
},
{
name: "Net",
title: title("Net Realized P&L"),
bottom: [dotsBaseline({ metric: r.netPnl.base.usd, name: "Net", unit: Unit.usd })],
},
{
name: "30d Change",
title: title("Realized P&L 30d Change"),
bottom: [baseline({ metric: r.netPnl.delta.change._1m.usd, name: "30d Change", unit: Unit.usd })],
},
{ name: "Rolling", tree: singleRollingRealizedTreeBasic(r.profit, r.loss, title) },
{
name: "Cumulative",
tree: [
{
name: "P&L",
title: title("Cumulative Realized P&L"),
bottom: [
line({ metric: r.profit.cumulative.usd, name: "Profit", color: colors.profit, unit: Unit.usd }),
line({ metric: r.loss.cumulative.usd, name: "Loss", color: colors.loss, unit: Unit.usd }),
],
},
{
name: "Net",
title: title("Cumulative Net Realized P&L"),
bottom: [baseline({ metric: r.netPnl.cumulative.usd, name: "Net", unit: Unit.usd })],
},
],
},
],
};
}
/**
* Basic realized subfolder (no netPnl, no relToRcap)
* @param {Brk.CapLossMvrvPriceProfitSoprPattern} r
* @param {(metric: string) => string} title
* @returns {PartialOptionsGroup}
*/
function realizedSubfolderBasic(r, title) {
return {
name: "Realized",
tree: [
{
name: "P&L",
title: title("Realized P&L"),
bottom: [
dots({ metric: r.profit.base.usd, name: "Profit", color: colors.profit, unit: Unit.usd }),
dots({ metric: r.loss.base.usd, name: "Loss", color: colors.loss, unit: Unit.usd, defaultActive: false }),
],
},
{ name: "Rolling", tree: singleRollingRealizedTreeBasic(r.profit, r.loss, title) },
{
name: "Cumulative",
title: title("Cumulative Realized P&L"),
bottom: [
line({ metric: r.profit.cumulative.usd, name: "Profit", color: colors.profit, unit: Unit.usd }),
line({ metric: r.loss.cumulative.usd, name: "Loss", color: colors.loss, unit: Unit.usd }),
],
},
],
};
}
// ============================================================================
// Single Cohort Section Builders
// ============================================================================
/**
* Basic profitability section (NUPL only unrealized, basic realized)
* @param {{ cohort: UtxoCohortObject | CohortWithoutRelative, title: (metric: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createProfitabilitySection({ cohort, title }) {
const { tree } = cohort;
return {
name: "Profitability",
tree: [
{
name: "Unrealized",
tree: [
{ name: "NUPL", title: title("NUPL"), bottom: nuplSeries(tree.unrealized.nupl) },
],
},
realizedSubfolderBasic(tree.realized, title),
],
};
}
/**
* Section for All cohort
* @param {{ cohort: CohortAll, title: (metric: 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: [
{ name: "P&L", title: title("Unrealized P&L"), bottom: unrealizedAll(u) },
{ name: "Net P&L", title: title("Net Unrealized P&L"), bottom: netUnrealizedAll(u) },
{ name: "NUPL", title: title("NUPL"), bottom: nuplSeries(u.nupl) },
],
},
realizedSubfolderFull(r, title),
{
name: "Invested Capital",
title: title("Invested Capital In Profit & Loss"),
bottom: investedCapitalSeries(u),
},
{ name: "Sentiment", title: title("Market Sentiment"), bottom: sentimentSeries(u) },
],
};
}
/**
* Section for Full cohorts (STH)
* @param {{ cohort: CohortFull, title: (metric: 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: [
{ name: "P&L", title: title("Unrealized P&L"), bottom: unrealizedFull(u) },
{ name: "Net P&L", title: title("Net Unrealized P&L"), bottom: netUnrealizedFull(u) },
{ name: "NUPL", title: title("NUPL"), bottom: nuplSeries(u.nupl) },
],
},
realizedSubfolderFull(r, title),
{
name: "Invested Capital",
title: title("Invested Capital In Profit & Loss"),
bottom: investedCapitalSeries(u),
},
{ name: "Sentiment", title: title("Market Sentiment"), bottom: sentimentSeries(u) },
],
};
}
/**
* Section with NUPL (basic cohorts with market cap — NuplPattern unrealized)
* @param {{ cohort: CohortBasicWithMarketCap, title: (metric: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createProfitabilitySectionWithNupl({ cohort, title }) {
const { tree } = cohort;
return {
name: "Profitability",
tree: [
{
name: "Unrealized",
tree: [
{ name: "NUPL", title: title("NUPL"), bottom: nuplSeries(tree.unrealized.nupl) },
],
},
realizedSubfolderBasic(tree.realized, title),
],
};
}
/**
* Section for LongTerm cohort
* @param {{ cohort: CohortLongTerm, title: (metric: 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: [
{ name: "P&L", title: title("Unrealized P&L"), bottom: unrealizedLongTerm(u) },
{ name: "Net P&L", title: title("Net Unrealized P&L"), bottom: netUnrealizedFull(u) },
{ name: "NUPL", title: title("NUPL"), bottom: nuplSeries(u.nupl) },
],
},
realizedSubfolderFull(r, title),
{
name: "Invested Capital",
title: title("Invested Capital In Profit & Loss"),
bottom: investedCapitalSeries(u),
},
{ 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: (metric: 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: [
{ name: "P&L", title: title("Unrealized P&L"), bottom: unrealizedMid(u) },
{ name: "Net P&L", title: title("Net Unrealized P&L"), bottom: netUnrealizedMid(u) },
{ name: "NUPL", title: title("NUPL"), bottom: nuplSeries(u.nupl) },
],
},
realizedSubfolderMid(r, title),
],
};
}
/**
* Section with invested capital % but no unrealized relative (basic cohorts)
* @param {{ cohort: CohortBasicWithoutMarketCap, title: (metric: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createProfitabilitySectionBasicWithInvestedCapitalPct({ cohort, title }) {
const { tree } = cohort;
return {
name: "Profitability",
tree: [
{
name: "Unrealized",
tree: [
{ name: "NUPL", title: title("NUPL"), bottom: nuplSeries(tree.unrealized.nupl) },
],
},
realizedSubfolderBasic(tree.realized, title),
],
};
}
// ============================================================================
// Grouped Cohort Helpers
// ============================================================================
/**
* Grouped realized P&L sum (basic — all cohorts have profit/loss)
* @template {{ name: string, color: Color, tree: { realized: { profit: Brk.BaseCumulativeSumPattern3, loss: Brk.BaseCumulativeSumPattern3 } } }} T
* @template {{ name: string, color: Color, tree: { realized: { profit: Brk.BaseCumulativeSumPattern3, loss: Brk.BaseCumulativeSumPattern3 } } }} A
* @param {readonly T[]} list
* @param {A} all
* @param {(metric: string) => string} title
* @returns {PartialOptionsTree}
*/
function groupedRealizedPnlSum(list, all, title) {
return [
{
name: "Profit",
title: title("Realized Profit"),
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
line({ metric: tree.realized.profit.base.usd, name, color, unit: Unit.usd }),
),
},
{
name: "Loss",
title: title("Realized Loss"),
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
line({ metric: tree.realized.loss.base.usd, name, color, unit: Unit.usd }),
),
},
];
}
/**
* Grouped realized P&L sum with extras (full cohorts)
* @param {readonly (CohortAll | CohortFull | CohortLongTerm)[]} list
* @param {CohortAll} all
* @param {(metric: string) => string} title
* @returns {PartialOptionsTree}
*/
function groupedRealizedPnlSumFull(list, all, title) {
return [
...groupedRealizedPnlSum(list, all, title),
{
name: "Total",
title: title("Total Realized P&L"),
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
line({ metric: tree.realized.grossPnl.cumulative.usd, name, color, unit: Unit.usd }),
),
},
{
name: "P/L Ratio",
title: title("Realized Profit/Loss Ratio"),
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
baseline({ metric: tree.realized.profitToLossRatio._1y, name, color, unit: Unit.ratio }),
),
},
];
}
/**
* Grouped rolling realized charts (basic — profit/loss sums only)
* @template {{ name: string, color: Color, tree: { realized: { profit: Brk.BaseCumulativeSumPattern3, loss: Brk.BaseCumulativeSumPattern3 } } }} T
* @template {{ name: string, color: Color, tree: { realized: { profit: Brk.BaseCumulativeSumPattern3, loss: Brk.BaseCumulativeSumPattern3 } } }} A
* @param {readonly T[]} list
* @param {A} all
* @param {(metric: string) => string} title
* @returns {PartialOptionsTree}
*/
function groupedRollingRealizedCharts(list, all, title) {
return [
{
name: "Profit",
tree: [
{ name: "24h", title: title("Realized Profit (24h)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ metric: tree.realized.profit.sum._24h.usd, name, color, unit: Unit.usd })) },
{ name: "7d", title: title("Realized Profit (7d)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ metric: tree.realized.profit.sum._1w.usd, name, color, unit: Unit.usd })) },
{ name: "30d", title: title("Realized Profit (30d)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ metric: tree.realized.profit.sum._1m.usd, name, color, unit: Unit.usd })) },
{ name: "1y", title: title("Realized Profit (1y)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ metric: tree.realized.profit.sum._1y.usd, name, color, unit: Unit.usd })) },
],
},
{
name: "Loss",
tree: [
{ name: "24h", title: title("Realized Loss (24h)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ metric: tree.realized.loss.sum._24h.usd, name, color, unit: Unit.usd })) },
{ name: "7d", title: title("Realized Loss (7d)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ metric: tree.realized.loss.sum._1w.usd, name, color, unit: Unit.usd })) },
{ name: "30d", title: title("Realized Loss (30d)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ metric: tree.realized.loss.sum._1m.usd, name, color, unit: Unit.usd })) },
{ name: "1y", title: title("Realized Loss (1y)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ metric: tree.realized.loss.sum._1y.usd, name, color, unit: Unit.usd })) },
],
},
];
}
/**
* Grouped rolling realized with P/L ratio (full cohorts)
* @param {readonly (CohortAll | CohortFull | CohortLongTerm)[]} list
* @param {CohortAll} all
* @param {(metric: string) => string} title
* @returns {PartialOptionsTree}
*/
function groupedRollingRealizedChartsFull(list, all, title) {
return [
...groupedRollingRealizedCharts(list, all, title),
{
name: "P/L Ratio",
tree: [
{ name: "24h", title: title("Realized P/L Ratio (24h)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => baseline({ metric: tree.realized.profitToLossRatio._24h, name, color, unit: Unit.ratio })) },
{ name: "7d", title: title("Realized P/L Ratio (7d)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => baseline({ metric: tree.realized.profitToLossRatio._1w, name, color, unit: Unit.ratio })) },
{ name: "30d", title: title("Realized P/L Ratio (30d)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => baseline({ metric: tree.realized.profitToLossRatio._1m, name, color, unit: Unit.ratio })) },
{ name: "1y", title: title("Realized P/L Ratio (1y)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => baseline({ metric: tree.realized.profitToLossRatio._1y, name, color, unit: Unit.ratio })) },
],
},
];
}
/**
* Grouped realized subfolder (basic)
* @template {{ name: string, color: Color, tree: { realized: { profit: Brk.BaseCumulativeSumPattern3, loss: Brk.BaseCumulativeSumPattern3 } } }} T
* @template {{ name: string, color: Color, tree: { realized: { profit: Brk.BaseCumulativeSumPattern3, loss: Brk.BaseCumulativeSumPattern3 } } }} A
* @param {readonly T[]} list
* @param {A} all
* @param {(metric: string) => string} title
* @returns {PartialOptionsGroup}
*/
function groupedRealizedSubfolder(list, all, title) {
return {
name: "Realized",
tree: [
{ name: "P&L", tree: groupedRealizedPnlSum(list, all, title) },
{ name: "Rolling", tree: groupedRollingRealizedCharts(list, all, title) },
{
name: "Cumulative",
tree: [
{
name: "Profit",
title: title("Cumulative Realized Profit"),
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
line({ metric: tree.realized.profit.cumulative.usd, name, color, unit: Unit.usd }),
),
},
{
name: "Loss",
title: title("Cumulative Realized Loss"),
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
line({ metric: tree.realized.loss.cumulative.usd, name, color, unit: Unit.usd }),
),
},
],
},
],
};
}
/**
* Grouped realized subfolder for full cohorts
* @param {readonly (CohortAll | CohortFull | CohortLongTerm)[]} list
* @param {CohortAll} all
* @param {(metric: string) => string} title
* @returns {PartialOptionsGroup}
*/
function groupedRealizedSubfolderFull(list, all, title) {
return {
name: "Realized",
tree: [
{ name: "P&L", tree: groupedRealizedPnlSumFull(list, all, title) },
{
name: "Net",
title: title("Net Realized P&L"),
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
baseline({ metric: tree.realized.netPnl.base.usd, name, color, unit: Unit.usd }),
),
},
{
name: "30d Change",
title: title("Realized P&L 30d Change"),
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
baseline({ metric: tree.realized.netPnl.delta.change._1m.usd, name, color, unit: Unit.usd }),
),
},
{ name: "Rolling", tree: groupedRollingRealizedChartsFull(list, all, title) },
{
name: "Cumulative",
tree: [
{
name: "Profit",
title: title("Cumulative Realized Profit"),
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
line({ metric: tree.realized.profit.cumulative.usd, name, color, unit: Unit.usd }),
),
},
{
name: "Loss",
title: title("Cumulative Realized Loss"),
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
line({ metric: tree.realized.loss.cumulative.usd, name, color, unit: Unit.usd }),
),
},
{
name: "Net",
title: title("Cumulative Net Realized P&L"),
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
baseline({ metric: tree.realized.netPnl.cumulative.usd, name, color, unit: Unit.usd }),
),
},
],
},
],
};
}
/**
* Grouped unrealized P&L (USD only — for all cohorts that at least have nupl)
* @template {{ name: string, color: Color, tree: { unrealized: { nupl: Brk.BpsRatioPattern } } }} T
* @template {{ name: string, color: Color, tree: { unrealized: { nupl: Brk.BpsRatioPattern } } }} A
* @param {readonly T[]} list
* @param {A} all
* @param {(metric: string) => string} title
* @returns {PartialOptionsTree}
*/
function groupedNuplCharts(list, all, title) {
return [
{
name: "NUPL",
title: title("NUPL"),
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
baseline({ metric: tree.unrealized.nupl.ratio, name, color, unit: Unit.ratio }),
),
},
];
}
/**
* Grouped unrealized for full cohorts with relToMcap
* @param {readonly (CohortFull | CohortLongTerm)[]} list
* @param {CohortAll} all
* @param {(metric: string) => string} title
* @returns {PartialOptionsTree}
*/
function groupedPnlChartsWithMarketCap(list, all, title) {
return [
{
name: "Profit",
title: title("Unrealized Profit"),
bottom: [
...mapCohortsWithAll(list, all, ({ name, color, tree }) =>
line({ metric: tree.unrealized.profit.base.usd, name, color, unit: Unit.usd }),
),
...mapCohortsWithAll(list, all, ({ name, color, tree }) =>
baseline({ metric: tree.unrealized.profit.relToMcap.ratio, name, color, unit: Unit.pctMcap }),
),
],
},
{
name: "Loss",
title: title("Unrealized Loss"),
bottom: [
...mapCohortsWithAll(list, all, ({ name, color, tree }) =>
line({ metric: tree.unrealized.loss.base.usd, name, color, unit: Unit.usd }),
),
...mapCohortsWithAll(list, all, ({ name, color, tree }) =>
baseline({ metric: tree.unrealized.loss.relToMcap.ratio, name, color, unit: Unit.pctMcap }),
),
],
},
{
name: "Net P&L",
title: title("Net Unrealized P&L"),
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
baseline({ metric: tree.unrealized.netPnl.usd, name, color, unit: Unit.usd }),
),
},
];
}
/**
* Grouped unrealized for AgeRange/MaxAge (profit/loss without relToMcap)
* @param {readonly (CohortAgeRange | CohortWithAdjusted)[]} list
* @param {CohortAll} all
* @param {(metric: string) => string} title
* @returns {PartialOptionsTree}
*/
function groupedPnlChartsWithOwnMarketCap(list, all, title) {
return [
{
name: "Profit",
title: title("Unrealized Profit"),
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
line({ metric: tree.unrealized.profit.base.usd, name, color, unit: Unit.usd }),
),
},
{
name: "Loss",
title: title("Unrealized Loss"),
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
line({ metric: tree.unrealized.loss.base.usd, name, color, unit: Unit.usd }),
),
},
{
name: "Net P&L",
title: title("Net Unrealized P&L"),
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
baseline({ metric: tree.unrealized.netPnl.usd, name, color, unit: Unit.usd }),
),
},
];
}
/**
* Grouped unrealized for LongTerm (profit/loss with relToOwnMcap + relToOwnGross)
* @param {readonly CohortLongTerm[]} list
* @param {CohortAll} all
* @param {(metric: string) => string} title
* @returns {PartialOptionsTree}
*/
function groupedPnlChartsLongTerm(list, all, title) {
return [
{
name: "Profit",
title: title("Unrealized Profit"),
bottom: [
...mapCohortsWithAll(list, all, ({ name, color, tree }) =>
line({ metric: tree.unrealized.profit.base.usd, name, color, unit: Unit.usd }),
),
...mapCohorts(list, ({ name, color, tree }) =>
line({ metric: tree.unrealized.profit.relToOwnMcap.ratio, name, color, unit: Unit.pctOwnMcap }),
),
...mapCohortsWithAll(list, all, ({ name, color, tree }) =>
line({ metric: tree.unrealized.profit.relToOwnGross.ratio, name, color, unit: Unit.pctOwnPnl }),
),
],
},
{
name: "Loss",
title: title("Unrealized Loss"),
bottom: [
...mapCohortsWithAll(list, all, ({ name, color, tree }) =>
line({ metric: tree.unrealized.loss.base.usd, name, color, unit: Unit.usd }),
),
...mapCohortsWithAll(list, all, ({ name, color, tree }) =>
line({ metric: tree.unrealized.loss.relToMcap.ratio, name, color, unit: Unit.pctMcap }),
),
...mapCohorts(list, ({ name, color, tree }) =>
line({ metric: tree.unrealized.loss.relToOwnMcap.ratio, name, color, unit: Unit.pctOwnMcap }),
),
...mapCohortsWithAll(list, all, ({ name, color, tree }) =>
line({ metric: tree.unrealized.loss.relToOwnGross.ratio, name, color, unit: Unit.pctOwnPnl }),
),
],
},
{
name: "Net P&L",
title: title("Net Unrealized P&L"),
bottom: [
...mapCohortsWithAll(list, all, ({ name, color, tree }) =>
baseline({ metric: tree.unrealized.netPnl.usd, name, color, unit: Unit.usd }),
),
...mapCohorts(list, ({ name, color, tree }) =>
baseline({ metric: tree.unrealized.netPnl.relToOwnMcap.ratio, name, color, unit: Unit.pctOwnMcap }),
),
...mapCohortsWithAll(list, all, ({ name, color, tree }) =>
baseline({ metric: tree.unrealized.netPnl.relToOwnGross.ratio, name, color, unit: Unit.pctOwnPnl }),
),
],
},
];
}
/**
* Grouped sentiment (full unrealized only)
* @param {readonly (CohortAll | CohortFull | CohortLongTerm)[]} list
* @param {CohortAll} all
* @param {(metric: 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({ metric: tree.unrealized.sentiment.net.usd, name, color, unit: Unit.usd }),
),
},
{
name: "Greed",
title: title("Greed Index"),
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
line({ metric: tree.unrealized.sentiment.greedIndex.usd, name, color, unit: Unit.usd }),
),
},
{
name: "Pain",
title: title("Pain Index"),
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
line({ metric: 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: (metric: 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 section with invested capital % (basic cohorts — uses NUPL only)
* @param {{ list: readonly CohortBasicWithoutMarketCap[], all: CohortAll, title: (metric: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createGroupedProfitabilitySectionBasicWithInvestedCapitalPct({ list, all, title }) {
return {
name: "Profitability",
tree: [
{ name: "Unrealized", tree: groupedNuplCharts(list, all, title) },
groupedRealizedSubfolder(list, all, title),
],
};
}
/**
* Grouped section for ageRange/maxAge cohorts
* @param {{ list: readonly (CohortAgeRange | CohortWithAdjusted)[], all: CohortAll, title: (metric: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createGroupedProfitabilitySectionWithInvestedCapitalPct({ list, all, title }) {
return {
name: "Profitability",
tree: [
{
name: "Unrealized",
tree: [
...groupedPnlChartsWithOwnMarketCap(list, all, title),
...groupedNuplCharts(list, all, title),
],
},
groupedRealizedSubfolder(list, all, title),
],
};
}
/**
* Grouped section with NUPL + relToMcap
* @param {{ list: readonly (CohortFull | CohortLongTerm)[], all: CohortAll, title: (metric: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createGroupedProfitabilitySectionWithNupl({ list, all, title }) {
return {
name: "Profitability",
tree: [
{
name: "Unrealized",
tree: [
...groupedPnlChartsWithMarketCap(list, all, title),
...groupedNuplCharts(list, all, title),
],
},
groupedRealizedSubfolder(list, all, title),
],
};
}
/**
* Grouped section for LongTerm cohorts
* @param {{ list: readonly CohortLongTerm[], all: CohortAll, title: (metric: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createGroupedProfitabilitySectionLongTerm({ list, all, title }) {
return {
name: "Profitability",
tree: [
{
name: "Unrealized",
tree: [
...groupedPnlChartsLongTerm(list, all, title),
...groupedNuplCharts(list, all, title),
],
},
groupedRealizedSubfolderFull(list, all, title),
groupedSentiment(list, all, title),
],
};
}