diff --git a/website/scripts/options/distribution/activity.js b/website/scripts/options/distribution/activity.js index 261da97c3..3aa93362f 100644 --- a/website/scripts/options/distribution/activity.js +++ b/website/scripts/options/distribution/activity.js @@ -12,15 +12,9 @@ */ import { Unit } from "../../utils/units.js"; -import { line, baseline, dotsBaseline } from "../series.js"; +import { line, baseline, dotsBaseline, dots } from "../series.js"; import { satsBtcUsd } from "../shared.js"; import { colors } from "../../utils/colors.js"; -import { - createSingleSellSideRiskSeries, - createGroupedSellSideRiskSeries, - createSingleValueCreatedDestroyedSeries, - createSingleCapitulationProfitFlowSeries, -} from "./shared.js"; // ============================================================================ // Shared Helpers @@ -801,3 +795,91 @@ export function createGroupedActivitySectionWithAdjusted({ list, title }) { valueTree: createGroupedValueTreeWithAdjusted(list, title), }); } + +/** + * Create sell side risk ratio series for single cohort + * @param {{ realized: AnyRealizedPattern }} tree + * @returns {AnyFetchedSeriesBlueprint[]} + */ +function createSingleSellSideRiskSeries(tree) { + return [ + line({ + metric: tree.realized.sellSideRiskRatio30dEma, + name: "30d EMA", + color: colors.ma._1m, + unit: Unit.ratio, + }), + line({ + metric: tree.realized.sellSideRiskRatio7dEma, + name: "7d EMA", + color: colors.ma._1w, + unit: Unit.ratio, + }), + dots({ + metric: tree.realized.sellSideRiskRatio, + name: "Raw", + color: colors.bitcoin, + unit: Unit.ratio, + }), + ]; +} + +/** + * Create sell side risk ratio series for grouped cohorts + * @param {readonly CohortObject[]} list + * @returns {AnyFetchedSeriesBlueprint[]} + */ +function createGroupedSellSideRiskSeries(list) { + return list.flatMap(({ color, name, tree }) => [ + line({ + metric: tree.realized.sellSideRiskRatio, + name, + color, + unit: Unit.ratio, + }), + ]); +} + +/** + * Create value created & destroyed series for single cohort + * @param {{ realized: AnyRealizedPattern }} tree + * @returns {AnyFetchedSeriesBlueprint[]} + */ +function createSingleValueCreatedDestroyedSeries(tree) { + return [ + line({ + metric: tree.realized.valueCreated, + name: "Created", + color: colors.usd, + unit: Unit.usd, + }), + line({ + metric: tree.realized.valueDestroyed, + name: "Destroyed", + color: colors.loss, + unit: Unit.usd, + }), + ]; +} + +/** + * Create capitulation & profit flow series for single cohort + * @param {{ realized: AnyRealizedPattern }} tree + * @returns {AnyFetchedSeriesBlueprint[]} + */ +function createSingleCapitulationProfitFlowSeries(tree) { + return [ + line({ + metric: tree.realized.profitFlow, + name: "Profit Flow", + color: colors.profit, + unit: Unit.usd, + }), + line({ + metric: tree.realized.capitulationFlow, + name: "Capitulation Flow", + color: colors.loss, + unit: Unit.usd, + }), + ]; +} diff --git a/website/scripts/options/distribution/address.js b/website/scripts/options/distribution/address.js deleted file mode 100644 index 88d335747..000000000 --- a/website/scripts/options/distribution/address.js +++ /dev/null @@ -1,1134 +0,0 @@ -/** - * Address cohort folder builder - * Creates option trees for address-based cohorts (has addrCount) - * Address cohorts use _0satsPattern which has CostBasisPattern (no percentiles) - */ - -import { colors } from "../../utils/colors.js"; -import { Unit } from "../../utils/units.js"; -import { priceLine } from "../constants.js"; -import { line, baseline, price } from "../series.js"; -import { formatCohortTitle, satsBtcUsd } from "../shared.js"; -import { - createSingleSupplySeries, - createGroupedSupplySection, - createUtxoCountSeries, - createAddressCountSeries, - createRealizedPriceSeries, - createRealizedPriceRatioSeries, - createSingleCoinsDestroyedSeries, - createGroupedCoinblocksDestroyedSeries, - createGroupedCoindaysDestroyedSeries, - createSingleSentSeries, - groupedSupplyRelativeGenerators, - createSingleSupplyRelativeOptions, - createSingleSellSideRiskSeries, - createSingleValueCreatedDestroyedSeries, - createSingleValueFlowBreakdownSeries, - createSingleCapitulationProfitFlowSeries, - createSingleSoprSeries, - createSingleInvestorPriceSeries, - createSingleInvestorPriceRatioSeries, - createInvestorPriceSeries, - createInvestorPriceRatioSeries, -} from "./shared.js"; - -/** - * Create a cohort folder for address cohorts - * Includes address count section (addrCount exists on AddressCohortObject) - * @param {AddressCohortObject | AddressCohortGroupObject} args - * @returns {PartialOptionsGroup} - */ -export function createAddressCohortFolder(args) { - const list = "list" in args ? args.list : [args]; - const useGroupName = "list" in args; - const isSingle = !("list" in args); - - const title = formatCohortTitle(args.name); - - return { - name: args.name || "all", - tree: [ - // Supply section - isSingle - ? { - name: "Supply", - title: title("Supply"), - bottom: createSingleSupplySeries( - /** @type {AddressCohortObject} */ (args), - createSingleSupplyRelativeOptions( - /** @type {AddressCohortObject} */ (args), - ), - ), - } - : createGroupedSupplySection( - list, - title, - groupedSupplyRelativeGenerators, - ), - - // UTXO count - { - name: "UTXO Count", - title: title("UTXO Count"), - bottom: createUtxoCountSeries(list, useGroupName), - }, - - // Address count (ADDRESS COHORTS ONLY - fully type safe!) - { - name: "Address Count", - title: title("Address Count"), - bottom: createAddressCountSeries(list, useGroupName), - }, - - // Realized section - { - name: "Realized", - tree: [ - ...(useGroupName - ? [ - { - name: "Price", - title: title("Realized Price"), - top: createRealizedPriceSeries(list), - }, - { - name: "Ratio", - title: title("Realized Price Ratio"), - bottom: createRealizedPriceRatioSeries(list), - }, - { - name: "Investor Price", - tree: [ - { - name: "Price", - title: title("Investor Price"), - top: createInvestorPriceSeries(list), - }, - { - name: "Ratio", - title: title("Investor Price Ratio"), - bottom: createInvestorPriceRatioSeries(list), - }, - ], - }, - ] - : createRealizedPriceOptions( - /** @type {AddressCohortObject} */ (args), - title, - )), - { - name: "Capitalization", - title: title("Realized Cap"), - bottom: createRealizedCapWithExtras(list, args, useGroupName), - }, - { - name: "Value", - title: title("Realized Value"), - bottom: list.map(({ color, name, tree }) => - line({ - metric: tree.realized.realizedValue, - name: useGroupName ? name : "Realized Value", - color, - unit: Unit.usd, - }), - ), - }, - ...(useGroupName - ? createGroupedRealizedPnlSection(list, title) - : createRealizedPnlSection( - /** @type {AddressCohortObject} */ (args), - title, - )), - ], - }, - - // Unrealized section - ...createUnrealizedSection(list, useGroupName, title), - - // Cost basis section (no percentiles for address cohorts) - ...createCostBasisSection(list, useGroupName, title), - - // Activity section - ...createActivitySection(args, title), - ], - }; -} - -/** - * Create realized price options for single cohort - * @param {AddressCohortObject} args - * @param {(metric: string) => string} title - * @returns {PartialOptionsTree} - */ -function createRealizedPriceOptions(args, title) { - const { tree, color } = args; - - return [ - { - name: "Price", - title: title("Realized Price"), - top: [ - price({ - metric: tree.realized.realizedPrice, - name: "Realized", - color, - }), - ], - }, - { - name: "Investor Price", - tree: [ - { - name: "Price", - title: title("Investor Price"), - top: createSingleInvestorPriceSeries(tree, color), - }, - { - name: "Ratio", - title: title("Investor Price Ratio"), - bottom: createSingleInvestorPriceRatioSeries(tree, color), - }, - ], - }, - ]; -} - -/** - * Create realized cap with extras - * @param {readonly AddressCohortObject[]} list - * @param {AddressCohortObject | AddressCohortGroupObject} args - * @param {boolean} useGroupName - * @returns {AnyFetchedSeriesBlueprint[]} - */ -function createRealizedCapWithExtras(list, args, useGroupName) { - const isSingle = !("list" in args); - - return list.flatMap(({ color, name, tree }) => [ - line({ - metric: tree.realized.realizedCap, - name: useGroupName ? name : "Capitalization", - color, - unit: Unit.usd, - }), - ...(isSingle - ? [ - baseline({ - metric: tree.realized.realizedCap30dDelta, - name: "30d Change", - unit: Unit.usd, - defaultActive: false, - }), - ] - : []), - // RealizedPattern (address cohorts) doesn't have realizedCapRelToOwnMarketCap - ]); -} - -/** - * Create realized PnL section for single cohort - * @param {AddressCohortObject} args - * @param {(metric: string) => string} title - * @returns {PartialOptionsTree} - */ -function createRealizedPnlSection(args, title) { - const { realized } = args.tree; - - return [ - { - name: "P&L", - title: title("Realized P&L"), - bottom: [ - line({ - metric: realized.realizedProfit.sum, - name: "Profit", - color: colors.profit, - unit: Unit.usd, - }), - line({ - metric: realized.realizedProfit7dEma, - name: "Profit 7d EMA", - color: colors.profit, - unit: Unit.usd, - }), - line({ - metric: realized.realizedProfit.cumulative, - name: "Profit Cumulative", - color: colors.profit, - unit: Unit.usd, - defaultActive: false, - }), - line({ - metric: realized.realizedLoss.sum, - name: "Loss", - color: colors.loss, - unit: Unit.usd, - }), - line({ - metric: realized.realizedLoss7dEma, - name: "Loss 7d EMA", - color: colors.loss, - unit: Unit.usd, - }), - line({ - metric: realized.realizedLoss.cumulative, - name: "Loss Cumulative", - color: colors.loss, - unit: Unit.usd, - defaultActive: false, - }), - line({ - metric: realized.negRealizedLoss.sum, - name: "Negative Loss", - color: colors.loss, - unit: Unit.usd, - defaultActive: false, - }), - line({ - metric: realized.negRealizedLoss.cumulative, - name: "Negative Loss Cumulative", - color: colors.loss, - unit: Unit.usd, - defaultActive: false, - }), - baseline({ - metric: realized.realizedProfitRelToRealizedCap.sum, - name: "Profit", - color: colors.profit, - unit: Unit.pctRcap, - }), - baseline({ - metric: realized.realizedProfitRelToRealizedCap.cumulative, - name: "Profit Cumulative", - color: colors.profit, - unit: Unit.pctRcap, - defaultActive: false, - }), - baseline({ - metric: realized.realizedLossRelToRealizedCap.sum, - name: "Loss", - color: colors.loss, - unit: Unit.pctRcap, - }), - baseline({ - metric: realized.realizedLossRelToRealizedCap.cumulative, - name: "Loss Cumulative", - color: colors.loss, - unit: Unit.pctRcap, - defaultActive: false, - }), - line({ - metric: realized.totalRealizedPnl, - name: "Total", - color: colors.default, - unit: Unit.usd, - defaultActive: false, - }), - ], - }, - { - name: "Net pnl", - title: title("Net Realized P&L"), - bottom: [ - baseline({ - metric: realized.netRealizedPnl.sum, - name: "Net", - unit: Unit.usd, - }), - baseline({ - metric: realized.netRealizedPnl7dEma, - name: "Net 7d EMA", - unit: Unit.usd, - }), - baseline({ - metric: realized.netRealizedPnl.cumulative, - name: "Net Cumulative", - unit: Unit.usd, - defaultActive: false, - }), - baseline({ - metric: realized.netRealizedPnlCumulative30dDelta, - name: "Cumulative 30d Change", - unit: Unit.usd, - defaultActive: false, - }), - baseline({ - metric: realized.netRealizedPnlRelToRealizedCap.sum, - name: "Net", - unit: Unit.pctRcap, - }), - baseline({ - metric: realized.netRealizedPnlRelToRealizedCap.cumulative, - name: "Net Cumulative", - unit: Unit.pctRcap, - defaultActive: false, - }), - baseline({ - metric: realized.netRealizedPnlCumulative30dDeltaRelToRealizedCap, - name: "Cumulative 30d Change", - unit: Unit.pctRcap, - defaultActive: false, - }), - baseline({ - metric: realized.netRealizedPnlCumulative30dDeltaRelToMarketCap, - name: "Cumulative 30d Change", - unit: Unit.pctMcap, - }), - priceLine({ - unit: Unit.usd, - number: 1, - }), - priceLine({ - unit: Unit.pctMcap, - }), - priceLine({ - unit: Unit.pctRcap, - }), - ], - }, - { - name: "SOPR", - title: title("SOPR"), - bottom: [ - ...createSingleSoprSeries(args.tree), - priceLine({ - unit: Unit.ratio, - number: 1, - }), - ], - }, - { - name: "Sell Side Risk", - title: title("Sell Side Risk Ratio"), - bottom: createSingleSellSideRiskSeries(args.tree), - }, - { - name: "Value", - tree: [ - { - name: "Created & Destroyed", - title: title("Value Created & Destroyed"), - bottom: createSingleValueCreatedDestroyedSeries(args.tree), - }, - { - name: "Breakdown", - title: title("Value Flow Breakdown"), - bottom: createSingleValueFlowBreakdownSeries(args.tree), - }, - { - name: "Flow", - title: title("Capitulation & Profit Flow"), - bottom: createSingleCapitulationProfitFlowSeries(args.tree), - }, - ], - }, - { - name: "Peak Regret", - title: title("Peak Regret"), - bottom: [ - line({ - metric: realized.peakRegret.sum, - name: "Sum", - color: colors.loss, - unit: Unit.usd, - }), - line({ - metric: realized.peakRegret.cumulative, - name: "Cumulative", - color: colors.loss, - unit: Unit.usd, - defaultActive: false, - }), - baseline({ - metric: realized.peakRegretRelToRealizedCap, - name: "Rel. to Realized Cap", - color: colors.realized, - unit: Unit.pctRcap, - }), - ], - }, - { - name: "Sent In P/L", - tree: [ - { - name: "In Profit 14d EMA", - title: title("Sent In Profit 14d EMA"), - bottom: satsBtcUsd({ - pattern: realized.sentInProfit14dEma, - name: "14d EMA", - color: colors.profit, - }), - }, - { - name: "In Loss 14d EMA", - title: title("Sent In Loss 14d EMA"), - bottom: satsBtcUsd({ - pattern: realized.sentInLoss14dEma, - name: "14d EMA", - color: colors.loss, - }), - }, - { - name: "In Profit", - title: title("Sent In Profit"), - bottom: [ - line({ - metric: realized.sentInProfit.bitcoin.sum, - name: "Sum", - color: colors.profit, - unit: Unit.btc, - }), - line({ - metric: realized.sentInProfit.bitcoin.cumulative, - name: "Cumulative", - color: colors.profit, - unit: Unit.btc, - defaultActive: false, - }), - line({ - metric: realized.sentInProfit.sats.sum, - name: "Sum", - color: colors.profit, - unit: Unit.sats, - }), - line({ - metric: realized.sentInProfit.sats.cumulative, - name: "Cumulative", - color: colors.profit, - unit: Unit.sats, - defaultActive: false, - }), - line({ - metric: realized.sentInProfit.dollars.sum, - name: "Sum", - color: colors.profit, - unit: Unit.usd, - }), - line({ - metric: realized.sentInProfit.dollars.cumulative, - name: "Cumulative", - color: colors.profit, - unit: Unit.usd, - defaultActive: false, - }), - ], - }, - { - name: "In Loss", - title: title("Sent In Loss"), - bottom: [ - line({ - metric: realized.sentInLoss.bitcoin.sum, - name: "Sum", - color: colors.loss, - unit: Unit.btc, - }), - line({ - metric: realized.sentInLoss.bitcoin.cumulative, - name: "Cumulative", - color: colors.loss, - unit: Unit.btc, - defaultActive: false, - }), - line({ - metric: realized.sentInLoss.sats.sum, - name: "Sum", - color: colors.loss, - unit: Unit.sats, - }), - line({ - metric: realized.sentInLoss.sats.cumulative, - name: "Cumulative", - color: colors.loss, - unit: Unit.sats, - defaultActive: false, - }), - line({ - metric: realized.sentInLoss.dollars.sum, - name: "Sum", - color: colors.loss, - unit: Unit.usd, - }), - line({ - metric: realized.sentInLoss.dollars.cumulative, - name: "Cumulative", - color: colors.loss, - unit: Unit.usd, - defaultActive: false, - }), - ], - }, - ], - }, - ]; -} - -/** - * Create grouped realized P&L section for address cohorts (for compare view) - * @param {readonly AddressCohortObject[]} list - * @param {(metric: string) => string} title - * @returns {PartialOptionsTree} - */ -function createGroupedRealizedPnlSection(list, title) { - const pnlConfigs = /** @type {const} */ ([ - { - name: "Profit", - sum: "realizedProfit", - ema: "realizedProfit7dEma", - rel: "realizedProfitRelToRealizedCap", - isNet: false, - }, - { - name: "Loss", - sum: "realizedLoss", - ema: "realizedLoss7dEma", - rel: "realizedLossRelToRealizedCap", - isNet: false, - }, - { - name: "Net P&L", - sum: "netRealizedPnl", - ema: "netRealizedPnl7dEma", - rel: "netRealizedPnlRelToRealizedCap", - isNet: true, - }, - ]); - - return [ - ...pnlConfigs.map(({ name, sum, ema, rel, isNet }) => ({ - name, - tree: [ - { - name: "Sum", - title: title(`Realized ${name}`), - bottom: [ - ...list.flatMap(({ color, name, tree }) => [ - (isNet ? baseline : line)({ - metric: tree.realized[sum].sum, - name, - color, - unit: Unit.usd, - }), - baseline({ - metric: tree.realized[rel].sum, - name, - color, - unit: Unit.pctRcap, - }), - ]), - ], - }, - { - name: "7d EMA", - title: title(`Realized ${name} 7d EMA`), - bottom: [ - ...list.map(({ color, name, tree }) => - (isNet ? baseline : line)({ - metric: tree.realized[ema], - name, - color, - unit: Unit.usd, - }), - ), - ], - }, - ], - })), - { - name: "Peak Regret", - tree: [ - { - name: "Sum", - title: title("Peak Regret"), - bottom: list.flatMap(({ color, name, tree }) => [ - line({ - metric: tree.realized.peakRegret.sum, - name, - color, - unit: Unit.usd, - }), - ]), - }, - { - name: "Cumulative", - title: title("Peak Regret Cumulative"), - bottom: list.flatMap(({ color, name, tree }) => [ - line({ - metric: tree.realized.peakRegret.cumulative, - name, - color, - unit: Unit.usd, - }), - ]), - }, - { - name: "Rel. to Realized Cap", - title: title("Peak Regret Rel. to Realized Cap"), - bottom: list.flatMap(({ color, name, tree }) => [ - baseline({ - metric: tree.realized.peakRegretRelToRealizedCap, - name, - color, - unit: Unit.pctRcap, - }), - ]), - }, - ], - }, - { - name: "Sent In P/L", - tree: [ - { - name: "In Profit 14d EMA", - title: title("Sent In Profit 14d EMA"), - bottom: list.flatMap(({ color, name, tree }) => - satsBtcUsd({ - pattern: tree.realized.sentInProfit14dEma, - name, - color, - }), - ), - }, - { - name: "In Loss 14d EMA", - title: title("Sent In Loss 14d EMA"), - bottom: list.flatMap(({ color, name, tree }) => - satsBtcUsd({ - pattern: tree.realized.sentInLoss14dEma, - name, - color, - }), - ), - }, - { - name: "In Profit", - title: title("Sent In Profit"), - bottom: list.flatMap(({ color, name, tree }) => [ - line({ - metric: tree.realized.sentInProfit.bitcoin.sum, - name, - color, - unit: Unit.btc, - }), - line({ - metric: tree.realized.sentInProfit.sats.sum, - name, - color, - unit: Unit.sats, - }), - line({ - metric: tree.realized.sentInProfit.dollars.sum, - name, - color, - unit: Unit.usd, - }), - ]), - }, - { - name: "In Profit Cumulative", - title: title("Sent In Profit Cumulative"), - bottom: list.flatMap(({ color, name, tree }) => [ - line({ - metric: tree.realized.sentInProfit.bitcoin.cumulative, - name, - color, - unit: Unit.btc, - }), - line({ - metric: tree.realized.sentInProfit.sats.cumulative, - name, - color, - unit: Unit.sats, - }), - line({ - metric: tree.realized.sentInProfit.dollars.cumulative, - name, - color, - unit: Unit.usd, - }), - ]), - }, - { - name: "In Loss", - title: title("Sent In Loss"), - bottom: list.flatMap(({ color, name, tree }) => [ - line({ - metric: tree.realized.sentInLoss.bitcoin.sum, - name, - color, - unit: Unit.btc, - }), - line({ - metric: tree.realized.sentInLoss.sats.sum, - name, - color, - unit: Unit.sats, - }), - line({ - metric: tree.realized.sentInLoss.dollars.sum, - name, - color, - unit: Unit.usd, - }), - ]), - }, - { - name: "In Loss Cumulative", - title: title("Sent In Loss Cumulative"), - bottom: list.flatMap(({ color, name, tree }) => [ - line({ - metric: tree.realized.sentInLoss.bitcoin.cumulative, - name, - color, - unit: Unit.btc, - }), - line({ - metric: tree.realized.sentInLoss.sats.cumulative, - name, - color, - unit: Unit.sats, - }), - line({ - metric: tree.realized.sentInLoss.dollars.cumulative, - name, - color, - unit: Unit.usd, - }), - ]), - }, - ], - }, - ]; -} - -/** - * Create unrealized section - * @param {readonly AddressCohortObject[]} list - * @param {boolean} useGroupName - * @param {(metric: string) => string} title - * @returns {PartialOptionsTree} - */ -function createUnrealizedSection(list, useGroupName, title) { - return [ - { - name: "Profitability", - tree: [ - { - name: "Profit", - title: title("Unrealized Profit"), - bottom: list.flatMap(({ color, name, tree }) => [ - line({ - metric: tree.unrealized.unrealizedProfit, - name: useGroupName ? name : "Profit", - color: useGroupName ? color : colors.profit, - unit: Unit.usd, - }), - ]), - }, - { - name: "Loss", - title: title("Unrealized Loss"), - bottom: list.flatMap(({ color, name, tree }) => [ - line({ - metric: tree.unrealized.unrealizedLoss, - name: useGroupName ? name : "Loss", - color: useGroupName ? color : colors.loss, - unit: Unit.usd, - }), - ]), - }, - { - name: "Total P&L", - title: title("Total Unrealized P&L"), - bottom: list.flatMap(({ color, name, tree }) => [ - baseline({ - metric: tree.unrealized.totalUnrealizedPnl, - name: useGroupName ? name : "Total", - color: useGroupName ? color : undefined, - unit: Unit.usd, - }), - ]), - }, - { - name: "Negative Loss", - title: title("Negative Unrealized Loss"), - bottom: list.flatMap(({ color, name, tree }) => [ - line({ - metric: tree.unrealized.negUnrealizedLoss, - name: useGroupName ? name : "Negative Loss", - color: useGroupName ? color : colors.loss, - unit: Unit.usd, - }), - ]), - }, - { - name: "Invested Capital", - tree: [ - { - name: "In Profit", - title: title("Invested Capital In Profit"), - bottom: list.flatMap(({ color, name, tree }) => [ - line({ - metric: tree.unrealized.investedCapitalInProfit, - name: useGroupName ? name : "In Profit", - color: useGroupName ? color : colors.profit, - unit: Unit.usd, - }), - ]), - }, - { - name: "In Loss", - title: title("Invested Capital In Loss"), - bottom: list.flatMap(({ color, name, tree }) => [ - line({ - metric: tree.unrealized.investedCapitalInLoss, - name: useGroupName ? name : "In Loss", - color: useGroupName ? color : colors.loss, - unit: Unit.usd, - }), - ]), - }, - ], - }, - { - name: "Relative", - tree: [ - { - name: "NUPL", - title: title("NUPL (Rel to Market Cap)"), - bottom: list.flatMap(({ color, name, tree }) => [ - baseline({ - metric: tree.relative.nupl, - name: useGroupName ? name : "NUPL", - color: useGroupName ? color : undefined, - unit: Unit.ratio, - options: { baseValue: { price: 0 } }, - }), - ]), - }, - { - name: "Profit", - title: title("Unrealized Profit (% of Market Cap)"), - bottom: list.flatMap(({ color, name, tree }) => [ - line({ - metric: tree.relative.unrealizedProfitRelToMarketCap, - name: useGroupName ? name : "Profit", - color: useGroupName ? color : colors.profit, - unit: Unit.pctMcap, - }), - ]), - }, - { - name: "Loss", - title: title("Unrealized Loss (% of Market Cap)"), - bottom: list.flatMap(({ color, name, tree }) => [ - line({ - metric: tree.relative.unrealizedLossRelToMarketCap, - name: useGroupName ? name : "Loss", - color: useGroupName ? color : colors.loss, - unit: Unit.pctMcap, - }), - ]), - }, - { - name: "Net P&L", - title: title("Net Unrealized P&L (% of Market Cap)"), - bottom: list.flatMap(({ color, name, tree }) => [ - baseline({ - metric: tree.relative.netUnrealizedPnlRelToMarketCap, - name: useGroupName ? name : "Net", - color: useGroupName ? color : undefined, - unit: Unit.pctMcap, - }), - ]), - }, - { - name: "Negative Loss", - title: title("Negative Unrealized Loss (% of Market Cap)"), - bottom: list.flatMap(({ color, name, tree }) => [ - line({ - metric: tree.relative.negUnrealizedLossRelToMarketCap, - name: useGroupName ? name : "Negative Loss", - color: useGroupName ? color : colors.loss, - unit: Unit.pctMcap, - }), - ]), - }, - { - name: "Invested Capital In Profit", - title: title("Invested Capital In Profit"), - bottom: list.flatMap(({ color, name, tree }) => [ - baseline({ - metric: tree.relative.investedCapitalInProfitPct, - name: useGroupName ? name : "In Profit", - color: useGroupName ? color : colors.profit, - unit: Unit.pctOwnRcap, - }), - ]), - }, - { - name: "Invested Capital In Loss", - title: title("Invested Capital In Loss"), - bottom: list.flatMap(({ color, name, tree }) => [ - baseline({ - metric: tree.relative.investedCapitalInLossPct, - name: useGroupName ? name : "In Loss", - color: useGroupName ? color : colors.loss, - unit: Unit.pctOwnRcap, - }), - ]), - }, - ], - }, - { - name: "NUPL", - title: title("Net Unrealized P&L"), - bottom: list.flatMap(({ color, name, tree }) => [ - baseline({ - metric: tree.unrealized.netUnrealizedPnl, - name: useGroupName ? name : "NUPL", - color: useGroupName ? color : undefined, - unit: Unit.ratio, - }), - priceLine({ - unit: Unit.ratio, - }), - ]), - }, - { - name: "Net Sentiment", - title: title("Net Sentiment"), - bottom: list.flatMap(({ color, name, tree }) => [ - baseline({ - metric: tree.unrealized.netSentiment, - name: useGroupName ? name : "Net Sentiment", - color: useGroupName ? color : undefined, - unit: Unit.usd, - }), - priceLine({ - unit: Unit.usd, - }), - ]), - }, - ], - }, - ]; -} - -/** - * Create cost basis section (no percentiles for address cohorts) - * @param {readonly AddressCohortObject[]} list - * @param {boolean} useGroupName - * @param {(metric: string) => string} title - * @returns {PartialOptionsTree} - */ -function createCostBasisSection(list, useGroupName, title) { - return [ - { - name: "Cost Basis", - tree: [ - { - name: "Min", - title: title("Min Cost Basis"), - top: list.map(({ color, name, tree }) => - price({ - metric: tree.costBasis.min, - name: useGroupName ? name : "Min", - color, - }), - ), - }, - { - name: "Max", - title: title("Max Cost Basis"), - top: list.map(({ color, name, tree }) => - price({ - metric: tree.costBasis.max, - name: useGroupName ? name : "Max", - color, - }), - ), - }, - ], - }, - ]; -} - -/** - * Create activity section - * @param {AddressCohortObject | AddressCohortGroupObject} args - * @param {(metric: string) => string} title - * @returns {PartialOptionsTree} - */ -function createActivitySection(args, title) { - const list = "list" in args ? args.list : [args]; - const isSingle = !("list" in args); - - // Single cohort: all metrics on one chart - if (isSingle) { - const cohort = /** @type {AddressCohortObject} */ (args); - return [ - { - name: "Activity", - tree: [ - { - name: "Coins Destroyed", - title: title("Coins Destroyed"), - bottom: createSingleCoinsDestroyedSeries(cohort), - }, - { - name: "Sent", - title: title("Sent"), - bottom: createSingleSentSeries(cohort), - }, - ], - }, - ]; - } - - // Grouped cohorts: split charts for comparison - return [ - { - name: "Activity", - tree: [ - { - name: "Coinblocks Destroyed", - title: title("Coinblocks Destroyed"), - bottom: createGroupedCoinblocksDestroyedSeries(list), - }, - { - name: "Coindays Destroyed", - title: title("Coindays Destroyed"), - bottom: createGroupedCoindaysDestroyedSeries(list), - }, - { - name: "Sent", - tree: [ - { - name: "14d EMA", - title: title("Sent 14d EMA"), - bottom: list.flatMap(({ color, name, tree }) => - satsBtcUsd({ pattern: tree.activity.sent14dEma, name, color }), - ), - }, - { - name: "Sum", - title: title("Sent"), - bottom: list.flatMap(({ color, name, tree }) => - satsBtcUsd({ - pattern: { - sats: tree.activity.sent.sats.sum, - bitcoin: tree.activity.sent.bitcoin.sum, - dollars: tree.activity.sent.dollars.sum, - }, - name, - color, - }), - ), - }, - ], - }, - ], - }, - ]; -} diff --git a/website/scripts/options/distribution/index.js b/website/scripts/options/distribution/index.js index ade64d2af..4a895e3c4 100644 --- a/website/scripts/options/distribution/index.js +++ b/website/scripts/options/distribution/index.js @@ -69,20 +69,6 @@ import { // Re-export data builder export { buildCohortData } from "./data.js"; -// Re-export shared helpers -export { - createSingleSupplySeries, - createGroupedSupplyTotalSeries, - createGroupedSupplyInProfitSeries, - createGroupedSupplyInLossSeries, - createUtxoCountSeries, - createAddressCountSeries, - createRealizedPriceSeries, - createRealizedPriceRatioSeries, - createRealizedCapSeries, - createCostBasisPercentilesSeries, -} from "./shared.js"; - // ============================================================================ // Folder Builders // ============================================================================ @@ -264,7 +250,10 @@ export function createCohortFolderAgeRange(args) { createGroupedValuationSectionWithOwnMarketCap({ list, title }), createGroupedPricesSection({ list, title }), createGroupedCostBasisSectionWithPercentiles({ list, title }), - createGroupedProfitabilitySectionWithInvestedCapitalPct({ list, title }), + createGroupedProfitabilitySectionWithInvestedCapitalPct({ + list, + title, + }), createGroupedActivitySection({ list, title }), ], }; diff --git a/website/scripts/options/distribution/shared.js b/website/scripts/options/distribution/shared.js deleted file mode 100644 index 9bad0aedf..000000000 --- a/website/scripts/options/distribution/shared.js +++ /dev/null @@ -1,1401 +0,0 @@ -/** Shared cohort chart section builders */ - -import { colors } from "../../utils/colors.js"; -import { Unit } from "../../utils/units.js"; -import { priceLine } from "../constants.js"; -import { baseline, dots, line, price } from "../series.js"; -import { - satsBtcUsd, - createPriceRatioCharts, - formatCohortTitle, -} from "../shared.js"; - -// ============================================================================ -// Generic Price Helpers -// ============================================================================ - -/** - * Create price folder (price + ratio + z-scores wrapped in folder) - * For cohorts with full extended ratio metrics (ActivePriceRatioPattern) - * @param {{ name: string, cohortTitle?: string, priceMetric: ActivePricePattern, ratioPattern: AnyRatioPattern, color: Color }} args - * @returns {PartialOptionsGroup} - */ -export function createPriceFolder({ - name, - cohortTitle, - priceMetric, - ratioPattern, - color, -}) { - const context = cohortTitle ? `${cohortTitle} ${name}` : name; - return { - name, - tree: createPriceRatioCharts({ - context, - legend: name, - pricePattern: priceMetric, - ratio: ratioPattern, - color, - }), - }; -} - -/** - * Create basic price charts (price + ratio only, no z-scores) - flat array - * For cohorts with basic ratio metrics (only .ratio field) - * @template {AnyMetricPattern} R - * @param {{ name: string, context: string, priceMetric: ActivePricePattern, ratioMetric: R, color: Color }} args - * @returns {PartialOptionsTree} - */ -export function createBasicPriceCharts({ - name, - context, - priceMetric, - ratioMetric, - color, -}) { - return [ - { - name: "Price", - title: context, - top: [price({ metric: priceMetric, name, color })], - }, - { - name: "Ratio", - title: formatCohortTitle(context)("Ratio"), - bottom: [ - baseline({ - metric: ratioMetric, - name: "Ratio", - color, - unit: Unit.ratio, - base: 1, - }), - ], - }, - ]; -} - -/** - * Create basic price folder (price + ratio wrapped in folder, no z-scores) - * For cohorts with basic ratio metrics (only .ratio field) - * @template {AnyMetricPattern} R - * @param {{ name: string, cohortTitle?: string, priceMetric: ActivePricePattern, ratioMetric: R, color: Color }} args - * @returns {PartialOptionsGroup} - */ -export function createBasicPriceFolder({ - name, - cohortTitle, - priceMetric, - ratioMetric, - color, -}) { - const context = cohortTitle ? `${cohortTitle} ${name}` : name; - return { - name, - tree: createBasicPriceCharts({ - name, - context, - priceMetric, - ratioMetric, - color, - }), - }; -} - -/** - * Create grouped price charts (price + ratio) - flat array, no z-scores - * @template {{ color: Color, name: string, tree: { realized: AnyRealizedPattern } }} T - * @param {{ name: string, title: (metric: string) => string, list: readonly T[], getPrice: (tree: T['tree']) => ActivePricePattern, getRatio: (tree: T['tree']) => AnyMetricPattern }} args - * @returns {PartialOptionsTree} - */ -export function createGroupedPriceCharts({ - name, - title, - list, - getPrice, - getRatio, -}) { - return [ - { - name: "Price", - title: title(name), - top: list.map(({ color, name: cohortName, tree }) => - price({ metric: getPrice(tree), name: cohortName, color }), - ), - }, - { - name: "Ratio", - title: title(`${name} Ratio`), - bottom: list.map(({ color, name: cohortName, tree }) => - baseline({ - metric: getRatio(tree), - name: cohortName, - color, - unit: Unit.ratio, - base: 1, - }), - ), - }, - ]; -} - -/** - * Create grouped price folder (price + ratio wrapped in folder) - * @template {{ color: Color, name: string, tree: { realized: AnyRealizedPattern } }} T - * @param {{ name: string, title: (metric: string) => string, list: readonly T[], getPrice: (tree: T['tree']) => ActivePricePattern, getRatio: (tree: T['tree']) => AnyMetricPattern }} args - * @returns {PartialOptionsGroup} - */ -export function createGroupedPriceFolder({ - name, - title, - list, - getPrice, - getRatio, -}) { - return { - name, - tree: createGroupedPriceCharts({ name, title, list, getPrice, getRatio }), - }; -} - -/** - * Create base supply series (without relative metrics) - * @param {CohortObject | CohortWithoutRelative} cohort - * @returns {AnyFetchedSeriesBlueprint[]} - */ -function createSingleSupplySeriesBase(cohort) { - const { tree } = cohort; - - return [ - ...satsBtcUsd({ - pattern: tree.supply.total, - name: "Supply", - color: colors.default, - }), - ...satsBtcUsd({ - pattern: tree.supply._30dChange, - name: "30d Change", - color: colors.bitcoin, - }), - ...satsBtcUsd({ - pattern: tree.unrealized.supplyInProfit, - name: "In Profit", - color: colors.profit, - }), - ...satsBtcUsd({ - pattern: tree.unrealized.supplyInLoss, - name: "In Loss", - color: colors.loss, - }), - ...satsBtcUsd({ - pattern: tree.supply.halved, - name: "half", - color: colors.gray, - }).map((s) => ({ - ...s, - options: { lineStyle: 4 }, - })), - ]; -} - -/** - * Create supply relative to own supply metrics - * @param {UtxoCohortObject | AddressCohortObject} cohort - * @returns {AnyFetchedSeriesBlueprint[]} - */ -function createSingleSupplyRelativeToOwnMetrics(cohort) { - const { tree } = cohort; - - return [ - line({ - metric: tree.relative.supplyInProfitRelToOwnSupply, - name: "In Profit", - color: colors.profit, - unit: Unit.pctOwn, - }), - line({ - metric: tree.relative.supplyInLossRelToOwnSupply, - name: "In Loss", - color: colors.loss, - unit: Unit.pctOwn, - }), - priceLine({ - unit: Unit.pctOwn, - number: 100, - style: 0, - color: colors.default, - }), - priceLine({ unit: Unit.pctOwn, number: 50 }), - ]; -} - -/** - * Create supply section for a single cohort (with relative metrics) - * @param {UtxoCohortObject | AddressCohortObject} cohort - * @param {Object} [options] - * @param {AnyFetchedSeriesBlueprint[]} [options.supplyRelative] - Supply relative to circulating supply metrics - * @param {AnyFetchedSeriesBlueprint[]} [options.pnlRelative] - Supply in profit/loss relative to circulating supply metrics - * @returns {AnyFetchedSeriesBlueprint[]} - */ -export function createSingleSupplySeries( - cohort, - { supplyRelative = [], pnlRelative = [] } = {}, -) { - return [ - ...createSingleSupplySeriesBase(cohort), - ...supplyRelative, - ...pnlRelative, - ...createSingleSupplyRelativeToOwnMetrics(cohort), - ]; -} - -/** - * Create supply series for cohorts WITHOUT relative metrics - * @param {CohortWithoutRelative} cohort - * @returns {AnyFetchedSeriesBlueprint[]} - */ -export function createSingleSupplySeriesWithoutRelative(cohort) { - return createSingleSupplySeriesBase(cohort); -} - -/** - * Create supply total series for grouped cohorts - * @template {readonly CohortObject[]} T - * @param {T} list - * @param {Object} [options] - * @param {(cohort: T[number]) => AnyFetchedSeriesBlueprint[]} [options.relativeMetrics] - Generator for relative supply metrics - * @returns {AnyFetchedSeriesBlueprint[]} - */ -export function createGroupedSupplyTotalSeries(list, { relativeMetrics } = {}) { - return list.flatMap((cohort) => [ - ...satsBtcUsd({ - pattern: cohort.tree.supply.total, - name: cohort.name, - color: cohort.color, - }), - ...(relativeMetrics ? relativeMetrics(cohort) : []), - ]); -} - -/** - * Create supply in profit series for grouped cohorts - * @template {readonly CohortObject[]} T - * @param {T} list - * @param {Object} [options] - * @param {(cohort: T[number]) => AnyFetchedSeriesBlueprint[]} [options.relativeMetrics] - Generator for relative supply metrics - * @returns {AnyFetchedSeriesBlueprint[]} - */ -export function createGroupedSupplyInProfitSeries( - list, - { relativeMetrics } = {}, -) { - return list.flatMap((cohort) => [ - ...satsBtcUsd({ - pattern: cohort.tree.unrealized.supplyInProfit, - name: cohort.name, - color: cohort.color, - }), - ...(relativeMetrics ? relativeMetrics(cohort) : []), - ]); -} - -/** - * Create supply in loss series for grouped cohorts - * @template {readonly CohortObject[]} T - * @param {T} list - * @param {Object} [options] - * @param {(cohort: T[number]) => AnyFetchedSeriesBlueprint[]} [options.relativeMetrics] - Generator for relative supply metrics - * @returns {AnyFetchedSeriesBlueprint[]} - */ -export function createGroupedSupplyInLossSeries( - list, - { relativeMetrics } = {}, -) { - return list.flatMap((cohort) => [ - ...satsBtcUsd({ - pattern: cohort.tree.unrealized.supplyInLoss, - name: cohort.name, - color: cohort.color, - }), - ...(relativeMetrics ? relativeMetrics(cohort) : []), - ]); -} - -/** - * Create supply section for grouped cohorts - * @template {readonly (CohortObject | CohortWithoutRelative)[]} T - * @param {T} list - * @param {(metric: string) => string} title - * @param {Object} [options] - * @param {(cohort: T[number]) => AnyFetchedSeriesBlueprint[]} [options.supplyRelativeMetrics] - Generator for supply relative metrics - * @param {(cohort: T[number]) => AnyFetchedSeriesBlueprint[]} [options.profitRelativeMetrics] - Generator for supply in profit relative metrics - * @param {(cohort: T[number]) => AnyFetchedSeriesBlueprint[]} [options.lossRelativeMetrics] - Generator for supply in loss relative metrics - * @returns {PartialOptionsGroup} - */ -export function createGroupedSupplySection( - list, - title, - { supplyRelativeMetrics, profitRelativeMetrics, lossRelativeMetrics } = {}, -) { - return { - name: "Supply", - tree: [ - { - name: "30d Change", - title: title("Supply 30d Change"), - bottom: list.flatMap(({ color, name, tree }) => - satsBtcUsd({ pattern: tree.supply._30dChange, name, color }), - ), - }, - { - name: "In Profit", - title: title("Supply In Profit"), - bottom: createGroupedSupplyInProfitSeries(list, { - relativeMetrics: profitRelativeMetrics, - }), - }, - { - name: "In Loss", - title: title("Supply In Loss"), - bottom: createGroupedSupplyInLossSeries(list, { - relativeMetrics: lossRelativeMetrics, - }), - }, - { - name: "Total", - title: title("Supply"), - bottom: createGroupedSupplyTotalSeries(list, { - relativeMetrics: supplyRelativeMetrics, - }), - }, - ], - }; -} - -// ============================================================================ -// Circulating Supply Relative Metrics Generators -// ============================================================================ - -/** - * Create supply relative to circulating supply series for single cohort - * @param {CohortWithCirculatingSupplyRelative} cohort - * @returns {AnyFetchedSeriesBlueprint[]} - */ -export function createSupplyRelativeToCirculatingSeries(cohort) { - return [ - line({ - metric: cohort.tree.relative.supplyRelToCirculatingSupply, - name: "Supply", - color: colors.default, - unit: Unit.pctSupply, - }), - ]; -} - -/** - * Create supply in profit/loss relative to circulating supply series for single cohort - * @param {CohortWithCirculatingSupplyRelative} cohort - * @returns {AnyFetchedSeriesBlueprint[]} - */ -export function createSupplyPnlRelativeToCirculatingSeries(cohort) { - return [ - line({ - metric: cohort.tree.relative.supplyInProfitRelToCirculatingSupply, - name: "In Profit", - color: colors.profit, - unit: Unit.pctSupply, - }), - line({ - metric: cohort.tree.relative.supplyInLossRelToCirculatingSupply, - name: "In Loss", - color: colors.loss, - unit: Unit.pctSupply, - }), - ]; -} - -/** - * Create supply relative to circulating supply metrics generator for grouped cohorts - * @param {CohortWithCirculatingSupplyRelative} cohort - * @returns {AnyFetchedSeriesBlueprint[]} - */ -export function createGroupedSupplyRelativeMetrics(cohort) { - return [ - line({ - metric: cohort.tree.relative.supplyRelToCirculatingSupply, - name: cohort.name, - color: cohort.color, - unit: Unit.pctSupply, - }), - ]; -} - -/** - * Create supply in profit relative to circulating supply metrics generator for grouped cohorts - * @param {CohortWithCirculatingSupplyRelative} cohort - * @returns {AnyFetchedSeriesBlueprint[]} - */ -export function createGroupedSupplyInProfitRelativeMetrics(cohort) { - return [ - line({ - metric: cohort.tree.relative.supplyInProfitRelToCirculatingSupply, - name: cohort.name, - color: cohort.color, - unit: Unit.pctSupply, - }), - ]; -} - -/** - * Create supply in loss relative to circulating supply metrics generator for grouped cohorts - * @param {CohortWithCirculatingSupplyRelative} cohort - * @returns {AnyFetchedSeriesBlueprint[]} - */ -export function createGroupedSupplyInLossRelativeMetrics(cohort) { - return [ - line({ - metric: cohort.tree.relative.supplyInLossRelToCirculatingSupply, - name: cohort.name, - color: cohort.color, - unit: Unit.pctSupply, - }), - ]; -} - -/** - * Grouped supply relative generators object for cohorts with circulating supply relative - * @type {{ supplyRelativeMetrics: typeof createGroupedSupplyRelativeMetrics, profitRelativeMetrics: typeof createGroupedSupplyInProfitRelativeMetrics, lossRelativeMetrics: typeof createGroupedSupplyInLossRelativeMetrics }} - */ -export const groupedSupplyRelativeGenerators = { - supplyRelativeMetrics: createGroupedSupplyRelativeMetrics, - profitRelativeMetrics: createGroupedSupplyInProfitRelativeMetrics, - lossRelativeMetrics: createGroupedSupplyInLossRelativeMetrics, -}; - -/** - * Create single cohort supply relative options for cohorts with circulating supply relative - * @param {CohortWithCirculatingSupplyRelative} cohort - * @returns {{ supplyRelative: AnyFetchedSeriesBlueprint[], pnlRelative: AnyFetchedSeriesBlueprint[] }} - */ -export function createSingleSupplyRelativeOptions(cohort) { - return { - supplyRelative: createSupplyRelativeToCirculatingSeries(cohort), - pnlRelative: createSupplyPnlRelativeToCirculatingSeries(cohort), - }; -} - -/** - * Create UTXO count series - * @param {readonly CohortObject[]} list - * @param {boolean} useGroupName - * @returns {AnyFetchedSeriesBlueprint[]} - */ -export function createUtxoCountSeries(list, useGroupName) { - return list.flatMap(({ color, name, tree }) => [ - line({ - metric: tree.outputs.utxoCount, - name: useGroupName ? name : "Count", - color, - unit: Unit.count, - }), - ]); -} - -/** - * Create address count series (for address cohorts only) - * @param {readonly AddressCohortObject[]} list - * @param {boolean} useGroupName - * @returns {AnyFetchedSeriesBlueprint[]} - */ -export function createAddressCountSeries(list, useGroupName) { - return list.flatMap(({ color, name, tree }) => [ - line({ - metric: tree.addrCount, - name: useGroupName ? name : "Count", - color: useGroupName ? color : colors.bitcoin, - unit: Unit.count, - }), - ]); -} - -/** - * Create realized price series for grouped cohorts - * @param {readonly CohortObject[]} list - * @returns {FetchedPriceSeriesBlueprint[]} - */ -export function createRealizedPriceSeries(list) { - return list.map(({ color, name, tree }) => - price({ metric: tree.realized.realizedPrice, name, color }), - ); -} - -/** - * Create realized price ratio series for grouped cohorts - * @param {readonly CohortObject[]} list - * @returns {AnyFetchedSeriesBlueprint[]} - */ -export function createRealizedPriceRatioSeries(list) { - return list.map(({ name, tree }) => - baseline({ - metric: tree.realized.realizedPriceExtra.ratio, - name, - unit: Unit.ratio, - base: 1, - }), - ); -} - -/** - * Create realized capitalization series - * @param {readonly CohortObject[]} list - * @param {boolean} useGroupName - * @returns {AnyFetchedSeriesBlueprint[]} - */ -export function createRealizedCapSeries(list, useGroupName) { - return list.flatMap(({ color, name, tree }) => [ - line({ - metric: tree.realized.realizedCap, - name: useGroupName ? name : "Capitalization", - color, - unit: Unit.usd, - }), - ]); -} - -/** - * Create cost basis percentile series (only for cohorts with CostBasisPattern2) - * Includes min (p0) and max (p100) with full rainbow coloring - * @param {readonly CohortWithCostBasisPercentiles[]} list - * @param {boolean} useGroupName - * @returns {FetchedPriceSeriesBlueprint[]} - */ -export function createCostBasisPercentilesSeries(list, useGroupName) { - return list.flatMap(({ name, tree }) => { - const cb = tree.costBasis; - const p = cb.percentiles; - const n = (/** @type {number} */ pct) => - useGroupName ? `${name} p${pct}` : `p${pct}`; - return [ - price({ - metric: cb.max, - name: n(100), - color: colors.pct._100, - defaultActive: false, - }), - price({ - metric: p.pct95, - name: n(95), - color: colors.pct._95, - defaultActive: false, - }), - price({ - metric: p.pct90, - name: n(90), - color: colors.pct._90, - defaultActive: false, - }), - price({ - metric: p.pct85, - name: n(85), - color: colors.pct._85, - defaultActive: false, - }), - price({ - metric: p.pct80, - name: n(80), - color: colors.pct._80, - defaultActive: false, - }), - price({ - metric: p.pct75, - name: n(75), - color: colors.pct._75, - defaultActive: false, - }), - price({ - metric: p.pct70, - name: n(70), - color: colors.pct._70, - defaultActive: false, - }), - price({ - metric: p.pct65, - name: n(65), - color: colors.pct._65, - defaultActive: false, - }), - price({ - metric: p.pct60, - name: n(60), - color: colors.pct._60, - defaultActive: false, - }), - price({ - metric: p.pct55, - name: n(55), - color: colors.pct._55, - defaultActive: false, - }), - price({ metric: p.pct50, name: n(50), color: colors.pct._50 }), - price({ - metric: p.pct45, - name: n(45), - color: colors.pct._45, - defaultActive: false, - }), - price({ - metric: p.pct40, - name: n(40), - color: colors.pct._40, - defaultActive: false, - }), - price({ - metric: p.pct35, - name: n(35), - color: colors.pct._35, - defaultActive: false, - }), - price({ - metric: p.pct30, - name: n(30), - color: colors.pct._30, - defaultActive: false, - }), - price({ - metric: p.pct25, - name: n(25), - color: colors.pct._25, - defaultActive: false, - }), - price({ - metric: p.pct20, - name: n(20), - color: colors.pct._20, - defaultActive: false, - }), - price({ - metric: p.pct15, - name: n(15), - color: colors.pct._15, - defaultActive: false, - }), - price({ - metric: p.pct10, - name: n(10), - color: colors.pct._10, - defaultActive: false, - }), - price({ - metric: p.pct05, - name: n(5), - color: colors.pct._05, - defaultActive: false, - }), - price({ - metric: cb.min, - name: n(0), - color: colors.pct._0, - defaultActive: false, - }), - ]; - }); -} - -/** - * Create invested capital percentile series (only for cohorts with CostBasisPattern2) - * Shows invested capital at each percentile level - * @param {readonly CohortWithCostBasisPercentiles[]} list - * @param {boolean} useGroupName - * @returns {FetchedPriceSeriesBlueprint[]} - */ -export function createInvestedCapitalPercentilesSeries(list, useGroupName) { - return list.flatMap(({ name, tree }) => { - const ic = tree.costBasis.investedCapital; - const n = (/** @type {number} */ pct) => - useGroupName ? `${name} p${pct}` : `p${pct}`; - return [ - price({ - metric: ic.pct95, - name: n(95), - color: colors.pct._95, - defaultActive: false, - }), - price({ - metric: ic.pct90, - name: n(90), - color: colors.pct._90, - defaultActive: false, - }), - price({ - metric: ic.pct85, - name: n(85), - color: colors.pct._85, - defaultActive: false, - }), - price({ - metric: ic.pct80, - name: n(80), - color: colors.pct._80, - defaultActive: false, - }), - price({ - metric: ic.pct75, - name: n(75), - color: colors.pct._75, - defaultActive: false, - }), - price({ - metric: ic.pct70, - name: n(70), - color: colors.pct._70, - defaultActive: false, - }), - price({ - metric: ic.pct65, - name: n(65), - color: colors.pct._65, - defaultActive: false, - }), - price({ - metric: ic.pct60, - name: n(60), - color: colors.pct._60, - defaultActive: false, - }), - price({ - metric: ic.pct55, - name: n(55), - color: colors.pct._55, - defaultActive: false, - }), - price({ metric: ic.pct50, name: n(50), color: colors.pct._50 }), - price({ - metric: ic.pct45, - name: n(45), - color: colors.pct._45, - defaultActive: false, - }), - price({ - metric: ic.pct40, - name: n(40), - color: colors.pct._40, - defaultActive: false, - }), - price({ - metric: ic.pct35, - name: n(35), - color: colors.pct._35, - defaultActive: false, - }), - price({ - metric: ic.pct30, - name: n(30), - color: colors.pct._30, - defaultActive: false, - }), - price({ - metric: ic.pct25, - name: n(25), - color: colors.pct._25, - defaultActive: false, - }), - price({ - metric: ic.pct20, - name: n(20), - color: colors.pct._20, - defaultActive: false, - }), - price({ - metric: ic.pct15, - name: n(15), - color: colors.pct._15, - defaultActive: false, - }), - price({ - metric: ic.pct10, - name: n(10), - color: colors.pct._10, - defaultActive: false, - }), - price({ - metric: ic.pct05, - name: n(5), - color: colors.pct._05, - defaultActive: false, - }), - ]; - }); -} - -/** - * Create spot percentile series (shows current percentile of price relative to cost basis/invested capital) - * @param {readonly CohortWithCostBasisPercentiles[]} list - * @param {boolean} useGroupName - * @returns {FetchedBaselineSeriesBlueprint[]} - */ -export function createSpotPercentileSeries(list, useGroupName) { - return list.flatMap(({ name, color, tree }) => [ - baseline({ - metric: tree.costBasis.spotCostBasisPercentile, - name: useGroupName ? `${name} Cost Basis` : "Cost Basis", - color: useGroupName ? color : colors.default, - unit: Unit.ratio, - }), - baseline({ - metric: tree.costBasis.spotInvestedCapitalPercentile, - name: useGroupName ? `${name} Invested Capital` : "Invested Capital", - color: useGroupName ? color : colors.bitcoin, - unit: Unit.ratio, - defaultActive: false, - }), - ]); -} - -// ============================================================================ -// Activity Section Helpers -// ============================================================================ - -/** - * Create coins destroyed series (coinblocks, coindays, satblocks, satdays) for single cohort - * All metrics on one chart - * @param {CohortObject} cohort - * @returns {AnyFetchedSeriesBlueprint[]} - */ -export function createSingleCoinsDestroyedSeries(cohort) { - const { tree, color } = cohort; - return [ - line({ - metric: tree.activity.coinblocksDestroyed.sum, - name: "Coinblocks", - color, - unit: Unit.coinblocks, - }), - line({ - metric: tree.activity.coinblocksDestroyed.cumulative, - name: "Coinblocks Cumulative", - color, - unit: Unit.coinblocks, - defaultActive: false, - }), - line({ - metric: tree.activity.coindaysDestroyed.sum, - name: "Coindays", - color, - unit: Unit.coindays, - }), - line({ - metric: tree.activity.coindaysDestroyed.cumulative, - name: "Coindays Cumulative", - color, - unit: Unit.coindays, - defaultActive: false, - }), - ]; -} - -/** - * Create coinblocks destroyed series for grouped cohorts (comparison) - * @param {readonly CohortObject[]} list - * @returns {AnyFetchedSeriesBlueprint[]} - */ -export function createGroupedCoinblocksDestroyedSeries(list) { - return list.flatMap(({ color, name, tree }) => [ - line({ - metric: tree.activity.coinblocksDestroyed.sum, - name, - color, - unit: Unit.coinblocks, - }), - ]); -} - -/** - * Create coindays destroyed series for grouped cohorts (comparison) - * @param {readonly CohortObject[]} list - * @returns {AnyFetchedSeriesBlueprint[]} - */ -export function createGroupedCoindaysDestroyedSeries(list) { - return list.flatMap(({ color, name, tree }) => [ - line({ - metric: tree.activity.coindaysDestroyed.sum, - name, - color, - unit: Unit.coindays, - }), - ]); -} - -/** - * Create sent series (sats, btc, usd) for single cohort - all on one chart - * @param {CohortObject} cohort - * @returns {AnyFetchedSeriesBlueprint[]} - */ -export function createSingleSentSeries(cohort) { - const { tree, color } = cohort; - return [ - line({ - metric: tree.activity.sent.sats.sum, - name: "Sent", - color, - unit: Unit.sats, - }), - line({ - metric: tree.activity.sent.sats.cumulative, - name: "Cumulative", - color, - unit: Unit.sats, - defaultActive: false, - }), - line({ - metric: tree.activity.sent.bitcoin.sum, - name: "Sent", - color, - unit: Unit.btc, - }), - line({ - metric: tree.activity.sent.bitcoin.cumulative, - name: "Cumulative", - color, - unit: Unit.btc, - defaultActive: false, - }), - line({ - metric: tree.activity.sent.dollars.sum, - name: "Sent", - color, - unit: Unit.usd, - }), - line({ - metric: tree.activity.sent.dollars.cumulative, - name: "Cumulative", - color, - unit: Unit.usd, - defaultActive: false, - }), - ...satsBtcUsd({ pattern: tree.activity.sent14dEma, name: "14d EMA" }), - ]; -} - -// ============================================================================ -// Sell Side Risk Ratio Helpers -// ============================================================================ - -/** - * Create sell side risk ratio series for single cohort - * @param {{ realized: AnyRealizedPattern }} tree - * @returns {AnyFetchedSeriesBlueprint[]} - */ -export function createSingleSellSideRiskSeries(tree) { - return [ - line({ - metric: tree.realized.sellSideRiskRatio30dEma, - name: "30d EMA", - color: colors.ma._1m, - unit: Unit.ratio, - }), - line({ - metric: tree.realized.sellSideRiskRatio7dEma, - name: "7d EMA", - color: colors.ma._1w, - unit: Unit.ratio, - }), - dots({ - metric: tree.realized.sellSideRiskRatio, - name: "Raw", - color: colors.bitcoin, - unit: Unit.ratio, - }), - ]; -} - -/** - * Create sell side risk ratio series for grouped cohorts - * @param {readonly CohortObject[]} list - * @returns {AnyFetchedSeriesBlueprint[]} - */ -export function createGroupedSellSideRiskSeries(list) { - return list.flatMap(({ color, name, tree }) => [ - line({ - metric: tree.realized.sellSideRiskRatio, - name, - color, - unit: Unit.ratio, - }), - ]); -} - -// ============================================================================ -// Value Created & Destroyed Helpers -// ============================================================================ - -/** - * Create value created & destroyed series for single cohort - * @param {{ realized: AnyRealizedPattern }} tree - * @returns {AnyFetchedSeriesBlueprint[]} - */ -export function createSingleValueCreatedDestroyedSeries(tree) { - return [ - line({ - metric: tree.realized.valueCreated, - name: "Created", - color: colors.usd, - unit: Unit.usd, - }), - line({ - metric: tree.realized.valueDestroyed, - name: "Destroyed", - color: colors.loss, - unit: Unit.usd, - }), - ]; -} - -/** - * Create profit/loss value breakdown series for single cohort - * Shows profit value created/destroyed and loss value created/destroyed - * @param {{ realized: AnyRealizedPattern }} tree - * @returns {AnyFetchedSeriesBlueprint[]} - */ -export function createSingleValueFlowBreakdownSeries(tree) { - return [ - line({ - metric: tree.realized.profitValueCreated, - name: "Profit Created", - color: colors.profit, - unit: Unit.usd, - }), - line({ - metric: tree.realized.profitValueDestroyed, - name: "Profit Destroyed", - color: colors.loss, - unit: Unit.usd, - defaultActive: false, - }), - line({ - metric: tree.realized.lossValueCreated, - name: "Loss Created", - color: colors.bitcoin, - unit: Unit.usd, - defaultActive: false, - }), - line({ - metric: tree.realized.lossValueDestroyed, - name: "Loss Destroyed", - color: colors.loss, - unit: Unit.usd, - }), - ]; -} - -/** - * Create capitulation & profit flow series for single cohort - * @param {{ realized: AnyRealizedPattern }} tree - * @returns {AnyFetchedSeriesBlueprint[]} - */ -export function createSingleCapitulationProfitFlowSeries(tree) { - return [ - line({ - metric: tree.realized.profitFlow, - name: "Profit Flow", - color: colors.profit, - unit: Unit.usd, - }), - line({ - metric: tree.realized.capitulationFlow, - name: "Capitulation Flow", - color: colors.loss, - unit: Unit.usd, - }), - ]; -} - -// ============================================================================ -// SOPR Helpers -// ============================================================================ - -/** - * Create base SOPR series for single cohort (all cohorts have base SOPR) - * @param {{ realized: AnyRealizedPattern }} tree - * @returns {AnyFetchedSeriesBlueprint[]} - */ -export function createSingleSoprSeries(tree) { - return [ - baseline({ - metric: tree.realized.sopr, - name: "SOPR", - unit: Unit.ratio, - base: 1, - }), - baseline({ - metric: tree.realized.sopr7dEma, - name: "7d EMA", - color: colors.bi.p2, - unit: Unit.ratio, - defaultActive: false, - base: 1, - }), - baseline({ - metric: tree.realized.sopr30dEma, - name: "30d EMA", - color: colors.bi.p2, - unit: Unit.ratio, - defaultActive: false, - base: 1, - }), - ]; -} - -// ============================================================================ -// Investor Price Helpers -// ============================================================================ - -/** - * Create investor price series for single cohort - * @param {{ realized: AnyRealizedPattern }} tree - * @param {Color} color - * @returns {FetchedPriceSeriesBlueprint[]} - */ -export function createSingleInvestorPriceSeries(tree, color) { - return [ - price({ - metric: tree.realized.investorPrice, - name: "Investor", - color, - }), - ]; -} - -/** - * Create investor price ratio series for single cohort - * @param {{ realized: AnyRealizedPattern }} tree - * @param {Color} color - * @returns {AnyFetchedSeriesBlueprint[]} - */ -export function createSingleInvestorPriceRatioSeries(tree, color) { - return [ - baseline({ - metric: tree.realized.investorPriceExtra.ratio, - name: "Investor Ratio", - color, - unit: Unit.ratio, - base: 1, - }), - ]; -} - -/** - * Create investor price series for grouped cohorts - * @param {readonly CohortObject[]} list - * @returns {FetchedPriceSeriesBlueprint[]} - */ -export function createInvestorPriceSeries(list) { - return list.map(({ color, name, tree }) => - price({ metric: tree.realized.investorPrice, name, color }), - ); -} - -/** - * Create investor price ratio series for grouped cohorts - * @param {readonly CohortObject[]} list - * @returns {AnyFetchedSeriesBlueprint[]} - */ -export function createInvestorPriceRatioSeries(list) { - return list.map(({ name, tree }) => - baseline({ - metric: tree.realized.investorPriceExtra.ratio, - name, - unit: Unit.ratio, - base: 1, - }), - ); -} - -/** - * Create investor price folder for extended cohorts (with full Z-scores) - * For cohorts with ActivePriceRatioPattern (all, term.*, ageRange.* UTXO cohorts) - * @param {{ tree: { realized: RealizedWithExtras }, color: Color }} cohort - * @param {string} [cohortTitle] - Cohort title (e.g., "STH") - * @returns {PartialOptionsGroup} - */ -export function createInvestorPriceFolderFull(cohort, cohortTitle) { - const { tree, color } = cohort; - return createPriceFolder({ - name: "Investor Price", - cohortTitle, - priceMetric: tree.realized.investorPrice, - ratioPattern: tree.realized.investorPriceExtra, - color, - }); -} - -/** - * Create investor price folder for basic cohorts (price + ratio only) - * For cohorts with InvestorPriceExtraPattern (only .ratio field) - * @param {{ tree: { realized: AnyRealizedPattern }, color: Color }} cohort - * @param {string} [cohortTitle] - Cohort title (e.g., "STH") - * @returns {PartialOptionsGroup} - */ -export function createInvestorPriceFolderBasic(cohort, cohortTitle) { - const { tree, color } = cohort; - return createBasicPriceFolder({ - name: "Investor Price", - cohortTitle, - priceMetric: tree.realized.investorPrice, - ratioMetric: tree.realized.investorPriceExtra.ratio, - color, - }); -} - -/** - * Create investor price folder for grouped cohorts - * @param {readonly CohortObject[]} list - * @param {(metric: string) => string} title - * @returns {PartialOptionsGroup} - */ -export function createGroupedInvestorPriceFolder(list, title) { - return createGroupedPriceFolder({ - name: "Investor Price", - title, - list, - getPrice: (tree) => tree.realized.investorPrice, - getRatio: (tree) => tree.realized.investorPriceExtra.ratio, - }); -} - -// ============================================================================ -// Peak Regret Helpers -// ============================================================================ - -/** - * Create realized peak regret series for single cohort - * @param {{ realized: AnyRealizedPattern }} tree - * @param {Color} color - * @returns {AnyFetchedSeriesBlueprint[]} - */ -export function createSingleRealizedAthRegretSeries(tree, color) { - return [ - line({ - metric: tree.realized.peakRegret.sum, - name: "Peak Regret", - color, - unit: Unit.usd, - }), - line({ - metric: tree.realized.peakRegret.cumulative, - name: "Cumulative", - color, - unit: Unit.usd, - defaultActive: false, - }), - baseline({ - metric: tree.realized.peakRegretRelToRealizedCap, - name: "Rel. to Realized Cap", - color, - unit: Unit.pctRcap, - }), - ]; -} - -/** - * Create realized ATH regret series for grouped cohorts - * @param {readonly CohortObject[]} list - * @returns {AnyFetchedSeriesBlueprint[]} - */ -export function createGroupedRealizedAthRegretSeries(list) { - return list.flatMap(({ color, name, tree }) => [ - line({ - metric: tree.realized.peakRegret.sum, - name, - color, - unit: Unit.usd, - }), - baseline({ - metric: tree.realized.peakRegretRelToRealizedCap, - name, - color, - unit: Unit.pctRcap, - }), - ]); -} - -// ============================================================================ -// Sentiment Helpers (greedIndex, painIndex, netSentiment) -// ============================================================================ - -/** - * Create sentiment series for single cohort - * @param {{ unrealized: UnrealizedPattern }} tree - * @returns {AnyFetchedSeriesBlueprint[]} - */ -export function createSingleSentimentSeries(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, - }), - ]; -} - -/** - * Create net sentiment series for grouped cohorts - * @param {readonly { color: Color, name: string, tree: { unrealized: UnrealizedPattern } }[]} list - * @returns {AnyFetchedSeriesBlueprint[]} - */ -export function createGroupedNetSentimentSeries(list) { - return list.flatMap(({ color, name, tree }) => [ - baseline({ - metric: tree.unrealized.netSentiment, - name, - color, - unit: Unit.usd, - }), - ]); -} - -/** - * Create greed index series for grouped cohorts - * @param {readonly { color: Color, name: string, tree: { unrealized: UnrealizedPattern } }[]} list - * @returns {AnyFetchedSeriesBlueprint[]} - */ -export function createGroupedGreedIndexSeries(list) { - return list.flatMap(({ color, name, tree }) => [ - line({ - metric: tree.unrealized.greedIndex, - name, - color, - unit: Unit.usd, - }), - ]); -} - -/** - * Create pain index series for grouped cohorts - * @param {readonly { color: Color, name: string, tree: { unrealized: UnrealizedPattern } }[]} list - * @returns {AnyFetchedSeriesBlueprint[]} - */ -export function createGroupedPainIndexSeries(list) { - return list.flatMap(({ color, name, tree }) => [ - line({ - metric: tree.unrealized.painIndex, - name, - color, - unit: Unit.usd, - }), - ]); -} diff --git a/website/scripts/options/distribution/utxo.js b/website/scripts/options/distribution/utxo.js deleted file mode 100644 index 0d39489ca..000000000 --- a/website/scripts/options/distribution/utxo.js +++ /dev/null @@ -1,2549 +0,0 @@ -/** - * UTXO cohort folder builders - * Creates option trees for UTXO-based cohorts (no addrCount) - * - * Cohort capabilities (based on brk client patterns): - * - * With adjustedSopr (RealizedPattern3/4): - * - all, term.short, maxAge.* - * - * Without adjustedSopr (RealizedPattern/2): - * - term.long, minAge.*, ageRange.*, epoch.*, all amount cohorts - * - * With cost basis percentiles (CostBasisPattern2): - * - all, term.*, ageRange.* - * - * Without percentiles (CostBasisPattern): - * - maxAge.*, minAge.*, epoch.*, all amount cohorts - * - * Folder builders: - * - createCohortFolderFull: adjustedSopr + percentiles (all, term.short) - * - createCohortFolderWithAdjusted: adjustedSopr only (maxAge.*) - * - createCohortFolderWithPercentiles: percentiles only (term.long, ageRange.*) - * - createCohortFolderBasic: neither (minAge.*, epoch.*, amount cohorts) - */ - -import { - createSingleSoprSeries, - createSingleRealizedAthRegretSeries, - createGroupedRealizedAthRegretSeries, - createSingleSentimentSeries, - createGroupedNetSentimentSeries, - createGroupedGreedIndexSeries, - createGroupedPainIndexSeries, -} from "./shared.js"; -import { formatCohortTitle, satsBtcUsd } from "../shared.js"; -import { - createCostBasisSection, - createCostBasisSectionWithPercentiles, - createGroupedCostBasisSection, - createGroupedCostBasisSectionWithPercentiles, -} from "./cost-basis.js"; -import { - createHoldingsSection, - createHoldingsSectionAll, - createHoldingsSectionAddress, - createHoldingsSectionWithRelative, - createGroupedHoldingsSection, - createGroupedHoldingsSectionAddress, - createGroupedHoldingsSectionWithRelative, -} from "./holdings.js"; -import { - createPricesSectionFull, - createPricesSectionBasic, - createGroupedPricesSection, -} from "./prices.js"; -import { - createValuationSection, - createValuationSectionFull, - createGroupedValuationSection, -} from "./valuation.js"; -import { - createActivitySection, - createActivitySectionWithAdjusted, - createGroupedActivitySection, - createGroupedActivitySectionWithAdjusted, -} from "./activity.js"; -import { - createProfitabilitySection, - createProfitabilitySectionWithNupl, - createProfitabilitySectionAll, - createProfitabilitySectionWithPeakRegret, - createGroupedProfitabilitySection, - createGroupedProfitabilitySectionWithNupl, -} from "./profitability.js"; -import { Unit } from "../../utils/units.js"; -import { line, baseline } from "../series.js"; -import { priceLine } from "../constants.js"; -import { colors } from "../../utils/colors.js"; - -// ============================================================================ -// Folder Builders (4 variants based on pattern capabilities) -// ============================================================================ - -/** - * All folder: for the special "All" cohort (adjustedSopr + percentiles + RelToMarketCap) - * @param {CohortAll} args - * @returns {PartialOptionsGroup} - */ -export function createCohortFolderAll(args) { - const title = formatCohortTitle(args.name); - return { - name: args.name || "all", - tree: [ - createHoldingsSectionAll({ cohort: args, title }), - createValuationSectionFull({ cohort: args, title }), - createPricesSectionFull({ cohort: args, title }), - createCostBasisSectionWithPercentiles({ cohort: args, title }), - createProfitabilitySectionAll({ cohort: args, title }), - createActivitySectionWithAdjusted({ cohort: args, title }), - ], - }; -} - -/** - * Full folder: adjustedSopr + percentiles + RelToMarketCap (term.short only) - * @param {CohortFull | CohortGroupFull} args - * @returns {PartialOptionsGroup} - */ -export function createCohortFolderFull(args) { - if ("list" in args) { - const { list } = args; - const title = formatCohortTitle(args.title); - return { - name: args.name || "all", - tree: [ - createGroupedHoldingsSectionWithRelative({ list, title }), - createGroupedValuationSection({ list, title }), - createGroupedPricesSection({ list, title }), - createGroupedCostBasisSectionWithPercentiles({ list, title }), - createGroupedProfitabilitySectionWithNupl({ list, title }), - createGroupedActivitySectionWithAdjusted({ list, title }), - ], - }; - } - const title = formatCohortTitle(args.name); - return { - name: args.name || "all", - tree: [ - createHoldingsSectionWithRelative({ cohort: args, title }), - createValuationSectionFull({ cohort: args, title }), - createPricesSectionFull({ cohort: args, title }), - createCostBasisSectionWithPercentiles({ cohort: args, title }), - createProfitabilitySectionWithNupl({ cohort: args, title }), - createActivitySectionWithAdjusted({ cohort: args, title }), - ], - }; -} - -/** - * Adjusted folder: adjustedSopr only, no percentiles (maxAge.*) - * @param {CohortWithAdjusted | CohortGroupWithAdjusted} args - * @returns {PartialOptionsGroup} - */ -export function createCohortFolderWithAdjusted(args) { - if ("list" in args) { - const { list } = args; - const title = formatCohortTitle(args.title); - return { - name: args.name || "all", - tree: [ - createGroupedHoldingsSectionWithRelative({ list, title }), - createGroupedValuationSection({ list, title }), - createGroupedPricesSection({ list, title }), - createGroupedCostBasisSection({ list, title }), - createGroupedProfitabilitySectionWithNupl({ list, title }), - createGroupedActivitySectionWithAdjusted({ list, title }), - ], - }; - } - const title = formatCohortTitle(args.name); - return { - name: args.name || "all", - tree: [ - createHoldingsSectionWithRelative({ cohort: args, title }), - createValuationSection({ cohort: args, title }), - createPricesSectionBasic({ cohort: args, title }), - createCostBasisSection({ cohort: args, title }), - createProfitabilitySectionWithNupl({ cohort: args, title }), - createActivitySectionWithAdjusted({ cohort: args, title }), - ], - }; -} - -/** - * Folder for cohorts with nupl + percentiles (term.short, term.long) - * @param {CohortWithNuplPercentiles | CohortGroupWithNuplPercentiles} args - * @returns {PartialOptionsGroup} - */ -export function createCohortFolderWithNupl(args) { - if ("list" in args) { - const { list } = args; - const title = formatCohortTitle(args.title); - return { - name: args.name || "all", - tree: [ - createGroupedHoldingsSectionWithRelative({ list, title }), - createGroupedValuationSection({ list, title }), - createGroupedPricesSection({ list, title }), - createGroupedCostBasisSectionWithPercentiles({ list, title }), - createGroupedProfitabilitySectionWithNupl({ list, title }), - createGroupedActivitySection({ list, title }), - ], - }; - } - const title = formatCohortTitle(args.name); - return { - name: args.name || "all", - tree: [ - createHoldingsSectionWithRelative({ cohort: args, title }), - createValuationSectionFull({ cohort: args, title }), - createPricesSectionFull({ cohort: args, title }), - createCostBasisSectionWithPercentiles({ cohort: args, title }), - createProfitabilitySectionWithNupl({ cohort: args, title }), - createActivitySection({ cohort: args, title }), - ], - }; -} - -/** - * Age range folder: ageRange.* (no nupl via RelativePattern2) - * @param {CohortAgeRange | CohortGroupAgeRange} args - * @returns {PartialOptionsGroup} - */ -export function createCohortFolderAgeRange(args) { - if ("list" in args) { - const { list } = args; - const title = formatCohortTitle(args.title); - return { - name: args.name || "all", - tree: [ - createGroupedHoldingsSection({ list, title }), - createGroupedValuationSection({ list, title }), - createGroupedPricesSection({ list, title }), - createGroupedCostBasisSectionWithPercentiles({ list, title }), - createGroupedProfitabilitySection({ list, title }), - createGroupedActivitySection({ list, title }), - ], - }; - } - const title = formatCohortTitle(args.name); - return { - name: args.name || "all", - tree: [ - createHoldingsSection({ cohort: args, title }), - createValuationSectionFull({ cohort: args, title }), - createPricesSectionFull({ cohort: args, title }), - createCostBasisSectionWithPercentiles({ cohort: args, title }), - createProfitabilitySection({ cohort: args, title }), - createActivitySection({ cohort: args, title }), - ], - }; -} - -/** - * MinAge folder - has peakRegret in unrealized (minAge.*) - * @param {CohortMinAge | CohortGroupMinAge} args - * @returns {PartialOptionsGroup} - */ -export function createCohortFolderMinAge(args) { - if ("list" in args) { - const { list } = args; - const title = formatCohortTitle(args.title); - return { - name: args.name || "all", - tree: [ - createGroupedHoldingsSectionWithRelative({ list, title }), - createGroupedValuationSection({ list, title }), - createGroupedPricesSection({ list, title }), - createGroupedCostBasisSection({ list, title }), - createGroupedProfitabilitySection({ list, title }), - createGroupedActivitySection({ list, title }), - ], - }; - } - const title = formatCohortTitle(args.name); - return { - name: args.name || "all", - tree: [ - createHoldingsSectionWithRelative({ cohort: args, title }), - createValuationSection({ cohort: args, title }), - createPricesSectionBasic({ cohort: args, title }), - createCostBasisSection({ cohort: args, title }), - createProfitabilitySectionWithPeakRegret({ cohort: args, title }), - createActivitySection({ cohort: args, title }), - ], - }; -} - -/** - * Basic folder WITH RelToMarketCap (geAmount.*, ltAmount.*) - * @param {CohortBasicWithMarketCap | CohortGroupBasicWithMarketCap} args - * @returns {PartialOptionsGroup} - */ -export function createCohortFolderBasicWithMarketCap(args) { - if ("list" in args) { - const { list } = args; - const title = formatCohortTitle(args.title); - return { - name: args.name || "all", - tree: [ - createGroupedHoldingsSectionWithRelative({ list, title }), - createGroupedValuationSection({ list, title }), - createGroupedPricesSection({ list, title }), - createGroupedCostBasisSection({ list, title }), - createGroupedProfitabilitySectionWithNupl({ list, title }), - createGroupedActivitySection({ list, title }), - ], - }; - } - const title = formatCohortTitle(args.name); - return { - name: args.name || "all", - tree: [ - createHoldingsSectionWithRelative({ cohort: args, title }), - createValuationSection({ cohort: args, title }), - createPricesSectionBasic({ cohort: args, title }), - createCostBasisSection({ cohort: args, title }), - createProfitabilitySectionWithNupl({ cohort: args, title }), - createActivitySection({ cohort: args, title }), - ], - }; -} - -/** - * Basic folder WITHOUT RelToMarketCap (epoch.*, amountRange.*, year.*) - * @param {CohortBasicWithoutMarketCap | CohortGroupBasicWithoutMarketCap} args - * @returns {PartialOptionsGroup} - */ -export function createCohortFolderBasicWithoutMarketCap(args) { - if ("list" in args) { - const { list } = args; - const title = formatCohortTitle(args.title); - return { - name: args.name || "all", - tree: [ - createGroupedHoldingsSection({ list, title }), - createGroupedValuationSection({ list, title }), - createGroupedPricesSection({ list, title }), - createGroupedCostBasisSection({ list, title }), - createGroupedProfitabilitySection({ list, title }), - createGroupedActivitySection({ list, title }), - ], - }; - } - const title = formatCohortTitle(args.name); - return { - name: args.name || "all", - tree: [ - createHoldingsSection({ cohort: args, title }), - createValuationSection({ cohort: args, title }), - createPricesSectionBasic({ cohort: args, title }), - createCostBasisSection({ cohort: args, title }), - createProfitabilitySection({ cohort: args, title }), - createActivitySection({ cohort: args, title }), - ], - }; -} - -/** - * Address folder: like basic but with address count (addressable type cohorts) - * Uses base unrealized section (no RelToMarketCap since it extends CohortBasicWithoutMarketCap) - * @param {CohortAddress | CohortGroupAddress} args - * @returns {PartialOptionsGroup} - */ -export function createCohortFolderAddress(args) { - if ("list" in args) { - const { list } = args; - const title = formatCohortTitle(args.title); - return { - name: args.name || "all", - tree: [ - createGroupedHoldingsSectionAddress({ list, title }), - createGroupedValuationSection({ list, title }), - createGroupedPricesSection({ list, title }), - createGroupedCostBasisSection({ list, title }), - createGroupedProfitabilitySection({ list, title }), - createGroupedActivitySection({ list, title }), - ], - }; - } - const title = formatCohortTitle(args.name); - return { - name: args.name || "all", - tree: [ - createHoldingsSectionAddress({ cohort: args, title }), - createValuationSection({ cohort: args, title }), - createPricesSectionBasic({ cohort: args, title }), - createCostBasisSection({ cohort: args, title }), - createProfitabilitySection({ cohort: args, title }), - createActivitySection({ cohort: args, title }), - ], - }; -} - -/** - * Folder for cohorts WITHOUT relative section (edge case types: empty, p2ms, unknown) - * @param {CohortWithoutRelative | CohortGroupWithoutRelative} args - * @returns {PartialOptionsGroup} - */ -export function createCohortFolderWithoutRelative(args) { - if ("list" in args) { - const { list } = args; - const title = formatCohortTitle(args.title); - return { - name: args.name || "all", - tree: [ - createGroupedHoldingsSection({ list, title }), - createGroupedValuationSection({ list, title }), - createGroupedPricesSection({ list, title }), - createGroupedCostBasisSection({ list, title }), - createGroupedProfitabilitySection({ list, title }), - createGroupedActivitySection({ list, title }), - ], - }; - } - const title = formatCohortTitle(args.name); - return { - name: args.name || "all", - tree: [ - createHoldingsSection({ cohort: args, title }), - createValuationSection({ cohort: args, title }), - createPricesSectionBasic({ cohort: args, title }), - createCostBasisSection({ cohort: args, title }), - createProfitabilitySection({ cohort: args, title }), - createActivitySection({ cohort: args, title }), - ], - }; -} - -/** - * Create realized section for CohortAll/CohortFull (adjustedSopr + full ratio) - * @param {CohortAll | CohortFull} cohort - * @param {(metric: string) => string} title - * @returns {PartialOptionsGroup} - */ -function createSingleRealizedSectionFull(cohort, title) { - const { tree, color } = cohort; - return { - name: "Realized", - tree: [ - { - name: "Capitalization", - title: title("Realized Cap"), - bottom: createSingleRealizedCapSeries(cohort, { - extra: createRealizedCapRatioSeries(tree), - }), - }, - ...createSingleRealizedPnlSection(cohort, title, { - extra: createRealizedPnlRatioSeries(tree), - }), - { - name: "Peak Regret", - title: title("Realized Peak Regret"), - bottom: createSingleRealizedAthRegretSeries(tree, color), - }, - createSingleSoprSectionWithAdjusted(cohort, title), - ], - }; -} - -/** - * Create realized section for CohortWithAdjusted (adjustedSopr but partial ratio) - * @param {CohortWithAdjusted} cohort - * @param {(metric: string) => string} title - * @returns {PartialOptionsGroup} - */ -function createSingleRealizedSectionWithAdjusted(cohort, title) { - const { tree, color } = cohort; - return { - name: "Realized", - tree: [ - { - name: "Capitalization", - title: title("Realized Cap"), - bottom: createSingleRealizedCapSeries(cohort), - }, - ...createSingleRealizedPnlSection(cohort, title), - { - name: "Peak Regret", - title: title("Realized Peak Regret"), - bottom: createSingleRealizedAthRegretSeries(tree, color), - }, - createSingleSoprSectionWithAdjusted(cohort, title), - ], - }; -} - -/** - * Create realized section with adjusted SOPR for grouped cohorts - * @template {readonly (CohortFull | CohortWithAdjusted)[]} T - * @param {T} list - * @param {(metric: string) => string} title - * @param {Object} [options] - * @param {(cohort: T[number]) => AnyFetchedSeriesBlueprint[]} [options.ratioMetrics] - Generator for ratio metrics per cohort - * @returns {PartialOptionsGroup} - */ -function createGroupedRealizedSectionWithAdjusted( - list, - title, - { ratioMetrics } = {}, -) { - return { - name: "Realized", - tree: [ - { - name: "Capitalization", - title: title("Realized Cap"), - bottom: createGroupedRealizedCapSeries(list), - }, - ...createGroupedRealizedPnlSections(list, title, { ratioMetrics }), - { - name: "Peak Regret", - title: title("Realized Peak Regret"), - bottom: createGroupedRealizedAthRegretSeries(list), - }, - createGroupedSoprSectionWithAdjusted(list, title), - ], - }; -} - -/** - * Create realized section for CohortWithPercentiles (no adjustedSopr but full ratio) - * @param {CohortWithPercentiles} cohort - * @param {(metric: string) => string} title - * @returns {PartialOptionsGroup} - */ -function createSingleRealizedSectionWithPercentiles(cohort, title) { - const { tree, color } = cohort; - return { - name: "Realized", - tree: [ - { - name: "Capitalization", - title: title("Realized Cap"), - bottom: createSingleRealizedCapSeries(cohort, { - extra: createRealizedCapRatioSeries(tree), - }), - }, - ...createSingleRealizedPnlSection(cohort, title, { - extra: createRealizedPnlRatioSeries(tree), - }), - { - name: "Peak Regret", - title: title("Realized Peak Regret"), - bottom: createSingleRealizedAthRegretSeries(tree, color), - }, - createSingleSoprSectionBasic(cohort, title), - ], - }; -} - -/** - * Create realized section for CohortBasic (no adjustedSopr, partial ratio) - * @param {CohortBasic | CohortAddress | CohortWithoutRelative} cohort - * @param {(metric: string) => string} title - * @returns {PartialOptionsGroup} - */ -function createSingleRealizedSectionBasic(cohort, title) { - const { tree, color } = cohort; - return { - name: "Realized", - tree: [ - { - name: "Capitalization", - title: title("Realized Cap"), - bottom: createSingleRealizedCapSeries(cohort), - }, - ...createSingleRealizedPnlSection(cohort, title), - { - name: "Peak Regret", - title: title("Realized Peak Regret"), - bottom: createSingleRealizedAthRegretSeries(tree, color), - }, - createSingleSoprSectionBasic(cohort, title), - ], - }; -} - -/** - * Create realized section without adjusted SOPR for grouped cohorts - * @template {readonly (CohortWithPercentiles | CohortBasic | CohortAddress | CohortWithoutRelative)[]} T - * @param {T} list - * @param {(metric: string) => string} title - * @param {Object} [options] - * @param {(cohort: T[number]) => AnyFetchedSeriesBlueprint[]} [options.ratioMetrics] - Generator for ratio metrics per cohort - * @returns {PartialOptionsGroup} - */ -function createGroupedRealizedSectionBasic(list, title, { ratioMetrics } = {}) { - return { - name: "Realized", - tree: [ - { - name: "Capitalization", - title: title("Realized Cap"), - bottom: createGroupedRealizedCapSeries(list), - }, - ...createGroupedRealizedPnlSections(list, title, { ratioMetrics }), - { - name: "Peak Regret", - title: title("Realized Peak Regret"), - bottom: createGroupedRealizedAthRegretSeries(list), - }, - createGroupedSoprSectionBasic(list, title), - ], - }; -} - -/** - * Create realized cap series for single cohort - * @param {UtxoCohortObject | CohortWithoutRelative} cohort - * @param {Object} [options] - * @param {AnyFetchedSeriesBlueprint[]} [options.extra] - Additional series (e.g., ratio for cohorts with RealizedWithCapRatio) - * @returns {AnyFetchedSeriesBlueprint[]} - */ -function createSingleRealizedCapSeries(cohort, { extra = [] } = {}) { - const { color, tree } = cohort; - - return [ - line({ - metric: tree.realized.realizedCap, - name: "Capitalization", - color, - unit: Unit.usd, - }), - line({ - metric: tree.realized.realizedValue, - name: "Value", - color, - unit: Unit.usd, - defaultActive: false, - }), - baseline({ - metric: tree.realized.realizedCap30dDelta, - name: "30d Change", - unit: Unit.usd, - defaultActive: false, - }), - ...extra, - ]; -} - -/** - * Create realized cap ratio series (for cohorts with RealizedPattern2 or RealizedPattern3) - * @param {{ realized: RealizedWithExtras }} tree - * @returns {AnyFetchedSeriesBlueprint[]} - */ -function createRealizedCapRatioSeries(tree) { - return [ - baseline({ - metric: tree.realized.realizedCapRelToOwnMarketCap, - name: "Ratio", - unit: Unit.pctOwnMcap, - options: { baseValue: { price: 100 } }, - }), - priceLine({ - unit: Unit.pctOwnMcap, - defaultActive: true, - number: 100, - }), - ]; -} - -/** - * Create realized cap series for grouped cohorts - * @param {readonly (UtxoCohortObject | CohortWithoutRelative)[]} list - * @returns {AnyFetchedSeriesBlueprint[]} - */ -function createGroupedRealizedCapSeries(list) { - return list.map(({ color, name, tree }) => - line({ - metric: tree.realized.realizedCap, - name, - color, - unit: Unit.usd, - }), - ); -} - -/** - * Create realized PnL ratio series (for cohorts with RealizedPattern2 or RealizedPattern3) - * @param {{ realized: RealizedWithExtras }} tree - * @returns {AnyFetchedSeriesBlueprint[]} - */ -function createRealizedPnlRatioSeries(tree) { - return [ - line({ - metric: tree.realized.realizedProfitToLossRatio, - name: "P/L Ratio", - color: colors.plRatio, - unit: Unit.ratio, - }), - ]; -} - -/** - * Create realized PnL ratio metrics generator for grouped cohorts with RealizedWithExtras - * @param {CohortWithRealizedExtras} cohort - * @returns {AnyFetchedSeriesBlueprint[]} - */ -function createGroupedRealizedPnlRatioMetrics(cohort) { - return [ - line({ - metric: cohort.tree.realized.realizedProfitToLossRatio, - name: cohort.name, - color: cohort.color, - unit: Unit.ratio, - }), - ]; -} - -/** - * Create realized PnL section for single cohort - * @param {UtxoCohortObject | CohortWithoutRelative} cohort - * @param {(metric: string) => string} title - * @param {Object} [options] - * @param {AnyFetchedSeriesBlueprint[]} [options.extra] - Extra series (e.g., pnl ratio for cohorts with RealizedWithPnlRatio) - * @returns {PartialOptionsTree} - */ -function createSingleRealizedPnlSection(cohort, title, { extra = [] } = {}) { - const { tree } = cohort; - - return [ - { - name: "P&L", - tree: [ - { - name: "Sum", - title: title("Realized P&L"), - bottom: [ - // USD - line({ - metric: tree.realized.realizedProfit.sum, - name: "Profit", - color: colors.profit, - unit: Unit.usd, - }), - line({ - metric: tree.realized.realizedProfit7dEma, - name: "Profit 7d EMA", - color: colors.profit, - unit: Unit.usd, - }), - line({ - metric: tree.realized.realizedLoss.sum, - name: "Loss", - color: colors.loss, - unit: Unit.usd, - }), - line({ - metric: tree.realized.realizedLoss7dEma, - name: "Loss 7d EMA", - color: colors.loss, - unit: Unit.usd, - }), - line({ - metric: tree.realized.negRealizedLoss.sum, - name: "Negative Loss", - color: colors.loss, - unit: Unit.usd, - defaultActive: false, - }), - ...extra, - line({ - metric: tree.realized.totalRealizedPnl, - name: "Total", - color: colors.default, - unit: Unit.usd, - defaultActive: false, - }), - // % of R.Cap - baseline({ - metric: tree.realized.realizedProfitRelToRealizedCap.sum, - name: "Profit", - color: colors.profit, - unit: Unit.pctRcap, - }), - baseline({ - metric: tree.realized.realizedLossRelToRealizedCap.sum, - name: "Loss", - color: colors.loss, - unit: Unit.pctRcap, - }), - ], - }, - { - name: "Cumulative", - title: title("Realized P&L (Total)"), - bottom: [ - // USD - line({ - metric: tree.realized.realizedProfit.cumulative, - name: "Profit", - color: colors.profit, - unit: Unit.usd, - }), - line({ - metric: tree.realized.realizedLoss.cumulative, - name: "Loss", - color: colors.loss, - unit: Unit.usd, - }), - line({ - metric: tree.realized.negRealizedLoss.cumulative, - name: "Negative Loss", - color: colors.loss, - unit: Unit.usd, - defaultActive: false, - }), - // % of R.Cap - baseline({ - metric: tree.realized.realizedProfitRelToRealizedCap.cumulative, - name: "Profit", - color: colors.profit, - unit: Unit.pctRcap, - }), - baseline({ - metric: tree.realized.realizedLossRelToRealizedCap.cumulative, - name: "Loss", - color: colors.loss, - unit: Unit.pctRcap, - }), - ], - }, - ], - }, - { - name: "Net P&L", - tree: [ - { - name: "Sum", - title: title("Net Realized P&L"), - bottom: [ - // USD - baseline({ - metric: tree.realized.netRealizedPnl.sum, - name: "Net", - unit: Unit.usd, - }), - baseline({ - metric: tree.realized.netRealizedPnl7dEma, - name: "Net 7d EMA", - unit: Unit.usd, - }), - // % of R.Cap - baseline({ - metric: tree.realized.netRealizedPnlRelToRealizedCap.sum, - name: "Net", - unit: Unit.pctRcap, - }), - ], - }, - { - name: "Cumulative", - title: title("Net Realized P&L (Total)"), - bottom: [ - // USD - baseline({ - metric: tree.realized.netRealizedPnl.cumulative, - name: "Net", - unit: Unit.usd, - }), - baseline({ - metric: tree.realized.netRealizedPnlCumulative30dDelta, - name: "30d Change", - unit: Unit.usd, - defaultActive: false, - }), - // % of R.Cap - baseline({ - metric: tree.realized.netRealizedPnlRelToRealizedCap.cumulative, - name: "Net", - unit: Unit.pctRcap, - }), - baseline({ - metric: - tree.realized.netRealizedPnlCumulative30dDeltaRelToRealizedCap, - name: "30d Change", - unit: Unit.pctRcap, - defaultActive: false, - }), - // % of M.Cap - baseline({ - metric: - tree.realized.netRealizedPnlCumulative30dDeltaRelToMarketCap, - name: "30d Change", - unit: Unit.pctMcap, - }), - ], - }, - ], - }, - { - name: "Sent In P/L", - tree: [ - { - name: "In Profit", - title: title("Sent In Profit"), - bottom: [ - line({ - metric: tree.realized.sentInProfit.bitcoin.sum, - name: "Sum", - color: colors.profit, - unit: Unit.btc, - }), - line({ - metric: tree.realized.sentInProfit.bitcoin.cumulative, - name: "Cumulative", - color: colors.profit, - unit: Unit.btc, - defaultActive: false, - }), - line({ - metric: tree.realized.sentInProfit.sats.sum, - name: "Sum", - color: colors.profit, - unit: Unit.sats, - }), - line({ - metric: tree.realized.sentInProfit.sats.cumulative, - name: "Cumulative", - color: colors.profit, - unit: Unit.sats, - defaultActive: false, - }), - line({ - metric: tree.realized.sentInProfit.dollars.sum, - name: "Sum", - color: colors.profit, - unit: Unit.usd, - }), - line({ - metric: tree.realized.sentInProfit.dollars.cumulative, - name: "Cumulative", - color: colors.profit, - unit: Unit.usd, - defaultActive: false, - }), - ], - }, - { - name: "In Loss", - title: title("Sent In Loss"), - bottom: [ - line({ - metric: tree.realized.sentInLoss.bitcoin.sum, - name: "Sum", - color: colors.loss, - unit: Unit.btc, - }), - line({ - metric: tree.realized.sentInLoss.bitcoin.cumulative, - name: "Cumulative", - color: colors.loss, - unit: Unit.btc, - defaultActive: false, - }), - line({ - metric: tree.realized.sentInLoss.sats.sum, - name: "Sum", - color: colors.loss, - unit: Unit.sats, - }), - line({ - metric: tree.realized.sentInLoss.sats.cumulative, - name: "Cumulative", - color: colors.loss, - unit: Unit.sats, - defaultActive: false, - }), - line({ - metric: tree.realized.sentInLoss.dollars.sum, - name: "Sum", - color: colors.loss, - unit: Unit.usd, - }), - line({ - metric: tree.realized.sentInLoss.dollars.cumulative, - name: "Cumulative", - color: colors.loss, - unit: Unit.usd, - defaultActive: false, - }), - ], - }, - { - name: "In Profit 14d EMA", - title: title("Sent In Profit 14d EMA"), - bottom: satsBtcUsd({ - pattern: tree.realized.sentInProfit14dEma, - name: "14d EMA", - color: colors.profit, - }), - }, - { - name: "In Loss 14d EMA", - title: title("Sent In Loss 14d EMA"), - bottom: satsBtcUsd({ - pattern: tree.realized.sentInLoss14dEma, - name: "14d EMA", - color: colors.loss, - }), - }, - ], - }, - ]; -} - -/** - * Create realized PnL sections for grouped cohorts - * @template {readonly (UtxoCohortObject | CohortWithoutRelative)[]} T - * @param {T} list - * @param {(metric: string) => string} title - * @param {Object} [options] - * @param {(cohort: T[number]) => AnyFetchedSeriesBlueprint[]} [options.ratioMetrics] - Generator for ratio metrics per cohort - * @returns {PartialOptionsTree} - */ -function createGroupedRealizedPnlSections(list, title, { ratioMetrics } = {}) { - const pnlConfigs = /** @type {const} */ ([ - { - name: "Profit", - sum: "realizedProfit", - ema: "realizedProfit7dEma", - rel: "realizedProfitRelToRealizedCap", - isNet: false, - }, - { - name: "Loss", - sum: "realizedLoss", - ema: "realizedLoss7dEma", - rel: "realizedLossRelToRealizedCap", - isNet: false, - }, - { - name: "Net P&L", - sum: "netRealizedPnl", - ema: "netRealizedPnl7dEma", - rel: "netRealizedPnlRelToRealizedCap", - isNet: true, - }, - ]); - - return [ - ...pnlConfigs.map(({ name, sum, ema, rel, isNet }) => ({ - name, - tree: [ - { - name: "Sum", - title: title(`Realized ${name}`), - bottom: [ - ...list.flatMap(({ color, name, tree }) => [ - (isNet ? baseline : line)({ - metric: tree.realized[sum].sum, - name, - color, - unit: Unit.usd, - }), - baseline({ - metric: tree.realized[rel].sum, - name, - color, - unit: Unit.pctRcap, - }), - ]), - ], - }, - { - name: "7d EMA", - title: title(`Realized ${name} 7d EMA`), - bottom: [ - ...list.map(({ color, name, tree }) => - (isNet ? baseline : line)({ - metric: tree.realized[ema], - name, - color, - unit: Unit.usd, - }), - ), - ], - }, - ], - })), - { - name: "Total P&L", - title: title("Total Realized P&L"), - bottom: list.flatMap((cohort) => [ - line({ - metric: cohort.tree.realized.totalRealizedPnl, - name: cohort.name, - color: cohort.color, - unit: Unit.usd, - }), - ...(ratioMetrics ? ratioMetrics(cohort) : []), - ]), - }, - { - name: "Cumulative", - tree: [ - { - name: "Profit", - title: title("Cumulative Realized Profit"), - bottom: list.flatMap(({ color, name, tree }) => [ - line({ - metric: tree.realized.realizedProfit.cumulative, - name, - color, - unit: Unit.usd, - }), - ]), - }, - { - name: "Loss", - title: title("Cumulative Realized Loss"), - bottom: list.flatMap(({ color, name, tree }) => [ - line({ - metric: tree.realized.realizedLoss.cumulative, - name, - color, - unit: Unit.usd, - }), - ]), - }, - { - name: "Net P&L", - title: title("Cumulative Net Realized P&L"), - bottom: [ - ...list.flatMap(({ color, name, tree }) => [ - baseline({ - metric: tree.realized.netRealizedPnl.cumulative, - name, - color, - unit: Unit.usd, - }), - ]), - ], - }, - { - name: "Net P&L 30d Change", - title: title("Net Realized P&L 30d Change"), - bottom: [ - ...list.flatMap(({ color, name, tree }) => [ - baseline({ - metric: tree.realized.netRealizedPnlCumulative30dDelta, - name, - color, - unit: Unit.usd, - }), - baseline({ - metric: - tree.realized - .netRealizedPnlCumulative30dDeltaRelToRealizedCap, - name, - color, - unit: Unit.pctRcap, - }), - baseline({ - metric: - tree.realized.netRealizedPnlCumulative30dDeltaRelToMarketCap, - name, - color, - unit: Unit.pctMcap, - }), - ]), - ], - }, - ], - }, - { - name: "Sent In P/L", - tree: [ - { - name: "In Profit", - title: title("Sent In Profit"), - bottom: list.flatMap(({ color, name, tree }) => [ - line({ - metric: tree.realized.sentInProfit.bitcoin.sum, - name, - color, - unit: Unit.btc, - }), - line({ - metric: tree.realized.sentInProfit.sats.sum, - name, - color, - unit: Unit.sats, - }), - line({ - metric: tree.realized.sentInProfit.dollars.sum, - name, - color, - unit: Unit.usd, - }), - ]), - }, - { - name: "In Profit Cumulative", - title: title("Sent In Profit Cumulative"), - bottom: list.flatMap(({ color, name, tree }) => [ - line({ - metric: tree.realized.sentInProfit.bitcoin.cumulative, - name, - color, - unit: Unit.btc, - }), - line({ - metric: tree.realized.sentInProfit.sats.cumulative, - name, - color, - unit: Unit.sats, - }), - line({ - metric: tree.realized.sentInProfit.dollars.cumulative, - name, - color, - unit: Unit.usd, - }), - ]), - }, - { - name: "In Loss", - title: title("Sent In Loss"), - bottom: list.flatMap(({ color, name, tree }) => [ - line({ - metric: tree.realized.sentInLoss.bitcoin.sum, - name, - color, - unit: Unit.btc, - }), - line({ - metric: tree.realized.sentInLoss.sats.sum, - name, - color, - unit: Unit.sats, - }), - line({ - metric: tree.realized.sentInLoss.dollars.sum, - name, - color, - unit: Unit.usd, - }), - ]), - }, - { - name: "In Loss Cumulative", - title: title("Sent In Loss Cumulative"), - bottom: list.flatMap(({ color, name, tree }) => [ - line({ - metric: tree.realized.sentInLoss.bitcoin.cumulative, - name, - color, - unit: Unit.btc, - }), - line({ - metric: tree.realized.sentInLoss.sats.cumulative, - name, - color, - unit: Unit.sats, - }), - line({ - metric: tree.realized.sentInLoss.dollars.cumulative, - name, - color, - unit: Unit.usd, - }), - ]), - }, - { - name: "In Profit 14d EMA", - title: title("Sent In Profit 14d EMA"), - bottom: list.flatMap(({ color, name, tree }) => - satsBtcUsd({ - pattern: tree.realized.sentInProfit14dEma, - name, - color, - }), - ), - }, - { - name: "In Loss 14d EMA", - title: title("Sent In Loss 14d EMA"), - bottom: list.flatMap(({ color, name, tree }) => - satsBtcUsd({ - pattern: tree.realized.sentInLoss14dEma, - name, - color, - }), - ), - }, - ], - }, - ]; -} - -// ============================================================================ -// SOPR Chart Builders (Composable) -// ============================================================================ - -/** - * Create single base SOPR chart (all UTXO cohorts have base SOPR) - * @param {CohortAll | CohortFull | CohortWithAdjusted | CohortLongTerm | CohortAgeRange | CohortBasicWithMarketCap | CohortBasicWithoutMarketCap | CohortAddress | CohortWithoutRelative} cohort - * @param {(metric: string) => string} title - * @returns {PartialChartOption} - */ -function createSingleBaseSoprChart(cohort, title) { - return { - name: "Normal", - title: title("SOPR"), - bottom: createSingleSoprSeries(cohort.tree), - }; -} - -/** - * Create single adjusted SOPR chart (cohorts with RealizedPattern3/4) - * @param {CohortAll | CohortFull | CohortWithAdjusted} cohort - * @param {(metric: string) => string} title - * @returns {PartialChartOption} - */ -function createSingleAdjustedSoprChart(cohort, title) { - const { tree } = cohort; - - return { - name: "Adjusted", - title: title("aSOPR"), - bottom: [ - baseline({ - metric: tree.realized.adjustedSopr, - name: "Adjusted", - color: colors.bi.p1, - unit: Unit.ratio, - base: 1, - }), - baseline({ - metric: tree.realized.adjustedSopr7dEma, - name: "Adj. 7d EMA", - color: colors.bi.p2, - unit: Unit.ratio, - defaultActive: false, - base: 1, - }), - baseline({ - metric: tree.realized.adjustedSopr30dEma, - name: "Adj. 30d EMA", - color: colors.bi.p3, - unit: Unit.ratio, - defaultActive: false, - base: 1, - }), - ], - }; -} - -/** - * Create grouped base SOPR chart (all UTXO cohorts have base SOPR) - * @param {readonly (CohortAll | CohortFull | CohortWithAdjusted | CohortLongTerm | CohortAgeRange | CohortBasicWithMarketCap | CohortBasicWithoutMarketCap | CohortAddress | CohortWithoutRelative)[]} list - * @param {(metric: string) => string} title - * @returns {PartialChartOption} - */ -function createGroupedBaseSoprChart(list, title) { - return { - name: "Normal", - title: title("SOPR"), - bottom: [ - ...list.flatMap(({ color, name, tree }) => [ - baseline({ - metric: tree.realized.sopr, - name, - color, - unit: Unit.ratio, - base: 1, - }), - baseline({ - metric: tree.realized.sopr7dEma, - name: `${name} 7d`, - color, - unit: Unit.ratio, - defaultActive: false, - base: 1, - }), - baseline({ - metric: tree.realized.sopr30dEma, - name: `${name} 30d`, - color, - unit: Unit.ratio, - defaultActive: false, - base: 1, - }), - ]), - ], - }; -} - -/** - * Create grouped adjusted SOPR chart (cohorts with RealizedPattern3/4) - * @param {readonly (CohortFull | CohortWithAdjusted)[]} list - * @param {(metric: string) => string} title - * @returns {PartialChartOption} - */ -function createGroupedAdjustedSoprChart(list, title) { - return { - name: "Adjusted", - title: title("aSOPR"), - bottom: [ - ...list.flatMap(({ color, name, tree }) => [ - baseline({ - metric: tree.realized.adjustedSopr, - name, - color, - unit: Unit.ratio, - base: 1, - }), - baseline({ - metric: tree.realized.adjustedSopr7dEma, - name: `${name} 7d`, - color, - unit: Unit.ratio, - defaultActive: false, - base: 1, - }), - baseline({ - metric: tree.realized.adjustedSopr30dEma, - name: `${name} 30d`, - color, - unit: Unit.ratio, - defaultActive: false, - base: 1, - }), - ]), - ], - }; -} - -// ============================================================================ -// SOPR Section Composers -// ============================================================================ - -/** - * Create SOPR section with adjusted SOPR (for cohorts with RealizedPattern3/4) - * @param {CohortAll | CohortFull | CohortWithAdjusted} cohort - * @param {(metric: string) => string} title - * @returns {PartialOptionsGroup} - */ -function createSingleSoprSectionWithAdjusted(cohort, title) { - return { - name: "SOPR", - tree: [ - createSingleBaseSoprChart(cohort, title), - createSingleAdjustedSoprChart(cohort, title), - ], - }; -} - -/** - * Create grouped SOPR section with adjusted SOPR - * @param {readonly (CohortFull | CohortWithAdjusted)[]} list - * @param {(metric: string) => string} title - * @returns {PartialOptionsGroup} - */ -function createGroupedSoprSectionWithAdjusted(list, title) { - return { - name: "SOPR", - tree: [ - createGroupedBaseSoprChart(list, title), - createGroupedAdjustedSoprChart(list, title), - ], - }; -} - -/** - * Create SOPR section without adjusted SOPR (for cohorts with RealizedPattern/2) - * @param {CohortWithPercentiles | CohortBasic | CohortAddress | CohortWithoutRelative} cohort - * @param {(metric: string) => string} title - * @returns {PartialOptionsGroup} - */ -function createSingleSoprSectionBasic(cohort, title) { - return { - name: "SOPR", - tree: [createSingleBaseSoprChart(cohort, title)], - }; -} - -/** - * Create grouped SOPR section without adjusted SOPR - * @param {readonly (CohortWithPercentiles | CohortBasic | CohortAddress | CohortWithoutRelative)[]} list - * @param {(metric: string) => string} title - * @returns {PartialOptionsGroup} - */ -function createGroupedSoprSectionBasic(list, title) { - return { - name: "SOPR", - tree: [createGroupedBaseSoprChart(list, title)], - }; -} - -// ============================================================================ -// Unrealized Section Helpers (by relative pattern capability) -// ============================================================================ - -/** - * @param {RelativeWithMarketCap} rel - */ -function createUnrealizedPnlRelToMarketCapMetrics(rel) { - return [ - line({ - metric: rel.unrealizedProfitRelToMarketCap, - name: "Profit", - color: colors.profit, - unit: Unit.pctMcap, - }), - line({ - metric: rel.unrealizedLossRelToMarketCap, - name: "Loss", - color: colors.loss, - unit: Unit.pctMcap, - defaultActive: false, - }), - line({ - metric: rel.negUnrealizedLossRelToMarketCap, - name: "Negative Loss", - color: colors.loss, - unit: Unit.pctMcap, - defaultActive: false, - }), - ]; -} - -/** - * @param {RelativeWithOwnMarketCap} rel - */ -function createUnrealizedPnlRelToOwnMarketCapMetrics(rel) { - return [ - line({ - metric: rel.unrealizedProfitRelToOwnMarketCap, - name: "Profit", - color: colors.profit, - unit: Unit.pctOwnMcap, - }), - line({ - metric: rel.unrealizedLossRelToOwnMarketCap, - name: "Loss", - color: colors.loss, - unit: Unit.pctOwnMcap, - defaultActive: false, - }), - line({ - metric: rel.negUnrealizedLossRelToOwnMarketCap, - name: "Negative Loss", - color: colors.loss, - unit: Unit.pctOwnMcap, - defaultActive: false, - }), - priceLine({ unit: Unit.pctOwnMcap, number: 100 }), - priceLine({ unit: Unit.pctOwnMcap }), - ]; -} - -/** - * @param {RelativeWithOwnPnl} rel - */ -function createUnrealizedPnlRelToOwnPnlMetrics(rel) { - return [ - line({ - metric: rel.unrealizedProfitRelToOwnTotalUnrealizedPnl, - name: "Profit", - color: colors.profit, - unit: Unit.pctOwnPnl, - }), - line({ - metric: rel.unrealizedLossRelToOwnTotalUnrealizedPnl, - name: "Loss", - color: colors.loss, - unit: Unit.pctOwnPnl, - defaultActive: false, - }), - line({ - metric: rel.negUnrealizedLossRelToOwnTotalUnrealizedPnl, - name: "Negative Loss", - color: colors.loss, - unit: Unit.pctOwnPnl, - defaultActive: false, - }), - priceLine({ unit: Unit.pctOwnPnl, number: 100 }), - priceLine({ unit: Unit.pctOwnPnl }), - ]; -} - -/** - * @param {RelativeWithMarketCap} rel - */ -function createNetUnrealizedPnlRelToMarketCapMetrics(rel) { - return [ - baseline({ - metric: rel.netUnrealizedPnlRelToMarketCap, - name: "Net", - unit: Unit.pctMcap, - }), - ]; -} - -/** - * @param {RelativeWithOwnMarketCap} rel - */ -function createNetUnrealizedPnlRelToOwnMarketCapMetrics(rel) { - return [ - baseline({ - metric: rel.netUnrealizedPnlRelToOwnMarketCap, - name: "Net", - unit: Unit.pctOwnMcap, - }), - priceLine({ unit: Unit.pctOwnMcap }), - ]; -} - -/** - * @param {RelativeWithOwnPnl} rel - */ -function createNetUnrealizedPnlRelToOwnPnlMetrics(rel) { - return [ - baseline({ - metric: rel.netUnrealizedPnlRelToOwnTotalUnrealizedPnl, - name: "Net", - unit: Unit.pctOwnPnl, - }), - ]; -} - -/** - * Create invested capital relative metrics (% of realized cap) - * @param {RelativeWithInvestedCapitalPct} rel - * @returns {AnyFetchedSeriesBlueprint[]} - */ -function createInvestedCapitalRelMetrics(rel) { - return [ - baseline({ - metric: rel.investedCapitalInProfitPct, - name: "In Profit", - color: colors.profit, - unit: Unit.pctOwnRcap, - }), - baseline({ - metric: rel.investedCapitalInLossPct, - name: "In Loss", - color: colors.loss, - unit: Unit.pctOwnRcap, - }), - ]; -} - -/** - * Base unrealized metrics (always present) - * @param {{ unrealized: UnrealizedPattern }} tree - */ -function createUnrealizedPnlBaseMetrics(tree) { - return [ - line({ - metric: tree.unrealized.unrealizedProfit, - name: "Profit", - color: colors.profit, - unit: Unit.usd, - }), - line({ - metric: tree.unrealized.unrealizedLoss, - name: "Loss", - color: colors.loss, - unit: Unit.usd, - defaultActive: false, - }), - line({ - metric: tree.unrealized.negUnrealizedLoss, - name: "Negative Loss", - color: colors.loss, - unit: Unit.usd, - defaultActive: false, - }), - line({ - metric: tree.unrealized.totalUnrealizedPnl, - name: "Total", - color: colors.default, - unit: Unit.usd, - }), - ]; -} - -/** - * Base net unrealized metric (always present) - * @param {{ unrealized: UnrealizedPattern }} tree - */ -function createNetUnrealizedPnlBaseMetric(tree) { - return baseline({ - metric: tree.unrealized.netUnrealizedPnl, - name: "Net", - unit: Unit.usd, - }); -} - -// ============================================================================ -// Unrealized Chart Builders (composable charts) -// ============================================================================ - -/** - * Create NUPL chart for single cohort - * @param {RelativeWithNupl} rel - * @param {(metric: string) => string} title - * @returns {PartialChartOption} - */ -function createNuplChart(rel, title) { - return { - name: "NUPL", - title: title("NUPL"), - bottom: [ - baseline({ - metric: rel.nupl, - name: "NUPL", - unit: Unit.ratio, - }), - ], - }; -} - -/** - * Create peak regret chart (basic - just absolute value) - * @param {{ unrealized: UnrealizedFullPattern }} tree - * @param {(metric: string) => string} title - * @returns {PartialChartOption} - */ -function createPeakRegretChart(tree, title) { - return { - name: "Peak Regret", - title: title("Unrealized Peak Regret"), - bottom: [ - line({ - metric: tree.unrealized.peakRegret, - name: "Peak Regret", - color: colors.bitcoin, - unit: Unit.usd, - }), - ], - }; -} - -/** - * Create peak regret chart with RelToMarketCap metric - * @param {{ unrealized: UnrealizedFullPattern, relative: RelativeWithPeakRegret }} tree - * @param {(metric: string) => string} title - * @returns {PartialChartOption} - */ -function createPeakRegretChartWithMarketCap(tree, title) { - return { - name: "Peak Regret", - title: title("Unrealized Peak Regret"), - bottom: [ - line({ - metric: tree.unrealized.peakRegret, - name: "Peak Regret", - color: colors.bitcoin, - unit: Unit.usd, - }), - baseline({ - metric: tree.relative.unrealizedPeakRegretRelToMarketCap, - name: "Peak Regret", - color: colors.bitcoin, - unit: Unit.pctMcap, - }), - ], - }; -} - -/** - * Create invested capital absolute chart - * @param {{ unrealized: UnrealizedPattern }} tree - * @param {(metric: string) => string} title - * @returns {PartialChartOption} - */ -function createSingleInvestedCapitalAbsoluteChart(tree, title) { - return { - name: "Absolute", - title: title("Invested Capital In Profit & Loss"), - bottom: [ - 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, - }), - ], - }; -} - -/** - * Create invested capital relative chart - * @param {RelativeWithInvestedCapitalPct} rel - * @param {(metric: string) => string} title - * @returns {PartialChartOption} - */ -function createSingleInvestedCapitalRelativeChart(rel, title) { - return { - name: "Relative", - title: title("Invested Capital In Profit & Loss %"), - bottom: [...createInvestedCapitalRelMetrics(rel)], - }; -} - -/** - * Create invested capital folder for cohorts WITHOUT relative metrics - * @param {{ unrealized: UnrealizedPattern }} tree - * @param {(metric: string) => string} title - * @returns {PartialOptionsGroup} - */ -function createSingleInvestedCapitalFolder(tree, title) { - return { - name: "Invested Capital", - tree: [createSingleInvestedCapitalAbsoluteChart(tree, title)], - }; -} - -/** - * Create invested capital folder for cohorts WITH relative metrics - * @param {{ unrealized: UnrealizedPattern, relative: RelativeWithInvestedCapitalPct }} tree - * @param {(metric: string) => string} title - * @returns {PartialOptionsGroup} - */ -function createSingleInvestedCapitalFolderFull(tree, title) { - return { - name: "Invested Capital", - tree: [ - createSingleInvestedCapitalAbsoluteChart(tree, title), - createSingleInvestedCapitalRelativeChart(tree.relative, title), - ], - }; -} - -/** - * Create NUPL chart for grouped cohorts - * @param {readonly { name: string, color: Color, tree: { relative: RelativeWithNupl } }[]} list - * @param {(metric: string) => string} title - * @returns {PartialChartOption} - */ -function createGroupedNuplChart(list, title) { - return { - name: "NUPL", - title: title("NUPL"), - bottom: [ - ...list.map(({ color, name, tree }) => - baseline({ - metric: tree.relative.nupl, - name, - color, - unit: Unit.ratio, - }), - ), - ], - }; -} - -/** - * Create grouped peak regret chart (basic - no RelToMarketCap) - * @param {readonly { color: Color, name: string, tree: { unrealized: UnrealizedFullPattern } }[]} list - * @param {(metric: string) => string} title - * @returns {PartialChartOption} - */ -function createGroupedPeakRegretChartBasic(list, title) { - return { - name: "Peak Regret", - title: title("Unrealized Peak Regret"), - bottom: list.flatMap(({ color, name, tree }) => [ - line({ - metric: tree.unrealized.peakRegret, - name, - color, - unit: Unit.usd, - }), - ]), - }; -} - -/** - * Create grouped peak regret chart with RelToMarketCap metric - * @param {readonly { color: Color, name: string, tree: { unrealized: UnrealizedFullPattern, relative: RelativeWithPeakRegret } }[]} list - * @param {(metric: string) => string} title - * @returns {PartialChartOption} - */ -function createGroupedPeakRegretChart(list, title) { - return { - name: "Peak Regret", - title: title("Unrealized Peak Regret"), - bottom: list.flatMap(({ color, name, tree }) => [ - line({ - metric: tree.unrealized.peakRegret, - name, - color, - unit: Unit.usd, - }), - baseline({ - metric: tree.relative.unrealizedPeakRegretRelToMarketCap, - name, - color, - unit: Unit.pctMcap, - }), - ]), - }; -} - -// ============================================================================ -// Unrealized Section Builder (generic, type-safe composition) -// ============================================================================ - -/** - * Generic single unrealized section builder - callers pass typed metrics - * @param {Object} args - * @param {{ unrealized: UnrealizedPattern }} args.tree - * @param {(metric: string) => string} args.title - * @param {AnyFetchedSeriesBlueprint[]} [args.pnl] - Extra pnl metrics - * @param {AnyFetchedSeriesBlueprint[]} [args.netPnl] - Extra net pnl metrics - * @param {PartialOptionsGroup} args.investedCapitalFolder - Invested capital folder (use createSingleInvestedCapitalFolder or createSingleInvestedCapitalFolderFull) - * @param {PartialChartOption[]} [args.charts] - Extra charts (e.g., nupl) - * @returns {PartialOptionsGroup} - */ -function createUnrealizedSection({ - tree, - title, - pnl = [], - netPnl = [], - investedCapitalFolder, - charts = [], -}) { - return { - name: "Profitability", - tree: [ - { - name: "P&L", - title: title("Unrealized P&L"), - bottom: [...createUnrealizedPnlBaseMetrics(tree), ...pnl], - }, - { - name: "Net P&L", - title: title("Net Unrealized P&L"), - bottom: [createNetUnrealizedPnlBaseMetric(tree), ...netPnl], - }, - investedCapitalFolder, - { - name: "Sentiment", - title: title("Market Sentiment"), - bottom: createSingleSentimentSeries(tree), - }, - ...charts, - ], - }; -} - -/** - * Create grouped invested capital absolute charts (In Profit, In Loss) - * @param {readonly { color: Color, name: string, tree: { unrealized: UnrealizedPattern } }[]} list - * @param {(metric: string) => string} title - * @returns {PartialChartOption[]} - */ -function createGroupedInvestedCapitalAbsoluteCharts(list, title) { - return [ - { - name: "In Profit", - title: title("Invested Capital In Profit"), - bottom: list.map(({ color, name, tree }) => - line({ - metric: tree.unrealized.investedCapitalInProfit, - name, - color, - unit: Unit.usd, - }), - ), - }, - { - name: "In Loss", - title: title("Invested Capital In Loss"), - bottom: list.map(({ color, name, tree }) => - line({ - metric: tree.unrealized.investedCapitalInLoss, - name, - color, - unit: Unit.usd, - }), - ), - }, - ]; -} - -/** - * Create grouped invested capital relative charts (In Profit %, In Loss %) - * @param {readonly { color: Color, name: string, tree: { relative: RelativeWithInvestedCapitalPct } }[]} list - * @param {(metric: string) => string} title - * @returns {PartialChartOption[]} - */ -function createGroupedInvestedCapitalRelativeCharts(list, title) { - return [ - { - name: "In Profit %", - title: title("Invested Capital In Profit %"), - bottom: list.map(({ color, name, tree }) => - baseline({ - metric: tree.relative.investedCapitalInProfitPct, - name, - color, - unit: Unit.pctOwnRcap, - }), - ), - }, - { - name: "In Loss %", - title: title("Invested Capital In Loss %"), - bottom: list.map(({ color, name, tree }) => - baseline({ - metric: tree.relative.investedCapitalInLossPct, - name, - color, - unit: Unit.pctOwnRcap, - }), - ), - }, - ]; -} - -/** - * Generic grouped unrealized section builder - callers pass typed metric generators - * @template {readonly { color: Color, name: string, tree: { unrealized: UnrealizedPattern, relative: RelativeWithInvestedCapitalPct } }[]} T - * @param {Object} args - * @param {T} args.list - * @param {(metric: string) => string} args.title - * @param {(cohort: T[number]) => AnyFetchedSeriesBlueprint[]} [args.netPnlMetrics] - Generator for extra net pnl metrics per cohort - * @param {PartialChartOption[]} [args.charts] - Extra charts - * @returns {PartialOptionsGroup} - */ -function createGroupedUnrealizedSection({ - list, - title, - netPnlMetrics, - charts = [], -}) { - return { - name: "Profitability", - tree: [ - ...createGroupedUnrealizedBaseCharts(list, title), - { - name: "Net P&L", - title: title("Net Unrealized P&L"), - bottom: [ - ...list.flatMap((cohort) => [ - baseline({ - metric: cohort.tree.unrealized.netUnrealizedPnl, - name: cohort.name, - color: cohort.color, - unit: Unit.usd, - }), - ...(netPnlMetrics ? netPnlMetrics(cohort) : []), - ]), - ], - }, - { - name: "Invested Capital", - tree: [ - ...createGroupedInvestedCapitalAbsoluteCharts(list, title), - ...createGroupedInvestedCapitalRelativeCharts(list, title), - ], - }, - { - name: "Sentiment", - tree: [ - { - name: "Net", - title: title("Net Sentiment"), - bottom: createGroupedNetSentimentSeries(list), - }, - { - name: "Greed", - title: title("Greed Index"), - bottom: createGroupedGreedIndexSeries(list), - }, - { - name: "Pain", - title: title("Pain Index"), - bottom: createGroupedPainIndexSeries(list), - }, - ], - }, - ...charts, - ], - }; -} - -/** - * Grouped unrealized section for cohorts WITHOUT relative (edge case types: empty, p2ms, unknown) - * @param {readonly { color: Color, name: string, tree: { unrealized: UnrealizedPattern } }[]} list - * @param {(metric: string) => string} title - * @returns {PartialOptionsGroup} - */ -function createGroupedUnrealizedSectionWithoutRelative(list, title) { - return { - name: "Profitability", - tree: [ - ...createGroupedUnrealizedBaseCharts(list, title), - { - name: "Net P&L", - title: title("Net Unrealized P&L"), - bottom: list.map((cohort) => - baseline({ - metric: cohort.tree.unrealized.netUnrealizedPnl, - name: cohort.name, - color: cohort.color, - unit: Unit.usd, - }), - ), - }, - { - name: "Invested Capital", - tree: createGroupedInvestedCapitalAbsoluteCharts(list, title), - }, - { - name: "Sentiment", - tree: [ - { - name: "Net", - title: title("Net Sentiment"), - bottom: createGroupedNetSentimentSeries(list), - }, - { - name: "Greed", - title: title("Greed Index"), - bottom: createGroupedGreedIndexSeries(list), - }, - { - name: "Pain", - title: title("Pain Index"), - bottom: createGroupedPainIndexSeries(list), - }, - ], - }, - ], - }; -} - -// ============================================================================ -// Unrealized Section Variants (by cohort capability) -// ============================================================================ - -/** - * Unrealized section for All cohort (only RelToOwnPnl) - * @param {CohortAll} cohort - * @param {(metric: string) => string} title - */ -function createSingleUnrealizedSectionAll(cohort, title) { - const { tree } = cohort; - return createUnrealizedSection({ - tree, - title, - pnl: [ - ...createUnrealizedPnlRelToMarketCapMetrics(tree.relative), - ...createUnrealizedPnlRelToOwnPnlMetrics(tree.relative), - ], - netPnl: [ - ...createNetUnrealizedPnlRelToMarketCapMetrics(tree.relative), - ...createNetUnrealizedPnlRelToOwnPnlMetrics(tree.relative), - ], - investedCapitalFolder: createSingleInvestedCapitalFolderFull(tree, title), - charts: [ - createNuplChart(tree.relative, title), - createPeakRegretChartWithMarketCap(tree, title), - ], - }); -} - -/** - * Unrealized section for Full cohort (all capabilities: MarketCap + OwnMarketCap + OwnPnl) - * @param {CohortFull} cohort - * @param {(metric: string) => string} title - */ -function createSingleUnrealizedSectionFull(cohort, title) { - const { tree } = cohort; - return createUnrealizedSection({ - tree, - title, - pnl: [ - ...createUnrealizedPnlRelToMarketCapMetrics(tree.relative), - ...createUnrealizedPnlRelToOwnMarketCapMetrics(tree.relative), - ...createUnrealizedPnlRelToOwnPnlMetrics(tree.relative), - ], - netPnl: [ - ...createNetUnrealizedPnlRelToMarketCapMetrics(tree.relative), - ...createNetUnrealizedPnlRelToOwnMarketCapMetrics(tree.relative), - ...createNetUnrealizedPnlRelToOwnPnlMetrics(tree.relative), - ], - investedCapitalFolder: createSingleInvestedCapitalFolderFull(tree, title), - charts: [ - createNuplChart(tree.relative, title), - createPeakRegretChartWithMarketCap(tree, title), - ], - }); -} - -/** - * Unrealized section for WithAdjusted cohort (MarketCap + nupl) - * @param {CohortWithAdjusted} cohort - * @param {(metric: string) => string} title - */ -function createSingleUnrealizedSectionWithMarketCap(cohort, title) { - const { tree } = cohort; - return createUnrealizedSection({ - tree, - title, - pnl: [...createUnrealizedPnlRelToMarketCapMetrics(tree.relative)], - netPnl: [...createNetUnrealizedPnlRelToMarketCapMetrics(tree.relative)], - investedCapitalFolder: createSingleInvestedCapitalFolderFull(tree, title), - charts: [ - createNuplChart(tree.relative, title), - createPeakRegretChartWithMarketCap(tree, title), - ], - }); -} - -/** - * Unrealized section WITH RelToMarketCap metrics (for CohortBasicWithMarketCap) - * @param {CohortBasicWithMarketCap} cohort - * @param {(metric: string) => string} title - */ -function createSingleUnrealizedSectionWithMarketCapOnly(cohort, title) { - const { tree } = cohort; - return createUnrealizedSection({ - tree, - title, - pnl: [...createUnrealizedPnlRelToMarketCapMetrics(tree.relative)], - netPnl: [...createNetUnrealizedPnlRelToMarketCapMetrics(tree.relative)], - investedCapitalFolder: createSingleInvestedCapitalFolderFull(tree, title), - charts: [createNuplChart(tree.relative, title)], - }); -} - -/** - * Unrealized section for minAge cohorts (has peakRegret) - * @param {CohortMinAge} cohort - * @param {(metric: string) => string} title - */ -function createSingleUnrealizedSectionMinAge(cohort, title) { - const { tree } = cohort; - return createUnrealizedSection({ - tree, - title, - pnl: [...createUnrealizedPnlRelToMarketCapMetrics(tree.relative)], - netPnl: [...createNetUnrealizedPnlRelToMarketCapMetrics(tree.relative)], - investedCapitalFolder: createSingleInvestedCapitalFolderFull(tree, title), - charts: [ - createNuplChart(tree.relative, title), - createPeakRegretChartWithMarketCap(tree, title), - ], - }); -} - -/** - * Unrealized section with only base metrics (no RelToMarketCap) - * @param {CohortBasicWithoutMarketCap} cohort - * @param {(metric: string) => string} title - */ -function createSingleUnrealizedSectionBase(cohort, title) { - const { tree } = cohort; - return createUnrealizedSection({ - tree, - title, - investedCapitalFolder: createSingleInvestedCapitalFolder(tree, title), - }); -} - -/** - * Unrealized section for cohorts WITHOUT relative (edge case types: empty, p2ms, unknown) - * @param {CohortWithoutRelative} cohort - * @param {(metric: string) => string} title - */ -function createSingleUnrealizedSectionWithoutRelative(cohort, title) { - const { tree } = cohort; - return createUnrealizedSection({ - tree, - title, - investedCapitalFolder: createSingleInvestedCapitalFolder(tree, title), - }); -} - -/** - * Grouped unrealized base charts (profit, loss, total pnl) - * @param {readonly { color: Color, name: string, tree: { unrealized: UnrealizedPattern } }[]} list - * @param {(metric: string) => string} title - */ -function createGroupedUnrealizedBaseCharts(list, title) { - return [ - { - name: "Profit", - title: title("Unrealized Profit"), - bottom: list.flatMap(({ color, name, tree }) => [ - line({ - metric: tree.unrealized.unrealizedProfit, - name, - color, - unit: Unit.usd, - }), - ]), - }, - { - name: "Loss", - title: title("Unrealized Loss"), - bottom: list.flatMap(({ color, name, tree }) => [ - line({ - metric: tree.unrealized.unrealizedLoss, - name, - color, - unit: Unit.usd, - }), - ]), - }, - { - name: "Total P&L", - title: title("Unrealized Total P&L"), - bottom: list.flatMap(({ color, name, tree }) => [ - line({ - metric: tree.unrealized.totalUnrealizedPnl, - name, - color, - unit: Unit.usd, - }), - ]), - }, - ]; -} - -/** - * Grouped unrealized section for Full cohorts (all relative capabilities) - * @param {readonly CohortFull[]} list - * @param {(metric: string) => string} title - */ -function createGroupedUnrealizedSectionFull(list, title) { - return createGroupedUnrealizedSection({ - list, - title, - netPnlMetrics: ({ color, name, tree }) => [ - baseline({ - metric: tree.relative.netUnrealizedPnlRelToMarketCap, - name, - color, - unit: Unit.pctMcap, - }), - baseline({ - metric: tree.relative.netUnrealizedPnlRelToOwnMarketCap, - name, - color, - unit: Unit.pctOwnMcap, - }), - baseline({ - metric: tree.relative.netUnrealizedPnlRelToOwnTotalUnrealizedPnl, - name, - color, - unit: Unit.pctOwnPnl, - }), - ], - charts: [ - createGroupedNuplChart(list, title), - createGroupedPeakRegretChart(list, title), - ], - }); -} - -/** - * Grouped unrealized section for WithAdjusted cohorts (MarketCap + nupl) - * @param {readonly CohortWithAdjusted[]} list - * @param {(metric: string) => string} title - */ -function createGroupedUnrealizedSectionWithMarketCap(list, title) { - return createGroupedUnrealizedSection({ - list, - title, - netPnlMetrics: ({ color, name, tree }) => [ - baseline({ - metric: tree.relative.netUnrealizedPnlRelToMarketCap, - name, - color, - unit: Unit.pctMcap, - }), - ], - charts: [ - createGroupedNuplChart(list, title), - createGroupedPeakRegretChart(list, title), - ], - }); -} - -/** - * Grouped unrealized section WITH RelToMarketCap (for CohortBasicWithMarketCap) - * @param {readonly CohortBasicWithMarketCap[]} list - * @param {(metric: string) => string} title - */ -function createGroupedUnrealizedSectionWithMarketCapOnly(list, title) { - return createGroupedUnrealizedSection({ - list, - title, - netPnlMetrics: ({ color, name, tree }) => [ - baseline({ - metric: tree.relative.netUnrealizedPnlRelToMarketCap, - name, - color, - unit: Unit.pctMcap, - }), - ], - charts: [createGroupedNuplChart(list, title)], - }); -} - -/** - * Grouped unrealized section for minAge cohorts (has peakRegret) - * @param {readonly CohortMinAge[]} list - * @param {(metric: string) => string} title - */ -function createGroupedUnrealizedSectionMinAge(list, title) { - return createGroupedUnrealizedSection({ - list, - title, - netPnlMetrics: ({ color, name, tree }) => [ - baseline({ - metric: tree.relative.netUnrealizedPnlRelToMarketCap, - name, - color, - unit: Unit.pctMcap, - }), - ], - charts: [ - createGroupedNuplChart(list, title), - createGroupedPeakRegretChart(list, title), - ], - }); -} - -/** - * Grouped unrealized section without RelToMarketCap (for CohortBasicWithoutMarketCap) - * @param {readonly CohortBasicWithoutMarketCap[]} list - * @param {(metric: string) => string} title - */ -function createGroupedUnrealizedSectionBase(list, title) { - return createGroupedUnrealizedSection({ list, title }); -} - -/** - * Unrealized section for cohorts with nupl (OwnMarketCap + OwnPnl + nupl) - * @param {Object} args - * @param {CohortWithNuplPercentiles} args.cohort - * @param {(metric: string) => string} args.title - */ -function createSingleUnrealizedSectionWithNupl({ cohort, title }) { - const { tree } = cohort; - return createUnrealizedSection({ - tree, - title, - pnl: [ - ...createUnrealizedPnlRelToMarketCapMetrics(tree.relative), - ...createUnrealizedPnlRelToOwnMarketCapMetrics(tree.relative), - ...createUnrealizedPnlRelToOwnPnlMetrics(tree.relative), - ], - netPnl: [ - ...createNetUnrealizedPnlRelToMarketCapMetrics(tree.relative), - ...createNetUnrealizedPnlRelToOwnMarketCapMetrics(tree.relative), - ...createNetUnrealizedPnlRelToOwnPnlMetrics(tree.relative), - ], - investedCapitalFolder: createSingleInvestedCapitalFolderFull(tree, title), - charts: [ - createNuplChart(tree.relative, title), - createPeakRegretChartWithMarketCap(tree, title), - ], - }); -} - -/** - * Grouped unrealized section for cohorts with nupl (OwnMarketCap + OwnPnl + nupl) - * @param {Object} args - * @param {readonly CohortWithNuplPercentiles[]} args.list - * @param {(metric: string) => string} args.title - */ -function createGroupedUnrealizedSectionWithNupl({ list, title }) { - return createGroupedUnrealizedSection({ - list, - title, - netPnlMetrics: ({ color, name, tree }) => [ - baseline({ - metric: tree.relative.netUnrealizedPnlRelToMarketCap, - name, - color, - unit: Unit.pctMcap, - }), - baseline({ - metric: tree.relative.netUnrealizedPnlRelToOwnMarketCap, - name, - color, - unit: Unit.pctOwnMcap, - }), - baseline({ - metric: tree.relative.netUnrealizedPnlRelToOwnTotalUnrealizedPnl, - name, - color, - unit: Unit.pctOwnPnl, - }), - ], - charts: [ - createGroupedNuplChart(list, title), - createGroupedPeakRegretChart(list, title), - ], - }); -} - -/** - * Unrealized section for AgeRange cohort (no nupl via RelativePattern2) - * @param {CohortAgeRange} cohort - * @param {(metric: string) => string} title - */ -function createSingleUnrealizedSectionAgeRange(cohort, title) { - const { tree } = cohort; - return createUnrealizedSection({ - tree, - title, - pnl: [ - ...createUnrealizedPnlRelToOwnMarketCapMetrics(tree.relative), - ...createUnrealizedPnlRelToOwnPnlMetrics(tree.relative), - ], - netPnl: [ - ...createNetUnrealizedPnlRelToOwnMarketCapMetrics(tree.relative), - ...createNetUnrealizedPnlRelToOwnPnlMetrics(tree.relative), - ], - investedCapitalFolder: createSingleInvestedCapitalFolderFull(tree, title), - charts: [createPeakRegretChart(tree, title)], - }); -} - -/** - * Grouped unrealized section for AgeRange cohorts (no nupl via RelativePattern2) - * @param {readonly CohortAgeRange[]} list - * @param {(metric: string) => string} title - */ -function createGroupedUnrealizedSectionAgeRange(list, title) { - return createGroupedUnrealizedSection({ - list, - title, - netPnlMetrics: ({ color, name, tree }) => [ - baseline({ - metric: tree.relative.netUnrealizedPnlRelToOwnMarketCap, - name, - color, - unit: Unit.pctOwnMcap, - }), - baseline({ - metric: tree.relative.netUnrealizedPnlRelToOwnTotalUnrealizedPnl, - name, - color, - unit: Unit.pctOwnPnl, - }), - ], - charts: [createGroupedPeakRegretChartBasic(list, title)], - }); -}