mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-04-25 07:09:59 -07:00
2475 lines
70 KiB
JavaScript
2475 lines
70 KiB
JavaScript
/**
|
|
* Profitability section builders
|
|
*/
|
|
|
|
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 {
|
|
satsBtcUsd,
|
|
satsBtcUsdFrom,
|
|
mapCohorts,
|
|
mapCohortsWithAll,
|
|
flatMapCohortsWithAll,
|
|
} from "../shared.js";
|
|
|
|
// ============================================================================
|
|
// Core Series Builders (Composable Primitives)
|
|
// ============================================================================
|
|
|
|
/**
|
|
* @typedef {Object} PnlSeriesConfig
|
|
* @property {AnyMetricPattern} profit
|
|
* @property {AnyMetricPattern} loss
|
|
* @property {AnyMetricPattern} negLoss
|
|
* @property {AnyMetricPattern} [total]
|
|
*/
|
|
|
|
/**
|
|
* Create profit/loss line series for a unit
|
|
* @param {PnlSeriesConfig} metrics
|
|
* @param {Unit} unit
|
|
* @returns {AnyFetchedSeriesBlueprint[]}
|
|
*/
|
|
function pnlLines(metrics, unit) {
|
|
const series = [
|
|
line({
|
|
metric: metrics.profit,
|
|
name: "Profit",
|
|
color: colors.profit,
|
|
unit,
|
|
}),
|
|
line({ metric: metrics.loss, name: "Loss", color: colors.loss, unit }),
|
|
];
|
|
if (metrics.total) {
|
|
series.push(
|
|
line({
|
|
metric: metrics.total,
|
|
name: "Total",
|
|
color: colors.default,
|
|
unit,
|
|
}),
|
|
);
|
|
}
|
|
series.push(
|
|
line({
|
|
metric: metrics.negLoss,
|
|
name: "Negative Loss",
|
|
color: colors.loss,
|
|
unit,
|
|
defaultActive: false,
|
|
}),
|
|
);
|
|
return series;
|
|
}
|
|
|
|
/**
|
|
* Create net P&L baseline
|
|
* @param {AnyMetricPattern} metric
|
|
* @param {Unit} unit
|
|
* @returns {AnyFetchedSeriesBlueprint}
|
|
*/
|
|
function netBaseline(metric, unit) {
|
|
return baseline({ metric, name: "Net P&L", unit });
|
|
}
|
|
|
|
// ============================================================================
|
|
// Unrealized P&L Builders
|
|
// ============================================================================
|
|
|
|
/**
|
|
* @typedef {Object} UnrealizedMetrics
|
|
* @property {AnyMetricPattern} profit
|
|
* @property {AnyMetricPattern} loss
|
|
* @property {AnyMetricPattern} negLoss
|
|
* @property {AnyMetricPattern} total
|
|
* @property {AnyMetricPattern} net
|
|
*/
|
|
|
|
/**
|
|
* Extract unrealized metrics from tree
|
|
* @param {{ unrealized: UnrealizedPattern }} tree
|
|
* @returns {UnrealizedMetrics}
|
|
*/
|
|
function getUnrealizedMetrics(tree) {
|
|
return {
|
|
profit: tree.unrealized.unrealizedProfit,
|
|
loss: tree.unrealized.unrealizedLoss,
|
|
negLoss: tree.unrealized.negUnrealizedLoss,
|
|
total: tree.unrealized.totalUnrealizedPnl,
|
|
net: tree.unrealized.netUnrealizedPnl,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Base unrealized P&L (USD only)
|
|
* @param {UnrealizedMetrics} m
|
|
* @returns {AnyFetchedSeriesBlueprint[]}
|
|
*/
|
|
function unrealizedUsd(m) {
|
|
return [
|
|
...pnlLines(
|
|
{ profit: m.profit, loss: m.loss, negLoss: m.negLoss, total: m.total },
|
|
Unit.usd,
|
|
),
|
|
priceLine({ unit: Unit.usd, defaultActive: false }),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Unrealized P&L with % of Market Cap
|
|
* @param {UnrealizedMetrics} m
|
|
* @param {RelativeWithNupl} rel
|
|
* @returns {AnyFetchedSeriesBlueprint[]}
|
|
*/
|
|
function unrealizedWithMarketCap(m, rel) {
|
|
return [
|
|
...unrealizedUsd(m),
|
|
...pnlLines(
|
|
{
|
|
profit: rel.unrealizedProfitRelToMarketCap,
|
|
loss: rel.unrealizedLossRelToMarketCap,
|
|
negLoss: rel.negUnrealizedLossRelToMarketCap,
|
|
},
|
|
Unit.pctMcap,
|
|
),
|
|
priceLine({ unit: Unit.pctMcap, defaultActive: false }),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Unrealized P&L with % of Own Market Cap + % of Own P&L
|
|
* @param {UnrealizedMetrics} m
|
|
* @param {RelativeWithOwnMarketCap} rel
|
|
* @returns {AnyFetchedSeriesBlueprint[]}
|
|
*/
|
|
function unrealizedWithOwnMarketCap(m, rel) {
|
|
return [
|
|
...unrealizedUsd(m),
|
|
...pnlLines(
|
|
{
|
|
profit: rel.unrealizedProfitRelToOwnMarketCap,
|
|
loss: rel.unrealizedLossRelToOwnMarketCap,
|
|
negLoss: rel.negUnrealizedLossRelToOwnMarketCap,
|
|
},
|
|
Unit.pctOwnMcap,
|
|
),
|
|
priceLine({ unit: Unit.pctOwnMcap, defaultActive: false }),
|
|
...pnlLines(
|
|
{
|
|
profit: rel.unrealizedProfitRelToOwnTotalUnrealizedPnl,
|
|
loss: rel.unrealizedLossRelToOwnTotalUnrealizedPnl,
|
|
negLoss: rel.negUnrealizedLossRelToOwnTotalUnrealizedPnl,
|
|
},
|
|
Unit.pctOwnPnl,
|
|
),
|
|
...priceLines({ numbers: [100, 50, 0], unit: Unit.pctOwnPnl }),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Unrealized P&L for "all" cohort (% M.Cap + % Own P&L, no Own M.Cap)
|
|
* @param {UnrealizedMetrics} m
|
|
* @param {AllRelativePattern} rel
|
|
* @returns {AnyFetchedSeriesBlueprint[]}
|
|
*/
|
|
function unrealizedAll(m, rel) {
|
|
return [
|
|
...unrealizedUsd(m),
|
|
...pnlLines(
|
|
{
|
|
profit: rel.unrealizedProfitRelToMarketCap,
|
|
loss: rel.unrealizedLossRelToMarketCap,
|
|
negLoss: rel.negUnrealizedLossRelToMarketCap,
|
|
},
|
|
Unit.pctMcap,
|
|
),
|
|
priceLine({ unit: Unit.pctMcap, defaultActive: false }),
|
|
...pnlLines(
|
|
{
|
|
profit: rel.unrealizedProfitRelToOwnTotalUnrealizedPnl,
|
|
loss: rel.unrealizedLossRelToOwnTotalUnrealizedPnl,
|
|
negLoss: rel.negUnrealizedLossRelToOwnTotalUnrealizedPnl,
|
|
},
|
|
Unit.pctOwnPnl,
|
|
),
|
|
...priceLines({ numbers: [100, 50, 0], unit: Unit.pctOwnPnl }),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Unrealized P&L for Full cohorts (all relative metrics)
|
|
* @param {UnrealizedMetrics} m
|
|
* @param {FullRelativePattern} rel
|
|
* @returns {AnyFetchedSeriesBlueprint[]}
|
|
*/
|
|
function unrealizedFull(m, rel) {
|
|
return [
|
|
...unrealizedUsd(m),
|
|
...pnlLines(
|
|
{
|
|
profit: rel.unrealizedProfitRelToMarketCap,
|
|
loss: rel.unrealizedLossRelToMarketCap,
|
|
negLoss: rel.negUnrealizedLossRelToMarketCap,
|
|
},
|
|
Unit.pctMcap,
|
|
),
|
|
priceLine({ unit: Unit.pctMcap, defaultActive: false }),
|
|
...pnlLines(
|
|
{
|
|
profit: rel.unrealizedProfitRelToOwnMarketCap,
|
|
loss: rel.unrealizedLossRelToOwnMarketCap,
|
|
negLoss: rel.negUnrealizedLossRelToOwnMarketCap,
|
|
},
|
|
Unit.pctOwnMcap,
|
|
),
|
|
priceLine({ unit: Unit.pctOwnMcap, defaultActive: false }),
|
|
...pnlLines(
|
|
{
|
|
profit: rel.unrealizedProfitRelToOwnTotalUnrealizedPnl,
|
|
loss: rel.unrealizedLossRelToOwnTotalUnrealizedPnl,
|
|
negLoss: rel.negUnrealizedLossRelToOwnTotalUnrealizedPnl,
|
|
},
|
|
Unit.pctOwnPnl,
|
|
),
|
|
...priceLines({ numbers: [100, 50, 0], unit: Unit.pctOwnPnl }),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Unrealized P&L for LongTerm (% M.Cap loss only + Own M.Cap + Own P&L)
|
|
* @param {UnrealizedMetrics} m
|
|
* @param {RelativeWithOwnMarketCap & RelativeWithNupl} rel
|
|
* @returns {AnyFetchedSeriesBlueprint[]}
|
|
*/
|
|
function unrealizedLongTerm(m, rel) {
|
|
return [
|
|
...unrealizedUsd(m),
|
|
line({
|
|
metric: rel.unrealizedLossRelToMarketCap,
|
|
name: "Loss",
|
|
color: colors.loss,
|
|
unit: Unit.pctMcap,
|
|
}),
|
|
...pnlLines(
|
|
{
|
|
profit: rel.unrealizedProfitRelToOwnMarketCap,
|
|
loss: rel.unrealizedLossRelToOwnMarketCap,
|
|
negLoss: rel.negUnrealizedLossRelToOwnMarketCap,
|
|
},
|
|
Unit.pctOwnMcap,
|
|
),
|
|
priceLine({ unit: Unit.pctOwnMcap, defaultActive: false }),
|
|
...pnlLines(
|
|
{
|
|
profit: rel.unrealizedProfitRelToOwnTotalUnrealizedPnl,
|
|
loss: rel.unrealizedLossRelToOwnTotalUnrealizedPnl,
|
|
negLoss: rel.negUnrealizedLossRelToOwnTotalUnrealizedPnl,
|
|
},
|
|
Unit.pctOwnPnl,
|
|
),
|
|
...priceLines({ numbers: [100, 50, 0], unit: Unit.pctOwnPnl }),
|
|
];
|
|
}
|
|
|
|
// ============================================================================
|
|
// Net Unrealized P&L Builders
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Net P&L (USD only)
|
|
* @param {AnyMetricPattern} net
|
|
* @returns {AnyFetchedSeriesBlueprint[]}
|
|
*/
|
|
function netUnrealizedUsd(net) {
|
|
return [netBaseline(net, Unit.usd)];
|
|
}
|
|
|
|
/**
|
|
* Net P&L with % of Market Cap
|
|
* @param {AnyMetricPattern} net
|
|
* @param {RelativeWithNupl} rel
|
|
* @returns {AnyFetchedSeriesBlueprint[]}
|
|
*/
|
|
function netUnrealizedWithMarketCap(net, rel) {
|
|
return [
|
|
netBaseline(net, Unit.usd),
|
|
netBaseline(rel.netUnrealizedPnlRelToMarketCap, Unit.pctMcap),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Net P&L with % of Own Market Cap + % of Own P&L
|
|
* @param {AnyMetricPattern} net
|
|
* @param {RelativeWithOwnMarketCap} rel
|
|
* @returns {AnyFetchedSeriesBlueprint[]}
|
|
*/
|
|
function netUnrealizedWithOwnMarketCap(net, rel) {
|
|
return [
|
|
netBaseline(net, Unit.usd),
|
|
netBaseline(rel.netUnrealizedPnlRelToOwnMarketCap, Unit.pctOwnMcap),
|
|
netBaseline(rel.netUnrealizedPnlRelToOwnTotalUnrealizedPnl, Unit.pctOwnPnl),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Net P&L for "all" cohort
|
|
* @param {AnyMetricPattern} net
|
|
* @param {AllRelativePattern} rel
|
|
* @returns {AnyFetchedSeriesBlueprint[]}
|
|
*/
|
|
function netUnrealizedAll(net, rel) {
|
|
return [
|
|
netBaseline(net, Unit.usd),
|
|
netBaseline(rel.netUnrealizedPnlRelToMarketCap, Unit.pctMcap),
|
|
netBaseline(rel.netUnrealizedPnlRelToOwnTotalUnrealizedPnl, Unit.pctOwnPnl),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Net P&L for Full cohorts
|
|
* @param {AnyMetricPattern} net
|
|
* @param {FullRelativePattern} rel
|
|
* @returns {AnyFetchedSeriesBlueprint[]}
|
|
*/
|
|
function netUnrealizedFull(net, rel) {
|
|
return [
|
|
netBaseline(net, Unit.usd),
|
|
netBaseline(rel.netUnrealizedPnlRelToMarketCap, Unit.pctMcap),
|
|
netBaseline(rel.netUnrealizedPnlRelToOwnMarketCap, Unit.pctOwnMcap),
|
|
netBaseline(rel.netUnrealizedPnlRelToOwnTotalUnrealizedPnl, Unit.pctOwnPnl),
|
|
];
|
|
}
|
|
|
|
// ============================================================================
|
|
// Invested Capital & Other Unrealized
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Invested capital (USD only)
|
|
* @param {{ unrealized: UnrealizedPattern }} tree
|
|
* @returns {AnyFetchedSeriesBlueprint[]}
|
|
*/
|
|
function investedCapitalAbsolute(tree) {
|
|
return [
|
|
line({
|
|
metric: tree.unrealized.investedCapitalInProfit,
|
|
name: "In Profit",
|
|
color: colors.profit,
|
|
unit: Unit.usd,
|
|
}),
|
|
line({
|
|
metric: tree.unrealized.investedCapitalInLoss,
|
|
name: "In Loss",
|
|
color: colors.loss,
|
|
unit: Unit.usd,
|
|
}),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Invested capital with % of Own R.Cap
|
|
* @param {{ unrealized: UnrealizedPattern, relative: RelativeWithInvestedCapitalPct }} tree
|
|
* @returns {AnyFetchedSeriesBlueprint[]}
|
|
*/
|
|
function investedCapitalWithPct(tree) {
|
|
return [
|
|
...investedCapitalAbsolute(tree),
|
|
baseline({
|
|
metric: tree.relative.investedCapitalInProfitPct,
|
|
name: "In Profit",
|
|
color: colors.profit,
|
|
unit: Unit.pctOwnRcap,
|
|
}),
|
|
baseline({
|
|
metric: tree.relative.investedCapitalInLossPct,
|
|
name: "In Loss",
|
|
color: colors.loss,
|
|
unit: Unit.pctOwnRcap,
|
|
}),
|
|
...priceLines({ numbers: [100, 50], unit: Unit.pctOwnRcap }),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* NUPL series
|
|
* @param {RelativeWithNupl} rel
|
|
* @returns {AnyFetchedSeriesBlueprint[]}
|
|
*/
|
|
function nuplSeries(rel) {
|
|
return [baseline({ metric: rel.nupl, name: "NUPL", unit: Unit.ratio })];
|
|
}
|
|
|
|
/**
|
|
* Peak regret (USD only)
|
|
* @param {{ unrealized: UnrealizedFullPattern }} tree
|
|
* @param {Color} color
|
|
* @returns {AnyFetchedSeriesBlueprint[]}
|
|
*/
|
|
function peakRegretAbsolute(tree, color) {
|
|
return [
|
|
line({
|
|
metric: tree.unrealized.peakRegret,
|
|
name: "Peak Regret",
|
|
color,
|
|
unit: Unit.usd,
|
|
}),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Peak regret with % of Market Cap
|
|
* @param {{ unrealized: UnrealizedFullPattern, relative: RelativeWithPeakRegret }} tree
|
|
* @param {Color} color
|
|
* @returns {AnyFetchedSeriesBlueprint[]}
|
|
*/
|
|
function peakRegretWithMarketCap(tree, color) {
|
|
return [
|
|
line({
|
|
metric: tree.unrealized.peakRegret,
|
|
name: "Peak Regret",
|
|
color,
|
|
unit: Unit.usd,
|
|
}),
|
|
baseline({
|
|
metric: tree.relative.unrealizedPeakRegretRelToMarketCap,
|
|
name: "Rel. to Market Cap",
|
|
color,
|
|
unit: Unit.pctMcap,
|
|
}),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Sentiment series
|
|
* @param {{ unrealized: UnrealizedPattern }} tree
|
|
* @returns {AnyFetchedSeriesBlueprint[]}
|
|
*/
|
|
function sentimentSeries(tree) {
|
|
return [
|
|
baseline({
|
|
metric: tree.unrealized.netSentiment,
|
|
name: "Net Sentiment",
|
|
unit: Unit.usd,
|
|
}),
|
|
line({
|
|
metric: tree.unrealized.greedIndex,
|
|
name: "Greed Index",
|
|
color: colors.profit,
|
|
unit: Unit.usd,
|
|
defaultActive: false,
|
|
}),
|
|
line({
|
|
metric: tree.unrealized.painIndex,
|
|
name: "Pain Index",
|
|
color: colors.loss,
|
|
unit: Unit.usd,
|
|
defaultActive: false,
|
|
}),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Sentiment chart for single cohort
|
|
* @param {{ unrealized: UnrealizedPattern }} tree
|
|
* @param {(metric: string) => string} title
|
|
* @returns {PartialChartOption}
|
|
*/
|
|
function sentimentChart(tree, title) {
|
|
return {
|
|
name: "Sentiment",
|
|
title: title("Market Sentiment"),
|
|
bottom: sentimentSeries(tree),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Volume subfolder for single cohort
|
|
* @param {{ realized: AnyRealizedPattern }} tree
|
|
* @param {(metric: string) => string} title
|
|
* @returns {PartialOptionsGroup}
|
|
*/
|
|
function volumeSubfolder(tree, title) {
|
|
return { name: "Volume", tree: sentInPnlTree(tree, title) };
|
|
}
|
|
|
|
// ============================================================================
|
|
// Realized P&L Builders
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Realized P&L sum series
|
|
* @param {{ realized: AnyRealizedPattern }} tree
|
|
* @returns {AnyFetchedSeriesBlueprint[]}
|
|
*/
|
|
function realizedPnlSum(tree) {
|
|
const r = tree.realized;
|
|
return [
|
|
line({
|
|
metric: r.realizedProfit7dEma,
|
|
name: "Profit 7d EMA",
|
|
color: colors.profit,
|
|
unit: Unit.usd,
|
|
}),
|
|
line({
|
|
metric: r.realizedLoss7dEma,
|
|
name: "Loss 7d EMA",
|
|
color: colors.loss,
|
|
unit: Unit.usd,
|
|
}),
|
|
dots({
|
|
metric: r.realizedProfit.height,
|
|
name: "Profit",
|
|
color: colors.profit,
|
|
unit: Unit.usd,
|
|
defaultActive: false,
|
|
}),
|
|
dots({
|
|
metric: r.negRealizedLoss,
|
|
name: "Negative Loss",
|
|
color: colors.loss,
|
|
unit: Unit.usd,
|
|
defaultActive: false,
|
|
}),
|
|
dots({
|
|
metric: r.realizedLoss.height,
|
|
name: "Loss",
|
|
color: colors.loss,
|
|
unit: Unit.usd,
|
|
defaultActive: false,
|
|
}),
|
|
dots({
|
|
metric: r.realizedValue,
|
|
name: "Value",
|
|
color: colors.default,
|
|
unit: Unit.usd,
|
|
defaultActive: false,
|
|
}),
|
|
baseline({
|
|
metric: r.realizedProfitRelToRealizedCap,
|
|
name: "Profit",
|
|
color: colors.profit,
|
|
unit: Unit.pctRcap,
|
|
}),
|
|
baseline({
|
|
metric: r.realizedLossRelToRealizedCap,
|
|
name: "Loss",
|
|
color: colors.loss,
|
|
unit: Unit.pctRcap,
|
|
}),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Realized Net P&L sum series
|
|
* @param {{ realized: AnyRealizedPattern }} tree
|
|
* @returns {AnyFetchedSeriesBlueprint[]}
|
|
*/
|
|
function realizedNetPnlSum(tree) {
|
|
const r = tree.realized;
|
|
return [
|
|
baseline({
|
|
metric: r.netRealizedPnl7dEma,
|
|
name: "Net 7d EMA",
|
|
unit: Unit.usd,
|
|
}),
|
|
dotsBaseline({
|
|
metric: r.netRealizedPnl.height,
|
|
name: "Net",
|
|
unit: Unit.usd,
|
|
defaultActive: false,
|
|
}),
|
|
baseline({
|
|
metric: r.netRealizedPnlRelToRealizedCap,
|
|
name: "Net",
|
|
unit: Unit.pctRcap,
|
|
}),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Realized P&L cumulative series
|
|
* @param {{ realized: AnyRealizedPattern }} tree
|
|
* @returns {AnyFetchedSeriesBlueprint[]}
|
|
*/
|
|
function realizedPnlCumulative(tree) {
|
|
const r = tree.realized;
|
|
return [
|
|
line({
|
|
metric: r.realizedProfit.cumulative,
|
|
name: "Profit",
|
|
color: colors.profit,
|
|
unit: Unit.usd,
|
|
}),
|
|
line({
|
|
metric: r.realizedLoss.cumulative,
|
|
name: "Loss",
|
|
color: colors.loss,
|
|
unit: Unit.usd,
|
|
}),
|
|
line({
|
|
metric: r.negRealizedLoss,
|
|
name: "Negative Loss",
|
|
color: colors.loss,
|
|
unit: Unit.usd,
|
|
defaultActive: false,
|
|
}),
|
|
baseline({
|
|
metric: r.realizedProfitRelToRealizedCap,
|
|
name: "Profit",
|
|
color: colors.profit,
|
|
unit: Unit.pctRcap,
|
|
}),
|
|
baseline({
|
|
metric: r.realizedLossRelToRealizedCap,
|
|
name: "Loss",
|
|
color: colors.loss,
|
|
unit: Unit.pctRcap,
|
|
}),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Realized Net P&L cumulative series
|
|
* @param {{ realized: AnyRealizedPattern }} tree
|
|
* @returns {AnyFetchedSeriesBlueprint[]}
|
|
*/
|
|
function realizedNetPnlCumulative(tree) {
|
|
const r = tree.realized;
|
|
return [
|
|
baseline({
|
|
metric: r.netRealizedPnl.cumulative,
|
|
name: "Net",
|
|
unit: Unit.usd,
|
|
}),
|
|
baseline({
|
|
metric: r.netRealizedPnlRelToRealizedCap,
|
|
name: "Net",
|
|
unit: Unit.pctRcap,
|
|
}),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Realized 30d change series
|
|
* @param {{ realized: AnyRealizedPattern }} tree
|
|
* @returns {AnyFetchedSeriesBlueprint[]}
|
|
*/
|
|
function realized30dChange(tree) {
|
|
const r = tree.realized;
|
|
return [
|
|
baseline({
|
|
metric: r.netRealizedPnlCumulative30dDelta,
|
|
name: "30d Change",
|
|
unit: Unit.usd,
|
|
}),
|
|
baseline({
|
|
metric: r.netRealizedPnlCumulative30dDeltaRelToMarketCap,
|
|
name: "30d Change",
|
|
unit: Unit.pctMcap,
|
|
}),
|
|
baseline({
|
|
metric: r.netRealizedPnlCumulative30dDeltaRelToRealizedCap,
|
|
name: "30d Change",
|
|
unit: Unit.pctRcap,
|
|
}),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Sent in profit/loss tree
|
|
* @param {{ realized: AnyRealizedPattern }} tree
|
|
* @param {(metric: string) => string} title
|
|
* @returns {PartialOptionsTree}
|
|
*/
|
|
function sentInPnlTree(tree, title) {
|
|
const r = tree.realized;
|
|
return [
|
|
{
|
|
name: "Sum",
|
|
title: title("Sent In Profit & Loss"),
|
|
bottom: [
|
|
...satsBtcUsd({
|
|
pattern: r.sentInProfit14dEma,
|
|
name: "In Profit 14d EMA",
|
|
color: colors.profit,
|
|
defaultActive: false,
|
|
}),
|
|
...satsBtcUsd({
|
|
pattern: r.sentInLoss14dEma,
|
|
name: "In Loss 14d EMA",
|
|
color: colors.loss,
|
|
defaultActive: false,
|
|
}),
|
|
...satsBtcUsdFrom({
|
|
source: r.sentInProfit,
|
|
key: "base",
|
|
name: "In Profit",
|
|
color: colors.profit,
|
|
}),
|
|
...satsBtcUsdFrom({
|
|
source: r.sentInLoss,
|
|
key: "base",
|
|
name: "In Loss",
|
|
color: colors.loss,
|
|
}),
|
|
],
|
|
},
|
|
{
|
|
name: "Cumulative",
|
|
title: title("Cumulative Sent In Profit & Loss"),
|
|
bottom: [
|
|
...satsBtcUsdFrom({
|
|
source: r.sentInProfit,
|
|
key: "cumulative",
|
|
name: "In Profit",
|
|
color: colors.profit,
|
|
}),
|
|
...satsBtcUsdFrom({
|
|
source: r.sentInLoss,
|
|
key: "cumulative",
|
|
name: "In Loss",
|
|
color: colors.loss,
|
|
}),
|
|
],
|
|
},
|
|
];
|
|
}
|
|
|
|
// ============================================================================
|
|
// Rolling Realized Helpers
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Rolling realized value tree for single cohort (available on all realized patterns)
|
|
* @param {AnyRealizedPattern} r
|
|
* @param {(metric: string) => string} title
|
|
* @returns {PartialOptionsTree}
|
|
*/
|
|
function singleRollingRealizedValueTree(r, title) {
|
|
return [
|
|
{
|
|
name: "Compare",
|
|
title: title("Rolling Realized Value"),
|
|
bottom: [
|
|
line({ metric: r.realizedValue24h, name: "24h", color: colors.time._24h, unit: Unit.usd }),
|
|
line({ metric: r.realizedValue7d, name: "7d", color: colors.time._1w, unit: Unit.usd }),
|
|
line({ metric: r.realizedValue30d, name: "30d", color: colors.time._1m, unit: Unit.usd }),
|
|
line({ metric: r.realizedValue1y, name: "1y", color: colors.time._1y, unit: Unit.usd }),
|
|
],
|
|
},
|
|
{ name: "24h", title: title("Realized Value (24h)"), bottom: [line({ metric: r.realizedValue24h, name: "Value", unit: Unit.usd })] },
|
|
{ name: "7d", title: title("Realized Value (7d)"), bottom: [line({ metric: r.realizedValue7d, name: "Value", unit: Unit.usd })] },
|
|
{ name: "30d", title: title("Realized Value (30d)"), bottom: [line({ metric: r.realizedValue30d, name: "Value", unit: Unit.usd })] },
|
|
{ name: "1y", title: title("Realized Value (1y)"), bottom: [line({ metric: r.realizedValue1y, name: "Value", unit: Unit.usd })] },
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Rolling realized tree with P/L for single cohort (for RealizedWithExtras patterns)
|
|
* @param {RealizedWithExtras} r
|
|
* @param {(metric: string) => string} title
|
|
* @returns {PartialOptionsTree}
|
|
*/
|
|
function singleRollingRealizedTreeWithExtras(r, title) {
|
|
return [
|
|
{
|
|
name: "Value",
|
|
tree: singleRollingRealizedValueTree(r, title),
|
|
},
|
|
{
|
|
name: "Profit",
|
|
tree: [
|
|
{
|
|
name: "Compare",
|
|
title: title("Rolling Realized Profit"),
|
|
bottom: [
|
|
line({ metric: r.realizedProfit24h, name: "24h", color: colors.time._24h, unit: Unit.usd }),
|
|
line({ metric: r.realizedProfit7d, name: "7d", color: colors.time._1w, unit: Unit.usd }),
|
|
line({ metric: r.realizedProfit30d, name: "30d", color: colors.time._1m, unit: Unit.usd }),
|
|
line({ metric: r.realizedProfit1y, name: "1y", color: colors.time._1y, unit: Unit.usd }),
|
|
],
|
|
},
|
|
{ name: "24h", title: title("Realized Profit (24h)"), bottom: [line({ metric: r.realizedProfit24h, name: "Profit", color: colors.profit, unit: Unit.usd })] },
|
|
{ name: "7d", title: title("Realized Profit (7d)"), bottom: [line({ metric: r.realizedProfit7d, name: "Profit", color: colors.profit, unit: Unit.usd })] },
|
|
{ name: "30d", title: title("Realized Profit (30d)"), bottom: [line({ metric: r.realizedProfit30d, name: "Profit", color: colors.profit, unit: Unit.usd })] },
|
|
{ name: "1y", title: title("Realized Profit (1y)"), bottom: [line({ metric: r.realizedProfit1y, name: "Profit", color: colors.profit, unit: Unit.usd })] },
|
|
],
|
|
},
|
|
{
|
|
name: "Loss",
|
|
tree: [
|
|
{
|
|
name: "Compare",
|
|
title: title("Rolling Realized Loss"),
|
|
bottom: [
|
|
line({ metric: r.realizedLoss24h, name: "24h", color: colors.time._24h, unit: Unit.usd }),
|
|
line({ metric: r.realizedLoss7d, name: "7d", color: colors.time._1w, unit: Unit.usd }),
|
|
line({ metric: r.realizedLoss30d, name: "30d", color: colors.time._1m, unit: Unit.usd }),
|
|
line({ metric: r.realizedLoss1y, name: "1y", color: colors.time._1y, unit: Unit.usd }),
|
|
],
|
|
},
|
|
{ name: "24h", title: title("Realized Loss (24h)"), bottom: [line({ metric: r.realizedLoss24h, name: "Loss", color: colors.loss, unit: Unit.usd })] },
|
|
{ name: "7d", title: title("Realized Loss (7d)"), bottom: [line({ metric: r.realizedLoss7d, name: "Loss", color: colors.loss, unit: Unit.usd })] },
|
|
{ name: "30d", title: title("Realized Loss (30d)"), bottom: [line({ metric: r.realizedLoss30d, name: "Loss", color: colors.loss, unit: Unit.usd })] },
|
|
{ name: "1y", title: title("Realized Loss (1y)"), bottom: [line({ metric: r.realizedLoss1y, 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.realizedProfitToLossRatio24h, name: "24h", color: colors.time._24h, unit: Unit.ratio }),
|
|
baseline({ metric: r.realizedProfitToLossRatio7d, name: "7d", color: colors.time._1w, unit: Unit.ratio }),
|
|
baseline({ metric: r.realizedProfitToLossRatio30d, name: "30d", color: colors.time._1m, unit: Unit.ratio }),
|
|
baseline({ metric: r.realizedProfitToLossRatio1y, name: "1y", color: colors.time._1y, unit: Unit.ratio }),
|
|
],
|
|
},
|
|
{ name: "24h", title: title("Realized P/L Ratio (24h)"), bottom: [baseline({ metric: r.realizedProfitToLossRatio24h, name: "P/L Ratio", unit: Unit.ratio })] },
|
|
{ name: "7d", title: title("Realized P/L Ratio (7d)"), bottom: [baseline({ metric: r.realizedProfitToLossRatio7d, name: "P/L Ratio", unit: Unit.ratio })] },
|
|
{ name: "30d", title: title("Realized P/L Ratio (30d)"), bottom: [baseline({ metric: r.realizedProfitToLossRatio30d, name: "P/L Ratio", unit: Unit.ratio })] },
|
|
{ name: "1y", title: title("Realized P/L Ratio (1y)"), bottom: [baseline({ metric: r.realizedProfitToLossRatio1y, name: "P/L Ratio", unit: Unit.ratio })] },
|
|
],
|
|
},
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Grouped rolling realized value charts (available on all realized patterns)
|
|
* @param {readonly CohortObject[]} list
|
|
* @param {CohortObject} all
|
|
* @param {(metric: string) => string} title
|
|
* @returns {PartialOptionsTree}
|
|
*/
|
|
function groupedRollingRealizedValueCharts(list, all, title) {
|
|
return [
|
|
{ name: "24h", title: title("Realized Value (24h)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ metric: tree.realized.realizedValue24h, name, color, unit: Unit.usd })) },
|
|
{ name: "7d", title: title("Realized Value (7d)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ metric: tree.realized.realizedValue7d, name, color, unit: Unit.usd })) },
|
|
{ name: "30d", title: title("Realized Value (30d)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ metric: tree.realized.realizedValue30d, name, color, unit: Unit.usd })) },
|
|
{ name: "1y", title: title("Realized Value (1y)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ metric: tree.realized.realizedValue1y, name, color, unit: Unit.usd })) },
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Grouped rolling realized charts with P/L (for RealizedWithExtras cohorts)
|
|
* @param {readonly (CohortAgeRange | CohortLongTerm | CohortAll | CohortFull)[]} list
|
|
* @param {CohortAll} all
|
|
* @param {(metric: string) => string} title
|
|
* @returns {PartialOptionsTree}
|
|
*/
|
|
function groupedRollingRealizedChartsWithExtras(list, all, title) {
|
|
return [
|
|
{
|
|
name: "Value",
|
|
tree: groupedRollingRealizedValueCharts(list, all, title),
|
|
},
|
|
{
|
|
name: "Profit",
|
|
tree: [
|
|
{ name: "24h", title: title("Realized Profit (24h)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ metric: tree.realized.realizedProfit24h, name, color, unit: Unit.usd })) },
|
|
{ name: "7d", title: title("Realized Profit (7d)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ metric: tree.realized.realizedProfit7d, name, color, unit: Unit.usd })) },
|
|
{ name: "30d", title: title("Realized Profit (30d)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ metric: tree.realized.realizedProfit30d, name, color, unit: Unit.usd })) },
|
|
{ name: "1y", title: title("Realized Profit (1y)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ metric: tree.realized.realizedProfit1y, 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.realizedLoss24h, name, color, unit: Unit.usd })) },
|
|
{ name: "7d", title: title("Realized Loss (7d)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ metric: tree.realized.realizedLoss7d, name, color, unit: Unit.usd })) },
|
|
{ name: "30d", title: title("Realized Loss (30d)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ metric: tree.realized.realizedLoss30d, name, color, unit: Unit.usd })) },
|
|
{ name: "1y", title: title("Realized Loss (1y)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ metric: tree.realized.realizedLoss1y, name, color, unit: Unit.usd })) },
|
|
],
|
|
},
|
|
{
|
|
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.realizedProfitToLossRatio24h, 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.realizedProfitToLossRatio7d, 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.realizedProfitToLossRatio30d, 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.realizedProfitToLossRatio1y, name, color, unit: Unit.ratio })) },
|
|
],
|
|
},
|
|
];
|
|
}
|
|
|
|
// ============================================================================
|
|
// Realized Subfolder Builders
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Base realized subfolder (no P/L ratio)
|
|
* @param {{ realized: AnyRealizedPattern }} tree
|
|
* @param {(metric: string) => string} title
|
|
* @param {PartialOptionsTree} [rollingTree]
|
|
* @returns {PartialOptionsGroup}
|
|
*/
|
|
function realizedSubfolder(tree, title, rollingTree) {
|
|
const r = tree.realized;
|
|
return {
|
|
name: "Realized",
|
|
tree: [
|
|
{
|
|
name: "P&L",
|
|
title: title("Realized P&L"),
|
|
bottom: realizedPnlSum(tree),
|
|
},
|
|
{
|
|
name: "Net",
|
|
title: title("Net Realized P&L"),
|
|
bottom: realizedNetPnlSum(tree),
|
|
},
|
|
{
|
|
name: "30d Change",
|
|
title: title("Realized P&L 30d Change"),
|
|
bottom: realized30dChange(tree),
|
|
},
|
|
{
|
|
name: "Total",
|
|
title: title("Total Realized P&L"),
|
|
bottom: [
|
|
line({
|
|
metric: r.totalRealizedPnl,
|
|
name: "Total",
|
|
unit: Unit.usd,
|
|
color: colors.bitcoin,
|
|
}),
|
|
],
|
|
},
|
|
{
|
|
name: "Peak Regret",
|
|
title: title("Realized Peak Regret"),
|
|
bottom: [
|
|
line({
|
|
metric: r.peakRegret.height,
|
|
name: "Peak Regret",
|
|
unit: Unit.usd,
|
|
}),
|
|
],
|
|
},
|
|
{
|
|
name: "Rolling",
|
|
tree: rollingTree ?? singleRollingRealizedValueTree(r, title),
|
|
},
|
|
{
|
|
name: "Cumulative",
|
|
tree: [
|
|
{
|
|
name: "P&L",
|
|
title: title("Cumulative Realized P&L"),
|
|
bottom: realizedPnlCumulative(tree),
|
|
},
|
|
{
|
|
name: "Net",
|
|
title: title("Cumulative Net Realized P&L"),
|
|
bottom: realizedNetPnlCumulative(tree),
|
|
},
|
|
{
|
|
name: "Peak Regret",
|
|
title: title("Cumulative Realized Peak Regret"),
|
|
bottom: [
|
|
line({
|
|
metric: r.peakRegret.cumulative,
|
|
name: "Peak Regret",
|
|
unit: Unit.usd,
|
|
}),
|
|
line({
|
|
metric: r.peakRegretRelToRealizedCap,
|
|
name: "Peak Regret",
|
|
unit: Unit.pctRcap,
|
|
}),
|
|
],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Realized subfolder with P/L ratio and rolling P/L
|
|
* @param {{ realized: RealizedWithExtras }} tree
|
|
* @param {(metric: string) => string} title
|
|
* @returns {PartialOptionsGroup}
|
|
*/
|
|
function realizedSubfolderWithExtras(tree, title) {
|
|
const r = tree.realized;
|
|
const base = realizedSubfolder(tree, title, singleRollingRealizedTreeWithExtras(r, title));
|
|
// Insert P/L Ratio after Total (index 3)
|
|
base.tree.splice(4, 0, {
|
|
name: "P/L Ratio",
|
|
title: title("Realized Profit/Loss Ratio"),
|
|
bottom: [
|
|
baseline({
|
|
metric: r.realizedProfitToLossRatio1y,
|
|
name: "P/L Ratio",
|
|
unit: Unit.ratio,
|
|
}),
|
|
],
|
|
});
|
|
return base;
|
|
}
|
|
|
|
// ============================================================================
|
|
// Single Cohort Section Builders
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Basic profitability section (USD only unrealized)
|
|
* @param {{ cohort: UtxoCohortObject | CohortWithoutRelative, title: (metric: string) => string }} args
|
|
* @returns {PartialOptionsGroup}
|
|
*/
|
|
export function createProfitabilitySection({ cohort, title }) {
|
|
const { tree } = cohort;
|
|
const m = getUnrealizedMetrics(tree);
|
|
return {
|
|
name: "Profitability",
|
|
tree: [
|
|
{
|
|
name: "Unrealized",
|
|
tree: [
|
|
{
|
|
name: "P&L",
|
|
title: title("Unrealized P&L"),
|
|
bottom: unrealizedUsd(m),
|
|
},
|
|
{
|
|
name: "Net P&L",
|
|
title: title("Net Unrealized P&L"),
|
|
bottom: netUnrealizedUsd(m.net),
|
|
},
|
|
],
|
|
},
|
|
realizedSubfolder(tree, title),
|
|
volumeSubfolder(tree, title),
|
|
{
|
|
name: "Invested Capital",
|
|
tree: [
|
|
{
|
|
name: "Absolute",
|
|
title: title("Invested Capital In Profit & Loss"),
|
|
bottom: investedCapitalAbsolute(tree),
|
|
},
|
|
],
|
|
},
|
|
sentimentChart(tree, 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;
|
|
const m = getUnrealizedMetrics(tree);
|
|
return {
|
|
name: "Profitability",
|
|
tree: [
|
|
{
|
|
name: "Unrealized",
|
|
tree: [
|
|
{
|
|
name: "P&L",
|
|
title: title("Unrealized P&L"),
|
|
bottom: unrealizedUsd(m),
|
|
},
|
|
{
|
|
name: "Net P&L",
|
|
title: title("Net Unrealized P&L"),
|
|
bottom: netUnrealizedUsd(m.net),
|
|
},
|
|
],
|
|
},
|
|
realizedSubfolder(tree, title),
|
|
volumeSubfolder(tree, title),
|
|
{
|
|
name: "Invested Capital",
|
|
title: title("Invested Capital In Profit & Loss"),
|
|
bottom: investedCapitalWithPct(tree),
|
|
},
|
|
sentimentChart(tree, title),
|
|
],
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Section for ageRange cohorts (Own M.Cap + Own P&L + peak regret)
|
|
* @param {{ cohort: CohortAgeRange, title: (metric: string) => string }} args
|
|
* @returns {PartialOptionsGroup}
|
|
*/
|
|
export function createProfitabilitySectionWithInvestedCapitalPct({
|
|
cohort,
|
|
title,
|
|
}) {
|
|
const { tree, color } = cohort;
|
|
const m = getUnrealizedMetrics(tree);
|
|
return {
|
|
name: "Profitability",
|
|
tree: [
|
|
{
|
|
name: "Unrealized",
|
|
tree: [
|
|
{
|
|
name: "P&L",
|
|
title: title("Unrealized P&L"),
|
|
bottom: unrealizedWithOwnMarketCap(m, tree.relative),
|
|
},
|
|
{
|
|
name: "Net P&L",
|
|
title: title("Net Unrealized P&L"),
|
|
bottom: netUnrealizedWithOwnMarketCap(m.net, tree.relative),
|
|
},
|
|
{
|
|
name: "Peak Regret",
|
|
title: title("Unrealized Peak Regret"),
|
|
bottom: peakRegretAbsolute(tree, color),
|
|
},
|
|
],
|
|
},
|
|
realizedSubfolderWithExtras(tree, title),
|
|
volumeSubfolder(tree, title),
|
|
{
|
|
name: "Invested Capital",
|
|
title: title("Invested Capital In Profit & Loss"),
|
|
bottom: investedCapitalWithPct(tree),
|
|
},
|
|
sentimentChart(tree, title),
|
|
],
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Section with NUPL (basic cohorts with market cap)
|
|
* @param {{ cohort: CohortBasicWithMarketCap, title: (metric: string) => string }} args
|
|
* @returns {PartialOptionsGroup}
|
|
*/
|
|
export function createProfitabilitySectionWithNupl({ cohort, title }) {
|
|
const { tree } = cohort;
|
|
const m = getUnrealizedMetrics(tree);
|
|
return {
|
|
name: "Profitability",
|
|
tree: [
|
|
{
|
|
name: "Unrealized",
|
|
tree: [
|
|
{
|
|
name: "P&L",
|
|
title: title("Unrealized P&L"),
|
|
bottom: unrealizedWithMarketCap(m, tree.relative),
|
|
},
|
|
{
|
|
name: "Net P&L",
|
|
title: title("Net Unrealized P&L"),
|
|
bottom: netUnrealizedWithMarketCap(m.net, tree.relative),
|
|
},
|
|
{
|
|
name: "NUPL",
|
|
title: title("NUPL"),
|
|
bottom: nuplSeries(tree.relative),
|
|
},
|
|
],
|
|
},
|
|
realizedSubfolder(tree, title),
|
|
volumeSubfolder(tree, title),
|
|
{
|
|
name: "Invested Capital",
|
|
title: title("Invested Capital In Profit & Loss"),
|
|
bottom: investedCapitalWithPct(tree),
|
|
},
|
|
sentimentChart(tree, title),
|
|
],
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Section for LongTerm cohort
|
|
* @param {{ cohort: CohortLongTerm, title: (metric: string) => string }} args
|
|
* @returns {PartialOptionsGroup}
|
|
*/
|
|
export function createProfitabilitySectionLongTerm({ cohort, title }) {
|
|
const { tree, color } = cohort;
|
|
const m = getUnrealizedMetrics(tree);
|
|
return {
|
|
name: "Profitability",
|
|
tree: [
|
|
{
|
|
name: "Unrealized",
|
|
tree: [
|
|
{
|
|
name: "P&L",
|
|
title: title("Unrealized P&L"),
|
|
bottom: unrealizedLongTerm(m, tree.relative),
|
|
},
|
|
{
|
|
name: "Net P&L",
|
|
title: title("Net Unrealized P&L"),
|
|
bottom: netUnrealizedWithOwnMarketCap(m.net, tree.relative),
|
|
},
|
|
{
|
|
name: "NUPL",
|
|
title: title("NUPL"),
|
|
bottom: nuplSeries(tree.relative),
|
|
},
|
|
{
|
|
name: "Peak Regret",
|
|
title: title("Unrealized Peak Regret"),
|
|
bottom: peakRegretWithMarketCap(tree, color),
|
|
},
|
|
],
|
|
},
|
|
realizedSubfolderWithExtras(tree, title),
|
|
volumeSubfolder(tree, title),
|
|
{
|
|
name: "Invested Capital",
|
|
title: title("Invested Capital In Profit & Loss"),
|
|
bottom: investedCapitalWithPct(tree),
|
|
},
|
|
sentimentChart(tree, title),
|
|
],
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Section for Full cohorts (all relative metrics)
|
|
* @param {{ cohort: CohortFull, title: (metric: string) => string }} args
|
|
* @returns {PartialOptionsGroup}
|
|
*/
|
|
export function createProfitabilitySectionFull({ cohort, title }) {
|
|
const { tree, color } = cohort;
|
|
const m = getUnrealizedMetrics(tree);
|
|
return {
|
|
name: "Profitability",
|
|
tree: [
|
|
{
|
|
name: "Unrealized",
|
|
tree: [
|
|
{
|
|
name: "P&L",
|
|
title: title("Unrealized P&L"),
|
|
bottom: unrealizedFull(m, tree.relative),
|
|
},
|
|
{
|
|
name: "Net P&L",
|
|
title: title("Net Unrealized P&L"),
|
|
bottom: netUnrealizedFull(m.net, tree.relative),
|
|
},
|
|
{
|
|
name: "NUPL",
|
|
title: title("NUPL"),
|
|
bottom: nuplSeries(tree.relative),
|
|
},
|
|
{
|
|
name: "Peak Regret",
|
|
title: title("Unrealized Peak Regret"),
|
|
bottom: peakRegretWithMarketCap(tree, color),
|
|
},
|
|
],
|
|
},
|
|
realizedSubfolderWithExtras(tree, title),
|
|
volumeSubfolder(tree, title),
|
|
{
|
|
name: "Invested Capital",
|
|
title: title("Invested Capital In Profit & Loss"),
|
|
bottom: investedCapitalWithPct(tree),
|
|
},
|
|
sentimentChart(tree, title),
|
|
],
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Section for "all" cohort
|
|
* @param {{ cohort: CohortAll, title: (metric: string) => string }} args
|
|
* @returns {PartialOptionsGroup}
|
|
*/
|
|
export function createProfitabilitySectionAll({ cohort, title }) {
|
|
const { tree, color } = cohort;
|
|
const m = getUnrealizedMetrics(tree);
|
|
return {
|
|
name: "Profitability",
|
|
tree: [
|
|
{
|
|
name: "Unrealized",
|
|
tree: [
|
|
{
|
|
name: "P&L",
|
|
title: title("Unrealized P&L"),
|
|
bottom: unrealizedAll(m, tree.relative),
|
|
},
|
|
{
|
|
name: "Net P&L",
|
|
title: title("Net Unrealized P&L"),
|
|
bottom: netUnrealizedAll(m.net, tree.relative),
|
|
},
|
|
{
|
|
name: "NUPL",
|
|
title: title("NUPL"),
|
|
bottom: nuplSeries(tree.relative),
|
|
},
|
|
{
|
|
name: "Peak Regret",
|
|
title: title("Unrealized Peak Regret"),
|
|
bottom: peakRegretWithMarketCap(tree, color),
|
|
},
|
|
],
|
|
},
|
|
realizedSubfolderWithExtras(tree, title),
|
|
volumeSubfolder(tree, title),
|
|
{
|
|
name: "Invested Capital",
|
|
title: title("Invested Capital In Profit & Loss"),
|
|
bottom: investedCapitalWithPct(tree),
|
|
},
|
|
sentimentChart(tree, title),
|
|
],
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Section with Peak Regret + NUPL (minAge cohorts)
|
|
* @param {{ cohort: CohortMinAge, title: (metric: string) => string }} args
|
|
* @returns {PartialOptionsGroup}
|
|
*/
|
|
export function createProfitabilitySectionWithPeakRegret({ cohort, title }) {
|
|
const { tree, color } = cohort;
|
|
const m = getUnrealizedMetrics(tree);
|
|
return {
|
|
name: "Profitability",
|
|
tree: [
|
|
{
|
|
name: "Unrealized",
|
|
tree: [
|
|
{
|
|
name: "P&L",
|
|
title: title("Unrealized P&L"),
|
|
bottom: unrealizedWithMarketCap(m, tree.relative),
|
|
},
|
|
{
|
|
name: "Net P&L",
|
|
title: title("Net Unrealized P&L"),
|
|
bottom: netUnrealizedWithMarketCap(m.net, tree.relative),
|
|
},
|
|
{
|
|
name: "NUPL",
|
|
title: title("NUPL"),
|
|
bottom: nuplSeries(tree.relative),
|
|
},
|
|
{
|
|
name: "Peak Regret",
|
|
title: title("Unrealized Peak Regret"),
|
|
bottom: peakRegretWithMarketCap(tree, color),
|
|
},
|
|
],
|
|
},
|
|
realizedSubfolder(tree, title),
|
|
volumeSubfolder(tree, title),
|
|
{
|
|
name: "Invested Capital",
|
|
title: title("Invested Capital In Profit & Loss"),
|
|
bottom: investedCapitalWithPct(tree),
|
|
},
|
|
sentimentChart(tree, title),
|
|
],
|
|
};
|
|
}
|
|
|
|
// ============================================================================
|
|
// Grouped Cohort Helpers
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Grouped P&L charts (USD only)
|
|
* @param {readonly CohortObject[]} list
|
|
* @param {CohortAll} all
|
|
* @param {(metric: string) => string} title
|
|
* @returns {PartialOptionsTree}
|
|
*/
|
|
function groupedPnlCharts(list, all, title) {
|
|
return [
|
|
{
|
|
name: "Profit",
|
|
title: title("Unrealized Profit"),
|
|
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
|
line({
|
|
metric: tree.unrealized.unrealizedProfit,
|
|
name,
|
|
color,
|
|
unit: Unit.usd,
|
|
}),
|
|
),
|
|
},
|
|
{
|
|
name: "Loss",
|
|
title: title("Unrealized Loss"),
|
|
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
|
line({
|
|
metric: tree.unrealized.negUnrealizedLoss,
|
|
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.netUnrealizedPnl,
|
|
name,
|
|
color,
|
|
unit: Unit.usd,
|
|
}),
|
|
),
|
|
},
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Grouped P&L with % of Market Cap
|
|
* @param {readonly (CohortFull | CohortBasicWithMarketCap | CohortMinAge | 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.unrealizedProfit,
|
|
name,
|
|
color,
|
|
unit: Unit.usd,
|
|
}),
|
|
),
|
|
...mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
|
baseline({
|
|
metric: tree.relative.unrealizedProfitRelToMarketCap,
|
|
name,
|
|
color,
|
|
unit: Unit.pctMcap,
|
|
}),
|
|
),
|
|
],
|
|
},
|
|
{
|
|
name: "Loss",
|
|
title: title("Unrealized Loss"),
|
|
bottom: [
|
|
...mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
|
line({
|
|
metric: tree.unrealized.negUnrealizedLoss,
|
|
name,
|
|
color,
|
|
unit: Unit.usd,
|
|
}),
|
|
),
|
|
...mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
|
baseline({
|
|
metric: tree.relative.negUnrealizedLossRelToMarketCap,
|
|
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.netUnrealizedPnl,
|
|
name,
|
|
color,
|
|
unit: Unit.usd,
|
|
}),
|
|
),
|
|
...mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
|
baseline({
|
|
metric: tree.relative.netUnrealizedPnlRelToMarketCap,
|
|
name,
|
|
color,
|
|
unit: Unit.pctMcap,
|
|
}),
|
|
),
|
|
],
|
|
},
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Grouped P&L with % of Own Market Cap
|
|
* @param {readonly CohortAgeRange[]} 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.unrealizedProfit,
|
|
name,
|
|
color,
|
|
unit: Unit.usd,
|
|
}),
|
|
),
|
|
// OwnMarketCap properties don't exist on CohortAll - use mapCohorts
|
|
...mapCohorts(list, ({ name, color, tree }) =>
|
|
line({
|
|
metric: tree.relative.unrealizedProfitRelToOwnMarketCap,
|
|
name,
|
|
color,
|
|
unit: Unit.pctOwnMcap,
|
|
}),
|
|
),
|
|
...mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
|
line({
|
|
metric: tree.relative.unrealizedProfitRelToOwnTotalUnrealizedPnl,
|
|
name,
|
|
color,
|
|
unit: Unit.pctOwnPnl,
|
|
}),
|
|
),
|
|
],
|
|
},
|
|
{
|
|
name: "Loss",
|
|
title: title("Unrealized Loss"),
|
|
bottom: [
|
|
...mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
|
line({
|
|
metric: tree.unrealized.negUnrealizedLoss,
|
|
name,
|
|
color,
|
|
unit: Unit.usd,
|
|
}),
|
|
),
|
|
// OwnMarketCap properties don't exist on CohortAll - use mapCohorts
|
|
...mapCohorts(list, ({ name, color, tree }) =>
|
|
line({
|
|
metric: tree.relative.negUnrealizedLossRelToOwnMarketCap,
|
|
name,
|
|
color,
|
|
unit: Unit.pctOwnMcap,
|
|
}),
|
|
),
|
|
...mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
|
line({
|
|
metric: tree.relative.negUnrealizedLossRelToOwnTotalUnrealizedPnl,
|
|
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.netUnrealizedPnl,
|
|
name,
|
|
color,
|
|
unit: Unit.usd,
|
|
}),
|
|
),
|
|
// OwnMarketCap properties don't exist on CohortAll - use mapCohorts
|
|
...mapCohorts(list, ({ name, color, tree }) =>
|
|
baseline({
|
|
metric: tree.relative.netUnrealizedPnlRelToOwnMarketCap,
|
|
name,
|
|
color,
|
|
unit: Unit.pctOwnMcap,
|
|
}),
|
|
),
|
|
...mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
|
baseline({
|
|
metric: tree.relative.netUnrealizedPnlRelToOwnTotalUnrealizedPnl,
|
|
name,
|
|
color,
|
|
unit: Unit.pctOwnPnl,
|
|
}),
|
|
),
|
|
],
|
|
},
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Grouped P&L for LongTerm cohorts
|
|
* @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.unrealizedProfit,
|
|
name,
|
|
color,
|
|
unit: Unit.usd,
|
|
}),
|
|
),
|
|
// OwnMarketCap properties don't exist on CohortAll - use mapCohorts
|
|
...mapCohorts(list, ({ name, color, tree }) =>
|
|
line({
|
|
metric: tree.relative.unrealizedProfitRelToOwnMarketCap,
|
|
name,
|
|
color,
|
|
unit: Unit.pctOwnMcap,
|
|
}),
|
|
),
|
|
...mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
|
line({
|
|
metric: tree.relative.unrealizedProfitRelToOwnTotalUnrealizedPnl,
|
|
name,
|
|
color,
|
|
unit: Unit.pctOwnPnl,
|
|
}),
|
|
),
|
|
],
|
|
},
|
|
{
|
|
name: "Loss",
|
|
title: title("Unrealized Loss"),
|
|
bottom: [
|
|
...mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
|
line({
|
|
metric: tree.unrealized.negUnrealizedLoss,
|
|
name,
|
|
color,
|
|
unit: Unit.usd,
|
|
}),
|
|
),
|
|
...mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
|
line({
|
|
metric: tree.relative.unrealizedLossRelToMarketCap,
|
|
name,
|
|
color,
|
|
unit: Unit.pctMcap,
|
|
}),
|
|
),
|
|
// OwnMarketCap properties don't exist on CohortAll - use mapCohorts
|
|
...mapCohorts(list, ({ name, color, tree }) =>
|
|
line({
|
|
metric: tree.relative.negUnrealizedLossRelToOwnMarketCap,
|
|
name,
|
|
color,
|
|
unit: Unit.pctOwnMcap,
|
|
}),
|
|
),
|
|
...mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
|
line({
|
|
metric: tree.relative.negUnrealizedLossRelToOwnTotalUnrealizedPnl,
|
|
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.netUnrealizedPnl,
|
|
name,
|
|
color,
|
|
unit: Unit.usd,
|
|
}),
|
|
),
|
|
// OwnMarketCap properties don't exist on CohortAll - use mapCohorts
|
|
...mapCohorts(list, ({ name, color, tree }) =>
|
|
baseline({
|
|
metric: tree.relative.netUnrealizedPnlRelToOwnMarketCap,
|
|
name,
|
|
color,
|
|
unit: Unit.pctOwnMcap,
|
|
}),
|
|
),
|
|
...mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
|
baseline({
|
|
metric: tree.relative.netUnrealizedPnlRelToOwnTotalUnrealizedPnl,
|
|
name,
|
|
color,
|
|
unit: Unit.pctOwnPnl,
|
|
}),
|
|
),
|
|
],
|
|
},
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Grouped invested capital (absolute only)
|
|
* @param {readonly CohortObject[]} list
|
|
* @param {CohortAll} all
|
|
* @param {(metric: string) => string} title
|
|
* @returns {PartialOptionsTree}
|
|
*/
|
|
function groupedInvestedCapitalAbsolute(list, all, title) {
|
|
return [
|
|
{
|
|
name: "In Profit",
|
|
title: title("Invested Capital In Profit"),
|
|
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
|
line({
|
|
metric: tree.unrealized.investedCapitalInProfit,
|
|
name,
|
|
color,
|
|
unit: Unit.usd,
|
|
}),
|
|
),
|
|
},
|
|
{
|
|
name: "In Loss",
|
|
title: title("Invested Capital In Loss"),
|
|
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
|
line({
|
|
metric: tree.unrealized.investedCapitalInLoss,
|
|
name,
|
|
color,
|
|
unit: Unit.usd,
|
|
}),
|
|
),
|
|
},
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Grouped invested capital with %
|
|
* @param {readonly (CohortBasicWithoutMarketCap | CohortAgeRange | CohortFull | CohortBasicWithMarketCap | CohortLongTerm | CohortMinAge)[]} list
|
|
* @param {CohortAll} all
|
|
* @param {(metric: string) => string} title
|
|
* @returns {PartialOptionsTree}
|
|
*/
|
|
function groupedInvestedCapital(list, all, title) {
|
|
return [
|
|
{
|
|
name: "In Profit",
|
|
title: title("Invested Capital In Profit"),
|
|
bottom: [
|
|
...mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
|
line({
|
|
metric: tree.unrealized.investedCapitalInProfit,
|
|
name,
|
|
color,
|
|
unit: Unit.usd,
|
|
}),
|
|
),
|
|
...mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
|
baseline({
|
|
metric: tree.relative.investedCapitalInProfitPct,
|
|
name,
|
|
color,
|
|
unit: Unit.pctOwnRcap,
|
|
}),
|
|
),
|
|
...priceLines({ numbers: [100, 50], unit: Unit.pctOwnRcap }),
|
|
],
|
|
},
|
|
{
|
|
name: "In Loss",
|
|
title: title("Invested Capital In Loss"),
|
|
bottom: [
|
|
...mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
|
line({
|
|
metric: tree.unrealized.investedCapitalInLoss,
|
|
name,
|
|
color,
|
|
unit: Unit.usd,
|
|
}),
|
|
),
|
|
...mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
|
baseline({
|
|
metric: tree.relative.investedCapitalInLossPct,
|
|
name,
|
|
color,
|
|
unit: Unit.pctOwnRcap,
|
|
}),
|
|
),
|
|
...priceLines({ numbers: [100, 50], unit: Unit.pctOwnRcap }),
|
|
],
|
|
},
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Grouped realized P&L sum
|
|
* @param {readonly CohortObject[]} list
|
|
* @param {CohortAll} 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.realizedProfit.height,
|
|
name,
|
|
color,
|
|
unit: Unit.usd,
|
|
}),
|
|
),
|
|
},
|
|
{
|
|
name: "Loss",
|
|
title: title("Realized Loss"),
|
|
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
|
line({
|
|
metric: tree.realized.negRealizedLoss,
|
|
name,
|
|
color,
|
|
unit: Unit.usd,
|
|
}),
|
|
),
|
|
},
|
|
{
|
|
name: "Total",
|
|
title: title("Total Realized P&L"),
|
|
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
|
line({
|
|
metric: tree.realized.totalRealizedPnl,
|
|
name,
|
|
color,
|
|
unit: Unit.usd,
|
|
}),
|
|
),
|
|
},
|
|
{
|
|
name: "Value",
|
|
title: title("Realized Value"),
|
|
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
|
line({
|
|
metric: tree.realized.realizedValue,
|
|
name,
|
|
color,
|
|
unit: Unit.usd,
|
|
}),
|
|
),
|
|
},
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Grouped realized P&L sum with P/L ratio
|
|
* @param {readonly (CohortAgeRange | CohortLongTerm | CohortAll | CohortFull)[]} list
|
|
* @param {CohortAll} all
|
|
* @param {(metric: string) => string} title
|
|
* @returns {PartialOptionsTree}
|
|
*/
|
|
function groupedRealizedPnlSumWithExtras(list, all, title) {
|
|
return [
|
|
...groupedRealizedPnlSum(list, all, title),
|
|
{
|
|
name: "P/L Ratio",
|
|
title: title("Realized Profit/Loss Ratio"),
|
|
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
|
baseline({
|
|
metric: tree.realized.realizedProfitToLossRatio1y,
|
|
name,
|
|
color,
|
|
unit: Unit.ratio,
|
|
}),
|
|
),
|
|
},
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Grouped realized cumulative
|
|
* @param {readonly CohortObject[]} list
|
|
* @param {CohortAll} all
|
|
* @param {(metric: string) => string} title
|
|
* @returns {PartialOptionsTree}
|
|
*/
|
|
function groupedRealizedPnlCumulative(list, all, title) {
|
|
return [
|
|
{
|
|
name: "Profit",
|
|
title: title("Cumulative Realized Profit"),
|
|
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
|
line({
|
|
metric: tree.realized.realizedProfit.cumulative,
|
|
name,
|
|
color,
|
|
unit: Unit.usd,
|
|
}),
|
|
),
|
|
},
|
|
{
|
|
name: "Loss",
|
|
title: title("Cumulative Realized Loss"),
|
|
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
|
line({
|
|
metric: tree.realized.realizedLoss.cumulative,
|
|
name,
|
|
color,
|
|
unit: Unit.usd,
|
|
}),
|
|
),
|
|
},
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Grouped sent in P/L
|
|
* @param {readonly CohortObject[]} list
|
|
* @param {CohortAll} all
|
|
* @param {(metric: string) => string} title
|
|
* @returns {PartialOptionsTree}
|
|
*/
|
|
function groupedSentInPnl(list, all, title) {
|
|
return [
|
|
{
|
|
name: "Sum",
|
|
tree: [
|
|
{
|
|
name: "In Profit",
|
|
title: title("Sent In Profit"),
|
|
bottom: [
|
|
...flatMapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
|
satsBtcUsd({
|
|
pattern: tree.realized.sentInProfit14dEma,
|
|
name: `${name} 14d EMA`,
|
|
color,
|
|
defaultActive: false,
|
|
}),
|
|
),
|
|
...flatMapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
|
satsBtcUsdFrom({
|
|
source: tree.realized.sentInProfit,
|
|
key: "base",
|
|
name,
|
|
color,
|
|
}),
|
|
),
|
|
],
|
|
},
|
|
{
|
|
name: "In Loss",
|
|
title: title("Sent In Loss"),
|
|
bottom: [
|
|
...flatMapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
|
satsBtcUsd({
|
|
pattern: tree.realized.sentInLoss14dEma,
|
|
name: `${name} 14d EMA`,
|
|
color,
|
|
defaultActive: false,
|
|
}),
|
|
),
|
|
...flatMapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
|
satsBtcUsdFrom({
|
|
source: tree.realized.sentInLoss,
|
|
key: "base",
|
|
name,
|
|
color,
|
|
}),
|
|
),
|
|
],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
name: "Cumulative",
|
|
tree: [
|
|
{
|
|
name: "In Profit",
|
|
title: title("Cumulative Sent In Profit"),
|
|
bottom: flatMapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
|
satsBtcUsdFrom({
|
|
source: tree.realized.sentInProfit,
|
|
key: "cumulative",
|
|
name,
|
|
color,
|
|
}),
|
|
),
|
|
},
|
|
{
|
|
name: "In Loss",
|
|
title: title("Cumulative Sent In Loss"),
|
|
bottom: flatMapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
|
satsBtcUsdFrom({
|
|
source: tree.realized.sentInLoss,
|
|
key: "cumulative",
|
|
name,
|
|
color,
|
|
}),
|
|
),
|
|
},
|
|
],
|
|
},
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Grouped sentiment
|
|
* @param {readonly CohortObject[]} 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.netSentiment,
|
|
name,
|
|
color,
|
|
unit: Unit.usd,
|
|
}),
|
|
),
|
|
},
|
|
{
|
|
name: "Greed",
|
|
title: title("Greed Index"),
|
|
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
|
line({
|
|
metric: tree.unrealized.greedIndex,
|
|
name,
|
|
color,
|
|
unit: Unit.usd,
|
|
}),
|
|
),
|
|
},
|
|
{
|
|
name: "Pain",
|
|
title: title("Pain Index"),
|
|
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
|
line({
|
|
metric: tree.unrealized.painIndex,
|
|
name,
|
|
color,
|
|
unit: Unit.usd,
|
|
}),
|
|
),
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Grouped realized subfolder
|
|
* @param {readonly CohortObject[]} list
|
|
* @param {CohortAll} 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: "Net",
|
|
title: title("Net Realized P&L"),
|
|
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
|
baseline({
|
|
metric: tree.realized.netRealizedPnl.height,
|
|
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.netRealizedPnlCumulative30dDelta,
|
|
name,
|
|
color,
|
|
unit: Unit.usd,
|
|
}),
|
|
),
|
|
},
|
|
{
|
|
name: "Rolling",
|
|
tree: groupedRollingRealizedValueCharts(list, all, title),
|
|
},
|
|
{
|
|
name: "Cumulative",
|
|
tree: [
|
|
{ name: "P&L", tree: groupedRealizedPnlCumulative(list, all, title) },
|
|
{
|
|
name: "Net",
|
|
title: title("Cumulative Net Realized P&L"),
|
|
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
|
baseline({
|
|
metric: tree.realized.netRealizedPnl.cumulative,
|
|
name,
|
|
color,
|
|
unit: Unit.usd,
|
|
}),
|
|
),
|
|
},
|
|
],
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Grouped realized with extras
|
|
* @param {readonly (CohortAgeRange | CohortLongTerm | CohortAll | CohortFull)[]} list
|
|
* @param {CohortAll} all
|
|
* @param {(metric: string) => string} title
|
|
* @returns {PartialOptionsGroup}
|
|
*/
|
|
function groupedRealizedSubfolderWithExtras(list, all, title) {
|
|
return {
|
|
name: "Realized",
|
|
tree: [
|
|
{ name: "P&L", tree: groupedRealizedPnlSumWithExtras(list, all, title) },
|
|
{
|
|
name: "Net",
|
|
title: title("Net Realized P&L"),
|
|
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
|
baseline({
|
|
metric: tree.realized.netRealizedPnl.height,
|
|
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.netRealizedPnlCumulative30dDelta,
|
|
name,
|
|
color,
|
|
unit: Unit.usd,
|
|
}),
|
|
),
|
|
},
|
|
{
|
|
name: "Rolling",
|
|
tree: groupedRollingRealizedChartsWithExtras(list, all, title),
|
|
},
|
|
{
|
|
name: "Cumulative",
|
|
tree: [
|
|
{ name: "P&L", tree: groupedRealizedPnlCumulative(list, all, title) },
|
|
{
|
|
name: "Net",
|
|
title: title("Cumulative Net Realized P&L"),
|
|
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
|
baseline({
|
|
metric: tree.realized.netRealizedPnl.cumulative,
|
|
name,
|
|
color,
|
|
unit: Unit.usd,
|
|
}),
|
|
),
|
|
},
|
|
],
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
// ============================================================================
|
|
// Grouped Section Builders
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Grouped profitability section (basic)
|
|
* @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: groupedPnlCharts(list, all, title) },
|
|
groupedRealizedSubfolder(list, all, title),
|
|
{ name: "Volume", tree: groupedSentInPnl(list, all, title) },
|
|
{
|
|
name: "Invested Capital",
|
|
tree: groupedInvestedCapitalAbsolute(list, all, title),
|
|
},
|
|
groupedSentiment(list, all, title),
|
|
],
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Grouped section with invested capital % (basic cohorts)
|
|
* @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: groupedPnlCharts(list, all, title) },
|
|
groupedRealizedSubfolder(list, all, title),
|
|
{ name: "Volume", tree: groupedSentInPnl(list, all, title) },
|
|
{
|
|
name: "Invested Capital",
|
|
tree: groupedInvestedCapital(list, all, title),
|
|
},
|
|
groupedSentiment(list, all, title),
|
|
],
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Grouped section for ageRange cohorts
|
|
* @param {{ list: readonly CohortAgeRange[], 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),
|
|
{
|
|
name: "Peak Regret",
|
|
title: title("Unrealized Peak Regret"),
|
|
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
|
line({
|
|
metric: tree.unrealized.peakRegret,
|
|
name,
|
|
color,
|
|
unit: Unit.usd,
|
|
}),
|
|
),
|
|
},
|
|
],
|
|
},
|
|
groupedRealizedSubfolderWithExtras(list, all, title),
|
|
{ name: "Volume", tree: groupedSentInPnl(list, all, title) },
|
|
{
|
|
name: "Invested Capital",
|
|
tree: groupedInvestedCapital(list, all, title),
|
|
},
|
|
groupedSentiment(list, all, title),
|
|
],
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Grouped section with NUPL
|
|
* @param {{ list: readonly (CohortFull | CohortBasicWithMarketCap)[], 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),
|
|
{
|
|
name: "NUPL",
|
|
title: title("NUPL"),
|
|
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
|
baseline({
|
|
metric: tree.relative.nupl,
|
|
name,
|
|
color,
|
|
unit: Unit.ratio,
|
|
}),
|
|
),
|
|
},
|
|
],
|
|
},
|
|
groupedRealizedSubfolder(list, all, title),
|
|
{ name: "Volume", tree: groupedSentInPnl(list, all, title) },
|
|
{
|
|
name: "Invested Capital",
|
|
tree: groupedInvestedCapital(list, all, title),
|
|
},
|
|
groupedSentiment(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),
|
|
{
|
|
name: "NUPL",
|
|
title: title("NUPL"),
|
|
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
|
baseline({
|
|
metric: tree.relative.nupl,
|
|
name,
|
|
color,
|
|
unit: Unit.ratio,
|
|
}),
|
|
),
|
|
},
|
|
{
|
|
name: "Peak Regret",
|
|
title: title("Unrealized Peak Regret"),
|
|
bottom: [
|
|
...mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
|
line({
|
|
metric: tree.unrealized.peakRegret,
|
|
name,
|
|
color,
|
|
unit: Unit.usd,
|
|
}),
|
|
),
|
|
...mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
|
baseline({
|
|
metric: tree.relative.unrealizedPeakRegretRelToMarketCap,
|
|
name,
|
|
color,
|
|
unit: Unit.pctMcap,
|
|
}),
|
|
),
|
|
],
|
|
},
|
|
],
|
|
},
|
|
groupedRealizedSubfolderWithExtras(list, all, title),
|
|
{ name: "Volume", tree: groupedSentInPnl(list, all, title) },
|
|
{
|
|
name: "Invested Capital",
|
|
tree: groupedInvestedCapital(list, all, title),
|
|
},
|
|
groupedSentiment(list, all, title),
|
|
],
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Grouped section with Peak Regret + NUPL (minAge cohorts)
|
|
* @param {{ list: readonly CohortMinAge[], all: CohortAll, title: (metric: string) => string }} args
|
|
* @returns {PartialOptionsGroup}
|
|
*/
|
|
export function createGroupedProfitabilitySectionWithPeakRegret({
|
|
list,
|
|
all,
|
|
title,
|
|
}) {
|
|
return {
|
|
name: "Profitability",
|
|
tree: [
|
|
{
|
|
name: "Unrealized",
|
|
tree: [
|
|
...groupedPnlChartsWithMarketCap(list, all, title),
|
|
{
|
|
name: "NUPL",
|
|
title: title("NUPL"),
|
|
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
|
baseline({
|
|
metric: tree.relative.nupl,
|
|
name,
|
|
color,
|
|
unit: Unit.ratio,
|
|
}),
|
|
),
|
|
},
|
|
{
|
|
name: "Peak Regret",
|
|
title: title("Unrealized Peak Regret"),
|
|
bottom: [
|
|
...mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
|
line({
|
|
metric: tree.unrealized.peakRegret,
|
|
name,
|
|
color,
|
|
unit: Unit.usd,
|
|
}),
|
|
),
|
|
...mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
|
baseline({
|
|
metric: tree.relative.unrealizedPeakRegretRelToMarketCap,
|
|
name,
|
|
color,
|
|
unit: Unit.pctMcap,
|
|
}),
|
|
),
|
|
],
|
|
},
|
|
],
|
|
},
|
|
groupedRealizedSubfolder(list, all, title),
|
|
{ name: "Volume", tree: groupedSentInPnl(list, all, title) },
|
|
{
|
|
name: "Invested Capital",
|
|
tree: groupedInvestedCapital(list, all, title),
|
|
},
|
|
groupedSentiment(list, all, title),
|
|
],
|
|
};
|
|
}
|