mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-04-25 07:09:59 -07:00
797 lines
24 KiB
JavaScript
797 lines
24 KiB
JavaScript
/**
|
|
* 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),
|
|
],
|
|
},
|
|
],
|
|
};
|
|
}
|