/** * Cohort module - exports all cohort-related functionality * * Folder builders compose sections from building blocks: * - holdings.js: Supply, UTXO Count, Address Count * - valuation.js: Realized Cap, Market Cap, MVRV * - prices.js: Realized Price, ratios * - cost-basis.js: Cost basis percentiles * - profitability.js: Unrealized/Realized P&L, Invested Capital * - activity.js: SOPR, Volume, Lifespan */ import { formatCohortTitle, satsBtcUsd, satsBtcUsdFullTree, simplePriceRatioTree, groupedSimplePriceRatioTree } from "../shared.js"; import { ROLLING_WINDOWS, line, baseline, percentRatio, rollingWindowsTree, rollingPercentRatioTree } from "../series.js"; import { Unit } from "../../utils/units.js"; // Section builders import { createHoldingsSection, createHoldingsSectionAll, createHoldingsSectionAddress, createHoldingsSectionAddressAmount, createHoldingsSectionWithProfitLoss, createHoldingsSectionWithRelative, createHoldingsSectionWithOwnSupply, createGroupedHoldingsSection, createGroupedHoldingsSectionWithProfitLoss, createGroupedHoldingsSectionAddress, createGroupedHoldingsSectionAddressAmount, createGroupedHoldingsSectionWithRelative, createGroupedHoldingsSectionWithOwnSupply, } from "./holdings.js"; import { createValuationSection, createValuationSectionFull, createGroupedValuationSection, createGroupedValuationSectionWithOwnMarketCap, } from "./valuation.js"; import { createPricesSectionFull, createPricesSectionBasic, createGroupedPricesSection, } from "./prices.js"; import { createCostBasisSectionWithPercentiles, createGroupedCostBasisSectionWithPercentiles, } from "./cost-basis.js"; import { createProfitabilitySectionAll, createProfitabilitySectionFull, createProfitabilitySectionWithNupl, createProfitabilitySectionWithInvestedCapitalPct, createProfitabilitySectionBasicWithInvestedCapitalPct, createProfitabilitySectionAddress, createProfitabilitySectionWithProfitLoss, createProfitabilitySectionLongTerm, createGroupedProfitabilitySection, createGroupedProfitabilitySectionWithProfitLoss, createGroupedProfitabilitySectionWithNupl, createGroupedProfitabilitySectionWithInvestedCapitalPct, createGroupedProfitabilitySectionBasicWithInvestedCapitalPct, createGroupedProfitabilitySectionLongTerm, } from "./profitability.js"; import { createActivitySection, createActivitySectionWithAdjusted, createActivitySectionWithActivity, createActivitySectionMinimal, createGroupedActivitySection, createGroupedActivitySectionWithAdjusted, createGroupedActivitySectionWithActivity, createGroupedActivitySectionMinimal, } from "./activity.js"; // Re-export data builder export { buildCohortData } from "./data.js"; // ============================================================================ // Single Cohort Folder Builders // ============================================================================ /** * All folder: for the special "All" cohort * @param {CohortAll} cohort * @returns {PartialOptionsGroup} */ export function createCohortFolderAll(cohort) { const title = formatCohortTitle(cohort.name); return { name: cohort.name || "all", tree: [ createHoldingsSectionAll({ cohort, title }), createValuationSectionFull({ cohort, title }), createPricesSectionFull({ cohort, title }), createCostBasisSectionWithPercentiles({ cohort, title }), createProfitabilitySectionAll({ cohort, title }), createActivitySectionWithAdjusted({ cohort, title }), ], }; } /** * Full folder: adjustedSopr + percentiles + RelToMarketCap * @param {CohortFull} cohort * @returns {PartialOptionsGroup} */ export function createCohortFolderFull(cohort) { const title = formatCohortTitle(cohort.name); return { name: cohort.name || "all", tree: [ createHoldingsSectionWithRelative({ cohort, title }), createValuationSectionFull({ cohort, title }), createPricesSectionFull({ cohort, title }), createCostBasisSectionWithPercentiles({ cohort, title }), createProfitabilitySectionFull({ cohort, title }), createActivitySectionWithAdjusted({ cohort, title }), ], }; } /** * Adjusted folder: adjustedSopr only, no percentiles * @param {CohortWithAdjusted} cohort * @returns {PartialOptionsGroup} */ export function createCohortFolderWithAdjusted(cohort) { const title = formatCohortTitle(cohort.name); return { name: cohort.name || "all", tree: [ createHoldingsSectionWithOwnSupply({ cohort, title }), createValuationSection({ cohort, title }), createPricesSectionBasic({ cohort, title }), createProfitabilitySectionWithInvestedCapitalPct({ cohort, title }), createActivitySectionWithActivity({ cohort, title }), ], }; } /** * Folder for cohorts with nupl + percentiles * @param {CohortWithNuplPercentiles} cohort * @returns {PartialOptionsGroup} */ export function createCohortFolderWithNupl(cohort) { const title = formatCohortTitle(cohort.name); return { name: cohort.name || "all", tree: [ createHoldingsSectionWithRelative({ cohort, title }), createValuationSectionFull({ cohort, title }), createPricesSectionFull({ cohort, title }), createCostBasisSectionWithPercentiles({ cohort, title }), createProfitabilitySectionWithNupl({ cohort, title }), createActivitySection({ cohort, title }), ], }; } /** * LongTerm folder: has own market cap + NUPL + peak regret + P/L ratio * @param {CohortLongTerm} cohort * @returns {PartialOptionsGroup} */ export function createCohortFolderLongTerm(cohort) { const title = formatCohortTitle(cohort.name); return { name: cohort.name || "all", tree: [ createHoldingsSectionWithRelative({ cohort, title }), createValuationSectionFull({ cohort, title }), createPricesSectionFull({ cohort, title }), createCostBasisSectionWithPercentiles({ cohort, title }), createProfitabilitySectionLongTerm({ cohort, title }), createActivitySection({ cohort, title }), ], }; } /** * Age range folder: no nupl * @param {CohortAgeRange} cohort * @returns {PartialOptionsGroup} */ export function createCohortFolderAgeRange(cohort) { const title = formatCohortTitle(cohort.name); return { name: cohort.name || "all", tree: [ createHoldingsSectionWithOwnSupply({ cohort, title }), createValuationSection({ cohort, title }), createPricesSectionBasic({ cohort, title }), createProfitabilitySectionWithInvestedCapitalPct({ cohort, title }), createActivitySectionWithActivity({ cohort, title }), ], }; } /** * Age range folder with matured supply * @param {CohortAgeRangeWithMatured} cohort * @returns {PartialOptionsGroup} */ export function createCohortFolderAgeRangeWithMatured(cohort) { const folder = createCohortFolderAgeRange(cohort); const title = formatCohortTitle(cohort.name); folder.tree.push({ name: "Matured", tree: satsBtcUsdFullTree({ pattern: cohort.matured, name: cohort.name, title: title("Matured Supply"), }), }); return folder; } /** * Basic folder WITH RelToMarketCap * @param {CohortBasicWithMarketCap} cohort * @returns {PartialOptionsGroup} */ export function createCohortFolderBasicWithMarketCap(cohort) { const title = formatCohortTitle(cohort.name); return { name: cohort.name || "all", tree: [ createHoldingsSection({ cohort, title }), createValuationSection({ cohort, title }), createPricesSectionBasic({ cohort, title }), createProfitabilitySectionWithNupl({ cohort, title }), createActivitySectionMinimal({ cohort, title }), ], }; } /** * Basic folder WITHOUT RelToMarketCap * @param {CohortBasicWithoutMarketCap} cohort * @returns {PartialOptionsGroup} */ export function createCohortFolderBasicWithoutMarketCap(cohort) { const title = formatCohortTitle(cohort.name); return { name: cohort.name || "all", tree: [ createHoldingsSection({ cohort, title }), createValuationSection({ cohort, title }), createPricesSectionBasic({ cohort, title }), createProfitabilitySectionBasicWithInvestedCapitalPct({ cohort, title }), createActivitySectionMinimal({ cohort, title }), ], }; } /** * Address folder: like basic but with address count * @param {CohortAddress} cohort * @returns {PartialOptionsGroup} */ export function createCohortFolderAddress(cohort) { const title = formatCohortTitle(cohort.name); return { name: cohort.name || "all", tree: [ createHoldingsSectionAddress({ cohort, title }), createValuationSection({ cohort, title }), createPricesSectionBasic({ cohort, title }), createProfitabilitySectionAddress({ cohort, title }), createActivitySectionMinimal({ cohort, title }), ], }; } /** * Folder for cohorts WITHOUT relative section * @param {CohortWithoutRelative} cohort * @returns {PartialOptionsGroup} */ export function createCohortFolderWithoutRelative(cohort) { const title = formatCohortTitle(cohort.name); return { name: cohort.name || "all", tree: [ createHoldingsSectionWithProfitLoss({ cohort, title }), createValuationSection({ cohort, title }), createPricesSectionBasic({ cohort, title }), createProfitabilitySectionWithProfitLoss({ cohort, title }), createActivitySectionMinimal({ cohort, title }), ], }; } /** * Address amount cohort folder: has NUPL + addrCount * @param {AddressCohortObject} cohort * @returns {PartialOptionsGroup} */ export function createAddressCohortFolder(cohort) { const title = formatCohortTitle(cohort.name); return { name: cohort.name || "all", tree: [ createHoldingsSectionAddressAmount({ cohort, title }), createValuationSection({ cohort, title }), createPricesSectionBasic({ cohort, title }), createProfitabilitySectionWithNupl({ cohort, title }), createActivitySectionMinimal({ cohort, title }), ], }; } // ============================================================================ // Grouped Cohort Folder Builders // ============================================================================ /** * @param {CohortGroupFull} args * @returns {PartialOptionsGroup} */ export function createGroupedCohortFolderFull({ name, title: groupTitle, list, all, }) { const title = formatCohortTitle(groupTitle); return { name: name || "all", tree: [ createGroupedHoldingsSectionWithRelative({ list, all, title }), createGroupedValuationSectionWithOwnMarketCap({ list, all, title }), createGroupedPricesSection({ list, all, title }), createGroupedCostBasisSectionWithPercentiles({ list, all, title }), createGroupedProfitabilitySectionWithNupl({ list, all, title }), createGroupedActivitySectionWithAdjusted({ list, all, title }), ], }; } /** * @param {CohortGroupWithAdjusted} args * @returns {PartialOptionsGroup} */ export function createGroupedCohortFolderWithAdjusted({ name, title: groupTitle, list, all, }) { const title = formatCohortTitle(groupTitle); return { name: name || "all", tree: [ createGroupedHoldingsSectionWithOwnSupply({ list, all, title }), createGroupedValuationSection({ list, all, title }), createGroupedPricesSection({ list, all, title }), createGroupedProfitabilitySectionWithInvestedCapitalPct({ list, all, title, }), createGroupedActivitySectionWithActivity({ list, all, title }), ], }; } /** * @param {CohortGroupWithNuplPercentiles} args * @returns {PartialOptionsGroup} */ export function createGroupedCohortFolderWithNupl({ name, title: groupTitle, list, all, }) { const title = formatCohortTitle(groupTitle); return { name: name || "all", tree: [ createGroupedHoldingsSectionWithRelative({ list, all, title }), createGroupedValuationSection({ list, all, title }), createGroupedPricesSection({ list, all, title }), createGroupedCostBasisSectionWithPercentiles({ list, all, title }), createGroupedProfitabilitySectionWithNupl({ list, all, title }), createGroupedActivitySection({ list, all, title }), ], }; } /** * @param {CohortGroupLongTerm} args * @returns {PartialOptionsGroup} */ export function createGroupedCohortFolderLongTerm({ name, title: groupTitle, list, all, }) { const title = formatCohortTitle(groupTitle); return { name: name || "all", tree: [ createGroupedHoldingsSectionWithRelative({ list, all, title }), createGroupedValuationSectionWithOwnMarketCap({ list, all, title }), createGroupedPricesSection({ list, all, title }), createGroupedCostBasisSectionWithPercentiles({ list, all, title }), createGroupedProfitabilitySectionLongTerm({ list, all, title }), createGroupedActivitySection({ list, all, title }), ], }; } /** * @param {CohortGroupAgeRange} args * @returns {PartialOptionsGroup} */ export function createGroupedCohortFolderAgeRange({ name, title: groupTitle, list, all, }) { const title = formatCohortTitle(groupTitle); return { name: name || "all", tree: [ createGroupedHoldingsSectionWithOwnSupply({ list, all, title }), createGroupedValuationSection({ list, all, title }), createGroupedPricesSection({ list, all, title }), createGroupedProfitabilitySectionWithInvestedCapitalPct({ list, all, title, }), createGroupedActivitySectionWithActivity({ list, all, title }), ], }; } /** * @param {{ name: string, title: string, list: readonly CohortAgeRangeWithMatured[], all: CohortAll }} args * @returns {PartialOptionsGroup} */ export function createGroupedCohortFolderAgeRangeWithMatured({ name, title: groupTitle, list, all, }) { const folder = createGroupedCohortFolderAgeRange({ name, title: groupTitle, list, all }); const title = formatCohortTitle(groupTitle); folder.tree.push({ name: "Matured", title: title("Matured Supply"), bottom: list.flatMap((cohort) => satsBtcUsd({ pattern: cohort.matured.base, name: cohort.name, color: cohort.color }), ), }); return folder; } /** * @param {CohortGroupBasicWithMarketCap} args * @returns {PartialOptionsGroup} */ export function createGroupedCohortFolderBasicWithMarketCap({ name, title: groupTitle, list, all, }) { const title = formatCohortTitle(groupTitle); return { name: name || "all", tree: [ createGroupedHoldingsSection({ list, all, title }), createGroupedValuationSection({ list, all, title }), createGroupedPricesSection({ list, all, title }), createGroupedProfitabilitySection({ list, all, title }), createGroupedActivitySectionMinimal({ list, all, title }), ], }; } /** * @param {CohortGroupBasicWithoutMarketCap} args * @returns {PartialOptionsGroup} */ export function createGroupedCohortFolderBasicWithoutMarketCap({ name, title: groupTitle, list, all, }) { const title = formatCohortTitle(groupTitle); return { name: name || "all", tree: [ createGroupedHoldingsSection({ list, all, title }), createGroupedValuationSection({ list, all, title }), createGroupedPricesSection({ list, all, title }), createGroupedProfitabilitySectionBasicWithInvestedCapitalPct({ list, all, title, }), createGroupedActivitySectionMinimal({ list, all, title }), ], }; } /** * @param {CohortGroupAddress} args * @returns {PartialOptionsGroup} */ export function createGroupedCohortFolderAddress({ name, title: groupTitle, list, all, }) { const title = formatCohortTitle(groupTitle); return { name: name || "all", tree: [ createGroupedHoldingsSectionAddress({ list, all, title }), createGroupedValuationSection({ list, all, title }), createGroupedPricesSection({ list, all, title }), createGroupedProfitabilitySectionBasicWithInvestedCapitalPct({ list, all, title, }), createGroupedActivitySectionMinimal({ list, all, title }), ], }; } /** * @param {CohortGroupWithoutRelative} args * @returns {PartialOptionsGroup} */ export function createGroupedCohortFolderWithoutRelative({ name, title: groupTitle, list, all, }) { const title = formatCohortTitle(groupTitle); return { name: name || "all", tree: [ createGroupedHoldingsSectionWithProfitLoss({ list, all, title }), createGroupedValuationSection({ list, all, title }), createGroupedPricesSection({ list, all, title }), createGroupedProfitabilitySectionWithProfitLoss({ list, all, title }), createGroupedActivitySectionMinimal({ list, all, title }), ], }; } /** * @param {AddressCohortGroupObject} args * @returns {PartialOptionsGroup} */ export function createGroupedAddressCohortFolder({ name, title: groupTitle, list, all, }) { const title = formatCohortTitle(groupTitle); return { name: name || "all", tree: [ createGroupedHoldingsSectionAddressAmount({ list, all, title }), createGroupedValuationSection({ list, all, title }), createGroupedPricesSection({ list, all, title }), createGroupedProfitabilitySection({ list, all, title }), createGroupedActivitySectionMinimal({ list, all, title }), ], }; } // ============================================================================ // UTXO Profitability Folder Builders // ============================================================================ /** * @param {{ name: string, color: Color, pattern: RealizedSupplyPattern }} bucket * @returns {PartialOptionsGroup} */ function singleBucketFolder({ name, color, pattern }) { return { name, tree: [ { name: "Supply", tree: [ { name: "All", title: `${name}: Supply`, bottom: satsBtcUsd({ pattern: pattern.supply.all, name, color }), }, { name: "STH", title: `${name}: STH Supply`, bottom: satsBtcUsd({ pattern: pattern.supply.sth, name, color }), }, { name: "Change", tree: [ { ...rollingWindowsTree({ windows: pattern.supply.all.delta.absolute, title: `${name}: Supply Change`, unit: Unit.sats, series: baseline }), name: "Absolute" }, { ...rollingPercentRatioTree({ windows: pattern.supply.all.delta.rate, title: `${name}: Supply Rate` }), name: "Rate" }, ], }, ], }, { name: "Realized Cap", tree: [ { name: "All", title: `${name}: Realized Cap`, bottom: [line({ metric: pattern.realizedCap.all, name, color, unit: Unit.usd })], }, { name: "STH", title: `${name}: STH Realized Cap`, bottom: [line({ metric: pattern.realizedCap.sth, name, color, unit: Unit.usd })], }, ], }, { name: "Realized Price", tree: simplePriceRatioTree({ pattern: pattern.realizedPrice, title: `${name}: Realized Price`, legend: name, color, }), }, { name: "NUPL", title: `${name}: NUPL`, bottom: [line({ metric: pattern.nupl.ratio, name, color, unit: Unit.ratio })], }, ], }; } /** * @param {{ name: string, color: Color, pattern: RealizedSupplyPattern }[]} list * @param {string} titlePrefix * @returns {PartialOptionsTree} */ function groupedBucketCharts(list, titlePrefix) { return [ { name: "Supply", tree: [ { name: "All", title: `${titlePrefix}: Supply`, bottom: list.flatMap(({ name, color, pattern }) => satsBtcUsd({ pattern: pattern.supply.all, name, color }), ), }, { name: "STH", title: `${titlePrefix}: STH Supply`, bottom: list.flatMap(({ name, color, pattern }) => satsBtcUsd({ pattern: pattern.supply.sth, name, color }), ), }, { name: "Change", tree: [ { name: "Absolute", tree: [ { name: "Compare", title: `${titlePrefix}: Supply Change`, bottom: ROLLING_WINDOWS.flatMap((w) => list.map(({ name, color, pattern }) => baseline({ metric: pattern.supply.all.delta.absolute[w.key], name: `${name} ${w.name}`, color, unit: Unit.sats }), ), ), }, ...ROLLING_WINDOWS.map((w) => ({ name: w.name, title: `${titlePrefix}: Supply Change ${w.name}`, bottom: list.map(({ name, color, pattern }) => baseline({ metric: pattern.supply.all.delta.absolute[w.key], name, color, unit: Unit.sats }), ), })), ], }, { name: "Rate", tree: [ { name: "Compare", title: `${titlePrefix}: Supply Rate`, bottom: ROLLING_WINDOWS.flatMap((w) => list.flatMap(({ name, color, pattern }) => percentRatio({ pattern: pattern.supply.all.delta.rate[w.key], name: `${name} ${w.name}`, color }), ), ), }, ...ROLLING_WINDOWS.map((w) => ({ name: w.name, title: `${titlePrefix}: Supply Rate ${w.name}`, bottom: list.flatMap(({ name, color, pattern }) => percentRatio({ pattern: pattern.supply.all.delta.rate[w.key], name, color }), ), })), ], }, ], }, ], }, { name: "Realized Cap", tree: [ { name: "All", title: `${titlePrefix}: Realized Cap`, bottom: list.map(({ name, color, pattern }) => line({ metric: pattern.realizedCap.all, name, color, unit: Unit.usd }), ), }, { name: "STH", title: `${titlePrefix}: STH Realized Cap`, bottom: list.map(({ name, color, pattern }) => line({ metric: pattern.realizedCap.sth, name, color, unit: Unit.usd }), ), }, ], }, { name: "Realized Price", tree: groupedSimplePriceRatioTree({ list: list.map(({ name, color, pattern }) => ({ name, color, pattern: pattern.realizedPrice })), title: `${titlePrefix}: Realized Price`, }), }, { name: "NUPL", title: `${titlePrefix}: NUPL`, bottom: list.map(({ name, color, pattern }) => line({ metric: pattern.nupl.ratio, name, color, unit: Unit.ratio }), ), }, ]; } /** * @param {{ range: { name: string, color: Color, pattern: RealizedSupplyPattern }[], profit: { name: string, color: Color, pattern: RealizedSupplyPattern }[], loss: { name: string, color: Color, pattern: RealizedSupplyPattern }[] }} args * @returns {PartialOptionsGroup} */ export function createUtxoProfitabilitySection({ range, profit, loss }) { return { name: "UTXO Profitability", tree: [ { name: "Range", tree: [ { name: "Compare", tree: groupedBucketCharts(range, "Profitability Range") }, ...range.map(singleBucketFolder), ], }, { name: "In Profit", tree: [ { name: "Compare", tree: groupedBucketCharts(profit, "In Profit") }, ...profit.map(singleBucketFolder), ], }, { name: "In Loss", tree: [ { name: "Compare", tree: groupedBucketCharts(loss, "In Loss") }, ...loss.map(singleBucketFolder), ], }, ], }; }