website: snapshot

This commit is contained in:
nym21
2026-02-04 17:26:35 +01:00
parent 0d5d7da70f
commit 0437ce1bb4
33 changed files with 5752 additions and 995 deletions

View File

@@ -0,0 +1,803 @@
/**
* Activity section builders
*
* Structure:
* - Volume: Sent volume (Sum, Cumulative, 14d EMA)
* - SOPR: Spent Output Profit Ratio (30d > 7d > raw)
* - Sell Side Risk: Risk ratio
* - Value: Flows, Created & Destroyed, Breakdown
* - Coins Destroyed: Coinblocks/Coindays (Sum, Cumulative)
*
* For cohorts WITH adjusted values: Additional Normal/Adjusted sub-sections
*/
import { Unit } from "../../utils/units.js";
import { line, baseline, dotsBaseline } from "../series.js";
import { satsBtcUsd } from "../shared.js";
import { colors } from "../../utils/colors.js";
import {
createSingleSellSideRiskSeries,
createGroupedSellSideRiskSeries,
createSingleValueCreatedDestroyedSeries,
createSingleCapitulationProfitFlowSeries,
} from "./shared.js";
// ============================================================================
// Shared Helpers
// ============================================================================
/**
* Create SOPR series from realized pattern (30d > 7d > raw order)
* @param {{ sopr: AnyMetricPattern, sopr7dEma: AnyMetricPattern, sopr30dEma: AnyMetricPattern }} realized
* @param {string} rawName - Name for the raw SOPR series
* @returns {AnyFetchedSeriesBlueprint[]}
*/
function soprSeries(realized, rawName = "SOPR") {
return [
baseline({
metric: realized.sopr30dEma,
name: "30d EMA",
color: colors.bi.p3,
unit: Unit.ratio,
base: 1,
}),
baseline({
metric: realized.sopr7dEma,
name: "7d EMA",
color: colors.bi.p2,
unit: Unit.ratio,
base: 1,
}),
dotsBaseline({
metric: realized.sopr,
name: rawName,
color: colors.bi.p1,
unit: Unit.ratio,
base: 1,
}),
];
}
/**
* Create grouped SOPR chart entries (Raw, 7d EMA, 30d EMA)
* @template {{ color: Color, name: string }} T
* @param {readonly T[]} list
* @param {(item: T) => AnyMetricPattern} getSopr
* @param {(item: T) => AnyMetricPattern} getSopr7d
* @param {(item: T) => AnyMetricPattern} getSopr30d
* @param {(metric: string) => string} title
* @param {string} titlePrefix
* @returns {PartialOptionsTree}
*/
function groupedSoprCharts(
list,
getSopr,
getSopr7d,
getSopr30d,
title,
titlePrefix,
) {
return [
{
name: "Raw",
title: title(`${titlePrefix}SOPR`),
bottom: list.map((item) =>
baseline({
metric: getSopr(item),
name: item.name,
color: item.color,
unit: Unit.ratio,
base: 1,
}),
),
},
{
name: "7d EMA",
title: title(`${titlePrefix}SOPR 7d EMA`),
bottom: list.map((item) =>
baseline({
metric: getSopr7d(item),
name: item.name,
color: item.color,
unit: Unit.ratio,
base: 1,
}),
),
},
{
name: "30d EMA",
title: title(`${titlePrefix}SOPR 30d EMA`),
bottom: list.map((item) =>
baseline({
metric: getSopr30d(item),
name: item.name,
color: item.color,
unit: Unit.ratio,
base: 1,
}),
),
},
];
}
/**
* Create value breakdown tree (Profit/Loss Created/Destroyed)
* @template {{ color: Color, name: string, tree: { realized: AnyRealizedPattern } }} T
* @param {readonly T[]} list
* @param {(metric: string) => string} title
* @returns {PartialOptionsTree}
*/
function valueBreakdownTree(list, title) {
return [
{
name: "Profit",
tree: [
{
name: "Created",
title: title("Profit Value Created"),
bottom: list.map(({ color, name, tree }) =>
line({
metric: tree.realized.profitValueCreated,
name,
color,
unit: Unit.usd,
}),
),
},
{
name: "Destroyed",
title: title("Profit Value Destroyed"),
bottom: list.map(({ color, name, tree }) =>
line({
metric: tree.realized.profitValueDestroyed,
name,
color,
unit: Unit.usd,
}),
),
},
],
},
{
name: "Loss",
tree: [
{
name: "Created",
title: title("Loss Value Created"),
bottom: list.map(({ color, name, tree }) =>
line({
metric: tree.realized.lossValueCreated,
name,
color,
unit: Unit.usd,
}),
),
},
{
name: "Destroyed",
title: title("Loss Value Destroyed"),
bottom: list.map(({ color, name, tree }) =>
line({
metric: tree.realized.lossValueDestroyed,
name,
color,
unit: Unit.usd,
}),
),
},
],
},
];
}
/**
* Create coins destroyed tree (Sum/Cumulative with Coinblocks/Coindays)
* @template {{ color: Color, name: string, tree: { activity: { coinblocksDestroyed: CountPattern<any>, coindaysDestroyed: CountPattern<any> } } }} T
* @param {readonly T[]} list
* @param {(metric: string) => string} title
* @returns {PartialOptionsTree}
*/
function coinsDestroyedTree(list, title) {
return [
{
name: "Sum",
title: title("Coins Destroyed"),
bottom: list.flatMap(({ color, name, tree }) => [
line({
metric: tree.activity.coinblocksDestroyed.sum,
name,
color,
unit: Unit.coinblocks,
}),
line({
metric: tree.activity.coindaysDestroyed.sum,
name,
color,
unit: Unit.coindays,
}),
]),
},
{
name: "Cumulative",
title: title("Cumulative Coins Destroyed"),
bottom: list.flatMap(({ color, name, tree }) => [
line({
metric: tree.activity.coinblocksDestroyed.cumulative,
name,
color,
unit: Unit.coinblocks,
}),
line({
metric: tree.activity.coindaysDestroyed.cumulative,
name,
color,
unit: Unit.coindays,
}),
]),
},
];
}
// ============================================================================
// SOPR Helpers
// ============================================================================
/**
* Create SOPR series for single cohort (30d > 7d > raw order)
* @param {UtxoCohortObject | CohortWithoutRelative} cohort
* @returns {AnyFetchedSeriesBlueprint[]}
*/
function createSingleSoprSeries(cohort) {
return soprSeries(cohort.tree.realized);
}
/**
* Create SOPR tree with normal and adjusted sub-sections
* @param {CohortAll | CohortFull | CohortWithAdjusted} cohort
* @param {(metric: string) => string} title
* @returns {PartialOptionsTree}
*/
function createSingleSoprTreeWithAdjusted(cohort, title) {
const { realized } = cohort.tree;
return [
{
name: "Normal",
title: title("SOPR"),
bottom: soprSeries(realized),
},
{
name: "Adjusted",
title: title("Adjusted SOPR"),
bottom: soprSeries(
{
sopr: realized.adjustedSopr,
sopr7dEma: realized.adjustedSopr7dEma,
sopr30dEma: realized.adjustedSopr30dEma,
},
"Adjusted SOPR",
),
},
];
}
/**
* Create grouped SOPR tree with separate charts for each variant
* @template {readonly (UtxoCohortObject | CohortWithoutRelative)[]} T
* @param {T} list
* @param {(metric: string) => string} title
* @returns {PartialOptionsTree}
*/
function createGroupedSoprTree(list, title) {
return groupedSoprCharts(
list,
(c) => c.tree.realized.sopr,
(c) => c.tree.realized.sopr7dEma,
(c) => c.tree.realized.sopr30dEma,
title,
"",
);
}
/**
* Create grouped SOPR tree with Normal and Adjusted sub-sections
* @param {readonly (CohortAll | CohortFull | CohortWithAdjusted)[]} list
* @param {(metric: string) => string} title
* @returns {PartialOptionsTree}
*/
function createGroupedSoprTreeWithAdjusted(list, title) {
return [
{
name: "Normal",
tree: groupedSoprCharts(
list,
(c) => c.tree.realized.sopr,
(c) => c.tree.realized.sopr7dEma,
(c) => c.tree.realized.sopr30dEma,
title,
"",
),
},
{
name: "Adjusted",
tree: groupedSoprCharts(
list,
(c) => c.tree.realized.adjustedSopr,
(c) => c.tree.realized.adjustedSopr7dEma,
(c) => c.tree.realized.adjustedSopr30dEma,
title,
"Adjusted ",
),
},
];
}
// ============================================================================
// Single Cohort Activity Section
// ============================================================================
/**
* Base activity section builder for single cohorts
* @param {Object} args
* @param {UtxoCohortObject | CohortWithoutRelative} args.cohort
* @param {(metric: string) => string} args.title
* @param {AnyFetchedSeriesBlueprint[]} [args.valueMetrics] - Optional additional value metrics
* @param {PartialOptionsTree} [args.soprTree] - Optional SOPR tree override
* @returns {PartialOptionsGroup}
*/
export function createActivitySection({
cohort,
title,
valueMetrics = [],
soprTree,
}) {
const { tree, color } = cohort;
return {
name: "Activity",
tree: [
{
name: "Volume",
tree: [
{
name: "Sum",
title: title("Sent Volume"),
bottom: [
line({
metric: tree.activity.sent14dEma.sats,
name: "14d EMA",
color: colors.ma._14d,
unit: Unit.sats,
defaultActive: false,
}),
line({
metric: tree.activity.sent14dEma.bitcoin,
name: "14d EMA",
color: colors.ma._14d,
unit: Unit.btc,
defaultActive: false,
}),
line({
metric: tree.activity.sent14dEma.dollars,
name: "14d EMA",
color: colors.ma._14d,
unit: Unit.usd,
defaultActive: false,
}),
line({
metric: tree.activity.sent.sats.sum,
name: "sum",
color,
unit: Unit.sats,
}),
line({
metric: tree.activity.sent.bitcoin.sum,
name: "sum",
color,
unit: Unit.btc,
}),
line({
metric: tree.activity.sent.dollars.sum,
name: "sum",
color,
unit: Unit.usd,
}),
],
},
{
name: "Cumulative",
title: title("Sent Volume (Total)"),
bottom: [
line({
metric: tree.activity.sent.sats.cumulative,
name: "all-time",
color,
unit: Unit.sats,
}),
line({
metric: tree.activity.sent.bitcoin.cumulative,
name: "all-time",
color,
unit: Unit.btc,
}),
line({
metric: tree.activity.sent.dollars.cumulative,
name: "all-time",
color,
unit: Unit.usd,
}),
],
},
],
},
soprTree
? { name: "SOPR", tree: soprTree }
: {
name: "SOPR",
title: title("SOPR"),
bottom: createSingleSoprSeries(cohort),
},
{
name: "Sell Side Risk",
title: title("Sell Side Risk Ratio"),
bottom: createSingleSellSideRiskSeries(tree),
},
{
name: "Value",
tree: [
{
name: "Flows",
title: title("Profit & Capitulation Flows"),
bottom: createSingleCapitulationProfitFlowSeries(tree),
},
{
name: "Created & Destroyed",
title: title("Value Created & Destroyed"),
bottom: [
...createSingleValueCreatedDestroyedSeries(tree),
...valueMetrics,
],
},
{
name: "Breakdown",
tree: [
{
name: "Profit",
title: title("Profit Value Created & Destroyed"),
bottom: [
line({
metric: tree.realized.profitValueCreated,
name: "Created",
color: colors.profit,
unit: Unit.usd,
}),
line({
metric: tree.realized.profitValueDestroyed,
name: "Destroyed",
color: colors.loss,
unit: Unit.usd,
}),
],
},
{
name: "Loss",
title: title("Loss Value Created & Destroyed"),
bottom: [
line({
metric: tree.realized.lossValueCreated,
name: "Created",
color: colors.profit,
unit: Unit.usd,
}),
line({
metric: tree.realized.lossValueDestroyed,
name: "Destroyed",
color: colors.loss,
unit: Unit.usd,
}),
],
},
],
},
],
},
{
name: "Coins Destroyed",
tree: [
{
name: "Sum",
title: title("Coins Destroyed"),
bottom: [
line({
metric: tree.activity.coinblocksDestroyed.sum,
name: "Coinblocks",
color,
unit: Unit.coinblocks,
}),
line({
metric: tree.activity.coindaysDestroyed.sum,
name: "Coindays",
color,
unit: Unit.coindays,
}),
],
},
{
name: "Cumulative",
title: title("Cumulative Coins Destroyed"),
bottom: [
line({
metric: tree.activity.coinblocksDestroyed.cumulative,
name: "Coinblocks",
color,
unit: Unit.coinblocks,
}),
line({
metric: tree.activity.coindaysDestroyed.cumulative,
name: "Coindays",
color,
unit: Unit.coindays,
}),
],
},
],
},
],
};
}
/**
* Activity section with adjusted values (for cohorts with RealizedPattern3/4)
* @param {{ cohort: CohortAll | CohortFull | CohortWithAdjusted, title: (metric: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createActivitySectionWithAdjusted({ cohort, title }) {
const { tree } = cohort;
return createActivitySection({
cohort,
title,
soprTree: createSingleSoprTreeWithAdjusted(cohort, title),
valueMetrics: [
line({
metric: tree.realized.adjustedValueCreated,
name: "Adjusted Created",
color: colors.adjustedCreated,
unit: Unit.usd,
defaultActive: false,
}),
line({
metric: tree.realized.adjustedValueDestroyed,
name: "Adjusted Destroyed",
color: colors.adjustedDestroyed,
unit: Unit.usd,
defaultActive: false,
}),
],
});
}
// ============================================================================
// Grouped Cohort Activity Section
// ============================================================================
/**
* Create grouped flows tree (Profit Flow, Capitulation Flow)
* @template {{ color: Color, name: string, tree: { realized: AnyRealizedPattern } }} T
* @param {readonly T[]} list
* @param {(metric: string) => string} title
* @returns {PartialOptionsTree}
*/
function groupedFlowsTree(list, title) {
return [
{
name: "Profit",
title: title("Profit Flow"),
bottom: list.map(({ color, name, tree }) =>
line({
metric: tree.realized.profitFlow,
name,
color,
unit: Unit.usd,
}),
),
},
{
name: "Capitulation",
title: title("Capitulation Flow"),
bottom: list.map(({ color, name, tree }) =>
line({
metric: tree.realized.capitulationFlow,
name,
color,
unit: Unit.usd,
}),
),
},
];
}
/**
* Create grouped value tree (Flows, Created, Destroyed, Breakdown)
* @template {{ color: Color, name: string, tree: { realized: AnyRealizedPattern } }} T
* @param {readonly T[]} list
* @param {(metric: string) => string} title
* @returns {PartialOptionsTree}
*/
function createGroupedValueTree(list, title) {
return [
{ name: "Flows", tree: groupedFlowsTree(list, title) },
{
name: "Created",
title: title("Value Created"),
bottom: list.map(({ color, name, tree }) =>
line({
metric: tree.realized.valueCreated,
name,
color,
unit: Unit.usd,
}),
),
},
{
name: "Destroyed",
title: title("Value Destroyed"),
bottom: list.map(({ color, name, tree }) =>
line({
metric: tree.realized.valueDestroyed,
name,
color,
unit: Unit.usd,
}),
),
},
{ name: "Breakdown", tree: valueBreakdownTree(list, title) },
];
}
/**
* Generic grouped activity section builder
* @template {readonly (UtxoCohortObject | CohortWithoutRelative)[]} T
* @param {Object} args
* @param {T} args.list
* @param {(metric: string) => string} args.title
* @param {PartialOptionsTree} [args.soprTree] - Optional SOPR tree override
* @param {PartialOptionsTree} [args.valueTree] - Optional value tree (defaults to basic created/destroyed)
* @returns {PartialOptionsGroup}
*/
export function createGroupedActivitySection({
list,
title,
soprTree,
valueTree,
}) {
return {
name: "Activity",
tree: [
{
name: "Volume",
tree: [
{
name: "14d EMA",
title: title("Sent Volume 14d EMA"),
bottom: list.flatMap(({ color, name, tree }) =>
satsBtcUsd({ pattern: tree.activity.sent14dEma, name, color }),
),
},
{
name: "Sum",
title: title("Sent Volume"),
bottom: list.flatMap(({ color, name, tree }) =>
satsBtcUsd({
pattern: {
sats: tree.activity.sent.sats.sum,
bitcoin: tree.activity.sent.bitcoin.sum,
dollars: tree.activity.sent.dollars.sum,
},
name,
color,
}),
),
},
],
},
{
name: "SOPR",
tree: soprTree ?? createGroupedSoprTree(list, title),
},
{
name: "Sell Side Risk",
title: title("Sell Side Risk Ratio"),
bottom: createGroupedSellSideRiskSeries(list),
},
{
name: "Value",
tree: valueTree ?? createGroupedValueTree(list, title),
},
{ name: "Coins Destroyed", tree: coinsDestroyedTree(list, title) },
],
};
}
/**
* Create grouped value tree with adjusted values (Flows, Normal, Adjusted, Breakdown)
* @param {readonly (CohortAll | CohortFull | CohortWithAdjusted)[]} list
* @param {(metric: string) => string} title
* @returns {PartialOptionsTree}
*/
function createGroupedValueTreeWithAdjusted(list, title) {
return [
{ name: "Flows", tree: groupedFlowsTree(list, title) },
{
name: "Normal",
tree: [
{
name: "Created",
title: title("Value Created"),
bottom: list.map(({ color, name, tree }) =>
line({
metric: tree.realized.valueCreated,
name,
color,
unit: Unit.usd,
}),
),
},
{
name: "Destroyed",
title: title("Value Destroyed"),
bottom: list.map(({ color, name, tree }) =>
line({
metric: tree.realized.valueDestroyed,
name,
color,
unit: Unit.usd,
}),
),
},
],
},
{
name: "Adjusted",
tree: [
{
name: "Created",
title: title("Adjusted Value Created"),
bottom: list.map(({ color, name, tree }) =>
line({
metric: tree.realized.adjustedValueCreated,
name,
color,
unit: Unit.usd,
}),
),
},
{
name: "Destroyed",
title: title("Adjusted Value Destroyed"),
bottom: list.map(({ color, name, tree }) =>
line({
metric: tree.realized.adjustedValueDestroyed,
name,
color,
unit: Unit.usd,
}),
),
},
],
},
{ name: "Breakdown", tree: valueBreakdownTree(list, title) },
];
}
/**
* Grouped activity section with adjusted values (for cohorts with RealizedPattern3/4)
* @param {{ list: readonly (CohortAll | CohortFull | CohortWithAdjusted)[], title: (metric: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createGroupedActivitySectionWithAdjusted({ list, title }) {
return createGroupedActivitySection({
list,
title,
soprTree: createGroupedSoprTreeWithAdjusted(list, title),
valueTree: createGroupedValueTreeWithAdjusted(list, title),
});
}

View File

@@ -291,13 +291,6 @@ function createRealizedPnlSection(args, title) {
unit: Unit.usd,
defaultActive: false,
}),
line({
metric: realized.totalRealizedPnl,
name: "Total",
color: colors.default,
unit: Unit.usd,
defaultActive: false,
}),
baseline({
metric: realized.realizedProfitRelToRealizedCap.sum,
name: "Profit",
@@ -324,6 +317,13 @@ function createRealizedPnlSection(args, title) {
unit: Unit.pctRcap,
defaultActive: false,
}),
line({
metric: realized.totalRealizedPnl,
name: "Total",
color: colors.default,
unit: Unit.usd,
defaultActive: false,
}),
],
},
{
@@ -450,6 +450,24 @@ function createRealizedPnlSection(args, title) {
{
name: "Sent In P/L",
tree: [
{
name: "In Profit 14d EMA",
title: title("Sent In Profit 14d EMA"),
bottom: satsBtcUsd({
pattern: realized.sentInProfit14dEma,
name: "14d EMA",
color: colors.profit,
}),
},
{
name: "In Loss 14d EMA",
title: title("Sent In Loss 14d EMA"),
bottom: satsBtcUsd({
pattern: realized.sentInLoss14dEma,
name: "14d EMA",
color: colors.loss,
}),
},
{
name: "In Profit",
title: title("Sent In Profit"),
@@ -540,24 +558,6 @@ function createRealizedPnlSection(args, title) {
}),
],
},
{
name: "In Profit 14d EMA",
title: title("Sent In Profit 14d EMA"),
bottom: satsBtcUsd({
pattern: realized.sentInProfit14dEma,
name: "14d EMA",
color: colors.profit,
}),
},
{
name: "In Loss 14d EMA",
title: title("Sent In Loss 14d EMA"),
bottom: satsBtcUsd({
pattern: realized.sentInLoss14dEma,
name: "14d EMA",
color: colors.loss,
}),
},
],
},
];
@@ -678,6 +678,28 @@ function createGroupedRealizedPnlSection(list, title) {
{
name: "Sent In P/L",
tree: [
{
name: "In Profit 14d EMA",
title: title("Sent In Profit 14d EMA"),
bottom: list.flatMap(({ color, name, tree }) =>
satsBtcUsd({
pattern: tree.realized.sentInProfit14dEma,
name,
color,
}),
),
},
{
name: "In Loss 14d EMA",
title: title("Sent In Loss 14d EMA"),
bottom: list.flatMap(({ color, name, tree }) =>
satsBtcUsd({
pattern: tree.realized.sentInLoss14dEma,
name,
color,
}),
),
},
{
name: "In Profit",
title: title("Sent In Profit"),
@@ -774,28 +796,6 @@ function createGroupedRealizedPnlSection(list, title) {
}),
]),
},
{
name: "In Profit 14d EMA",
title: title("Sent In Profit 14d EMA"),
bottom: list.flatMap(({ color, name, tree }) =>
satsBtcUsd({
pattern: tree.realized.sentInProfit14dEma,
name,
color,
}),
),
},
{
name: "In Loss 14d EMA",
title: title("Sent In Loss 14d EMA"),
bottom: list.flatMap(({ color, name, tree }) =>
satsBtcUsd({
pattern: tree.realized.sentInLoss14dEma,
name,
color,
}),
),
},
],
},
];
@@ -811,7 +811,7 @@ function createGroupedRealizedPnlSection(list, title) {
function createUnrealizedSection(list, useGroupName, title) {
return [
{
name: "Unrealized",
name: "Profitability",
tree: [
{
name: "Profit",
@@ -962,7 +962,7 @@ function createUnrealizedSection(list, useGroupName, title) {
metric: tree.relative.investedCapitalInProfitPct,
name: useGroupName ? name : "In Profit",
color: useGroupName ? color : colors.profit,
unit: Unit.pctRcap,
unit: Unit.pctOwnRcap,
}),
]),
},
@@ -974,7 +974,7 @@ function createUnrealizedSection(list, useGroupName, title) {
metric: tree.relative.investedCapitalInLossPct,
name: useGroupName ? name : "In Loss",
color: useGroupName ? color : colors.loss,
unit: Unit.pctRcap,
unit: Unit.pctOwnRcap,
}),
]),
},
@@ -1104,6 +1104,13 @@ function createActivitySection(args, title) {
{
name: "Sent",
tree: [
{
name: "14d EMA",
title: title("Sent 14d EMA"),
bottom: list.flatMap(({ color, name, tree }) =>
satsBtcUsd({ pattern: tree.activity.sent14dEma, name, color }),
),
},
{
name: "Sum",
title: title("Sent"),
@@ -1119,13 +1126,6 @@ function createActivitySection(args, title) {
}),
),
},
{
name: "14d EMA",
title: title("Sent 14d EMA"),
bottom: list.flatMap(({ color, name, tree }) =>
satsBtcUsd({ pattern: tree.activity.sent14dEma, name, color }),
),
},
],
},
],

View File

@@ -123,13 +123,17 @@ export function buildCohortData() {
// Addresses above amount
const addressesAboveAmount = entries(addressCohorts.geAmount).map(
([key, tree]) => {
([key, cohort]) => {
const names = GE_AMOUNT_NAMES[key];
return {
name: names.short,
title: `Addresses ${names.long}`,
color: colors.amount[key],
tree,
tree: cohort,
addrCount: {
count: cohort.addrCount,
_30dChange: cohort.addrCount30dChange,
},
};
},
);
@@ -147,13 +151,17 @@ export function buildCohortData() {
// Addresses under amount
const addressesUnderAmount = entries(addressCohorts.ltAmount).map(
([key, tree]) => {
([key, cohort]) => {
const names = LT_AMOUNT_NAMES[key];
return {
name: names.short,
title: `Addresses ${names.long}`,
color: colors.amount[key],
tree,
tree: cohort,
addrCount: {
count: cohort.addrCount,
_30dChange: cohort.addrCount30dChange,
},
};
},
);
@@ -173,13 +181,17 @@ export function buildCohortData() {
// Addresses amount ranges
const addressesAmountRanges = entries(addressCohorts.amountRange).map(
([key, tree]) => {
([key, cohort]) => {
const names = AMOUNT_RANGE_NAMES[key];
return {
name: names.short,
title: `Addresses ${names.long}`,
color: colors.amountRange[key],
tree,
tree: cohort,
addrCount: {
count: cohort.addrCount,
_30dChange: cohort.addrCount30dChange,
},
};
},
);

View File

@@ -16,14 +16,89 @@
import { Unit } from "../../utils/units.js";
import { line, baseline } from "../series.js";
import { satsBtcUsd, satsBtcUsdBaseline } from "../shared.js";
import { colors } from "../../utils/colors.js";
import { priceLines } from "../constants.js";
/**
* @param {UtxoCohortObject | CohortWithoutRelative} cohort
* @returns {AnyFetchedSeriesBlueprint[]}
*/
function createSingleSupplySeries(cohort) {
const { color, tree } = cohort;
return [...satsBtcUsd({ pattern: tree.supply.total, name: "Supply", color })];
const { tree } = cohort;
return [
...satsBtcUsd({
pattern: tree.supply.total,
name: "Total",
color: colors.default,
}),
...satsBtcUsd({
pattern: tree.unrealized.supplyInProfit,
name: "In Profit",
color: colors.profit,
}),
...satsBtcUsd({
pattern: tree.unrealized.supplyInLoss,
name: "In Loss",
color: colors.loss,
}),
// Halved supply (sparse line)
...satsBtcUsd({
pattern: tree.supply.halved,
name: "Halved",
color: colors.gray,
style: 4,
}),
];
}
/**
* Supply series for CohortAll (has % of Own Supply but not % of Circulating)
* @param {CohortAll} cohort
* @returns {AnyFetchedSeriesBlueprint[]}
*/
function createSingleSupplySeriesAll(cohort) {
const { tree } = cohort;
return [
...satsBtcUsd({
pattern: tree.supply.total,
name: "Total",
color: colors.default,
}),
...satsBtcUsd({
pattern: tree.unrealized.supplyInProfit,
name: "In Profit",
color: colors.profit,
}),
...satsBtcUsd({
pattern: tree.unrealized.supplyInLoss,
name: "In Loss",
color: colors.loss,
}),
// Halved supply (sparse line)
...satsBtcUsd({
pattern: tree.supply.halved,
name: "Halved",
color: colors.gray,
style: 4,
}),
// % of Own Supply
line({
metric: tree.relative.supplyInProfitRelToOwnSupply,
name: "In Profit",
color: colors.profit,
unit: Unit.pctOwn,
}),
line({
metric: tree.relative.supplyInLossRelToOwnSupply,
name: "In Loss",
color: colors.loss,
unit: Unit.pctOwn,
}),
...priceLines({
numbers: [100, 50, 0],
unit: Unit.pctOwn,
}),
];
}
/**
@@ -31,7 +106,10 @@ function createSingleSupplySeries(cohort) {
* @returns {AnyFetchedSeriesBlueprint[]}
*/
function createSingle30dChangeSeries(cohort) {
return satsBtcUsdBaseline({ pattern: cohort.tree.supply._30dChange, name: "30d Change" });
return satsBtcUsdBaseline({
pattern: cohort.tree.supply._30dChange,
name: "30d Change",
});
}
/**
@@ -79,18 +157,121 @@ function createSingleAddrCount30dChangeSeries(cohort) {
}
/**
* Create supply series with % of Circulating (for cohorts with relative data)
* @param {CohortFull | CohortWithAdjusted | CohortBasicWithMarketCap | CohortMinAge} cohort
* @returns {AnyFetchedSeriesBlueprint[]}
*/
function createSingleRelativeSeries(cohort) {
const { color, tree } = cohort;
function createSingleSupplySeriesWithRelative(cohort) {
const { tree } = cohort;
return [
...satsBtcUsd({
pattern: tree.supply.total,
name: "Total",
color: colors.default,
}),
...satsBtcUsd({
pattern: tree.unrealized.supplyInProfit,
name: "In Profit",
color: colors.profit,
}),
...satsBtcUsd({
pattern: tree.unrealized.supplyInLoss,
name: "In Loss",
color: colors.loss,
}),
// Halved supply (sparse line)
...satsBtcUsd({
pattern: tree.supply.halved,
name: "Halved",
color: colors.gray,
style: 4,
}),
// % of Circulating Supply
line({
metric: tree.relative.supplyRelToCirculatingSupply,
name: "% of Circulating",
color,
name: "Total",
color: colors.default,
unit: Unit.pctSupply,
}),
line({
metric: tree.relative.supplyInProfitRelToCirculatingSupply,
name: "In Profit",
color: colors.profit,
unit: Unit.pctSupply,
}),
line({
metric: tree.relative.supplyInLossRelToCirculatingSupply,
name: "In Loss",
color: colors.loss,
unit: Unit.pctSupply,
}),
// % of Own Supply
line({
metric: tree.relative.supplyInProfitRelToOwnSupply,
name: "In Profit",
color: colors.profit,
unit: Unit.pctOwn,
}),
line({
metric: tree.relative.supplyInLossRelToOwnSupply,
name: "In Loss",
color: colors.loss,
unit: Unit.pctOwn,
}),
...priceLines({
numbers: [100, 50, 0],
unit: Unit.pctOwn,
}),
];
}
/**
* Supply series with % of Own Supply only (for cohorts without % of Circulating)
* @param {CohortAgeRange | CohortBasicWithoutMarketCap} cohort
* @returns {AnyFetchedSeriesBlueprint[]}
*/
function createSingleSupplySeriesWithOwnSupply(cohort) {
const { tree } = cohort;
return [
...satsBtcUsd({
pattern: tree.unrealized.supplyInProfit,
name: "In Profit",
color: colors.profit,
}),
...satsBtcUsd({
pattern: tree.unrealized.supplyInLoss,
name: "In Loss",
color: colors.loss,
}),
...satsBtcUsd({
pattern: tree.supply.total,
name: "Total",
color: colors.default,
}),
// Halved supply (sparse line)
...satsBtcUsd({
pattern: tree.supply.halved,
name: "Halved",
color: colors.gray,
style: 4,
}),
// % of Own Supply
line({
metric: tree.relative.supplyInProfitRelToOwnSupply,
name: "In Profit",
color: colors.profit,
unit: Unit.pctOwn,
}),
line({
metric: tree.relative.supplyInLossRelToOwnSupply,
name: "In Loss",
color: colors.loss,
unit: Unit.pctOwn,
}),
...priceLines({
numbers: [100, 50, 0],
unit: Unit.pctOwn,
}),
];
}
@@ -132,17 +313,18 @@ export function createHoldingsSection({ cohort, title }) {
}
/**
* @param {{ cohort: CohortFull | CohortWithAdjusted | CohortBasicWithMarketCap | CohortMinAge, title: (metric: string) => string }} args
* Holdings section with % of Own Supply only (for cohorts without % of Circulating)
* @param {{ cohort: CohortAgeRange | CohortBasicWithoutMarketCap, title: (metric: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createHoldingsSectionWithRelative({ cohort, title }) {
export function createHoldingsSectionWithOwnSupply({ cohort, title }) {
return {
name: "Holdings",
tree: [
{
name: "Supply",
title: title("Supply"),
bottom: createSingleSupplySeries(cohort),
bottom: createSingleSupplySeriesWithOwnSupply(cohort),
},
{
name: "UTXO Count",
@@ -164,10 +346,42 @@ export function createHoldingsSectionWithRelative({ cohort, title }) {
},
],
},
],
};
}
/**
* @param {{ cohort: CohortFull | CohortWithAdjusted | CohortBasicWithMarketCap | CohortMinAge, title: (metric: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createHoldingsSectionWithRelative({ cohort, title }) {
return {
name: "Holdings",
tree: [
{
name: "Relative",
title: title("Relative to Circulating Supply"),
bottom: createSingleRelativeSeries(cohort),
name: "Supply",
title: title("Supply"),
bottom: createSingleSupplySeriesWithRelative(cohort),
},
{
name: "UTXO Count",
title: title("UTXO Count"),
bottom: createSingleUtxoCountSeries(cohort),
},
{
name: "30d Changes",
tree: [
{
name: "Supply",
title: title("Supply 30d Change"),
bottom: createSingle30dChangeSeries(cohort),
},
{
name: "UTXO Count",
title: title("UTXO Count 30d Change"),
bottom: createSingleUtxoCount30dChangeSeries(cohort),
},
],
},
],
};
@@ -184,7 +398,7 @@ export function createHoldingsSectionAll({ cohort, title }) {
{
name: "Supply",
title: title("Supply"),
bottom: createSingleSupplySeries(cohort),
bottom: createSingleSupplySeriesAll(cohort),
},
{
name: "UTXO Count",
@@ -238,7 +452,62 @@ export function createHoldingsSectionAddress({ cohort, title }) {
{
name: "Supply",
title: title("Supply"),
bottom: createSingleSupplySeries(cohort),
bottom: createSingleSupplySeriesWithOwnSupply(cohort),
},
{
name: "UTXO Count",
title: title("UTXO Count"),
bottom: createSingleUtxoCountSeries(cohort),
},
{
name: "Address Count",
title: title("Address Count"),
bottom: [
line({
metric: cohort.addrCount.count,
name: "Address Count",
color: cohort.color,
unit: Unit.count,
}),
],
},
{
name: "30d Changes",
tree: [
{
name: "Supply",
title: title("Supply 30d Change"),
bottom: createSingle30dChangeSeries(cohort),
},
{
name: "UTXO Count",
title: title("UTXO Count 30d Change"),
bottom: createSingleUtxoCount30dChangeSeries(cohort),
},
{
name: "Address Count",
title: title("Address Count 30d Change"),
bottom: createSingleAddrCount30dChangeSeries(cohort),
},
],
},
],
};
}
/**
* Holdings section for address amount cohorts (has relative supply + address count)
* @param {{ cohort: AddressCohortObject, title: (metric: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createHoldingsSectionAddressAmount({ cohort, title }) {
return {
name: "Holdings",
tree: [
{
name: "Supply",
title: title("Supply"),
bottom: createSingleSupplySeriesWithRelative(cohort),
},
{
name: "UTXO Count",
@@ -291,10 +560,67 @@ export function createGroupedHoldingsSectionAddress({ list, title }) {
tree: [
{
name: "Supply",
title: title("Supply"),
bottom: list.flatMap(({ name, color, tree }) =>
satsBtcUsd({ pattern: tree.supply.total, name, color }),
),
tree: [
{
name: "Total",
title: title("Supply"),
bottom: list.flatMap(({ name, color, tree }) =>
satsBtcUsd({ pattern: tree.supply.total, name, color }),
),
},
{
name: "In Profit",
title: title("Supply In Profit"),
bottom: [
...list.flatMap(({ name, color, tree }) =>
satsBtcUsd({
pattern: tree.unrealized.supplyInProfit,
name,
color,
}),
),
// % of Own Supply
...list.map(({ name, color, tree }) =>
line({
metric: tree.relative.supplyInProfitRelToOwnSupply,
name,
color,
unit: Unit.pctOwn,
}),
),
...priceLines({
numbers: [100, 50, 0],
unit: Unit.pctOwn,
}),
],
},
{
name: "In Loss",
title: title("Supply In Loss"),
bottom: [
...list.flatMap(({ name, color, tree }) =>
satsBtcUsd({
pattern: tree.unrealized.supplyInLoss,
name,
color,
}),
),
// % of Own Supply
...list.map(({ name, color, tree }) =>
line({
metric: tree.relative.supplyInLossRelToOwnSupply,
name,
color,
unit: Unit.pctOwn,
}),
),
...priceLines({
numbers: [100, 50, 0],
unit: Unit.pctOwn,
}),
],
},
],
},
{
name: "UTXO Count",
@@ -322,7 +648,176 @@ export function createGroupedHoldingsSectionAddress({ list, title }) {
name: "Supply",
title: title("Supply 30d Change"),
bottom: list.flatMap(({ name, color, tree }) =>
satsBtcUsdBaseline({ pattern: tree.supply._30dChange, name, color }),
satsBtcUsdBaseline({
pattern: tree.supply._30dChange,
name,
color,
}),
),
},
{
name: "UTXO Count",
title: title("UTXO Count 30d Change"),
bottom: list.map(({ name, color, tree }) =>
baseline({
metric: tree.outputs.utxoCount30dChange,
name,
unit: Unit.count,
color,
}),
),
},
{
name: "Address Count",
title: title("Address Count 30d Change"),
bottom: list.map(({ name, color, addrCount }) =>
baseline({
metric: addrCount._30dChange,
name,
unit: Unit.count,
color,
}),
),
},
],
},
],
};
}
/**
* Grouped holdings section for address amount cohorts (has relative supply + address count)
* @param {{ list: readonly AddressCohortObject[], title: (metric: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createGroupedHoldingsSectionAddressAmount({ list, title }) {
return {
name: "Holdings",
tree: [
{
name: "Supply",
tree: [
{
name: "Total",
title: title("Supply"),
bottom: [
...list.flatMap(({ name, color, tree }) =>
satsBtcUsd({ pattern: tree.supply.total, name, color }),
),
// % of Circulating
...list.map(({ name, color, tree }) =>
line({
metric: tree.relative.supplyRelToCirculatingSupply,
name,
color,
unit: Unit.pctSupply,
}),
),
],
},
{
name: "In Profit",
title: title("Supply In Profit"),
bottom: [
...list.flatMap(({ name, color, tree }) =>
satsBtcUsd({
pattern: tree.unrealized.supplyInProfit,
name,
color,
}),
),
// % of Circulating
...list.map(({ name, color, tree }) =>
line({
metric: tree.relative.supplyInProfitRelToCirculatingSupply,
name,
color,
unit: Unit.pctSupply,
}),
),
// % of Own Supply
...list.map(({ name, color, tree }) =>
line({
metric: tree.relative.supplyInProfitRelToOwnSupply,
name,
color,
unit: Unit.pctOwn,
}),
),
...priceLines({
numbers: [100, 50, 0],
unit: Unit.pctOwn,
}),
],
},
{
name: "In Loss",
title: title("Supply In Loss"),
bottom: [
...list.flatMap(({ name, color, tree }) =>
satsBtcUsd({
pattern: tree.unrealized.supplyInLoss,
name,
color,
}),
),
// % of Circulating
...list.map(({ name, color, tree }) =>
line({
metric: tree.relative.supplyInLossRelToCirculatingSupply,
name,
color,
unit: Unit.pctSupply,
}),
),
// % of Own Supply
...list.map(({ name, color, tree }) =>
line({
metric: tree.relative.supplyInLossRelToOwnSupply,
name,
color,
unit: Unit.pctOwn,
}),
),
...priceLines({
numbers: [100, 50, 0],
unit: Unit.pctOwn,
}),
],
},
],
},
{
name: "UTXO Count",
title: title("UTXO Count"),
bottom: list.map(({ name, color, tree }) =>
line({
metric: tree.outputs.utxoCount,
name,
color,
unit: Unit.count,
}),
),
},
{
name: "Address Count",
title: title("Address Count"),
bottom: list.map(({ name, color, addrCount }) =>
line({ metric: addrCount.count, name, color, unit: Unit.count }),
),
},
{
name: "30d Changes",
tree: [
{
name: "Supply",
title: title("Supply 30d Change"),
bottom: list.flatMap(({ name, color, tree }) =>
satsBtcUsdBaseline({
pattern: tree.supply._30dChange,
name,
color,
}),
),
},
{
@@ -366,10 +861,37 @@ export function createGroupedHoldingsSection({ list, title }) {
tree: [
{
name: "Supply",
title: title("Supply"),
bottom: list.flatMap(({ name, color, tree }) =>
satsBtcUsd({ pattern: tree.supply.total, name, color }),
),
tree: [
{
name: "Total",
title: title("Supply"),
bottom: list.flatMap(({ name, color, tree }) =>
satsBtcUsd({ pattern: tree.supply.total, name, color }),
),
},
{
name: "In Profit",
title: title("Supply In Profit"),
bottom: list.flatMap(({ name, color, tree }) =>
satsBtcUsd({
pattern: tree.unrealized.supplyInProfit,
name,
color,
}),
),
},
{
name: "In Loss",
title: title("Supply In Loss"),
bottom: list.flatMap(({ name, color, tree }) =>
satsBtcUsd({
pattern: tree.unrealized.supplyInLoss,
name,
color,
}),
),
},
],
},
{
name: "UTXO Count",
@@ -390,7 +912,128 @@ export function createGroupedHoldingsSection({ list, title }) {
name: "Supply",
title: title("Supply 30d Change"),
bottom: list.flatMap(({ name, color, tree }) =>
satsBtcUsdBaseline({ pattern: tree.supply._30dChange, name, color }),
satsBtcUsdBaseline({
pattern: tree.supply._30dChange,
name,
color,
}),
),
},
{
name: "UTXO Count",
title: title("UTXO Count 30d Change"),
bottom: list.map(({ name, color, tree }) =>
baseline({
metric: tree.outputs.utxoCount30dChange,
name,
unit: Unit.count,
color,
}),
),
},
],
},
],
};
}
/**
* Grouped holdings section with % of Own Supply only (for cohorts without % of Circulating)
* @param {{ list: readonly (CohortAgeRange | CohortBasicWithoutMarketCap)[], title: (metric: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createGroupedHoldingsSectionWithOwnSupply({ list, title }) {
return {
name: "Holdings",
tree: [
{
name: "Supply",
tree: [
{
name: "In Profit",
title: title("Supply In Profit"),
bottom: [
...list.flatMap(({ name, color, tree }) =>
satsBtcUsd({
pattern: tree.unrealized.supplyInProfit,
name,
color,
}),
),
// % of Own Supply
...list.map(({ name, color, tree }) =>
line({
metric: tree.relative.supplyInProfitRelToOwnSupply,
name,
color,
unit: Unit.pctOwn,
}),
),
...priceLines({
numbers: [100, 50, 0],
unit: Unit.pctOwn,
}),
],
},
{
name: "In Loss",
title: title("Supply In Loss"),
bottom: [
...list.flatMap(({ name, color, tree }) =>
satsBtcUsd({
pattern: tree.unrealized.supplyInLoss,
name,
color,
}),
),
// % of Own Supply
...list.map(({ name, color, tree }) =>
line({
metric: tree.relative.supplyInLossRelToOwnSupply,
name,
color,
unit: Unit.pctOwn,
}),
),
...priceLines({
numbers: [100, 50, 0],
unit: Unit.pctOwn,
}),
],
},
{
name: "Total",
title: title("Supply"),
bottom: list.flatMap(({ name, color, tree }) =>
satsBtcUsd({ pattern: tree.supply.total, name, color }),
),
},
],
},
{
name: "UTXO Count",
title: title("UTXO Count"),
bottom: list.map(({ name, color, tree }) =>
line({
metric: tree.outputs.utxoCount,
name,
color,
unit: Unit.count,
}),
),
},
{
name: "30d Changes",
tree: [
{
name: "Supply",
title: title("Supply 30d Change"),
bottom: list.flatMap(({ name, color, tree }) =>
satsBtcUsdBaseline({
pattern: tree.supply._30dChange,
name,
color,
}),
),
},
{
@@ -421,10 +1064,96 @@ export function createGroupedHoldingsSectionWithRelative({ list, title }) {
tree: [
{
name: "Supply",
title: title("Supply"),
bottom: list.flatMap(({ name, color, tree }) =>
satsBtcUsd({ pattern: tree.supply.total, name, color }),
),
tree: [
{
name: "Total",
title: title("Supply"),
bottom: [
...list.flatMap(({ name, color, tree }) =>
satsBtcUsd({ pattern: tree.supply.total, name, color }),
),
// % of Circulating
...list.map(({ name, color, tree }) =>
line({
metric: tree.relative.supplyRelToCirculatingSupply,
name,
color,
unit: Unit.pctSupply,
}),
),
],
},
{
name: "In Profit",
title: title("Supply In Profit"),
bottom: [
...list.flatMap(({ name, color, tree }) =>
satsBtcUsd({
pattern: tree.unrealized.supplyInProfit,
name,
color,
}),
),
// % of Circulating
...list.map(({ name, color, tree }) =>
line({
metric: tree.relative.supplyInProfitRelToCirculatingSupply,
name,
color,
unit: Unit.pctSupply,
}),
),
// % of Own Supply
...list.map(({ name, color, tree }) =>
line({
metric: tree.relative.supplyInProfitRelToOwnSupply,
name,
color,
unit: Unit.pctOwn,
}),
),
...priceLines({
numbers: [100, 50, 0],
unit: Unit.pctOwn,
}),
],
},
{
name: "In Loss",
title: title("Supply In Loss"),
bottom: [
...list.flatMap(({ name, color, tree }) =>
satsBtcUsd({
pattern: tree.unrealized.supplyInLoss,
name,
color,
}),
),
// % of Circulating
...list.map(({ name, color, tree }) =>
line({
metric: tree.relative.supplyInLossRelToCirculatingSupply,
name,
color,
unit: Unit.pctSupply,
}),
),
// % of Own Supply
...list.map(({ name, color, tree }) =>
line({
metric: tree.relative.supplyInLossRelToOwnSupply,
name,
color,
unit: Unit.pctOwn,
}),
),
...priceLines({
numbers: [100, 50, 0],
unit: Unit.pctOwn,
}),
],
},
],
},
{
name: "UTXO Count",
@@ -445,7 +1174,11 @@ export function createGroupedHoldingsSectionWithRelative({ list, title }) {
name: "Supply",
title: title("Supply 30d Change"),
bottom: list.flatMap(({ name, color, tree }) =>
satsBtcUsdBaseline({ pattern: tree.supply._30dChange, name, color }),
satsBtcUsdBaseline({
pattern: tree.supply._30dChange,
name,
color,
}),
),
},
{
@@ -462,18 +1195,6 @@ export function createGroupedHoldingsSectionWithRelative({ list, title }) {
},
],
},
{
name: "Relative",
title: title("Relative to Circulating Supply"),
bottom: list.map(({ name, color, tree }) =>
line({
metric: tree.relative.supplyRelToCirculatingSupply,
name,
color,
unit: Unit.pctSupply,
}),
),
},
],
};
}

View File

@@ -1,26 +1,75 @@
/**
* 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
*/
// Cohort data builder
import { formatCohortTitle } from "../shared.js";
// Section builders
import {
createHoldingsSection,
createHoldingsSectionAll,
createHoldingsSectionAddress,
createHoldingsSectionAddressAmount,
createHoldingsSectionWithRelative,
createHoldingsSectionWithOwnSupply,
createGroupedHoldingsSection,
createGroupedHoldingsSectionAddress,
createGroupedHoldingsSectionAddressAmount,
createGroupedHoldingsSectionWithRelative,
createGroupedHoldingsSectionWithOwnSupply,
} from "./holdings.js";
import {
createValuationSection,
createValuationSectionFull,
createGroupedValuationSection,
createGroupedValuationSectionWithOwnMarketCap,
} from "./valuation.js";
import {
createPricesSectionFull,
createPricesSectionBasic,
createGroupedPricesSection,
} from "./prices.js";
import {
createCostBasisSection,
createCostBasisSectionWithPercentiles,
createGroupedCostBasisSection,
createGroupedCostBasisSectionWithPercentiles,
} from "./cost-basis.js";
import {
createProfitabilitySection,
createProfitabilitySectionAll,
createProfitabilitySectionFull,
createProfitabilitySectionWithNupl,
createProfitabilitySectionWithPeakRegret,
createProfitabilitySectionWithInvestedCapitalPct,
createProfitabilitySectionBasicWithInvestedCapitalPct,
createProfitabilitySectionLongTerm,
createGroupedProfitabilitySection,
createGroupedProfitabilitySectionWithNupl,
createGroupedProfitabilitySectionWithPeakRegret,
createGroupedProfitabilitySectionWithInvestedCapitalPct,
createGroupedProfitabilitySectionBasicWithInvestedCapitalPct,
createGroupedProfitabilitySectionLongTerm,
} from "./profitability.js";
import {
createActivitySection,
createActivitySectionWithAdjusted,
createGroupedActivitySection,
createGroupedActivitySectionWithAdjusted,
} from "./activity.js";
// Re-export data builder
export { buildCohortData } from "./data.js";
// Cohort folder builders (type-safe!)
export {
createCohortFolderAll,
createCohortFolderFull,
createCohortFolderWithAdjusted,
createCohortFolderWithNupl,
createCohortFolderAgeRange,
createCohortFolderMinAge,
createCohortFolderBasicWithMarketCap,
createCohortFolderBasicWithoutMarketCap,
createCohortFolderWithoutRelative,
createCohortFolderAddress,
} from "./utxo.js";
export { createAddressCohortFolder } from "./address.js";
// Shared helpers
// Re-export shared helpers
export {
createSingleSupplySeries,
createGroupedSupplyTotalSeries,
@@ -33,3 +82,426 @@ export {
createRealizedCapSeries,
createCostBasisPercentilesSeries,
} from "./shared.js";
// ============================================================================
// Folder Builders
// ============================================================================
/**
* All folder: for the special "All" cohort (adjustedSopr + percentiles + RelToMarketCap)
* @param {CohortAll} args
* @returns {PartialOptionsGroup}
*/
export function createCohortFolderAll(args) {
const title = formatCohortTitle(args.name);
return {
name: args.name || "all",
tree: [
createHoldingsSectionAll({ cohort: args, title }),
createValuationSectionFull({ cohort: args, title }),
createPricesSectionFull({ cohort: args, title }),
createCostBasisSectionWithPercentiles({ cohort: args, title }),
createProfitabilitySectionAll({ cohort: args, title }),
createActivitySectionWithAdjusted({ cohort: args, title }),
],
};
}
/**
* Full folder: adjustedSopr + percentiles + RelToMarketCap (term.short only)
* @param {CohortFull | CohortGroupFull} args
* @returns {PartialOptionsGroup}
*/
export function createCohortFolderFull(args) {
if ("list" in args) {
const { list } = args;
const title = formatCohortTitle(args.title);
return {
name: args.name || "all",
tree: [
createGroupedHoldingsSectionWithRelative({ list, title }),
createGroupedValuationSectionWithOwnMarketCap({ list, title }),
createGroupedPricesSection({ list, title }),
createGroupedCostBasisSectionWithPercentiles({ list, title }),
createGroupedProfitabilitySectionWithNupl({ list, title }),
createGroupedActivitySectionWithAdjusted({ list, title }),
],
};
}
const title = formatCohortTitle(args.name);
return {
name: args.name || "all",
tree: [
createHoldingsSectionWithRelative({ cohort: args, title }),
createValuationSectionFull({ cohort: args, title }),
createPricesSectionFull({ cohort: args, title }),
createCostBasisSectionWithPercentiles({ cohort: args, title }),
createProfitabilitySectionFull({ cohort: args, title }),
createActivitySectionWithAdjusted({ cohort: args, title }),
],
};
}
/**
* Adjusted folder: adjustedSopr only, no percentiles (maxAge.*)
* Has Peak Regret metrics like minAge
* @param {CohortWithAdjusted | CohortGroupWithAdjusted} args
* @returns {PartialOptionsGroup}
*/
export function createCohortFolderWithAdjusted(args) {
if ("list" in args) {
const { list } = args;
const title = formatCohortTitle(args.title);
return {
name: args.name || "all",
tree: [
createGroupedHoldingsSectionWithRelative({ list, title }),
createGroupedValuationSection({ list, title }),
createGroupedPricesSection({ list, title }),
createGroupedCostBasisSection({ list, title }),
createGroupedProfitabilitySectionWithPeakRegret({ list, title }),
createGroupedActivitySectionWithAdjusted({ list, title }),
],
};
}
const title = formatCohortTitle(args.name);
return {
name: args.name || "all",
tree: [
createHoldingsSectionWithRelative({ cohort: args, title }),
createValuationSection({ cohort: args, title }),
createPricesSectionBasic({ cohort: args, title }),
createCostBasisSection({ cohort: args, title }),
createProfitabilitySectionWithPeakRegret({ cohort: args, title }),
createActivitySectionWithAdjusted({ cohort: args, title }),
],
};
}
/**
* Folder for cohorts with nupl + percentiles (no longer used for term.long which has own folder)
* @param {CohortWithNuplPercentiles | CohortGroupWithNuplPercentiles} args
* @returns {PartialOptionsGroup}
*/
export function createCohortFolderWithNupl(args) {
if ("list" in args) {
const { list } = args;
const title = formatCohortTitle(args.title);
return {
name: args.name || "all",
tree: [
createGroupedHoldingsSectionWithRelative({ list, title }),
createGroupedValuationSection({ list, title }),
createGroupedPricesSection({ list, title }),
createGroupedCostBasisSectionWithPercentiles({ list, title }),
createGroupedProfitabilitySectionWithNupl({ list, title }),
createGroupedActivitySection({ list, title }),
],
};
}
const title = formatCohortTitle(args.name);
return {
name: args.name || "all",
tree: [
createHoldingsSectionWithRelative({ cohort: args, title }),
createValuationSectionFull({ cohort: args, title }),
createPricesSectionFull({ cohort: args, title }),
createCostBasisSectionWithPercentiles({ cohort: args, title }),
createProfitabilitySectionWithNupl({ cohort: args, title }),
createActivitySection({ cohort: args, title }),
],
};
}
/**
* LongTerm folder: term.long (has own market cap + NUPL + peak regret + P/L ratio)
* @param {CohortLongTerm | CohortGroupLongTerm} args
* @returns {PartialOptionsGroup}
*/
export function createCohortFolderLongTerm(args) {
if ("list" in args) {
const { list } = args;
const title = formatCohortTitle(args.title);
return {
name: args.name || "all",
tree: [
createGroupedHoldingsSectionWithRelative({ list, title }),
createGroupedValuationSectionWithOwnMarketCap({ list, title }),
createGroupedPricesSection({ list, title }),
createGroupedCostBasisSectionWithPercentiles({ list, title }),
createGroupedProfitabilitySectionLongTerm({ list, title }),
createGroupedActivitySection({ list, title }),
],
};
}
const title = formatCohortTitle(args.name);
return {
name: args.name || "all",
tree: [
createHoldingsSectionWithRelative({ cohort: args, title }),
createValuationSectionFull({ cohort: args, title }),
createPricesSectionFull({ cohort: args, title }),
createCostBasisSectionWithPercentiles({ cohort: args, title }),
createProfitabilitySectionLongTerm({ cohort: args, title }),
createActivitySection({ cohort: args, title }),
],
};
}
/**
* Age range folder: ageRange.* (no nupl via RelativePattern2)
* @param {CohortAgeRange | CohortGroupAgeRange} args
* @returns {PartialOptionsGroup}
*/
export function createCohortFolderAgeRange(args) {
if ("list" in args) {
const { list } = args;
const title = formatCohortTitle(args.title);
return {
name: args.name || "all",
tree: [
createGroupedHoldingsSectionWithOwnSupply({ list, title }),
createGroupedValuationSectionWithOwnMarketCap({ list, title }),
createGroupedPricesSection({ list, title }),
createGroupedCostBasisSectionWithPercentiles({ list, title }),
createGroupedProfitabilitySectionWithInvestedCapitalPct({ list, title }),
createGroupedActivitySection({ list, title }),
],
};
}
const title = formatCohortTitle(args.name);
return {
name: args.name || "all",
tree: [
createHoldingsSectionWithOwnSupply({ cohort: args, title }),
createValuationSectionFull({ cohort: args, title }),
createPricesSectionFull({ cohort: args, title }),
createCostBasisSectionWithPercentiles({ cohort: args, title }),
createProfitabilitySectionWithInvestedCapitalPct({ cohort: args, title }),
createActivitySection({ cohort: args, title }),
],
};
}
/**
* MinAge folder - has peakRegret in unrealized (minAge.*)
* @param {CohortMinAge | CohortGroupMinAge} args
* @returns {PartialOptionsGroup}
*/
export function createCohortFolderMinAge(args) {
if ("list" in args) {
const { list } = args;
const title = formatCohortTitle(args.title);
return {
name: args.name || "all",
tree: [
createGroupedHoldingsSectionWithRelative({ list, title }),
createGroupedValuationSection({ list, title }),
createGroupedPricesSection({ list, title }),
createGroupedCostBasisSection({ list, title }),
createGroupedProfitabilitySectionWithPeakRegret({ list, title }),
createGroupedActivitySection({ list, title }),
],
};
}
const title = formatCohortTitle(args.name);
return {
name: args.name || "all",
tree: [
createHoldingsSectionWithRelative({ cohort: args, title }),
createValuationSection({ cohort: args, title }),
createPricesSectionBasic({ cohort: args, title }),
createCostBasisSection({ cohort: args, title }),
createProfitabilitySectionWithPeakRegret({ cohort: args, title }),
createActivitySection({ cohort: args, title }),
],
};
}
/**
* Basic folder WITH RelToMarketCap (geAmount.*, ltAmount.*)
* @param {CohortBasicWithMarketCap | CohortGroupBasicWithMarketCap} args
* @returns {PartialOptionsGroup}
*/
export function createCohortFolderBasicWithMarketCap(args) {
if ("list" in args) {
const { list } = args;
const title = formatCohortTitle(args.title);
return {
name: args.name || "all",
tree: [
createGroupedHoldingsSectionWithRelative({ list, title }),
createGroupedValuationSection({ list, title }),
createGroupedPricesSection({ list, title }),
createGroupedCostBasisSection({ list, title }),
createGroupedProfitabilitySectionWithNupl({ list, title }),
createGroupedActivitySection({ list, title }),
],
};
}
const title = formatCohortTitle(args.name);
return {
name: args.name || "all",
tree: [
createHoldingsSectionWithRelative({ cohort: args, title }),
createValuationSection({ cohort: args, title }),
createPricesSectionBasic({ cohort: args, title }),
createCostBasisSection({ cohort: args, title }),
createProfitabilitySectionWithNupl({ cohort: args, title }),
createActivitySection({ cohort: args, title }),
],
};
}
/**
* Basic folder WITHOUT RelToMarketCap (epoch.*, amountRange.*, year.*)
* @param {CohortBasicWithoutMarketCap | CohortGroupBasicWithoutMarketCap} args
* @returns {PartialOptionsGroup}
*/
export function createCohortFolderBasicWithoutMarketCap(args) {
if ("list" in args) {
const { list } = args;
const title = formatCohortTitle(args.title);
return {
name: args.name || "all",
tree: [
createGroupedHoldingsSectionWithOwnSupply({ list, title }),
createGroupedValuationSection({ list, title }),
createGroupedPricesSection({ list, title }),
createGroupedCostBasisSection({ list, title }),
createGroupedProfitabilitySectionBasicWithInvestedCapitalPct({
list,
title,
}),
createGroupedActivitySection({ list, title }),
],
};
}
const title = formatCohortTitle(args.name);
return {
name: args.name || "all",
tree: [
createHoldingsSectionWithOwnSupply({ cohort: args, title }),
createValuationSection({ cohort: args, title }),
createPricesSectionBasic({ cohort: args, title }),
createCostBasisSection({ cohort: args, title }),
createProfitabilitySectionBasicWithInvestedCapitalPct({
cohort: args,
title,
}),
createActivitySection({ cohort: args, title }),
],
};
}
/**
* Address folder: like basic but with address count (addressable type cohorts)
* Has invested capital percentage metrics
* @param {CohortAddress | CohortGroupAddress} args
* @returns {PartialOptionsGroup}
*/
export function createCohortFolderAddress(args) {
if ("list" in args) {
const { list } = args;
const title = formatCohortTitle(args.title);
return {
name: args.name || "all",
tree: [
createGroupedHoldingsSectionAddress({ list, title }),
createGroupedValuationSection({ list, title }),
createGroupedPricesSection({ list, title }),
createGroupedCostBasisSection({ list, title }),
createGroupedProfitabilitySectionBasicWithInvestedCapitalPct({
list,
title,
}),
createGroupedActivitySection({ list, title }),
],
};
}
const title = formatCohortTitle(args.name);
return {
name: args.name || "all",
tree: [
createHoldingsSectionAddress({ cohort: args, title }),
createValuationSection({ cohort: args, title }),
createPricesSectionBasic({ cohort: args, title }),
createCostBasisSection({ cohort: args, title }),
createProfitabilitySectionBasicWithInvestedCapitalPct({
cohort: args,
title,
}),
createActivitySection({ cohort: args, title }),
],
};
}
/**
* Folder for cohorts WITHOUT relative section (edge case types: empty, p2ms, unknown)
* @param {CohortWithoutRelative | CohortGroupWithoutRelative} args
* @returns {PartialOptionsGroup}
*/
export function createCohortFolderWithoutRelative(args) {
if ("list" in args) {
const { list } = args;
const title = formatCohortTitle(args.title);
return {
name: args.name || "all",
tree: [
createGroupedHoldingsSection({ list, title }),
createGroupedValuationSection({ list, title }),
createGroupedPricesSection({ list, title }),
createGroupedCostBasisSection({ list, title }),
createGroupedProfitabilitySection({ list, title }),
createGroupedActivitySection({ list, title }),
],
};
}
const title = formatCohortTitle(args.name);
return {
name: args.name || "all",
tree: [
createHoldingsSection({ cohort: args, title }),
createValuationSection({ cohort: args, title }),
createPricesSectionBasic({ cohort: args, title }),
createCostBasisSection({ cohort: args, title }),
createProfitabilitySection({ cohort: args, title }),
createActivitySection({ cohort: args, title }),
],
};
}
/**
* Address amount cohort folder - for address balance cohorts (has NUPL + addrCount)
* @param {AddressCohortObject | AddressCohortGroupObject} args
* @returns {PartialOptionsGroup}
*/
export function createAddressCohortFolder(args) {
if ("list" in args) {
const { list } = args;
const title = formatCohortTitle(args.title);
return {
name: args.name || "all",
tree: [
createGroupedHoldingsSectionAddressAmount({ list, title }),
createGroupedValuationSection({ list, title }),
createGroupedPricesSection({ list, title }),
createGroupedCostBasisSection({ list, title }),
createGroupedProfitabilitySectionWithNupl({ list, title }),
createGroupedActivitySection({ list, title }),
],
};
}
const title = formatCohortTitle(args.name);
return {
name: args.name || "all",
tree: [
createHoldingsSectionAddressAmount({ cohort: args, title }),
createValuationSection({ cohort: args, title }),
createPricesSectionBasic({ cohort: args, title }),
createCostBasisSection({ cohort: args, title }),
createProfitabilitySectionWithNupl({ cohort: args, title }),
createActivitySection({ cohort: args, title }),
],
};
}

File diff suppressed because it is too large Load Diff

View File

@@ -341,13 +341,6 @@ export function createGroupedSupplySection(
return {
name: "Supply",
tree: [
{
name: "Total",
title: title("Supply"),
bottom: createGroupedSupplyTotalSeries(list, {
relativeMetrics: supplyRelativeMetrics,
}),
},
{
name: "30d Change",
title: title("Supply 30d Change"),
@@ -369,6 +362,13 @@ export function createGroupedSupplySection(
relativeMetrics: lossRelativeMetrics,
}),
},
{
name: "Total",
title: title("Supply"),
bottom: createGroupedSupplyTotalSeries(list, {
relativeMetrics: supplyRelativeMetrics,
}),
},
],
};
}
@@ -988,10 +988,10 @@ export function createSingleSentSeries(cohort) {
*/
export function createSingleSellSideRiskSeries(tree) {
return [
dots({
metric: tree.realized.sellSideRiskRatio,
name: "Raw",
color: colors.bitcoin,
line({
metric: tree.realized.sellSideRiskRatio30dEma,
name: "30d EMA",
color: colors.ma._1m,
unit: Unit.ratio,
}),
line({
@@ -1000,10 +1000,10 @@ export function createSingleSellSideRiskSeries(tree) {
color: colors.ma._1w,
unit: Unit.ratio,
}),
line({
metric: tree.realized.sellSideRiskRatio30dEma,
name: "30d EMA",
color: colors.ma._1m,
dots({
metric: tree.realized.sellSideRiskRatio,
name: "Raw",
color: colors.bitcoin,
unit: Unit.ratio,
}),
];
@@ -1130,7 +1130,7 @@ export function createSingleSoprSeries(tree) {
baseline({
metric: tree.realized.sopr7dEma,
name: "7d EMA",
color: colors.bi.sopr7d,
color: colors.bi.p2,
unit: Unit.ratio,
defaultActive: false,
base: 1,
@@ -1138,7 +1138,7 @@ export function createSingleSoprSeries(tree) {
baseline({
metric: tree.realized.sopr30dEma,
name: "30d EMA",
color: colors.bi.sopr30d,
color: colors.bi.p2,
unit: Unit.ratio,
defaultActive: false,
base: 1,
@@ -1340,12 +1340,14 @@ export function createSingleSentimentSeries(tree) {
name: "Greed Index",
color: colors.profit,
unit: Unit.usd,
defaultActive: false,
}),
line({
metric: tree.unrealized.painIndex,
name: "Pain Index",
color: colors.loss,
unit: Unit.usd,
defaultActive: false,
}),
];
}

View File

@@ -24,13 +24,7 @@
*/
import {
createSingleSellSideRiskSeries,
createGroupedSellSideRiskSeries,
createSingleValueCreatedDestroyedSeries,
createSingleValueFlowBreakdownSeries,
createSingleCapitulationProfitFlowSeries,
createSingleSoprSeries,
createSingleCoinsDestroyedSeries,
createSingleRealizedAthRegretSeries,
createGroupedRealizedAthRegretSeries,
createSingleSentimentSeries,
@@ -64,6 +58,20 @@ import {
createValuationSectionFull,
createGroupedValuationSection,
} from "./valuation.js";
import {
createActivitySection,
createActivitySectionWithAdjusted,
createGroupedActivitySection,
createGroupedActivitySectionWithAdjusted,
} from "./activity.js";
import {
createProfitabilitySection,
createProfitabilitySectionWithNupl,
createProfitabilitySectionAll,
createProfitabilitySectionWithPeakRegret,
createGroupedProfitabilitySection,
createGroupedProfitabilitySectionWithNupl,
} from "./profitability.js";
import { Unit } from "../../utils/units.js";
import { line, baseline } from "../series.js";
import { priceLine } from "../constants.js";
@@ -84,12 +92,11 @@ export function createCohortFolderAll(args) {
name: args.name || "all",
tree: [
createHoldingsSectionAll({ cohort: args, title }),
createPricesSectionFull({ cohort: args, title }),
createValuationSectionFull({ cohort: args, title }),
createSingleRealizedSectionFull(args, title),
createSingleUnrealizedSectionAll(args, title),
createPricesSectionFull({ cohort: args, title }),
createCostBasisSectionWithPercentiles({ cohort: args, title }),
createSingleActivitySectionWithAdjusted(args, title),
createProfitabilitySectionAll({ cohort: args, title }),
createActivitySectionWithAdjusted({ cohort: args, title }),
],
};
}
@@ -107,14 +114,11 @@ export function createCohortFolderFull(args) {
name: args.name || "all",
tree: [
createGroupedHoldingsSectionWithRelative({ list, title }),
createGroupedPricesSection({ list, title }),
createGroupedValuationSection({ list, title }),
createGroupedRealizedSectionWithAdjusted(list, title, {
ratioMetrics: createGroupedRealizedPnlRatioMetrics,
}),
createGroupedUnrealizedSectionFull(list, title),
createGroupedPricesSection({ list, title }),
createGroupedCostBasisSectionWithPercentiles({ list, title }),
createGroupedActivitySectionWithAdjusted(list, title),
createGroupedProfitabilitySectionWithNupl({ list, title }),
createGroupedActivitySectionWithAdjusted({ list, title }),
],
};
}
@@ -123,12 +127,11 @@ export function createCohortFolderFull(args) {
name: args.name || "all",
tree: [
createHoldingsSectionWithRelative({ cohort: args, title }),
createPricesSectionFull({ cohort: args, title }),
createValuationSectionFull({ cohort: args, title }),
createSingleRealizedSectionFull(args, title),
createSingleUnrealizedSectionFull(args, title),
createPricesSectionFull({ cohort: args, title }),
createCostBasisSectionWithPercentiles({ cohort: args, title }),
createSingleActivitySectionWithAdjusted(args, title),
createProfitabilitySectionWithNupl({ cohort: args, title }),
createActivitySectionWithAdjusted({ cohort: args, title }),
],
};
}
@@ -146,12 +149,11 @@ export function createCohortFolderWithAdjusted(args) {
name: args.name || "all",
tree: [
createGroupedHoldingsSectionWithRelative({ list, title }),
createGroupedPricesSection({ list, title }),
createGroupedValuationSection({ list, title }),
createGroupedRealizedSectionWithAdjusted(list, title),
createGroupedUnrealizedSectionWithMarketCap(list, title),
createGroupedPricesSection({ list, title }),
createGroupedCostBasisSection({ list, title }),
createGroupedActivitySectionWithAdjusted(list, title),
createGroupedProfitabilitySectionWithNupl({ list, title }),
createGroupedActivitySectionWithAdjusted({ list, title }),
],
};
}
@@ -160,12 +162,11 @@ export function createCohortFolderWithAdjusted(args) {
name: args.name || "all",
tree: [
createHoldingsSectionWithRelative({ cohort: args, title }),
createPricesSectionBasic({ cohort: args, title }),
createValuationSection({ cohort: args, title }),
createSingleRealizedSectionWithAdjusted(args, title),
createSingleUnrealizedSectionWithMarketCap(args, title),
createPricesSectionBasic({ cohort: args, title }),
createCostBasisSection({ cohort: args, title }),
createSingleActivitySectionWithAdjusted(args, title),
createProfitabilitySectionWithNupl({ cohort: args, title }),
createActivitySectionWithAdjusted({ cohort: args, title }),
],
};
}
@@ -183,13 +184,10 @@ export function createCohortFolderWithNupl(args) {
name: args.name || "all",
tree: [
createGroupedHoldingsSectionWithRelative({ list, title }),
createGroupedPricesSection({ list, title }),
createGroupedValuationSection({ list, title }),
createGroupedRealizedSectionBasic(list, title, {
ratioMetrics: createGroupedRealizedPnlRatioMetrics,
}),
createGroupedUnrealizedSectionWithNupl({ list, title }),
createGroupedPricesSection({ list, title }),
createGroupedCostBasisSectionWithPercentiles({ list, title }),
createGroupedProfitabilitySectionWithNupl({ list, title }),
createGroupedActivitySection({ list, title }),
],
};
@@ -199,11 +197,10 @@ export function createCohortFolderWithNupl(args) {
name: args.name || "all",
tree: [
createHoldingsSectionWithRelative({ cohort: args, title }),
createPricesSectionFull({ cohort: args, title }),
createValuationSectionFull({ cohort: args, title }),
createSingleRealizedSectionWithPercentiles(args, title),
createSingleUnrealizedSectionWithNupl({ cohort: args, title }),
createPricesSectionFull({ cohort: args, title }),
createCostBasisSectionWithPercentiles({ cohort: args, title }),
createProfitabilitySectionWithNupl({ cohort: args, title }),
createActivitySection({ cohort: args, title }),
],
};
@@ -222,13 +219,10 @@ export function createCohortFolderAgeRange(args) {
name: args.name || "all",
tree: [
createGroupedHoldingsSection({ list, title }),
createGroupedPricesSection({ list, title }),
createGroupedValuationSection({ list, title }),
createGroupedRealizedSectionBasic(list, title, {
ratioMetrics: createGroupedRealizedPnlRatioMetrics,
}),
createGroupedUnrealizedSectionAgeRange(list, title),
createGroupedPricesSection({ list, title }),
createGroupedCostBasisSectionWithPercentiles({ list, title }),
createGroupedProfitabilitySection({ list, title }),
createGroupedActivitySection({ list, title }),
],
};
@@ -238,11 +232,10 @@ export function createCohortFolderAgeRange(args) {
name: args.name || "all",
tree: [
createHoldingsSection({ cohort: args, title }),
createPricesSectionFull({ cohort: args, title }),
createValuationSectionFull({ cohort: args, title }),
createSingleRealizedSectionWithPercentiles(args, title),
createSingleUnrealizedSectionAgeRange(args, title),
createPricesSectionFull({ cohort: args, title }),
createCostBasisSectionWithPercentiles({ cohort: args, title }),
createProfitabilitySection({ cohort: args, title }),
createActivitySection({ cohort: args, title }),
],
};
@@ -261,11 +254,10 @@ export function createCohortFolderMinAge(args) {
name: args.name || "all",
tree: [
createGroupedHoldingsSectionWithRelative({ list, title }),
createGroupedPricesSection({ list, title }),
createGroupedValuationSection({ list, title }),
createGroupedRealizedSectionBasic(list, title),
createGroupedUnrealizedSectionMinAge(list, title),
createGroupedPricesSection({ list, title }),
createGroupedCostBasisSection({ list, title }),
createGroupedProfitabilitySection({ list, title }),
createGroupedActivitySection({ list, title }),
],
};
@@ -275,11 +267,10 @@ export function createCohortFolderMinAge(args) {
name: args.name || "all",
tree: [
createHoldingsSectionWithRelative({ cohort: args, title }),
createPricesSectionBasic({ cohort: args, title }),
createValuationSection({ cohort: args, title }),
createSingleRealizedSectionBasic(args, title),
createSingleUnrealizedSectionMinAge(args, title),
createPricesSectionBasic({ cohort: args, title }),
createCostBasisSection({ cohort: args, title }),
createProfitabilitySectionWithPeakRegret({ cohort: args, title }),
createActivitySection({ cohort: args, title }),
],
};
@@ -298,11 +289,10 @@ export function createCohortFolderBasicWithMarketCap(args) {
name: args.name || "all",
tree: [
createGroupedHoldingsSectionWithRelative({ list, title }),
createGroupedPricesSection({ list, title }),
createGroupedValuationSection({ list, title }),
createGroupedRealizedSectionBasic(list, title),
createGroupedUnrealizedSectionWithMarketCapOnly(list, title),
createGroupedPricesSection({ list, title }),
createGroupedCostBasisSection({ list, title }),
createGroupedProfitabilitySectionWithNupl({ list, title }),
createGroupedActivitySection({ list, title }),
],
};
@@ -312,11 +302,10 @@ export function createCohortFolderBasicWithMarketCap(args) {
name: args.name || "all",
tree: [
createHoldingsSectionWithRelative({ cohort: args, title }),
createPricesSectionBasic({ cohort: args, title }),
createValuationSection({ cohort: args, title }),
createSingleRealizedSectionBasic(args, title),
createSingleUnrealizedSectionWithMarketCapOnly(args, title),
createPricesSectionBasic({ cohort: args, title }),
createCostBasisSection({ cohort: args, title }),
createProfitabilitySectionWithNupl({ cohort: args, title }),
createActivitySection({ cohort: args, title }),
],
};
@@ -335,11 +324,10 @@ export function createCohortFolderBasicWithoutMarketCap(args) {
name: args.name || "all",
tree: [
createGroupedHoldingsSection({ list, title }),
createGroupedPricesSection({ list, title }),
createGroupedValuationSection({ list, title }),
createGroupedRealizedSectionBasic(list, title),
createGroupedUnrealizedSectionBase(list, title),
createGroupedPricesSection({ list, title }),
createGroupedCostBasisSection({ list, title }),
createGroupedProfitabilitySection({ list, title }),
createGroupedActivitySection({ list, title }),
],
};
@@ -349,11 +337,10 @@ export function createCohortFolderBasicWithoutMarketCap(args) {
name: args.name || "all",
tree: [
createHoldingsSection({ cohort: args, title }),
createPricesSectionBasic({ cohort: args, title }),
createValuationSection({ cohort: args, title }),
createSingleRealizedSectionBasic(args, title),
createSingleUnrealizedSectionBase(args, title),
createPricesSectionBasic({ cohort: args, title }),
createCostBasisSection({ cohort: args, title }),
createProfitabilitySection({ cohort: args, title }),
createActivitySection({ cohort: args, title }),
],
};
@@ -373,11 +360,10 @@ export function createCohortFolderAddress(args) {
name: args.name || "all",
tree: [
createGroupedHoldingsSectionAddress({ list, title }),
createGroupedPricesSection({ list, title }),
createGroupedValuationSection({ list, title }),
createGroupedRealizedSectionBasic(list, title),
createGroupedUnrealizedSectionBase(list, title),
createGroupedPricesSection({ list, title }),
createGroupedCostBasisSection({ list, title }),
createGroupedProfitabilitySection({ list, title }),
createGroupedActivitySection({ list, title }),
],
};
@@ -387,11 +373,10 @@ export function createCohortFolderAddress(args) {
name: args.name || "all",
tree: [
createHoldingsSectionAddress({ cohort: args, title }),
createPricesSectionBasic({ cohort: args, title }),
createValuationSection({ cohort: args, title }),
createSingleRealizedSectionBasic(args, title),
createSingleUnrealizedSectionBase(args, title),
createPricesSectionBasic({ cohort: args, title }),
createCostBasisSection({ cohort: args, title }),
createProfitabilitySection({ cohort: args, title }),
createActivitySection({ cohort: args, title }),
],
};
@@ -410,11 +395,10 @@ export function createCohortFolderWithoutRelative(args) {
name: args.name || "all",
tree: [
createGroupedHoldingsSection({ list, title }),
createGroupedPricesSection({ list, title }),
createGroupedValuationSection({ list, title }),
createGroupedRealizedSectionBasic(list, title),
createGroupedUnrealizedSectionWithoutRelative(list, title),
createGroupedPricesSection({ list, title }),
createGroupedCostBasisSection({ list, title }),
createGroupedProfitabilitySection({ list, title }),
createGroupedActivitySection({ list, title }),
],
};
@@ -424,11 +408,10 @@ export function createCohortFolderWithoutRelative(args) {
name: args.name || "all",
tree: [
createHoldingsSection({ cohort: args, title }),
createPricesSectionBasic({ cohort: args, title }),
createValuationSection({ cohort: args, title }),
createSingleRealizedSectionBasic(args, title),
createSingleUnrealizedSectionWithoutRelative(args, title),
createPricesSectionBasic({ cohort: args, title }),
createCostBasisSection({ cohort: args, title }),
createProfitabilitySection({ cohort: args, title }),
createActivitySection({ cohort: args, title }),
],
};
@@ -734,6 +717,7 @@ function createSingleRealizedPnlSection(cohort, title, { extra = [] } = {}) {
name: "Sum",
title: title("Realized P&L"),
bottom: [
// USD
line({
metric: tree.realized.realizedProfit.sum,
name: "Profit",
@@ -773,12 +757,26 @@ function createSingleRealizedPnlSection(cohort, title, { extra = [] } = {}) {
unit: Unit.usd,
defaultActive: false,
}),
// % of R.Cap
baseline({
metric: tree.realized.realizedProfitRelToRealizedCap.sum,
name: "Profit",
color: colors.profit,
unit: Unit.pctRcap,
}),
baseline({
metric: tree.realized.realizedLossRelToRealizedCap.sum,
name: "Loss",
color: colors.loss,
unit: Unit.pctRcap,
}),
],
},
{
name: "Cumulative",
title: title("Realized P&L (Total)"),
bottom: [
// USD
line({
metric: tree.realized.realizedProfit.cumulative,
name: "Profit",
@@ -798,42 +796,23 @@ function createSingleRealizedPnlSection(cohort, title, { extra = [] } = {}) {
unit: Unit.usd,
defaultActive: false,
}),
// % of R.Cap
baseline({
metric: tree.realized.realizedProfitRelToRealizedCap.cumulative,
name: "Profit",
color: colors.profit,
unit: Unit.pctRcap,
}),
baseline({
metric: tree.realized.realizedLossRelToRealizedCap.cumulative,
name: "Loss",
color: colors.loss,
unit: Unit.pctRcap,
}),
],
},
],
},
{
name: "P&L Relative",
title: title("Realized P&L"),
bottom: [
baseline({
metric: tree.realized.realizedProfitRelToRealizedCap.sum,
name: "Profit",
color: colors.profit,
unit: Unit.pctRcap,
}),
baseline({
metric: tree.realized.realizedProfitRelToRealizedCap.cumulative,
name: "Profit Cumulative",
color: colors.profit,
unit: Unit.pctRcap,
defaultActive: false,
}),
baseline({
metric: tree.realized.realizedLossRelToRealizedCap.sum,
name: "Loss",
color: colors.loss,
unit: Unit.pctRcap,
}),
baseline({
metric: tree.realized.realizedLossRelToRealizedCap.cumulative,
name: "Loss Cumulative",
color: colors.loss,
unit: Unit.pctRcap,
defaultActive: false,
}),
],
},
{
name: "Net P&L",
tree: [
@@ -841,6 +820,7 @@ function createSingleRealizedPnlSection(cohort, title, { extra = [] } = {}) {
name: "Sum",
title: title("Net Realized P&L"),
bottom: [
// USD
baseline({
metric: tree.realized.netRealizedPnl.sum,
name: "Net",
@@ -851,23 +831,19 @@ function createSingleRealizedPnlSection(cohort, title, { extra = [] } = {}) {
name: "Net 7d EMA",
unit: Unit.usd,
}),
// % of R.Cap
baseline({
metric: tree.realized.netRealizedPnlRelToRealizedCap.sum,
name: "Net",
unit: Unit.pctRcap,
}),
baseline({
metric:
tree.realized.netRealizedPnlCumulative30dDeltaRelToMarketCap,
name: "30d Change",
unit: Unit.pctMcap,
}),
],
},
{
name: "Cumulative",
title: title("Net Realized P&L (Total)"),
bottom: [
// USD
baseline({
metric: tree.realized.netRealizedPnl.cumulative,
name: "Net",
@@ -879,6 +855,7 @@ function createSingleRealizedPnlSection(cohort, title, { extra = [] } = {}) {
unit: Unit.usd,
defaultActive: false,
}),
// % of R.Cap
baseline({
metric: tree.realized.netRealizedPnlRelToRealizedCap.cumulative,
name: "Net",
@@ -891,6 +868,13 @@ function createSingleRealizedPnlSection(cohort, title, { extra = [] } = {}) {
unit: Unit.pctRcap,
defaultActive: false,
}),
// % of M.Cap
baseline({
metric:
tree.realized.netRealizedPnlCumulative30dDeltaRelToMarketCap,
name: "30d Change",
unit: Unit.pctMcap,
}),
],
},
],
@@ -1330,14 +1314,14 @@ function createSingleAdjustedSoprChart(cohort, title) {
baseline({
metric: tree.realized.adjustedSopr,
name: "Adjusted",
color: colors.bi.adjustedSopr,
color: colors.bi.p1,
unit: Unit.ratio,
base: 1,
}),
baseline({
metric: tree.realized.adjustedSopr7dEma,
name: "Adj. 7d EMA",
color: colors.bi.adjustedSopr7d,
color: colors.bi.p2,
unit: Unit.ratio,
defaultActive: false,
base: 1,
@@ -1345,7 +1329,7 @@ function createSingleAdjustedSoprChart(cohort, title) {
baseline({
metric: tree.realized.adjustedSopr30dEma,
name: "Adj. 30d EMA",
color: colors.bi.adjustedSopr30d,
color: colors.bi.p3,
unit: Unit.ratio,
defaultActive: false,
base: 1,
@@ -1639,13 +1623,13 @@ function createInvestedCapitalRelMetrics(rel) {
metric: rel.investedCapitalInProfitPct,
name: "In Profit",
color: colors.profit,
unit: Unit.pctRcap,
unit: Unit.pctOwnRcap,
}),
baseline({
metric: rel.investedCapitalInLossPct,
name: "In Loss",
color: colors.loss,
unit: Unit.pctRcap,
unit: Unit.pctOwnRcap,
}),
];
}
@@ -1656,12 +1640,6 @@ function createInvestedCapitalRelMetrics(rel) {
*/
function createUnrealizedPnlBaseMetrics(tree) {
return [
line({
metric: tree.unrealized.totalUnrealizedPnl,
name: "Total",
color: colors.default,
unit: Unit.usd,
}),
line({
metric: tree.unrealized.unrealizedProfit,
name: "Profit",
@@ -1682,6 +1660,12 @@ function createUnrealizedPnlBaseMetrics(tree) {
unit: Unit.usd,
defaultActive: false,
}),
line({
metric: tree.unrealized.totalUnrealizedPnl,
name: "Total",
color: colors.default,
unit: Unit.usd,
}),
];
}
@@ -1934,7 +1918,7 @@ function createUnrealizedSection({
charts = [],
}) {
return {
name: "Unrealized",
name: "Profitability",
tree: [
{
name: "P&L",
@@ -2008,7 +1992,7 @@ function createGroupedInvestedCapitalRelativeCharts(list, title) {
metric: tree.relative.investedCapitalInProfitPct,
name,
color,
unit: Unit.pctRcap,
unit: Unit.pctOwnRcap,
}),
),
},
@@ -2020,7 +2004,7 @@ function createGroupedInvestedCapitalRelativeCharts(list, title) {
metric: tree.relative.investedCapitalInLossPct,
name,
color,
unit: Unit.pctRcap,
unit: Unit.pctOwnRcap,
}),
),
},
@@ -2044,7 +2028,7 @@ function createGroupedUnrealizedSection({
charts = [],
}) {
return {
name: "Unrealized",
name: "Profitability",
tree: [
...createGroupedUnrealizedBaseCharts(list, title),
{
@@ -2102,7 +2086,7 @@ function createGroupedUnrealizedSection({
*/
function createGroupedUnrealizedSectionWithoutRelative(list, title) {
return {
name: "Unrealized",
name: "Profitability",
tree: [
...createGroupedUnrealizedBaseCharts(list, title),
{
@@ -2563,439 +2547,3 @@ function createGroupedUnrealizedSectionAgeRange(list, title) {
charts: [createGroupedPeakRegretChartBasic(list, title)],
});
}
// ============================================================================
// Cost Basis Section Builders (generic, type-safe composition)
// ============================================================================
// ============================================================================
// Activity Section Builders (generic, type-safe composition)
// ============================================================================
/**
* Generic single activity section builder - callers pass optional extra value metrics
* @param {Object} args
* @param {UtxoCohortObject | CohortWithoutRelative} args.cohort
* @param {(metric: string) => string} args.title
* @param {AnyFetchedSeriesBlueprint[]} [args.valueMetrics] - Extra value metrics (e.g., adjusted)
* @returns {PartialOptionsGroup}
*/
function createActivitySection({ cohort, title, valueMetrics = [] }) {
const { tree, color } = cohort;
return {
name: "Activity",
tree: [
{
name: "Sent",
tree: [
{
name: "Sum",
title: title("Sent"),
bottom: [
line({
metric: tree.activity.sent.sats.sum,
name: "sum",
color,
unit: Unit.sats,
}),
line({
metric: tree.activity.sent.bitcoin.sum,
name: "sum",
color,
unit: Unit.btc,
}),
line({
metric: tree.activity.sent.dollars.sum,
name: "sum",
color,
unit: Unit.usd,
}),
line({
metric: tree.activity.sent14dEma.sats,
name: "14d EMA",
unit: Unit.sats,
}),
line({
metric: tree.activity.sent14dEma.bitcoin,
name: "14d EMA",
unit: Unit.btc,
}),
line({
metric: tree.activity.sent14dEma.dollars,
name: "14d EMA",
unit: Unit.usd,
}),
],
},
{
name: "Cumulative",
title: title("Sent (Total)"),
bottom: [
line({
metric: tree.activity.sent.sats.cumulative,
name: "all-time",
color,
unit: Unit.sats,
}),
line({
metric: tree.activity.sent.bitcoin.cumulative,
name: "all-time",
color,
unit: Unit.btc,
}),
line({
metric: tree.activity.sent.dollars.cumulative,
name: "all-time",
color,
unit: Unit.usd,
}),
],
},
],
},
{
name: "Sell Side Risk",
title: title("Sell Side Risk Ratio"),
bottom: createSingleSellSideRiskSeries(tree),
},
{
name: "Value",
tree: [
{
name: "Created & Destroyed",
title: title("Value Created & Destroyed"),
bottom: [
...createSingleValueCreatedDestroyedSeries(tree),
...valueMetrics,
],
},
{
name: "Breakdown",
title: title("Value Flow Breakdown"),
bottom: createSingleValueFlowBreakdownSeries(tree),
},
{
name: "Flow",
title: title("Capitulation & Profit Flow"),
bottom: createSingleCapitulationProfitFlowSeries(tree),
},
],
},
{
name: "Coins Destroyed",
title: title("Coins Destroyed"),
bottom: createSingleCoinsDestroyedSeries(cohort),
},
],
};
}
/**
* Create grouped value flow charts (profit/loss created/destroyed, profit/capitulation flow)
* @template {readonly (UtxoCohortObject | CohortWithoutRelative)[]} T
* @param {T} list
* @param {(metric: string) => string} title
* @returns {PartialOptionsTree}
*/
function createGroupedValueFlowCharts(list, title) {
return [
{
name: "Profit Created",
title: title("Profit Value Created"),
bottom: list.flatMap(({ color, name, tree }) => [
line({
metric: tree.realized.profitValueCreated,
name,
color,
unit: Unit.usd,
}),
]),
},
{
name: "Profit Destroyed",
title: title("Profit Value Destroyed"),
bottom: list.flatMap(({ color, name, tree }) => [
line({
metric: tree.realized.profitValueDestroyed,
name,
color,
unit: Unit.usd,
}),
]),
},
{
name: "Loss Created",
title: title("Loss Value Created"),
bottom: list.flatMap(({ color, name, tree }) => [
line({
metric: tree.realized.lossValueCreated,
name,
color,
unit: Unit.usd,
}),
]),
},
{
name: "Loss Destroyed",
title: title("Loss Value Destroyed"),
bottom: list.flatMap(({ color, name, tree }) => [
line({
metric: tree.realized.lossValueDestroyed,
name,
color,
unit: Unit.usd,
}),
]),
},
{
name: "Profit Flow",
title: title("Profit Flow"),
bottom: list.flatMap(({ color, name, tree }) => [
line({
metric: tree.realized.profitFlow,
name,
color,
unit: Unit.usd,
}),
]),
},
{
name: "Capitulation Flow",
title: title("Capitulation Flow"),
bottom: list.flatMap(({ color, name, tree }) => [
line({
metric: tree.realized.capitulationFlow,
name,
color,
unit: Unit.usd,
}),
]),
},
];
}
/**
* Generic grouped activity section builder - callers pass optional value tree
* @template {readonly (UtxoCohortObject | CohortWithoutRelative)[]} T
* @param {Object} args
* @param {T} args.list
* @param {(metric: string) => string} args.title
* @param {PartialOptionsTree} [args.valueTree] - Optional value tree (defaults to basic created/destroyed)
* @returns {PartialOptionsGroup}
*/
function createGroupedActivitySection({ list, title, valueTree }) {
return {
name: "Activity",
tree: [
{
name: "Sent",
tree: [
{
name: "Sum",
title: title("Sent"),
bottom: list.flatMap(({ color, name, tree }) =>
satsBtcUsd({
pattern: {
sats: tree.activity.sent.sats.sum,
bitcoin: tree.activity.sent.bitcoin.sum,
dollars: tree.activity.sent.dollars.sum,
},
name,
color,
}),
),
},
{
name: "14d EMA",
title: title("Sent 14d EMA"),
bottom: list.flatMap(({ color, name, tree }) =>
satsBtcUsd({ pattern: tree.activity.sent14dEma, name, color }),
),
},
],
},
{
name: "Sell Side Risk",
title: title("Sell Side Risk Ratio"),
bottom: createGroupedSellSideRiskSeries(list),
},
{
name: "Value",
tree: valueTree ?? [
{
name: "Created",
title: title("Value Created"),
bottom: list.flatMap(({ color, name, tree }) => [
line({
metric: tree.realized.valueCreated,
name,
color,
unit: Unit.usd,
}),
]),
},
{
name: "Destroyed",
title: title("Value Destroyed"),
bottom: list.flatMap(({ color, name, tree }) => [
line({
metric: tree.realized.valueDestroyed,
name,
color,
unit: Unit.usd,
}),
]),
},
...createGroupedValueFlowCharts(list, title),
],
},
{
name: "Coins Destroyed",
tree: [
{
name: "Sum",
title: title("Coins Destroyed"),
bottom: list.flatMap(({ color, name, tree }) => [
line({
metric: tree.activity.coinblocksDestroyed.sum,
name,
color,
unit: Unit.coinblocks,
}),
line({
metric: tree.activity.coindaysDestroyed.sum,
name,
color,
unit: Unit.coindays,
}),
]),
},
{
name: "Cumulative",
title: title("Cumulative Coins Destroyed"),
bottom: list.flatMap(({ color, name, tree }) => [
line({
metric: tree.activity.coinblocksDestroyed.cumulative,
name,
color,
unit: Unit.coinblocks,
}),
line({
metric: tree.activity.coindaysDestroyed.cumulative,
name,
color,
unit: Unit.coindays,
}),
]),
},
],
},
],
};
}
// ============================================================================
// Activity Section Variants (by cohort capability)
// ============================================================================
/**
* Create activity section with adjusted values (for cohorts with RealizedPattern3/4)
* @param {CohortAll | CohortFull | CohortWithAdjusted} cohort
* @param {(metric: string) => string} title
* @returns {PartialOptionsGroup}
*/
function createSingleActivitySectionWithAdjusted(cohort, title) {
const { tree } = cohort;
return createActivitySection({
cohort,
title,
valueMetrics: [
line({
metric: tree.realized.adjustedValueCreated,
name: "Adjusted Created",
color: colors.adjustedCreated,
unit: Unit.usd,
}),
line({
metric: tree.realized.adjustedValueDestroyed,
name: "Adjusted Destroyed",
color: colors.adjustedDestroyed,
unit: Unit.usd,
}),
],
});
}
/**
* Create activity section for grouped cohorts with adjusted values (for cohorts with RealizedPattern3/4)
* @param {readonly (CohortFull | CohortWithAdjusted)[]} list
* @param {(metric: string) => string} title
* @returns {PartialOptionsGroup}
*/
function createGroupedActivitySectionWithAdjusted(list, title) {
return createGroupedActivitySection({
list,
title,
valueTree: [
{
name: "Created",
tree: [
{
name: "Normal",
title: title("Value Created"),
bottom: list.flatMap(({ color, name, tree }) => [
line({
metric: tree.realized.valueCreated,
name,
color,
unit: Unit.usd,
}),
]),
},
{
name: "Adjusted",
title: title("Adjusted Value Created"),
bottom: list.flatMap(({ color, name, tree }) => [
line({
metric: tree.realized.adjustedValueCreated,
name,
color,
unit: Unit.usd,
}),
]),
},
],
},
{
name: "Destroyed",
tree: [
{
name: "Normal",
title: title("Value Destroyed"),
bottom: list.flatMap(({ color, name, tree }) => [
line({
metric: tree.realized.valueDestroyed,
name,
color,
unit: Unit.usd,
}),
]),
},
{
name: "Adjusted",
title: title("Adjusted Value Destroyed"),
bottom: list.flatMap(({ color, name, tree }) => [
line({
metric: tree.realized.adjustedValueDestroyed,
name,
color,
unit: Unit.usd,
}),
]),
},
],
},
...createGroupedValueFlowCharts(list, title),
],
});
}

View File

@@ -58,7 +58,15 @@ export function createValuationSectionFull({ cohort, title }) {
{
name: "Realized Cap",
title: title("Realized Cap"),
bottom: createSingleRealizedCapSeries(cohort),
bottom: [
...createSingleRealizedCapSeries(cohort),
baseline({
metric: tree.realized.realizedCapRelToOwnMarketCap,
name: "Rel. to Own M.Cap",
color,
unit: Unit.pctOwnMcap,
}),
],
},
{
name: "30d Change",
@@ -163,3 +171,63 @@ export function createGroupedValuationSection({ list, title }) {
],
};
}
/**
* @template {{ name: string, color: Color, tree: { realized: { realizedCap: AnyMetricPattern, realizedCap30dDelta: AnyMetricPattern, realizedCapRelToOwnMarketCap: AnyMetricPattern, realizedPriceExtra: { ratio: AnyMetricPattern } } } }} T
* @param {{ list: readonly T[], title: (metric: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createGroupedValuationSectionWithOwnMarketCap({ list, title }) {
return {
name: "Valuation",
tree: [
{
name: "Realized Cap",
title: title("Realized Cap"),
bottom: [
...list.map(({ name, color, tree }) =>
line({
metric: tree.realized.realizedCap,
name,
color,
unit: Unit.usd,
}),
),
...list.map(({ name, color, tree }) =>
baseline({
metric: tree.realized.realizedCapRelToOwnMarketCap,
name,
color,
unit: Unit.pctOwnMcap,
}),
),
],
},
{
name: "30d Change",
title: title("Realized Cap 30d Change"),
bottom: list.map(({ name, color, tree }) =>
baseline({
metric: tree.realized.realizedCap30dDelta,
name,
color,
unit: Unit.usd,
}),
),
},
{
name: "MVRV",
title: title("MVRV"),
bottom: list.map(({ name, color, tree }) =>
baseline({
metric: tree.realized.realizedPriceExtra.ratio,
name,
color,
unit: Unit.ratio,
base: 1,
}),
),
},
],
};
}