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,
}),
),
},
],
};
}

View File

@@ -327,7 +327,7 @@ export function createDcaVsLumpSumSection({ dca, lookback, returns }) {
baseline({
metric: dca.periodLumpSumMaxReturn[key],
name: "Lump Sum",
color: colors.bi.lumpSum,
color: colors.bi.p2,
unit: Unit.percentage,
}),
],
@@ -345,7 +345,7 @@ export function createDcaVsLumpSumSection({ dca, lookback, returns }) {
baseline({
metric: dca.periodLumpSumMinReturn[key],
name: "Lump Sum",
color: colors.bi.lumpSum,
color: colors.bi.p2,
unit: Unit.percentage,
}),
],
@@ -369,7 +369,7 @@ export function createDcaVsLumpSumSection({ dca, lookback, returns }) {
baseline({
metric: dca.periodLumpSumReturns[key],
name: "Lump Sum",
color: colors.bi.lumpSum,
color: colors.bi.p2,
unit: Unit.percentage,
}),
],
@@ -395,7 +395,7 @@ export function createDcaVsLumpSumSection({ dca, lookback, returns }) {
baseline({
metric: dca.periodLumpSumReturns[key],
name: "Lump Sum",
color: colors.bi.lumpSum,
color: colors.bi.p2,
unit: Unit.percentage,
}),
baseline({
@@ -406,7 +406,7 @@ export function createDcaVsLumpSumSection({ dca, lookback, returns }) {
baseline({
metric: returns.cagr[key],
name: "Lump Sum",
color: colors.bi.lumpSum,
color: colors.bi.p2,
unit: Unit.cagr,
}),
],

View File

@@ -415,7 +415,7 @@ export function createMiningSection() {
},
{
name: "Distribution",
title: "Coinbase Rewards Distribution",
title: "Coinbase Rewards per Block Distribution",
bottom: distributionBtcSatsUsd(blocks.rewards.coinbase),
},
{
@@ -476,7 +476,7 @@ export function createMiningSection() {
tree: [
{
name: "Sum",
title: "Transaction Fee Revenue",
title: "Transaction Fee Revenue per Block",
bottom: satsBtcUsdFrom({
source: transactions.fees.fee,
key: "sum",
@@ -485,7 +485,7 @@ export function createMiningSection() {
},
{
name: "Distribution",
title: "Transaction Fee Revenue Distribution",
title: "Transaction Fee Revenue per Block Distribution",
bottom: distributionBtcSatsUsd(transactions.fees.fee),
},
{

View File

@@ -10,10 +10,10 @@ import {
baseline,
fromSupplyPattern,
fromBaseStatsPattern,
chartsFromFull,
chartsFromFullPerBlock,
chartsFromValueFull,
fromStatsPattern,
chartsFromSum,
chartsFromSumPerBlock,
} from "./series.js";
import { satsBtcUsd, satsBtcUsdFrom } from "./shared.js";
@@ -93,20 +93,20 @@ export function createNetworkSection() {
{
key: "sending",
name: "Sending",
title: "Sending Address Count",
compareTitle: "Sending Address Count by Type",
title: "Unique Sending Addresses per Block",
compareTitle: "Unique Sending Addresses per Block by Type",
},
{
key: "receiving",
name: "Receiving",
title: "Receiving Address Count",
compareTitle: "Receiving Address Count by Type",
title: "Unique Receiving Addresses per Block",
compareTitle: "Unique Receiving Addresses per Block by Type",
},
{
key: "both",
name: "Sending & Receiving",
title: "Addresses Sending & Receiving",
compareTitle: "Addresses Sending & Receiving by Type",
title: "Unique Addresses Sending & Receiving per Block",
compareTitle: "Unique Addresses Sending & Receiving per Block by Type",
},
]);
@@ -114,15 +114,15 @@ export function createNetworkSection() {
const balanceTypes = /** @type {const} */ ([
{
key: "balanceIncreased",
name: "Increased",
title: "Addresses with Increased Balance",
compareTitle: "Addresses with Increased Balance by Type",
name: "Accumulating",
title: "Accumulating Addresses per Block",
compareTitle: "Accumulating Addresses per Block by Type",
},
{
key: "balanceDecreased",
name: "Decreased",
title: "Addresses with Decreased Balance",
compareTitle: "Addresses with Decreased Balance by Type",
name: "Distributing",
title: "Distributing Addresses per Block",
compareTitle: "Distributing Addresses per Block by Type",
},
]);
@@ -165,16 +165,16 @@ export function createNetworkSection() {
unit: Unit.count,
}),
line({
metric: distribution.totalAddrCount[key],
name: "Total",
color: colors.default,
metric: distribution.emptyAddrCount[key].count,
name: "Empty",
color: colors.gray,
unit: Unit.count,
defaultActive: false,
}),
line({
metric: distribution.emptyAddrCount[key].count,
name: "Empty",
color: colors.gray,
metric: distribution.totalAddrCount[key],
name: "Total",
color: colors.default,
unit: Unit.count,
defaultActive: false,
}),
@@ -189,14 +189,21 @@ export function createNetworkSection() {
bottom: [
baseline({
metric: distribution.addrCount[key]._30dChange,
name: "30d Change",
name: "Funded",
unit: Unit.count,
}),
baseline({
metric: distribution.emptyAddrCount[key]._30dChange,
name: "Empty",
color: colors.gray,
unit: Unit.count,
defaultActive: false,
}),
],
},
{
name: "New",
tree: chartsFromFull({
tree: chartsFromFullPerBlock({
pattern: distribution.newAddrCount[key],
title: `${titlePrefix}New Address Count`,
unit: Unit.count,
@@ -204,7 +211,7 @@ export function createNetworkSection() {
},
{
name: "Reactivated",
title: `${titlePrefix}Reactivated Address Count`,
title: `${titlePrefix}Reactivated Addresses per Block`,
bottom: fromBaseStatsPattern({
pattern: distribution.addressActivity[key].reactivated,
unit: Unit.count,
@@ -212,7 +219,7 @@ export function createNetworkSection() {
},
{
name: "Growth Rate",
title: `${titlePrefix}Address Growth Rate`,
title: `${titlePrefix}Address Growth Rate per Block`,
bottom: fromBaseStatsPattern({
pattern: distribution.growthRate[key],
unit: Unit.ratio,
@@ -288,7 +295,7 @@ export function createNetworkSection() {
},
{
name: "Reactivated",
title: `${groupName} Reactivated Address Count`,
title: `${groupName} Reactivated Addresses per Block`,
bottom: types.flatMap((t) => [
line({
metric: distribution.addressActivity[t.key].reactivated.base,
@@ -306,7 +313,7 @@ export function createNetworkSection() {
},
{
name: "Growth Rate",
title: `${groupName} Address Growth Rate`,
title: `${groupName} Address Growth Rate per Block`,
bottom: types.flatMap((t) => [
dots({
metric: distribution.growthRate[t.key].base,
@@ -489,7 +496,7 @@ export function createNetworkSection() {
tree: [
{
name: "Count",
tree: chartsFromFull({
tree: chartsFromFullPerBlock({
pattern: transactions.count.txCount,
title: "Transaction Count",
unit: Unit.count,
@@ -497,7 +504,7 @@ export function createNetworkSection() {
},
{
name: "Fee Rate",
title: "Fee Rate",
title: "Transaction Fee Rate",
bottom: fromStatsPattern({
pattern: transactions.fees.feeRate,
unit: Unit.feeRate,
@@ -897,7 +904,7 @@ export function createNetworkSection() {
},
{
name: "Inputs",
tree: chartsFromSum({
tree: chartsFromSumPerBlock({
pattern: inputs.count,
title: "Input Count",
unit: Unit.count,
@@ -905,7 +912,7 @@ export function createNetworkSection() {
},
{
name: "Outputs",
tree: chartsFromSum({
tree: chartsFromSumPerBlock({
pattern: outputs.count.totalCount,
title: "Output Count",
unit: Unit.count,
@@ -985,7 +992,7 @@ export function createNetworkSection() {
},
{
name: "Reactivated",
title: "Reactivated Address Count by Type",
title: "Reactivated Addresses per Block by Type",
bottom: addressTypes.flatMap((t) => [
line({
metric:
@@ -1007,7 +1014,7 @@ export function createNetworkSection() {
},
{
name: "Growth Rate",
title: "Address Growth Rate by Type",
title: "Address Growth Rate per Block by Type",
bottom: addressTypes.flatMap((t) => [
dots({
metric: distribution.growthRate[t.key].base,
@@ -1159,7 +1166,7 @@ export function createNetworkSection() {
createScriptCompare("Legacy", legacyScripts),
...legacyScripts.map((t) => ({
name: t.name,
tree: chartsFromFull({
tree: chartsFromFullPerBlock({
pattern: scripts.count[t.key],
title: `${t.name} Output Count`,
unit: Unit.count,
@@ -1173,7 +1180,7 @@ export function createNetworkSection() {
createScriptCompare("Script Hash", scriptHashScripts),
...scriptHashScripts.map((t) => ({
name: t.name,
tree: chartsFromFull({
tree: chartsFromFullPerBlock({
pattern: scripts.count[t.key],
title: `${t.name} Output Count`,
unit: Unit.count,
@@ -1187,7 +1194,7 @@ export function createNetworkSection() {
createScriptCompare("SegWit", segwitScripts),
...segwitScripts.map((t) => ({
name: t.name,
tree: chartsFromFull({
tree: chartsFromFullPerBlock({
pattern: scripts.count[t.key],
title: `${t.name} Output Count`,
unit: Unit.count,
@@ -1201,7 +1208,7 @@ export function createNetworkSection() {
createScriptCompare("Taproot", taprootAddresses),
...taprootAddresses.map((t) => ({
name: t.name,
tree: chartsFromFull({
tree: chartsFromFullPerBlock({
pattern: scripts.count[t.key],
title: `${t.name} Output Count`,
unit: Unit.count,
@@ -1215,7 +1222,7 @@ export function createNetworkSection() {
createScriptCompare("Other", otherScripts),
...otherScripts.map((t) => ({
name: t.name,
tree: chartsFromFull({
tree: chartsFromFullPerBlock({
pattern: scripts.count[t.key],
title: `${t.name} Output Count`,
unit: Unit.count,

View File

@@ -6,6 +6,7 @@ import {
createCohortFolderFull,
createCohortFolderWithAdjusted,
createCohortFolderWithNupl,
createCohortFolderLongTerm,
createCohortFolderAgeRange,
createCohortFolderMinAge,
createCohortFolderBasicWithMarketCap,
@@ -90,8 +91,8 @@ export function createPartialOptions() {
// STH - Short term holder cohort (Full capability)
createCohortFolderFull(termShort),
// LTH - Long term holder cohort (nupl)
createCohortFolderWithNupl(termLong),
// LTH - Long term holder cohort (own market cap + nupl + peak regret + P/L ratio)
createCohortFolderLongTerm(termLong),
// Ages cohorts
{

View File

@@ -211,7 +211,6 @@ export function candlestick({
metric,
name,
key,
defaultActive,
unit,
options,
@@ -221,7 +220,6 @@ export function candlestick({
metric,
title: name,
key,
unit,
defaultActive,
options,
@@ -238,6 +236,7 @@ export function candlestick({
* @param {Color | [Color, Color]} [args.color]
* @param {boolean} [args.defaultActive]
* @param {number | undefined} [args.base]
* @param {number} [args.style] - Line style (0: Solid, 1: Dotted, 2: Dashed, 3: LargeDashed, 4: SparseDotted)
* @param {BaselineSeriesPartialOptions} [args.options]
* @returns {FetchedBaselineSeriesBlueprint}
*/
@@ -249,6 +248,7 @@ export function baseline({
defaultActive,
unit,
base,
style,
options,
}) {
const isTuple = Array.isArray(color);
@@ -261,6 +261,58 @@ export function baseline({
colors: isTuple ? color : undefined,
unit,
defaultActive,
options: {
baseValue: {
price: base,
},
lineStyle: style,
...options,
},
};
}
/**
* @param {Omit<Parameters<typeof baseline>[0], 'style'>} args
*/
export function dottedBaseline(args) {
const _args = /** @type {Parameters<typeof baseline>[0]} */ (args);
_args.style = 1;
return baseline(_args);
}
/**
* Baseline series rendered as dots (points only, no line)
* @param {Object} args
* @param {AnyMetricPattern} args.metric
* @param {string} args.name
* @param {Unit} args.unit
* @param {string} [args.key]
* @param {Color | [Color, Color]} [args.color]
* @param {boolean} [args.defaultActive]
* @param {number | undefined} [args.base]
* @param {BaselineSeriesPartialOptions} [args.options]
* @returns {FetchedDotsBaselineSeriesBlueprint}
*/
export function dotsBaseline({
metric,
name,
key,
color,
defaultActive,
unit,
base,
options,
}) {
const isTuple = Array.isArray(color);
return {
type: /** @type {const} */ ("DotsBaseline"),
metric,
title: name,
key,
color: isTuple ? undefined : color,
colors: isTuple ? color : undefined,
unit,
defaultActive,
options: {
baseValue: {
price: base,
@@ -509,9 +561,18 @@ function btcSatsUsdSeries({ metrics, name, color, defaultActive }) {
* @param {FullStatsPattern<any>} args.pattern
* @param {string} args.title
* @param {Unit} args.unit
* @param {string} [args.distributionSuffix]
* @returns {PartialOptionsTree}
*/
export function chartsFromFull({ pattern, title, unit }) {
export function chartsFromFull({
pattern,
title,
unit,
distributionSuffix = "",
}) {
const distTitle = distributionSuffix
? `${title} ${distributionSuffix} Distribution`
: `${title} Distribution`;
return [
{
name: "Sum",
@@ -523,7 +584,7 @@ export function chartsFromFull({ pattern, title, unit }) {
},
{
name: "Distribution",
title: `${title} Distribution`,
title: distTitle,
bottom: distributionSeries(pattern, unit),
},
{
@@ -534,16 +595,36 @@ export function chartsFromFull({ pattern, title, unit }) {
];
}
/**
* Split pattern into 3 charts with "per Block" in distribution title
* @param {Object} args
* @param {FullStatsPattern<any>} args.pattern
* @param {string} args.title
* @param {Unit} args.unit
* @returns {PartialOptionsTree}
*/
export const chartsFromFullPerBlock = (args) =>
chartsFromFull({ ...args, distributionSuffix: "per Block" });
/**
* Split pattern with sum + distribution + cumulative into 3 charts (no base)
* @param {Object} args
* @param {AnyStatsPattern} args.pattern
* @param {string} args.title
* @param {Unit} args.unit
* @param {string} [args.distributionSuffix]
* @returns {PartialOptionsTree}
*/
export function chartsFromSum({ pattern, title, unit }) {
export function chartsFromSum({
pattern,
title,
unit,
distributionSuffix = "",
}) {
const { stat } = colors;
const distTitle = distributionSuffix
? `${title} ${distributionSuffix} Distribution`
: `${title} Distribution`;
return [
{
name: "Sum",
@@ -552,7 +633,7 @@ export function chartsFromSum({ pattern, title, unit }) {
},
{
name: "Distribution",
title: `${title} Distribution`,
title: distTitle,
bottom: distributionSeries(pattern, unit),
},
{
@@ -563,6 +644,17 @@ export function chartsFromSum({ pattern, title, unit }) {
];
}
/**
* Split pattern into 3 charts with "per Block" in distribution title (no base)
* @param {Object} args
* @param {AnyStatsPattern} args.pattern
* @param {string} args.title
* @param {Unit} args.unit
* @returns {PartialOptionsTree}
*/
export const chartsFromSumPerBlock = (args) =>
chartsFromSum({ ...args, distributionSuffix: "per Block" });
/**
* Split pattern with sum + cumulative into 2 charts
* @param {Object} args

View File

@@ -20,9 +20,10 @@ export const formatCohortTitle = (cohortTitle) => (metric) =>
* @param {string} args.name
* @param {Color} [args.color]
* @param {boolean} [args.defaultActive]
* @param {number} [args.style]
* @returns {FetchedLineSeriesBlueprint[]}
*/
export function satsBtcUsd({ pattern, name, color, defaultActive }) {
export function satsBtcUsd({ pattern, name, color, defaultActive, style }) {
return [
line({
metric: pattern.bitcoin,
@@ -30,14 +31,23 @@ export function satsBtcUsd({ pattern, name, color, defaultActive }) {
color,
unit: Unit.btc,
defaultActive,
style,
}),
line({
metric: pattern.sats,
name,
color,
unit: Unit.sats,
defaultActive,
style,
}),
line({ metric: pattern.sats, name, color, unit: Unit.sats, defaultActive }),
line({
metric: pattern.dollars,
name,
color,
unit: Unit.usd,
defaultActive,
style,
}),
];
}
@@ -60,7 +70,13 @@ export function satsBtcUsdBaseline({ pattern, name, color, defaultActive }) {
unit: Unit.btc,
defaultActive,
}),
baseline({ metric: pattern.sats, name, color, unit: Unit.sats, defaultActive }),
baseline({
metric: pattern.sats,
name,
color,
unit: Unit.sats,
defaultActive,
}),
baseline({
metric: pattern.dollars,
name,
@@ -146,7 +162,12 @@ export function revenueBtcSatsUsd({ coinbase, subsidy, fee, key }) {
name: "Subsidy",
color: colors.mining.subsidy,
}),
...satsBtcUsdFrom({ source: fee, key, name: "Fees", color: colors.mining.fee }),
...satsBtcUsdFrom({
source: fee,
key,
name: "Fees",
color: colors.mining.fee,
}),
];
}
@@ -477,8 +498,7 @@ export function createPriceRatioCharts({
],
},
createRatioChart({
title: (name) =>
titleFn(titlePrefix ? `${titlePrefix} ${name}` : name),
title: (name) => titleFn(titlePrefix ? `${titlePrefix} ${name}` : name),
pricePattern,
ratio,
color,

View File

@@ -35,7 +35,14 @@
* @property {LineSeriesPartialOptions} [options]
* @typedef {BaseSeriesBlueprint & DotsSeriesBlueprintSpecific} DotsSeriesBlueprint
*
* @typedef {BaselineSeriesBlueprint | CandlestickSeriesBlueprint | LineSeriesBlueprint | HistogramSeriesBlueprint | DotsSeriesBlueprint} AnySeriesBlueprint
* @typedef {Object} DotsBaselineSeriesBlueprintSpecific
* @property {"DotsBaseline"} type
* @property {Color} [color]
* @property {[Color, Color]} [colors]
* @property {BaselineSeriesPartialOptions} [options]
* @typedef {BaseSeriesBlueprint & DotsBaselineSeriesBlueprintSpecific} DotsBaselineSeriesBlueprint
*
* @typedef {BaselineSeriesBlueprint | CandlestickSeriesBlueprint | LineSeriesBlueprint | HistogramSeriesBlueprint | DotsSeriesBlueprint | DotsBaselineSeriesBlueprint} AnySeriesBlueprint
*
* @typedef {AnySeriesBlueprint["type"]} SeriesType
*
@@ -46,6 +53,7 @@
* @typedef {LineSeriesBlueprint & FetchedAnySeriesOptions} FetchedLineSeriesBlueprint
* @typedef {HistogramSeriesBlueprint & FetchedAnySeriesOptions} FetchedHistogramSeriesBlueprint
* @typedef {DotsSeriesBlueprint & FetchedAnySeriesOptions} FetchedDotsSeriesBlueprint
* @typedef {DotsBaselineSeriesBlueprint & FetchedAnySeriesOptions} FetchedDotsBaselineSeriesBlueprint
* @typedef {AnySeriesBlueprint & FetchedAnySeriesOptions} AnyFetchedSeriesBlueprint
*
* Any pattern with dollars and sats sub-metrics (auto-expands to USD + sats)
@@ -317,6 +325,7 @@
* @property {string} title
* @property {Color} color
* @property {AddressCohortPattern} tree
* @property {Brk._30dCountPattern} addrCount
*
* @typedef {UtxoCohortObject | AddressCohortObject | CohortWithoutRelative} CohortObject
*

View File

@@ -28,22 +28,27 @@ function walk(node, map, path) {
for (const [key, value] of Object.entries(node)) {
const kn = key.toLowerCase();
if (
// kn === "mvrv" ||
key.endsWith("Raw") ||
key.endsWith("Cents") ||
key.endsWith("State") ||
key.endsWith("Start") ||
kn === "mvrv" ||
// kn === "time" ||
// kn === "height" ||
// kn === "constants" ||
kn === "constants" ||
kn === "blockhash" ||
kn === "date" ||
// kn === "oracle" ||
// kn === "split" ||
kn === "split" ||
// kn === "ohlc" ||
// kn === "outpoint" ||
kn === "outpoint" ||
kn === "positions" ||
// kn === "outputtype" ||
kn === "heighttopool" ||
// kn === "txid" ||
kn === "txid" ||
kn.startsWith("timestamp") ||
// kn.startsWith("satdays") ||
kn.startsWith("satdays") ||
kn.startsWith("satblocks") ||
// kn.endsWith("state") ||
// kn.endsWith("cents") ||
kn.endsWith("index") ||