/** * 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, } from "../shared.js"; import { ROLLING_WINDOWS, line, baseline, percentRatio, sumsTree, rollingPercentRatioTree, } from "../series.js"; import { Unit } from "../../utils/units.js"; import { colors } from "../../utils/colors.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, createGroupedActivitySection, createGroupedActivitySectionWithAdjusted, createGroupedActivitySectionWithActivity, createActivitySectionMinimal, 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, 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 {CohortAddr} 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 {AddrCohortObject} 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 {CohortGroupAddr} 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 {AddrCohortGroupObject} 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: "Value", title: `${name}: Supply`, bottom: [ ...satsBtcUsd({ pattern: pattern.supply.all, name: "Total" }), ...satsBtcUsd({ pattern: pattern.supply.sth, name: "STH", color: colors.term.short, }), ], }, { name: "Change", tree: [ { ...sumsTree({ 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", title: `${name}: Realized Cap`, bottom: [ line({ series: pattern.realizedCap.all, name: "Total", unit: Unit.usd, }), line({ series: pattern.realizedCap.sth, name: "STH", color: colors.term.short, unit: Unit.usd, }), ], }, { name: "NUPL", title: `${name}: NUPL`, bottom: [ line({ series: 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({ series: 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.title})`, bottom: list.map(({ name, color, pattern }) => baseline({ series: 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.title})`, 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({ series: pattern.realizedCap.all, name, color, unit: Unit.usd, }), ), }, { name: "STH", title: `${titlePrefix}: STH Realized Cap`, bottom: list.map(({ name, color, pattern }) => line({ series: pattern.realizedCap.sth, name, color, unit: Unit.usd, }), ), }, ], }, { name: "NUPL", title: `${titlePrefix}: NUPL`, bottom: list.map(({ name, color, pattern }) => line({ series: 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), ], }, ], }; }