/** Shared cohort chart section builders */ import { colors } from "../../utils/colors.js"; import { Unit } from "../../utils/units.js"; import { priceLine } from "../constants.js"; import { baseline, dots, line, price } from "../series.js"; import { satsBtcUsd, createPriceRatioCharts, formatCohortTitle, } from "../shared.js"; // ============================================================================ // Generic Price Helpers // ============================================================================ /** * Create price folder (price + ratio + z-scores wrapped in folder) * For cohorts with full extended ratio metrics (ActivePriceRatioPattern) * @param {{ name: string, cohortTitle?: string, priceMetric: ActivePricePattern, ratioPattern: AnyRatioPattern, color: Color }} args * @returns {PartialOptionsGroup} */ export function createPriceFolder({ name, cohortTitle, priceMetric, ratioPattern, color, }) { const context = cohortTitle ? `${cohortTitle} ${name}` : name; return { name, tree: createPriceRatioCharts({ context, legend: name, pricePattern: priceMetric, ratio: ratioPattern, color, }), }; } /** * Create basic price charts (price + ratio only, no z-scores) - flat array * For cohorts with basic ratio metrics (only .ratio field) * @template {AnyMetricPattern} R * @param {{ name: string, context: string, priceMetric: ActivePricePattern, ratioMetric: R, color: Color }} args * @returns {PartialOptionsTree} */ export function createBasicPriceCharts({ name, context, priceMetric, ratioMetric, color, }) { return [ { name: "Price", title: context, top: [price({ metric: priceMetric, name, color })], }, { name: "Ratio", title: formatCohortTitle(context)("Ratio"), bottom: [ baseline({ metric: ratioMetric, name: "Ratio", color, unit: Unit.ratio, base: 1, }), ], }, ]; } /** * Create basic price folder (price + ratio wrapped in folder, no z-scores) * For cohorts with basic ratio metrics (only .ratio field) * @template {AnyMetricPattern} R * @param {{ name: string, cohortTitle?: string, priceMetric: ActivePricePattern, ratioMetric: R, color: Color }} args * @returns {PartialOptionsGroup} */ export function createBasicPriceFolder({ name, cohortTitle, priceMetric, ratioMetric, color, }) { const context = cohortTitle ? `${cohortTitle} ${name}` : name; return { name, tree: createBasicPriceCharts({ name, context, priceMetric, ratioMetric, color, }), }; } /** * Create grouped price charts (price + ratio) - flat array, no z-scores * @template {{ color: Color, name: string, tree: { realized: AnyRealizedPattern } }} T * @param {{ name: string, title: (metric: string) => string, list: readonly T[], getPrice: (tree: T['tree']) => ActivePricePattern, getRatio: (tree: T['tree']) => AnyMetricPattern }} args * @returns {PartialOptionsTree} */ export function createGroupedPriceCharts({ name, title, list, getPrice, getRatio, }) { return [ { name: "Price", title: title(name), top: list.map(({ color, name: cohortName, tree }) => price({ metric: getPrice(tree), name: cohortName, color }), ), }, { name: "Ratio", title: title(`${name} Ratio`), bottom: list.map(({ color, name: cohortName, tree }) => baseline({ metric: getRatio(tree), name: cohortName, color, unit: Unit.ratio, base: 1, }), ), }, ]; } /** * Create grouped price folder (price + ratio wrapped in folder) * @template {{ color: Color, name: string, tree: { realized: AnyRealizedPattern } }} T * @param {{ name: string, title: (metric: string) => string, list: readonly T[], getPrice: (tree: T['tree']) => ActivePricePattern, getRatio: (tree: T['tree']) => AnyMetricPattern }} args * @returns {PartialOptionsGroup} */ export function createGroupedPriceFolder({ name, title, list, getPrice, getRatio, }) { return { name, tree: createGroupedPriceCharts({ name, title, list, getPrice, getRatio }), }; } /** * Create base supply series (without relative metrics) * @param {CohortObject | CohortWithoutRelative} cohort * @returns {AnyFetchedSeriesBlueprint[]} */ function createSingleSupplySeriesBase(cohort) { const { tree } = cohort; return [ ...satsBtcUsd({ pattern: tree.supply.total, name: "Supply", color: colors.default, }), ...satsBtcUsd({ pattern: tree.supply._30dChange, name: "30d Change", color: colors.orange, }), ...satsBtcUsd({ pattern: tree.unrealized.supplyInProfit, name: "In Profit", color: colors.green, }), ...satsBtcUsd({ pattern: tree.unrealized.supplyInLoss, name: "In Loss", color: colors.red, }), ...satsBtcUsd({ pattern: tree.supply.halved, name: "half", color: colors.gray, }).map((s) => ({ ...s, options: { lineStyle: 4 }, })), ]; } /** * Create supply relative to own supply metrics * @param {UtxoCohortObject | AddressCohortObject} cohort * @returns {AnyFetchedSeriesBlueprint[]} */ function createSingleSupplyRelativeToOwnMetrics(cohort) { const { tree } = cohort; return [ line({ metric: tree.relative.supplyInProfitRelToOwnSupply, name: "In Profit", color: colors.green, unit: Unit.pctOwn, }), line({ metric: tree.relative.supplyInLossRelToOwnSupply, name: "In Loss", color: colors.red, unit: Unit.pctOwn, }), priceLine({ unit: Unit.pctOwn, number: 100, style: 0, color: colors.default, }), priceLine({ unit: Unit.pctOwn, number: 50 }), ]; } /** * Create supply section for a single cohort (with relative metrics) * @param {UtxoCohortObject | AddressCohortObject} cohort * @param {Object} [options] * @param {AnyFetchedSeriesBlueprint[]} [options.supplyRelative] - Supply relative to circulating supply metrics * @param {AnyFetchedSeriesBlueprint[]} [options.pnlRelative] - Supply in profit/loss relative to circulating supply metrics * @returns {AnyFetchedSeriesBlueprint[]} */ export function createSingleSupplySeries( cohort, { supplyRelative = [], pnlRelative = [] } = {}, ) { return [ ...createSingleSupplySeriesBase(cohort), ...supplyRelative, ...pnlRelative, ...createSingleSupplyRelativeToOwnMetrics(cohort), ]; } /** * Create supply series for cohorts WITHOUT relative metrics * @param {CohortWithoutRelative} cohort * @returns {AnyFetchedSeriesBlueprint[]} */ export function createSingleSupplySeriesWithoutRelative(cohort) { return createSingleSupplySeriesBase(cohort); } /** * Create supply total series for grouped cohorts * @template {readonly CohortObject[]} T * @param {T} list * @param {Object} [options] * @param {(cohort: T[number]) => AnyFetchedSeriesBlueprint[]} [options.relativeMetrics] - Generator for relative supply metrics * @returns {AnyFetchedSeriesBlueprint[]} */ export function createGroupedSupplyTotalSeries(list, { relativeMetrics } = {}) { return list.flatMap((cohort) => [ ...satsBtcUsd({ pattern: cohort.tree.supply.total, name: cohort.name, color: cohort.color, }), ...(relativeMetrics ? relativeMetrics(cohort) : []), ]); } /** * Create supply in profit series for grouped cohorts * @template {readonly CohortObject[]} T * @param {T} list * @param {Object} [options] * @param {(cohort: T[number]) => AnyFetchedSeriesBlueprint[]} [options.relativeMetrics] - Generator for relative supply metrics * @returns {AnyFetchedSeriesBlueprint[]} */ export function createGroupedSupplyInProfitSeries( list, { relativeMetrics } = {}, ) { return list.flatMap((cohort) => [ ...satsBtcUsd({ pattern: cohort.tree.unrealized.supplyInProfit, name: cohort.name, color: cohort.color, }), ...(relativeMetrics ? relativeMetrics(cohort) : []), ]); } /** * Create supply in loss series for grouped cohorts * @template {readonly CohortObject[]} T * @param {T} list * @param {Object} [options] * @param {(cohort: T[number]) => AnyFetchedSeriesBlueprint[]} [options.relativeMetrics] - Generator for relative supply metrics * @returns {AnyFetchedSeriesBlueprint[]} */ export function createGroupedSupplyInLossSeries( list, { relativeMetrics } = {}, ) { return list.flatMap((cohort) => [ ...satsBtcUsd({ pattern: cohort.tree.unrealized.supplyInLoss, name: cohort.name, color: cohort.color, }), ...(relativeMetrics ? relativeMetrics(cohort) : []), ]); } /** * Create supply section for grouped cohorts * @template {readonly (CohortObject | CohortWithoutRelative)[]} T * @param {T} list * @param {(metric: string) => string} title * @param {Object} [options] * @param {(cohort: T[number]) => AnyFetchedSeriesBlueprint[]} [options.supplyRelativeMetrics] - Generator for supply relative metrics * @param {(cohort: T[number]) => AnyFetchedSeriesBlueprint[]} [options.profitRelativeMetrics] - Generator for supply in profit relative metrics * @param {(cohort: T[number]) => AnyFetchedSeriesBlueprint[]} [options.lossRelativeMetrics] - Generator for supply in loss relative metrics * @returns {PartialOptionsGroup} */ export function createGroupedSupplySection( list, title, { supplyRelativeMetrics, profitRelativeMetrics, lossRelativeMetrics } = {}, ) { return { name: "Supply", tree: [ { name: "Total", title: title("Supply"), bottom: createGroupedSupplyTotalSeries(list, { relativeMetrics: supplyRelativeMetrics, }), }, { name: "30d Change", title: title("Supply 30d Change"), bottom: list.flatMap(({ color, name, tree }) => satsBtcUsd({ pattern: tree.supply._30dChange, name, color }), ), }, { name: "In Profit", title: title("Supply In Profit"), bottom: createGroupedSupplyInProfitSeries(list, { relativeMetrics: profitRelativeMetrics, }), }, { name: "In Loss", title: title("Supply In Loss"), bottom: createGroupedSupplyInLossSeries(list, { relativeMetrics: lossRelativeMetrics, }), }, ], }; } // ============================================================================ // Circulating Supply Relative Metrics Generators // ============================================================================ /** * Create supply relative to circulating supply series for single cohort * @param {CohortWithCirculatingSupplyRelative} cohort * @returns {AnyFetchedSeriesBlueprint[]} */ export function createSupplyRelativeToCirculatingSeries(cohort) { return [ line({ metric: cohort.tree.relative.supplyRelToCirculatingSupply, name: "Supply", color: colors.default, unit: Unit.pctSupply, }), ]; } /** * Create supply in profit/loss relative to circulating supply series for single cohort * @param {CohortWithCirculatingSupplyRelative} cohort * @returns {AnyFetchedSeriesBlueprint[]} */ export function createSupplyPnlRelativeToCirculatingSeries(cohort) { return [ line({ metric: cohort.tree.relative.supplyInProfitRelToCirculatingSupply, name: "In Profit", color: colors.green, unit: Unit.pctSupply, }), line({ metric: cohort.tree.relative.supplyInLossRelToCirculatingSupply, name: "In Loss", color: colors.red, unit: Unit.pctSupply, }), ]; } /** * Create supply relative to circulating supply metrics generator for grouped cohorts * @param {CohortWithCirculatingSupplyRelative} cohort * @returns {AnyFetchedSeriesBlueprint[]} */ export function createGroupedSupplyRelativeMetrics(cohort) { return [ line({ metric: cohort.tree.relative.supplyRelToCirculatingSupply, name: cohort.name, color: cohort.color, unit: Unit.pctSupply, }), ]; } /** * Create supply in profit relative to circulating supply metrics generator for grouped cohorts * @param {CohortWithCirculatingSupplyRelative} cohort * @returns {AnyFetchedSeriesBlueprint[]} */ export function createGroupedSupplyInProfitRelativeMetrics(cohort) { return [ line({ metric: cohort.tree.relative.supplyInProfitRelToCirculatingSupply, name: cohort.name, color: cohort.color, unit: Unit.pctSupply, }), ]; } /** * Create supply in loss relative to circulating supply metrics generator for grouped cohorts * @param {CohortWithCirculatingSupplyRelative} cohort * @returns {AnyFetchedSeriesBlueprint[]} */ export function createGroupedSupplyInLossRelativeMetrics(cohort) { return [ line({ metric: cohort.tree.relative.supplyInLossRelToCirculatingSupply, name: cohort.name, color: cohort.color, unit: Unit.pctSupply, }), ]; } /** * Grouped supply relative generators object for cohorts with circulating supply relative * @type {{ supplyRelativeMetrics: typeof createGroupedSupplyRelativeMetrics, profitRelativeMetrics: typeof createGroupedSupplyInProfitRelativeMetrics, lossRelativeMetrics: typeof createGroupedSupplyInLossRelativeMetrics }} */ export const groupedSupplyRelativeGenerators = { supplyRelativeMetrics: createGroupedSupplyRelativeMetrics, profitRelativeMetrics: createGroupedSupplyInProfitRelativeMetrics, lossRelativeMetrics: createGroupedSupplyInLossRelativeMetrics, }; /** * Create single cohort supply relative options for cohorts with circulating supply relative * @param {CohortWithCirculatingSupplyRelative} cohort * @returns {{ supplyRelative: AnyFetchedSeriesBlueprint[], pnlRelative: AnyFetchedSeriesBlueprint[] }} */ export function createSingleSupplyRelativeOptions(cohort) { return { supplyRelative: createSupplyRelativeToCirculatingSeries(cohort), pnlRelative: createSupplyPnlRelativeToCirculatingSeries(cohort), }; } /** * Create UTXO count series * @param {readonly CohortObject[]} list * @param {boolean} useGroupName * @returns {AnyFetchedSeriesBlueprint[]} */ export function createUtxoCountSeries(list, useGroupName) { return list.flatMap(({ color, name, tree }) => [ line({ metric: tree.outputs.utxoCount, name: useGroupName ? name : "Count", color, unit: Unit.count, }), ]); } /** * Create address count series (for address cohorts only) * @param {readonly AddressCohortObject[]} list * @param {boolean} useGroupName * @returns {AnyFetchedSeriesBlueprint[]} */ export function createAddressCountSeries(list, useGroupName) { return list.flatMap(({ color, name, tree }) => [ line({ metric: tree.addrCount, name: useGroupName ? name : "Count", color: useGroupName ? color : colors.orange, unit: Unit.count, }), ]); } /** * Create realized price series for grouped cohorts * @param {readonly CohortObject[]} list * @returns {FetchedPriceSeriesBlueprint[]} */ export function createRealizedPriceSeries(list) { return list.map(({ color, name, tree }) => price({ metric: tree.realized.realizedPrice, name, color }), ); } /** * Create realized price ratio series for grouped cohorts * @param {readonly CohortObject[]} list * @returns {AnyFetchedSeriesBlueprint[]} */ export function createRealizedPriceRatioSeries(list) { return list.map(({ name, tree }) => baseline({ metric: tree.realized.realizedPriceExtra.ratio, name, unit: Unit.ratio, base: 1, }), ); } /** * Create realized capitalization series * @param {readonly CohortObject[]} list * @param {boolean} useGroupName * @returns {AnyFetchedSeriesBlueprint[]} */ export function createRealizedCapSeries(list, useGroupName) { return list.flatMap(({ color, name, tree }) => [ line({ metric: tree.realized.realizedCap, name: useGroupName ? name : "Capitalization", color, unit: Unit.usd, }), ]); } /** * Create cost basis percentile series (only for cohorts with CostBasisPattern2) * Includes min (p0) and max (p100) with full rainbow coloring * @param {Colors} colors * @param {readonly CohortWithCostBasisPercentiles[]} list * @param {boolean} useGroupName * @returns {FetchedPriceSeriesBlueprint[]} */ export function createCostBasisPercentilesSeries(colors, list, useGroupName) { return list.flatMap(({ name, tree }) => { const cb = tree.costBasis; const p = cb.percentiles; const n = (/** @type {number} */ pct) => useGroupName ? `${name} p${pct}` : `p${pct}`; return [ price({ metric: cb.max, name: n(100), color: colors.purple, defaultActive: false, }), price({ metric: p.pct95, name: n(95), color: colors.fuchsia, defaultActive: false, }), price({ metric: p.pct90, name: n(90), color: colors.pink, defaultActive: false, }), price({ metric: p.pct85, name: n(85), color: colors.pink, defaultActive: false, }), price({ metric: p.pct80, name: n(80), color: colors.rose, defaultActive: false, }), price({ metric: p.pct75, name: n(75), color: colors.red, defaultActive: false, }), price({ metric: p.pct70, name: n(70), color: colors.orange, defaultActive: false, }), price({ metric: p.pct65, name: n(65), color: colors.amber, defaultActive: false, }), price({ metric: p.pct60, name: n(60), color: colors.yellow, defaultActive: false, }), price({ metric: p.pct55, name: n(55), color: colors.yellow, defaultActive: false, }), price({ metric: p.pct50, name: n(50), color: colors.avocado }), price({ metric: p.pct45, name: n(45), color: colors.lime, defaultActive: false, }), price({ metric: p.pct40, name: n(40), color: colors.green, defaultActive: false, }), price({ metric: p.pct35, name: n(35), color: colors.emerald, defaultActive: false, }), price({ metric: p.pct30, name: n(30), color: colors.teal, defaultActive: false, }), price({ metric: p.pct25, name: n(25), color: colors.teal, defaultActive: false, }), price({ metric: p.pct20, name: n(20), color: colors.cyan, defaultActive: false, }), price({ metric: p.pct15, name: n(15), color: colors.sky, defaultActive: false, }), price({ metric: p.pct10, name: n(10), color: colors.blue, defaultActive: false, }), price({ metric: p.pct05, name: n(5), color: colors.indigo, defaultActive: false, }), price({ metric: cb.min, name: n(0), color: colors.violet, defaultActive: false, }), ]; }); } /** * Create invested capital percentile series (only for cohorts with CostBasisPattern2) * Shows invested capital at each percentile level * @param {Colors} colors * @param {readonly CohortWithCostBasisPercentiles[]} list * @param {boolean} useGroupName * @returns {FetchedPriceSeriesBlueprint[]} */ export function createInvestedCapitalPercentilesSeries( colors, list, useGroupName, ) { return list.flatMap(({ name, tree }) => { const ic = tree.costBasis.investedCapital; const n = (/** @type {number} */ pct) => useGroupName ? `${name} p${pct}` : `p${pct}`; return [ price({ metric: ic.pct95, name: n(95), color: colors.fuchsia, defaultActive: false, }), price({ metric: ic.pct90, name: n(90), color: colors.pink, defaultActive: false, }), price({ metric: ic.pct85, name: n(85), color: colors.pink, defaultActive: false, }), price({ metric: ic.pct80, name: n(80), color: colors.rose, defaultActive: false, }), price({ metric: ic.pct75, name: n(75), color: colors.red, defaultActive: false, }), price({ metric: ic.pct70, name: n(70), color: colors.orange, defaultActive: false, }), price({ metric: ic.pct65, name: n(65), color: colors.amber, defaultActive: false, }), price({ metric: ic.pct60, name: n(60), color: colors.yellow, defaultActive: false, }), price({ metric: ic.pct55, name: n(55), color: colors.yellow, defaultActive: false, }), price({ metric: ic.pct50, name: n(50), color: colors.avocado }), price({ metric: ic.pct45, name: n(45), color: colors.lime, defaultActive: false, }), price({ metric: ic.pct40, name: n(40), color: colors.green, defaultActive: false, }), price({ metric: ic.pct35, name: n(35), color: colors.emerald, defaultActive: false, }), price({ metric: ic.pct30, name: n(30), color: colors.teal, defaultActive: false, }), price({ metric: ic.pct25, name: n(25), color: colors.teal, defaultActive: false, }), price({ metric: ic.pct20, name: n(20), color: colors.cyan, defaultActive: false, }), price({ metric: ic.pct15, name: n(15), color: colors.sky, defaultActive: false, }), price({ metric: ic.pct10, name: n(10), color: colors.blue, defaultActive: false, }), price({ metric: ic.pct05, name: n(5), color: colors.indigo, defaultActive: false, }), ]; }); } /** * Create spot percentile series (shows current percentile of price relative to cost basis/invested capital) * @param {Colors} colors * @param {readonly CohortWithCostBasisPercentiles[]} list * @param {boolean} useGroupName * @returns {FetchedBaselineSeriesBlueprint[]} */ export function createSpotPercentileSeries(colors, list, useGroupName) { return list.flatMap(({ name, color, tree }) => [ baseline({ metric: tree.costBasis.spotCostBasisPercentile, name: useGroupName ? `${name} Cost Basis` : "Cost Basis", color: useGroupName ? color : colors.default, unit: Unit.ratio, }), baseline({ metric: tree.costBasis.spotInvestedCapitalPercentile, name: useGroupName ? `${name} Invested Capital` : "Invested Capital", color: useGroupName ? color : colors.orange, unit: Unit.ratio, defaultActive: false, }), ]); } // ============================================================================ // Activity Section Helpers // ============================================================================ /** * Create coins destroyed series (coinblocks, coindays, satblocks, satdays) for single cohort * All metrics on one chart * @param {CohortObject} cohort * @returns {AnyFetchedSeriesBlueprint[]} */ export function createSingleCoinsDestroyedSeries(cohort) { const { tree, color } = cohort; return [ line({ metric: tree.activity.coinblocksDestroyed.sum, name: "Coinblocks", color, unit: Unit.coinblocks, }), line({ metric: tree.activity.coinblocksDestroyed.cumulative, name: "Coinblocks Cumulative", color, unit: Unit.coinblocks, defaultActive: false, }), line({ metric: tree.activity.coindaysDestroyed.sum, name: "Coindays", color, unit: Unit.coindays, }), line({ metric: tree.activity.coindaysDestroyed.cumulative, name: "Coindays Cumulative", color, unit: Unit.coindays, defaultActive: false, }), ]; } /** * Create coinblocks destroyed series for grouped cohorts (comparison) * @param {readonly CohortObject[]} list * @returns {AnyFetchedSeriesBlueprint[]} */ export function createGroupedCoinblocksDestroyedSeries(list) { return list.flatMap(({ color, name, tree }) => [ line({ metric: tree.activity.coinblocksDestroyed.sum, name, color, unit: Unit.coinblocks, }), ]); } /** * Create coindays destroyed series for grouped cohorts (comparison) * @param {readonly CohortObject[]} list * @returns {AnyFetchedSeriesBlueprint[]} */ export function createGroupedCoindaysDestroyedSeries(list) { return list.flatMap(({ color, name, tree }) => [ line({ metric: tree.activity.coindaysDestroyed.sum, name, color, unit: Unit.coindays, }), ]); } /** * Create sent series (sats, btc, usd) for single cohort - all on one chart * @param {CohortObject} cohort * @returns {AnyFetchedSeriesBlueprint[]} */ export function createSingleSentSeries(cohort) { const { tree, color } = cohort; return [ line({ metric: tree.activity.sent.sats.sum, name: "Sent", color, unit: Unit.sats, }), line({ metric: tree.activity.sent.sats.cumulative, name: "Cumulative", color, unit: Unit.sats, defaultActive: false, }), line({ metric: tree.activity.sent.bitcoin.sum, name: "Sent", color, unit: Unit.btc, }), line({ metric: tree.activity.sent.bitcoin.cumulative, name: "Cumulative", color, unit: Unit.btc, defaultActive: false, }), line({ metric: tree.activity.sent.dollars.sum, name: "Sent", color, unit: Unit.usd, }), line({ metric: tree.activity.sent.dollars.cumulative, name: "Cumulative", color, unit: Unit.usd, defaultActive: false, }), ...satsBtcUsd({ pattern: tree.activity.sent14dEma, name: "14d EMA" }), ]; } // ============================================================================ // Sell Side Risk Ratio Helpers // ============================================================================ /** * Create sell side risk ratio series for single cohort * @param {Colors} colors * @param {{ realized: AnyRealizedPattern }} tree * @returns {AnyFetchedSeriesBlueprint[]} */ export function createSingleSellSideRiskSeries(colors, tree) { return [ dots({ metric: tree.realized.sellSideRiskRatio, name: "Raw", color: colors.orange, unit: Unit.ratio, }), line({ metric: tree.realized.sellSideRiskRatio7dEma, name: "7d EMA", color: colors.red, unit: Unit.ratio, }), line({ metric: tree.realized.sellSideRiskRatio30dEma, name: "30d EMA", color: colors.pink, unit: Unit.ratio, }), ]; } /** * Create sell side risk ratio series for grouped cohorts * @param {readonly CohortObject[]} list * @returns {AnyFetchedSeriesBlueprint[]} */ export function createGroupedSellSideRiskSeries(list) { return list.flatMap(({ color, name, tree }) => [ line({ metric: tree.realized.sellSideRiskRatio, name, color, unit: Unit.ratio, }), ]); } // ============================================================================ // Value Created & Destroyed Helpers // ============================================================================ /** * Create value created & destroyed series for single cohort * @param {Colors} colors * @param {{ realized: AnyRealizedPattern }} tree * @returns {AnyFetchedSeriesBlueprint[]} */ export function createSingleValueCreatedDestroyedSeries(colors, tree) { return [ line({ metric: tree.realized.valueCreated, name: "Created", color: colors.emerald, unit: Unit.usd, }), line({ metric: tree.realized.valueDestroyed, name: "Destroyed", color: colors.red, unit: Unit.usd, }), ]; } /** * Create profit/loss value breakdown series for single cohort * Shows profit value created/destroyed and loss value created/destroyed * @param {Colors} colors * @param {{ realized: AnyRealizedPattern }} tree * @returns {AnyFetchedSeriesBlueprint[]} */ export function createSingleValueFlowBreakdownSeries(colors, tree) { return [ line({ metric: tree.realized.profitValueCreated, name: "Profit Created", color: colors.green, unit: Unit.usd, }), line({ metric: tree.realized.profitValueDestroyed, name: "Profit Destroyed", color: colors.lime, unit: Unit.usd, defaultActive: false, }), line({ metric: tree.realized.lossValueCreated, name: "Loss Created", color: colors.orange, unit: Unit.usd, defaultActive: false, }), line({ metric: tree.realized.lossValueDestroyed, name: "Loss Destroyed", color: colors.red, unit: Unit.usd, }), ]; } /** * Create capitulation & profit flow series for single cohort * @param {Colors} colors * @param {{ realized: AnyRealizedPattern }} tree * @returns {AnyFetchedSeriesBlueprint[]} */ export function createSingleCapitulationProfitFlowSeries(colors, tree) { return [ line({ metric: tree.realized.profitFlow, name: "Profit Flow", color: colors.green, unit: Unit.usd, }), line({ metric: tree.realized.capitulationFlow, name: "Capitulation Flow", color: colors.red, unit: Unit.usd, }), ]; } // ============================================================================ // SOPR Helpers // ============================================================================ /** * Create base SOPR series for single cohort (all cohorts have base SOPR) * @param {Colors} colors * @param {{ realized: AnyRealizedPattern }} tree * @returns {AnyFetchedSeriesBlueprint[]} */ export function createSingleSoprSeries(colors, tree) { return [ baseline({ metric: tree.realized.sopr, name: "SOPR", unit: Unit.ratio, base: 1, }), baseline({ metric: tree.realized.sopr7dEma, name: "7d EMA", color: [colors.lime, colors.rose], unit: Unit.ratio, defaultActive: false, base: 1, }), baseline({ metric: tree.realized.sopr30dEma, name: "30d EMA", color: [colors.avocado, colors.pink], unit: Unit.ratio, defaultActive: false, base: 1, }), ]; } // ============================================================================ // Investor Price Helpers // ============================================================================ /** * Create investor price series for single cohort * @param {{ realized: AnyRealizedPattern }} tree * @param {Color} color * @returns {FetchedPriceSeriesBlueprint[]} */ export function createSingleInvestorPriceSeries(tree, color) { return [ price({ metric: tree.realized.investorPrice, name: "Investor", color, }), ]; } /** * Create investor price ratio series for single cohort * @param {{ realized: AnyRealizedPattern }} tree * @param {Color} color * @returns {AnyFetchedSeriesBlueprint[]} */ export function createSingleInvestorPriceRatioSeries(tree, color) { return [ baseline({ metric: tree.realized.investorPriceExtra.ratio, name: "Investor Ratio", color, unit: Unit.ratio, base: 1, }), ]; } /** * Create investor price series for grouped cohorts * @param {readonly CohortObject[]} list * @returns {FetchedPriceSeriesBlueprint[]} */ export function createInvestorPriceSeries(list) { return list.map(({ color, name, tree }) => price({ metric: tree.realized.investorPrice, name, color }), ); } /** * Create investor price ratio series for grouped cohorts * @param {readonly CohortObject[]} list * @returns {AnyFetchedSeriesBlueprint[]} */ export function createInvestorPriceRatioSeries(list) { return list.map(({ name, tree }) => baseline({ metric: tree.realized.investorPriceExtra.ratio, name, unit: Unit.ratio, base: 1, }), ); } /** * Create investor price folder for extended cohorts (with full Z-scores) * For cohorts with ActivePriceRatioPattern (all, term.*, ageRange.* UTXO cohorts) * @param {{ tree: { realized: RealizedWithExtras }, color: Color }} cohort * @param {string} [cohortTitle] - Cohort title (e.g., "STH") * @returns {PartialOptionsGroup} */ export function createInvestorPriceFolderFull(cohort, cohortTitle) { const { tree, color } = cohort; return createPriceFolder({ name: "Investor Price", cohortTitle, priceMetric: tree.realized.investorPrice, ratioPattern: tree.realized.investorPriceExtra, color, }); } /** * Create investor price folder for basic cohorts (price + ratio only) * For cohorts with InvestorPriceExtraPattern (only .ratio field) * @param {{ tree: { realized: AnyRealizedPattern }, color: Color }} cohort * @param {string} [cohortTitle] - Cohort title (e.g., "STH") * @returns {PartialOptionsGroup} */ export function createInvestorPriceFolderBasic(cohort, cohortTitle) { const { tree, color } = cohort; return createBasicPriceFolder({ name: "Investor Price", cohortTitle, priceMetric: tree.realized.investorPrice, ratioMetric: tree.realized.investorPriceExtra.ratio, color, }); } /** * Create investor price folder for grouped cohorts * @param {readonly CohortObject[]} list * @param {(metric: string) => string} title * @returns {PartialOptionsGroup} */ export function createGroupedInvestorPriceFolder(list, title) { return createGroupedPriceFolder({ name: "Investor Price", title, list, getPrice: (tree) => tree.realized.investorPrice, getRatio: (tree) => tree.realized.investorPriceExtra.ratio, }); } // ============================================================================ // Peak Regret Helpers // ============================================================================ /** * Create realized peak regret series for single cohort * @param {{ realized: AnyRealizedPattern }} tree * @param {Color} color * @returns {AnyFetchedSeriesBlueprint[]} */ export function createSingleRealizedAthRegretSeries(tree, color) { return [ line({ metric: tree.realized.peakRegret.sum, name: "Peak Regret", color, unit: Unit.usd, }), line({ metric: tree.realized.peakRegret.cumulative, name: "Cumulative", color, unit: Unit.usd, defaultActive: false, }), baseline({ metric: tree.realized.peakRegretRelToRealizedCap, name: "Rel. to Realized Cap", color, unit: Unit.pctRcap, }), ]; } /** * Create realized ATH regret series for grouped cohorts * @param {readonly CohortObject[]} list * @returns {AnyFetchedSeriesBlueprint[]} */ export function createGroupedRealizedAthRegretSeries(list) { return list.flatMap(({ color, name, tree }) => [ line({ metric: tree.realized.peakRegret.sum, name, color, unit: Unit.usd, }), baseline({ metric: tree.realized.peakRegretRelToRealizedCap, name, color, unit: Unit.pctRcap, }), ]); } // ============================================================================ // Sentiment Helpers (greedIndex, painIndex, netSentiment) // ============================================================================ /** * Create sentiment series for single cohort * @param {Colors} colors * @param {{ unrealized: UnrealizedPattern }} tree * @returns {AnyFetchedSeriesBlueprint[]} */ export function createSingleSentimentSeries(colors, tree) { return [ baseline({ metric: tree.unrealized.netSentiment, name: "Net Sentiment", unit: Unit.usd, }), line({ metric: tree.unrealized.greedIndex, name: "Greed Index", color: colors.green, unit: Unit.usd, }), line({ metric: tree.unrealized.painIndex, name: "Pain Index", color: colors.red, unit: Unit.usd, }), ]; } /** * Create net sentiment series for grouped cohorts * @param {readonly { color: Color, name: string, tree: { unrealized: UnrealizedPattern } }[]} list * @returns {AnyFetchedSeriesBlueprint[]} */ export function createGroupedNetSentimentSeries(list) { return list.flatMap(({ color, name, tree }) => [ baseline({ metric: tree.unrealized.netSentiment, name, color, unit: Unit.usd, }), ]); } /** * Create greed index series for grouped cohorts * @param {readonly { color: Color, name: string, tree: { unrealized: UnrealizedPattern } }[]} list * @returns {AnyFetchedSeriesBlueprint[]} */ export function createGroupedGreedIndexSeries(list) { return list.flatMap(({ color, name, tree }) => [ line({ metric: tree.unrealized.greedIndex, name, color, unit: Unit.usd, }), ]); } /** * Create pain index series for grouped cohorts * @param {readonly { color: Color, name: string, tree: { unrealized: UnrealizedPattern } }[]} list * @returns {AnyFetchedSeriesBlueprint[]} */ export function createGroupedPainIndexSeries(list) { return list.flatMap(({ color, name, tree }) => [ line({ metric: tree.unrealized.painIndex, name, color, unit: Unit.usd, }), ]); }