mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-07-01 06:19:02 -07:00
website: redesign part 1
This commit is contained in:
@@ -1,740 +0,0 @@
|
||||
/**
|
||||
* Activity section builders
|
||||
*
|
||||
* Capabilities by cohort type:
|
||||
* - All/STH: activity (full), SOPR (rolling + adjusted), sell side risk, value (flows + breakdown), coins
|
||||
* - LTH: activity (full), SOPR (rolling), sell side risk, value (flows + breakdown), coins
|
||||
* - AgeRange/MaxAge: activity (basic), SOPR (24h only), value (no flows/breakdown), coins
|
||||
* - Others (UtxoAmount, Empty, Address): no activity, value only
|
||||
*/
|
||||
|
||||
import { Unit } from "../../utils/units.js";
|
||||
import {
|
||||
line,
|
||||
baseline,
|
||||
dotsBaseline,
|
||||
percentRatio,
|
||||
chartsFromCount,
|
||||
averagesArray,
|
||||
ROLLING_WINDOWS,
|
||||
} from "../series.js";
|
||||
import {
|
||||
satsBtcUsd,
|
||||
satsBtcUsdFullTree,
|
||||
mapCohortsWithAll,
|
||||
groupedWindowsCumulativeWithAll,
|
||||
groupedWindowsCumulativeSatsBtcUsd,
|
||||
} from "../shared.js";
|
||||
import { colors } from "../../utils/colors.js";
|
||||
|
||||
// ============================================================================
|
||||
// Shared Volume Helpers
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @param {TransferVolumePattern} tv
|
||||
* @param {Color} color
|
||||
* @param {(name: string) => string} title
|
||||
* @returns {PartialOptionsTree}
|
||||
*/
|
||||
function volumeTree(tv, color, title) {
|
||||
return [
|
||||
...satsBtcUsdFullTree({
|
||||
pattern: tv,
|
||||
title,
|
||||
metric: "Transfer Volume",
|
||||
color,
|
||||
}),
|
||||
{
|
||||
name: "Profitability",
|
||||
tree: [
|
||||
...ROLLING_WINDOWS.map((w) => ({
|
||||
name: w.name,
|
||||
title: title(`${w.title} Transfer Volume Profitability`),
|
||||
bottom: [
|
||||
...satsBtcUsd({
|
||||
pattern: tv.inProfit.sum[w.key],
|
||||
name: "In Profit",
|
||||
color: colors.profit,
|
||||
}),
|
||||
...satsBtcUsd({
|
||||
pattern: tv.inLoss.sum[w.key],
|
||||
name: "In Loss",
|
||||
color: colors.loss,
|
||||
}),
|
||||
],
|
||||
})),
|
||||
{
|
||||
name: "Cumulative",
|
||||
title: title("Cumulative Transfer Volume Profitability"),
|
||||
bottom: [
|
||||
...satsBtcUsd({
|
||||
pattern: tv.inProfit.cumulative,
|
||||
name: "In Profit",
|
||||
color: colors.profit,
|
||||
}),
|
||||
...satsBtcUsd({
|
||||
pattern: tv.inLoss.cumulative,
|
||||
name: "In Loss",
|
||||
color: colors.loss,
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "In Profit",
|
||||
tree: satsBtcUsdFullTree({
|
||||
pattern: tv.inProfit,
|
||||
title,
|
||||
metric: "Transfer Volume In Profit",
|
||||
color: colors.profit,
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "In Loss",
|
||||
tree: satsBtcUsdFullTree({
|
||||
pattern: tv.inLoss,
|
||||
title,
|
||||
metric: "Transfer Volume In Loss",
|
||||
color: colors.loss,
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{ transferVolume: TransferVolumePattern }} activity
|
||||
* @param {Color} color
|
||||
* @param {(name: string) => string} title
|
||||
* @returns {PartialOptionsGroup}
|
||||
*/
|
||||
function volumeFolder(activity, color, title) {
|
||||
return { name: "Volume", tree: volumeTree(activity.transferVolume, color, title) };
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{ transferVolume: TransferVolumePattern }} activity
|
||||
* @param {CountPattern<number>} adjustedTransferVolume
|
||||
* @param {Color} color
|
||||
* @param {(name: string) => string} title
|
||||
* @returns {PartialOptionsGroup}
|
||||
*/
|
||||
function volumeFolderWithAdjusted(activity, adjustedTransferVolume, color, title) {
|
||||
return {
|
||||
name: "Volume",
|
||||
tree: [
|
||||
...volumeTree(activity.transferVolume, color, title),
|
||||
{ name: "Adjusted", tree: chartsFromCount({ pattern: adjustedTransferVolume, title, metric: "Adjusted Transfer Volume", unit: Unit.usd }) },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Shared SOPR Helpers
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @param {RollingWindowPattern<number>} ratio
|
||||
* @param {(name: string) => string} title
|
||||
* @param {string} [prefix]
|
||||
* @returns {PartialOptionsTree}
|
||||
*/
|
||||
function singleRollingSoprTree(ratio, title, prefix = "") {
|
||||
return [
|
||||
{
|
||||
name: "Compare",
|
||||
title: title(`${prefix}SOPR`),
|
||||
bottom: ROLLING_WINDOWS.map((w) =>
|
||||
baseline({
|
||||
series: ratio[w.key],
|
||||
name: w.name,
|
||||
color: w.color,
|
||||
unit: Unit.ratio,
|
||||
base: 1,
|
||||
}),
|
||||
),
|
||||
},
|
||||
...ROLLING_WINDOWS.map((w) => ({
|
||||
name: w.name,
|
||||
title: title(`${w.title} ${prefix}SOPR`.trim()),
|
||||
bottom: [
|
||||
baseline({
|
||||
series: ratio[w.key],
|
||||
name: "SOPR",
|
||||
unit: Unit.ratio,
|
||||
base: 1,
|
||||
}),
|
||||
],
|
||||
})),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {CountPattern<number>} valueDestroyed
|
||||
* @param {(name: string) => string} title
|
||||
* @returns {PartialOptionsTree}
|
||||
*/
|
||||
function valueDestroyedTree(valueDestroyed, title) {
|
||||
return chartsFromCount({ pattern: valueDestroyed, title, metric: "Value Destroyed", unit: Unit.usd });
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {CountPattern<number>} valueDestroyed
|
||||
* @param {(name: string) => string} title
|
||||
* @returns {PartialOptionsGroup}
|
||||
*/
|
||||
function valueDestroyedFolder(valueDestroyed, title) {
|
||||
return { name: "Value Destroyed", tree: valueDestroyedTree(valueDestroyed, title) };
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {CountPattern<number>} valueDestroyed
|
||||
* @param {CountPattern<number>} adjusted
|
||||
* @param {(name: string) => string} title
|
||||
* @returns {PartialOptionsGroup}
|
||||
*/
|
||||
function valueDestroyedFolderWithAdjusted(valueDestroyed, adjusted, title) {
|
||||
return {
|
||||
name: "Value Destroyed",
|
||||
tree: [
|
||||
...valueDestroyedTree(valueDestroyed, title),
|
||||
{ name: "Adjusted", tree: chartsFromCount({ pattern: adjusted, title, metric: "Adjusted Value Destroyed", unit: Unit.usd }) },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Shared Sell Side Risk Helpers
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @param {SellSideRiskPattern} sellSideRisk
|
||||
* @param {(name: string) => string} title
|
||||
* @returns {PartialOptionsTree}
|
||||
*/
|
||||
function singleSellSideRiskTree(sellSideRisk, title) {
|
||||
return [
|
||||
{
|
||||
name: "Compare",
|
||||
title: title("Sell Side Risk"),
|
||||
bottom: ROLLING_WINDOWS.flatMap((w) =>
|
||||
percentRatio({
|
||||
pattern: sellSideRisk[w.key],
|
||||
name: w.name,
|
||||
color: w.color,
|
||||
}),
|
||||
),
|
||||
},
|
||||
...ROLLING_WINDOWS.map((w) => ({
|
||||
name: w.name,
|
||||
title: title(`${w.title} Sell Side Risk`),
|
||||
bottom: percentRatio({
|
||||
pattern: sellSideRisk[w.key],
|
||||
name: "Sell Side Risk",
|
||||
color: w.color,
|
||||
}),
|
||||
})),
|
||||
];
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Single Cohort Activity Sections
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Single activity tree items shared between WithAdjusted and basic
|
||||
* @param {CohortAll | CohortFull | CohortLongTerm} cohort
|
||||
* @param {(name: string) => string} title
|
||||
* @param {PartialOptionsGroup} volumeItem
|
||||
* @param {PartialOptionsGroup} soprFolder
|
||||
* @param {PartialOptionsGroup} valueDestroyedItem
|
||||
* @returns {PartialOptionsTree}
|
||||
*/
|
||||
function singleFullActivityTree(cohort, title, volumeItem, soprFolder, valueDestroyedItem) {
|
||||
const { tree, color } = cohort;
|
||||
return [
|
||||
volumeItem,
|
||||
soprFolder,
|
||||
valueDestroyedItem,
|
||||
{
|
||||
name: "Coindays Destroyed",
|
||||
tree: chartsFromCount({
|
||||
pattern: tree.activity.coindaysDestroyed,
|
||||
title,
|
||||
metric: "Coindays Destroyed",
|
||||
unit: Unit.coindays,
|
||||
color,
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "Dormancy",
|
||||
tree: averagesArray({
|
||||
windows: tree.activity.dormancy,
|
||||
title,
|
||||
metric: "Dormancy",
|
||||
unit: Unit.days,
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "Sell Side Risk",
|
||||
tree: singleSellSideRiskTree(tree.realized.sellSideRiskRatio, title),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** @param {{ cohort: CohortAll | CohortFull, title: (name: string) => string }} args */
|
||||
export function createActivitySectionWithAdjusted({ cohort, title }) {
|
||||
const { tree, color } = cohort;
|
||||
const sopr = tree.realized.sopr;
|
||||
return {
|
||||
name: "Activity",
|
||||
tree: singleFullActivityTree(cohort, title,
|
||||
volumeFolderWithAdjusted(tree.activity, sopr.adjusted.transferVolume, color, title),
|
||||
{
|
||||
name: "SOPR",
|
||||
tree: [
|
||||
...singleRollingSoprTree(sopr.ratio, title),
|
||||
{ name: "Adjusted", tree: singleRollingSoprTree(sopr.adjusted.ratio, title, "Adjusted ") },
|
||||
],
|
||||
},
|
||||
valueDestroyedFolderWithAdjusted(sopr.valueDestroyed, sopr.adjusted.valueDestroyed, title),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/** @param {{ cohort: CohortFull | CohortLongTerm, title: (name: string) => string }} args */
|
||||
export function createActivitySection({ cohort, title }) {
|
||||
const { tree, color } = cohort;
|
||||
return {
|
||||
name: "Activity",
|
||||
tree: singleFullActivityTree(cohort, title,
|
||||
volumeFolder(tree.activity, color, title),
|
||||
{ name: "SOPR", tree: singleRollingSoprTree(tree.realized.sopr.ratio, title) },
|
||||
valueDestroyedFolder(tree.realized.sopr.valueDestroyed, title),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Activity section for cohorts with activity but basic realized (AgeRange/MaxAge — 24h SOPR only)
|
||||
* @param {{ cohort: CohortAgeRange | CohortWithAdjusted, title: (name: string) => string }} args
|
||||
* @returns {PartialOptionsGroup}
|
||||
*/
|
||||
export function createActivitySectionWithActivity({ cohort, title }) {
|
||||
const { tree, color } = cohort;
|
||||
const sopr = tree.realized.sopr;
|
||||
|
||||
return {
|
||||
name: "Activity",
|
||||
tree: [
|
||||
volumeFolder(tree.activity, color, title),
|
||||
{
|
||||
name: "SOPR",
|
||||
title: title("SOPR (24h)"),
|
||||
bottom: [
|
||||
dotsBaseline({
|
||||
series: sopr.ratio._24h,
|
||||
name: "SOPR",
|
||||
unit: Unit.ratio,
|
||||
base: 1,
|
||||
}),
|
||||
],
|
||||
},
|
||||
valueDestroyedFolder(sopr.valueDestroyed, title),
|
||||
{
|
||||
name: "Coindays Destroyed",
|
||||
tree: chartsFromCount({
|
||||
pattern: tree.activity.coindaysDestroyed,
|
||||
title,
|
||||
metric: "Coindays Destroyed",
|
||||
unit: Unit.coindays,
|
||||
color,
|
||||
}),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimal activity section: volume only
|
||||
* @param {{ cohort: CohortBasicWithMarketCap | CohortBasicWithoutMarketCap | CohortWithoutRelative | CohortAddr | AddrCohortObject, title: (name: string) => string }} args
|
||||
* @returns {PartialOptionsGroup}
|
||||
*/
|
||||
export function createActivitySectionMinimal({ cohort, title }) {
|
||||
return {
|
||||
name: "Activity",
|
||||
tree: satsBtcUsdFullTree({
|
||||
pattern: cohort.tree.activity.transferVolume,
|
||||
title,
|
||||
metric: "Transfer Volume",
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Grouped minimal activity: volume
|
||||
* @param {{ list: readonly (UtxoCohortObject | CohortWithoutRelative | CohortAddr | AddrCohortObject)[], all: CohortAll, title: (name: string) => string }} args
|
||||
* @returns {PartialOptionsGroup}
|
||||
*/
|
||||
export function createGroupedActivitySectionMinimal({ list, all, title }) {
|
||||
return {
|
||||
name: "Activity",
|
||||
tree: groupedWindowsCumulativeSatsBtcUsd({
|
||||
list, all, title, metricTitle: "Transfer Volume",
|
||||
getMetric: (c) => c.tree.activity.transferVolume,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Grouped profitability folder (compare + in profit + in loss)
|
||||
* @template {{ name: string, color: Color }} T
|
||||
* @template {{ name: string, color: Color }} A
|
||||
* @param {readonly T[]} list
|
||||
* @param {A} all
|
||||
* @param {(name: string) => string} title
|
||||
* @param {(c: T | A) => { sum: Record<string, AnyValuePattern>, cumulative: AnyValuePattern }} getInProfit
|
||||
* @param {(c: T | A) => { sum: Record<string, AnyValuePattern>, cumulative: AnyValuePattern }} getInLoss
|
||||
* @returns {PartialOptionsTree}
|
||||
*/
|
||||
function groupedProfitabilityArray(list, all, title, getInProfit, getInLoss) {
|
||||
return [
|
||||
{
|
||||
name: "In Profit",
|
||||
tree: groupedWindowsCumulativeSatsBtcUsd({
|
||||
list,
|
||||
all,
|
||||
title,
|
||||
metricTitle: "Transfer Volume In Profit",
|
||||
getMetric: (c) => getInProfit(c),
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "In Loss",
|
||||
tree: groupedWindowsCumulativeSatsBtcUsd({
|
||||
list,
|
||||
all,
|
||||
title,
|
||||
metricTitle: "Transfer Volume In Loss",
|
||||
getMetric: (c) => getInLoss(c),
|
||||
}),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @template {{ name: string, color: Color }} T
|
||||
* @template {{ name: string, color: Color }} A
|
||||
* @param {readonly T[]} list
|
||||
* @param {A} all
|
||||
* @param {(name: string) => string} title
|
||||
* @param {(c: T | A) => { sum: Record<string, AnyValuePattern>, cumulative: AnyValuePattern, inProfit: { sum: Record<string, AnyValuePattern>, cumulative: AnyValuePattern }, inLoss: { sum: Record<string, AnyValuePattern>, cumulative: AnyValuePattern } }} getTransferVolume
|
||||
* @returns {PartialOptionsTree}
|
||||
*/
|
||||
function groupedVolumeTree(list, all, title, getTransferVolume) {
|
||||
return [
|
||||
...groupedWindowsCumulativeSatsBtcUsd({
|
||||
list,
|
||||
all,
|
||||
title,
|
||||
metricTitle: "Transfer Volume",
|
||||
getMetric: (c) => getTransferVolume(c),
|
||||
}),
|
||||
...groupedProfitabilityArray(
|
||||
list,
|
||||
all,
|
||||
title,
|
||||
(c) => getTransferVolume(c).inProfit,
|
||||
(c) => getTransferVolume(c).inLoss,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @template {{ name: string, color: Color }} T
|
||||
* @template {{ name: string, color: Color }} A
|
||||
* @param {readonly T[]} list
|
||||
* @param {A} all
|
||||
* @param {(name: string) => string} title
|
||||
* @param {(c: T | A) => { sum: Record<string, AnyValuePattern>, cumulative: AnyValuePattern, inProfit: { sum: Record<string, AnyValuePattern>, cumulative: AnyValuePattern }, inLoss: { sum: Record<string, AnyValuePattern>, cumulative: AnyValuePattern } }} getTransferVolume
|
||||
* @returns {PartialOptionsGroup}
|
||||
*/
|
||||
function groupedVolumeFolder(list, all, title, getTransferVolume) {
|
||||
return { name: "Volume", tree: groupedVolumeTree(list, all, title, getTransferVolume) };
|
||||
}
|
||||
|
||||
/**
|
||||
* @template {{ name: string, color: Color }} T
|
||||
* @template {{ name: string, color: Color }} A
|
||||
* @param {readonly T[]} list
|
||||
* @param {A} all
|
||||
* @param {(name: string) => string} title
|
||||
* @param {(c: T | A) => { sum: Record<string, AnyValuePattern>, cumulative: AnyValuePattern, inProfit: { sum: Record<string, AnyValuePattern>, cumulative: AnyValuePattern }, inLoss: { sum: Record<string, AnyValuePattern>, cumulative: AnyValuePattern } }} getTransferVolume
|
||||
* @param {(c: T | A) => CountPattern<number>} getAdjustedTransferVolume
|
||||
* @returns {PartialOptionsGroup}
|
||||
*/
|
||||
function groupedVolumeFolderWithAdjusted(list, all, title, getTransferVolume, getAdjustedTransferVolume) {
|
||||
return {
|
||||
name: "Volume",
|
||||
tree: [
|
||||
...groupedVolumeTree(list, all, title, getTransferVolume),
|
||||
{
|
||||
name: "Adjusted",
|
||||
tree: groupedWindowsCumulativeWithAll({
|
||||
list, all, title, metricTitle: "Adjusted Transfer Volume",
|
||||
getWindowSeries: (c, key) => getAdjustedTransferVolume(c).sum[key],
|
||||
getCumulativeSeries: (c) => getAdjustedTransferVolume(c).cumulative,
|
||||
seriesFn: line, unit: Unit.usd,
|
||||
}),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Grouped SOPR Helpers
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @template {{ color: Color, name: string }} T
|
||||
* @template {{ color: Color, name: string }} A
|
||||
* @param {readonly T[]} list
|
||||
* @param {A} all
|
||||
* @param {(item: T | A) => { _24h: AnySeriesPattern, _1w: AnySeriesPattern, _1m: AnySeriesPattern, _1y: AnySeriesPattern }} getRatio
|
||||
* @param {(name: string) => string} title
|
||||
* @param {string} [prefix]
|
||||
* @returns {PartialOptionsTree}
|
||||
*/
|
||||
function groupedSoprCharts(list, all, getRatio, title, prefix = "") {
|
||||
return ROLLING_WINDOWS.map((w) => ({
|
||||
name: w.name,
|
||||
title: title(`${w.title} ${prefix}SOPR`.trim()),
|
||||
bottom: mapCohortsWithAll(list, all, (c) =>
|
||||
baseline({
|
||||
series: getRatio(c)[w.key],
|
||||
name: c.name,
|
||||
color: c.color,
|
||||
unit: Unit.ratio,
|
||||
base: 1,
|
||||
}),
|
||||
),
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* @template {{ name: string, color: Color }} T
|
||||
* @template {{ name: string, color: Color }} A
|
||||
* @param {readonly T[]} list
|
||||
* @param {A} all
|
||||
* @param {(name: string) => string} title
|
||||
* @param {(c: T | A) => CountPattern<number>} getValueDestroyed
|
||||
* @returns {PartialOptionsTree}
|
||||
*/
|
||||
function groupedValueDestroyedTree(list, all, title, getValueDestroyed) {
|
||||
return groupedWindowsCumulativeWithAll({
|
||||
list, all, title, metricTitle: "Value Destroyed",
|
||||
getWindowSeries: (c, key) => getValueDestroyed(c).sum[key],
|
||||
getCumulativeSeries: (c) => getValueDestroyed(c).cumulative,
|
||||
seriesFn: line, unit: Unit.usd,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @template {{ name: string, color: Color }} T
|
||||
* @template {{ name: string, color: Color }} A
|
||||
* @param {readonly T[]} list
|
||||
* @param {A} all
|
||||
* @param {(name: string) => string} title
|
||||
* @param {(c: T | A) => CountPattern<number>} getValueDestroyed
|
||||
* @returns {PartialOptionsGroup}
|
||||
*/
|
||||
function groupedValueDestroyedFolder(list, all, title, getValueDestroyed) {
|
||||
return { name: "Value Destroyed", tree: groupedValueDestroyedTree(list, all, title, getValueDestroyed) };
|
||||
}
|
||||
|
||||
/**
|
||||
* @template {{ name: string, color: Color }} T
|
||||
* @template {{ name: string, color: Color }} A
|
||||
* @param {readonly T[]} list
|
||||
* @param {A} all
|
||||
* @param {(name: string) => string} title
|
||||
* @param {(c: T | A) => CountPattern<number>} getValueDestroyed
|
||||
* @param {(c: T | A) => CountPattern<number>} getAdjustedValueDestroyed
|
||||
* @returns {PartialOptionsGroup}
|
||||
*/
|
||||
function groupedValueDestroyedFolderWithAdjusted(list, all, title, getValueDestroyed, getAdjustedValueDestroyed) {
|
||||
return {
|
||||
name: "Value Destroyed",
|
||||
tree: [
|
||||
...groupedValueDestroyedTree(list, all, title, getValueDestroyed),
|
||||
{ name: "Adjusted", tree: groupedValueDestroyedTree(list, all, title, getAdjustedValueDestroyed) },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Grouped Activity Sections
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Grouped activity tree items shared between WithAdjusted and basic
|
||||
* @param {readonly (CohortFull | CohortLongTerm)[]} list
|
||||
* @param {CohortAll} all
|
||||
* @param {(name: string) => string} title
|
||||
* @param {PartialOptionsGroup} volumeItem
|
||||
* @param {PartialOptionsGroup} soprFolder
|
||||
* @param {PartialOptionsGroup} valueDestroyedItem
|
||||
* @returns {PartialOptionsTree}
|
||||
*/
|
||||
function groupedFullActivityTree(list, all, title, volumeItem, soprFolder, valueDestroyedItem) {
|
||||
return [
|
||||
volumeItem,
|
||||
soprFolder,
|
||||
valueDestroyedItem,
|
||||
...groupedActivitySharedItems(list, all, title),
|
||||
];
|
||||
}
|
||||
|
||||
/** @param {{ list: readonly CohortFull[], all: CohortAll, title: (name: string) => string }} args */
|
||||
export function createGroupedActivitySectionWithAdjusted({ list, all, title }) {
|
||||
return {
|
||||
name: "Activity",
|
||||
tree: groupedFullActivityTree(list, all, title,
|
||||
groupedVolumeFolderWithAdjusted(list, all, title, (c) => c.tree.activity.transferVolume, (c) => c.tree.realized.sopr.adjusted.transferVolume),
|
||||
{
|
||||
name: "SOPR",
|
||||
tree: [
|
||||
...groupedSoprCharts(list, all, (c) => c.tree.realized.sopr.ratio, title),
|
||||
{ name: "Adjusted", tree: groupedSoprCharts(list, all, (c) => c.tree.realized.sopr.adjusted.ratio, title, "Adjusted ") },
|
||||
],
|
||||
},
|
||||
groupedValueDestroyedFolderWithAdjusted(list, all, title, (c) => c.tree.realized.sopr.valueDestroyed, (c) => c.tree.realized.sopr.adjusted.valueDestroyed),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/** @param {{ list: readonly (CohortFull | CohortLongTerm)[], all: CohortAll, title: (name: string) => string }} args */
|
||||
export function createGroupedActivitySection({ list, all, title }) {
|
||||
return {
|
||||
name: "Activity",
|
||||
tree: groupedFullActivityTree(list, all, title,
|
||||
groupedVolumeFolder(list, all, title, (c) => c.tree.activity.transferVolume),
|
||||
{ name: "SOPR", tree: groupedSoprCharts(list, all, (c) => c.tree.realized.sopr.ratio, title) },
|
||||
groupedValueDestroyedFolder(list, all, title, (c) => c.tree.realized.sopr.valueDestroyed),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared grouped activity items: coindays, dormancy, sell side risk
|
||||
* @param {readonly (CohortFull | CohortLongTerm)[]} list
|
||||
* @param {CohortAll} all
|
||||
* @param {(name: string) => string} title
|
||||
* @returns {PartialOptionsTree}
|
||||
*/
|
||||
function groupedActivitySharedItems(list, all, title) {
|
||||
return [
|
||||
{
|
||||
name: "Coindays Destroyed",
|
||||
tree: groupedWindowsCumulativeWithAll({
|
||||
list,
|
||||
all,
|
||||
title,
|
||||
metricTitle: "Coindays Destroyed",
|
||||
getWindowSeries: (c, key) => c.tree.activity.coindaysDestroyed.sum[key],
|
||||
getCumulativeSeries: (c) =>
|
||||
c.tree.activity.coindaysDestroyed.cumulative,
|
||||
seriesFn: line,
|
||||
unit: Unit.coindays,
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "Dormancy",
|
||||
tree: ROLLING_WINDOWS.map((w) => ({
|
||||
name: w.name,
|
||||
title: title(`${w.title} Dormancy`),
|
||||
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
line({
|
||||
series: tree.activity.dormancy[w.key],
|
||||
name,
|
||||
color,
|
||||
unit: Unit.days,
|
||||
}),
|
||||
),
|
||||
})),
|
||||
},
|
||||
{
|
||||
name: "Sell Side Risk",
|
||||
tree: ROLLING_WINDOWS.map((w) => ({
|
||||
name: w.name,
|
||||
title: title(`${w.title} Sell Side Risk`),
|
||||
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
line({
|
||||
series: tree.realized.sellSideRiskRatio[w.key].ratio,
|
||||
name,
|
||||
color,
|
||||
unit: Unit.ratio,
|
||||
}),
|
||||
),
|
||||
})),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Grouped activity for cohorts with activity but basic realized (AgeRange/MaxAge)
|
||||
* @param {{ list: readonly (CohortAgeRange | CohortWithAdjusted)[], all: CohortAll, title: (name: string) => string }} args
|
||||
* @returns {PartialOptionsGroup}
|
||||
*/
|
||||
export function createGroupedActivitySectionWithActivity({ list, all, title }) {
|
||||
return {
|
||||
name: "Activity",
|
||||
tree: [
|
||||
groupedVolumeFolder(list, all, title, (c) => c.tree.activity.transferVolume),
|
||||
{
|
||||
name: "SOPR",
|
||||
title: title("SOPR (24h)"),
|
||||
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
baseline({
|
||||
series: tree.realized.sopr.ratio._24h,
|
||||
name,
|
||||
color,
|
||||
unit: Unit.ratio,
|
||||
base: 1,
|
||||
}),
|
||||
),
|
||||
},
|
||||
groupedValueDestroyedFolder(list, all, title, (c) => c.tree.realized.sopr.valueDestroyed),
|
||||
{
|
||||
name: "Coindays Destroyed",
|
||||
tree: [
|
||||
...ROLLING_WINDOWS.map((w) => ({
|
||||
name: w.name,
|
||||
title: title(`${w.title} Coindays Destroyed`),
|
||||
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
line({
|
||||
series: tree.activity.coindaysDestroyed.sum[w.key],
|
||||
name,
|
||||
color,
|
||||
unit: Unit.coindays,
|
||||
}),
|
||||
),
|
||||
})),
|
||||
{
|
||||
name: "Cumulative",
|
||||
title: title("Cumulative Coindays Destroyed"),
|
||||
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
line({
|
||||
series: tree.activity.coindaysDestroyed.cumulative,
|
||||
name,
|
||||
color,
|
||||
unit: Unit.coindays,
|
||||
}),
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -1,211 +0,0 @@
|
||||
/**
|
||||
* Cost Basis section builders
|
||||
*
|
||||
* Structure:
|
||||
* - Per Coin: sats-weighted (profitability + distribution)
|
||||
* - Per Dollar: value-weighted (profitability + distribution)
|
||||
* - Profitability: cross-cutting (per coin + per dollar on same chart)
|
||||
* - Supply Density: cost basis supply density percentage
|
||||
*
|
||||
* Only for cohorts WITH costBasis (All, STH, LTH)
|
||||
*/
|
||||
|
||||
import { colors } from "../../utils/colors.js";
|
||||
import { entries } from "../../utils/array.js";
|
||||
import { price, percentRatio } from "../series.js";
|
||||
import { mapCohortsWithAll, flatMapCohortsWithAll } from "../shared.js";
|
||||
|
||||
const ACTIVE_PCTS = new Set(["pct75", "pct50", "pct25"]);
|
||||
|
||||
/**
|
||||
* @param {PercentilesPattern} p
|
||||
* @param {(name: string) => string} [n]
|
||||
* @returns {FetchedPriceSeriesBlueprint[]}
|
||||
*/
|
||||
function percentileSeries(p, n = (x) => x) {
|
||||
return entries(p)
|
||||
.reverse()
|
||||
.map(([key, s], i, arr) =>
|
||||
price({
|
||||
series: s,
|
||||
name: n(key.replace("pct", "P")),
|
||||
color: colors.at(i, arr.length),
|
||||
...(ACTIVE_PCTS.has(key) ? {} : { defaultActive: false }),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Single cohort helpers
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Per Coin or Per Dollar folder for a single cohort
|
||||
* @param {Object} args
|
||||
* @param {AnyPricePattern} args.avgPrice - realized price (per coin) or capitalized price (per dollar)
|
||||
* @param {string} args.avgName
|
||||
* @param {AnyPricePattern} args.inProfit
|
||||
* @param {AnyPricePattern} args.inLoss
|
||||
* @param {PercentilesPattern} args.percentiles
|
||||
* @param {Color} args.color
|
||||
* @param {string} args.weightLabel
|
||||
* @param {(name: string) => string} args.title
|
||||
* @param {AnyPricePattern} [args.min]
|
||||
* @param {AnyPricePattern} [args.max]
|
||||
* @returns {PartialOptionsTree}
|
||||
*/
|
||||
function singleWeightFolder({ avgPrice, avgName, inProfit, inLoss, percentiles, color, weightLabel, title, min, max }) {
|
||||
return [
|
||||
{
|
||||
name: "Average",
|
||||
title: title(`Cost Basis Average (${weightLabel})`),
|
||||
top: [
|
||||
price({ series: inProfit, name: "In Profit", color: colors.profit }),
|
||||
price({ series: avgPrice, name: avgName, color }),
|
||||
price({ series: inLoss, name: "In Loss", color: colors.loss }),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Distribution",
|
||||
title: title(`Cost Basis Distribution (${weightLabel})`),
|
||||
top: [
|
||||
price({ series: avgPrice, name: avgName, color }),
|
||||
...(max ? [price({ series: max, name: "P100", color: colors.stat.max, defaultActive: false })] : []),
|
||||
...percentileSeries(percentiles),
|
||||
...(min ? [price({ series: min, name: "P0", color: colors.stat.min, defaultActive: false })] : []),
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{ cohort: CohortAll | CohortFull | CohortLongTerm, title: (name: string) => string }} args
|
||||
* @returns {PartialOptionsGroup}
|
||||
*/
|
||||
export function createCostBasisSectionWithPercentiles({ cohort, title }) {
|
||||
const { tree, color } = cohort;
|
||||
const cb = tree.costBasis;
|
||||
return {
|
||||
name: "Cost Basis",
|
||||
tree: [
|
||||
{
|
||||
name: "Per Coin",
|
||||
tree: singleWeightFolder({
|
||||
avgPrice: tree.realized.price, avgName: "All",
|
||||
inProfit: cb.inProfit.perCoin, inLoss: cb.inLoss.perCoin,
|
||||
percentiles: cb.perCoin, color, weightLabel: "BTC-weighted", title,
|
||||
min: cb.min, max: cb.max,
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "Per Dollar",
|
||||
tree: singleWeightFolder({
|
||||
avgPrice: tree.realized.capitalized.price, avgName: "All",
|
||||
inProfit: cb.inProfit.perDollar, inLoss: cb.inLoss.perDollar,
|
||||
percentiles: cb.perDollar, color, weightLabel: "USD-weighted", title,
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "Supply Density",
|
||||
title: title("Cost Basis Supply Density"),
|
||||
bottom: percentRatio({ pattern: cb.supplyDensity, name: "Supply Density", color: colors.bitcoin }),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Grouped cohort helpers
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Per Coin or Per Dollar folder for grouped cohorts
|
||||
* @param {Object} args
|
||||
* @param {readonly (CohortAll | CohortFull | CohortLongTerm)[]} args.list
|
||||
* @param {CohortAll} args.all
|
||||
* @param {(c: CohortAll | CohortFull | CohortLongTerm) => AnyPricePattern} args.getAvgPrice
|
||||
* @param {(c: CohortAll | CohortFull | CohortLongTerm) => AnyPricePattern} args.getInProfit
|
||||
* @param {(c: CohortAll | CohortFull | CohortLongTerm) => AnyPricePattern} args.getInLoss
|
||||
* @param {(c: CohortAll | CohortFull | CohortLongTerm) => PercentilesPattern} args.getPercentiles
|
||||
* @param {string} args.avgTitle
|
||||
* @param {string} args.weightLabel
|
||||
* @param {(name: string) => string} args.title
|
||||
* @returns {PartialOptionsTree}
|
||||
*/
|
||||
function groupedWeightFolder({ list, all, getAvgPrice, getInProfit, getInLoss, getPercentiles, avgTitle, weightLabel, title }) {
|
||||
return [
|
||||
{
|
||||
name: "Average",
|
||||
title: title(`Cost Basis ${avgTitle} (${weightLabel})`),
|
||||
top: mapCohortsWithAll(list, all, (c) =>
|
||||
price({ series: getAvgPrice(c), name: c.name, color: c.color }),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "In Profit",
|
||||
title: title(`Cost Basis In Profit (${weightLabel})`),
|
||||
top: mapCohortsWithAll(list, all, (c) =>
|
||||
price({ series: getInProfit(c), name: c.name, color: c.color }),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "In Loss",
|
||||
title: title(`Cost Basis In Loss (${weightLabel})`),
|
||||
top: mapCohortsWithAll(list, all, (c) =>
|
||||
price({ series: getInLoss(c), name: c.name, color: c.color }),
|
||||
),
|
||||
},
|
||||
...(/** @type {const} */ ([
|
||||
["pct50", "Median"],
|
||||
["pct75", "Q3"],
|
||||
["pct25", "Q1"],
|
||||
])).map(([pct, label]) => ({
|
||||
name: label,
|
||||
title: title(`Cost Basis ${label} (${weightLabel})`),
|
||||
top: mapCohortsWithAll(list, all, (c) =>
|
||||
price({ series: getPercentiles(c)[pct], name: c.name, color: c.color }),
|
||||
),
|
||||
})),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{ list: readonly (CohortAll | CohortFull | CohortLongTerm)[], all: CohortAll, title: (name: string) => string }} args
|
||||
* @returns {PartialOptionsGroup}
|
||||
*/
|
||||
export function createGroupedCostBasisSectionWithPercentiles({ list, all, title }) {
|
||||
return {
|
||||
name: "Cost Basis",
|
||||
tree: [
|
||||
{
|
||||
name: "Per Coin",
|
||||
tree: groupedWeightFolder({
|
||||
list, all, title,
|
||||
getAvgPrice: (c) => c.tree.realized.price,
|
||||
getInProfit: (c) => c.tree.costBasis.inProfit.perCoin,
|
||||
getInLoss: (c) => c.tree.costBasis.inLoss.perCoin,
|
||||
getPercentiles: (c) => c.tree.costBasis.perCoin,
|
||||
avgTitle: "Average", weightLabel: "BTC-weighted",
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "Per Dollar",
|
||||
tree: groupedWeightFolder({
|
||||
list, all, title,
|
||||
getAvgPrice: (c) => c.tree.realized.capitalized.price,
|
||||
getInProfit: (c) => c.tree.costBasis.inProfit.perDollar,
|
||||
getInLoss: (c) => c.tree.costBasis.inLoss.perDollar,
|
||||
getPercentiles: (c) => c.tree.costBasis.perDollar,
|
||||
avgTitle: "Average", weightLabel: "USD-weighted",
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "Supply Density",
|
||||
title: title("Cost Basis Supply Density"),
|
||||
bottom: flatMapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
percentRatio({ pattern: tree.costBasis.supplyDensity, name, color }),
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -1,249 +0,0 @@
|
||||
import { colors } from "../../utils/colors.js";
|
||||
import { entries } from "../../utils/array.js";
|
||||
import { brk } from "../../utils/client.js";
|
||||
|
||||
/** @type {readonly AddressableType[]} */
|
||||
const ADDRESSABLE_TYPES = [
|
||||
"p2a",
|
||||
"p2tr",
|
||||
"p2wsh",
|
||||
"p2wpkh",
|
||||
"p2sh",
|
||||
"p2pkh",
|
||||
"p2pk33",
|
||||
"p2pk65",
|
||||
];
|
||||
|
||||
/** @type {(key: SpendableType) => key is AddressableType} */
|
||||
const isAddressable = (key) =>
|
||||
/** @type {readonly string[]} */ (ADDRESSABLE_TYPES).includes(key);
|
||||
|
||||
export function buildCohortData() {
|
||||
const utxoCohorts = brk.series.cohorts.utxo;
|
||||
const addressCohorts = brk.series.cohorts.addr;
|
||||
const { addrs } = brk.series;
|
||||
const {
|
||||
TERM_NAMES,
|
||||
EPOCH_NAMES,
|
||||
UNDER_AGE_NAMES,
|
||||
OVER_AGE_NAMES,
|
||||
AGE_RANGE_NAMES,
|
||||
OVER_AMOUNT_NAMES,
|
||||
UNDER_AMOUNT_NAMES,
|
||||
AMOUNT_RANGE_NAMES,
|
||||
SPENDABLE_TYPE_NAMES,
|
||||
CLASS_NAMES,
|
||||
PROFITABILITY_RANGE_NAMES,
|
||||
PROFIT_NAMES,
|
||||
LOSS_NAMES,
|
||||
} = brk;
|
||||
|
||||
const cohortAll = {
|
||||
name: "",
|
||||
title: "",
|
||||
color: colors.bitcoin,
|
||||
tree: utxoCohorts.all,
|
||||
addressCount: {
|
||||
base: addrs.funded.all,
|
||||
delta: addrs.delta.all,
|
||||
},
|
||||
avgAmount: addrs.avgAmount.all,
|
||||
};
|
||||
|
||||
const shortNames = TERM_NAMES.short;
|
||||
const termShort = {
|
||||
name: shortNames.short,
|
||||
title: shortNames.long,
|
||||
color: colors.term.short,
|
||||
tree: utxoCohorts.sth,
|
||||
};
|
||||
|
||||
const longNames = TERM_NAMES.long;
|
||||
const termLong = {
|
||||
name: longNames.short,
|
||||
title: longNames.long,
|
||||
color: colors.term.long,
|
||||
tree: utxoCohorts.lth,
|
||||
};
|
||||
|
||||
// Under age cohorts
|
||||
const underAge = entries(UNDER_AGE_NAMES).map(([key, names], i, arr) => ({
|
||||
name: names.short,
|
||||
title: `UTXOs ${names.long}`,
|
||||
color: colors.at(i, arr.length),
|
||||
tree: utxoCohorts.underAge[key],
|
||||
}));
|
||||
|
||||
// Over age cohorts
|
||||
const overAge = entries(OVER_AGE_NAMES).map(([key, names], i, arr) => ({
|
||||
name: names.short,
|
||||
title: `UTXOs ${names.long}`,
|
||||
color: colors.at(i, arr.length),
|
||||
tree: utxoCohorts.overAge[key],
|
||||
}));
|
||||
|
||||
const ageRange = entries(AGE_RANGE_NAMES).map(([key, names], i, arr) => ({
|
||||
name: names.short,
|
||||
title: `UTXOs ${names.long}`,
|
||||
color: colors.at(i, arr.length),
|
||||
tree: utxoCohorts.ageRange[key],
|
||||
matured: utxoCohorts.matured[key],
|
||||
}));
|
||||
|
||||
const epoch = entries(EPOCH_NAMES).map(([key, names], i, arr) => ({
|
||||
name: names.short,
|
||||
title: names.long,
|
||||
color: colors.at(i, arr.length),
|
||||
tree: utxoCohorts.epoch[key],
|
||||
}));
|
||||
|
||||
const utxosOverAmount = entries(OVER_AMOUNT_NAMES).map(
|
||||
([key, names], i, arr) => ({
|
||||
name: names.short,
|
||||
title: `UTXOs ${names.long}`,
|
||||
color: colors.at(i, arr.length),
|
||||
tree: utxoCohorts.overAmount[key],
|
||||
}),
|
||||
);
|
||||
|
||||
const addressesOverAmount = entries(OVER_AMOUNT_NAMES).map(
|
||||
([key, names], i, arr) => {
|
||||
const cohort = addressCohorts.overAmount[key];
|
||||
return {
|
||||
name: names.short,
|
||||
title: `Addresses ${names.long}`,
|
||||
color: colors.at(i, arr.length),
|
||||
tree: cohort,
|
||||
addressCount: cohort.addrCount,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
const utxosUnderAmount = entries(UNDER_AMOUNT_NAMES).map(
|
||||
([key, names], i, arr) => ({
|
||||
name: names.short,
|
||||
title: `UTXOs ${names.long}`,
|
||||
color: colors.at(i, arr.length),
|
||||
tree: utxoCohorts.underAmount[key],
|
||||
}),
|
||||
);
|
||||
|
||||
const addressesUnderAmount = entries(UNDER_AMOUNT_NAMES).map(
|
||||
([key, names], i, arr) => {
|
||||
const cohort = addressCohorts.underAmount[key];
|
||||
return {
|
||||
name: names.short,
|
||||
title: `Addresses ${names.long}`,
|
||||
color: colors.at(i, arr.length),
|
||||
tree: cohort,
|
||||
addressCount: cohort.addrCount,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
const utxosAmountRange = entries(AMOUNT_RANGE_NAMES).map(
|
||||
([key, names], i, arr) => ({
|
||||
name: names.short,
|
||||
title: `UTXOs ${names.long}`,
|
||||
color: colors.at(i, arr.length),
|
||||
tree: utxoCohorts.amountRange[key],
|
||||
}),
|
||||
);
|
||||
|
||||
const addressesAmountRange = entries(AMOUNT_RANGE_NAMES).map(
|
||||
([key, names], i, arr) => {
|
||||
const cohort = addressCohorts.amountRange[key];
|
||||
return {
|
||||
name: names.short,
|
||||
title: `Addresses ${names.long}`,
|
||||
color: colors.at(i, arr.length),
|
||||
tree: cohort,
|
||||
addressCount: cohort.addrCount,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
const typeAddressable = ADDRESSABLE_TYPES.map((key) => {
|
||||
const names = SPENDABLE_TYPE_NAMES[key];
|
||||
return {
|
||||
key,
|
||||
name: names.short,
|
||||
title: names.short,
|
||||
color: colors.scriptType[key],
|
||||
tree: utxoCohorts.type[key],
|
||||
addressCount: {
|
||||
base: addrs.funded[key],
|
||||
delta: addrs.delta[key],
|
||||
},
|
||||
avgAmount: addrs.avgAmount[key],
|
||||
exposed: addrs.exposed,
|
||||
reused: addrs.reused,
|
||||
respent: addrs.respent,
|
||||
};
|
||||
});
|
||||
|
||||
const typeOther = entries(SPENDABLE_TYPE_NAMES)
|
||||
.filter(([key]) => !isAddressable(key))
|
||||
.map(([key, names]) => ({
|
||||
key,
|
||||
name: names.short,
|
||||
title: names.short,
|
||||
color: colors.scriptType[key],
|
||||
tree: utxoCohorts.type[key],
|
||||
}));
|
||||
|
||||
const class_ = entries(CLASS_NAMES)
|
||||
.reverse()
|
||||
.map(([key, names], i, arr) => ({
|
||||
name: names.short,
|
||||
title: names.long,
|
||||
color: colors.at(i, arr.length),
|
||||
tree: utxoCohorts.class[key],
|
||||
}));
|
||||
|
||||
const { range, profit, loss } = utxoCohorts.profitability;
|
||||
|
||||
const profitabilityRange = entries(PROFITABILITY_RANGE_NAMES).map(
|
||||
([key, names], i, arr) => ({
|
||||
name: names.short,
|
||||
color: colors.at(i, arr.length),
|
||||
pattern: range[key],
|
||||
}),
|
||||
);
|
||||
|
||||
const profitabilityProfit = entries(PROFIT_NAMES).map(
|
||||
([key, names], i, arr) => ({
|
||||
name: names.short,
|
||||
color: colors.at(i, arr.length),
|
||||
pattern: profit[key],
|
||||
}),
|
||||
);
|
||||
|
||||
const profitabilityLoss = entries(LOSS_NAMES).map(([key, names], i, arr) => ({
|
||||
name: names.short,
|
||||
color: colors.at(i, arr.length),
|
||||
pattern: loss[key],
|
||||
}));
|
||||
|
||||
return {
|
||||
cohortAll,
|
||||
termShort,
|
||||
termLong,
|
||||
underAge,
|
||||
overAge,
|
||||
ageRange,
|
||||
epoch,
|
||||
utxosOverAmount,
|
||||
addressesOverAmount,
|
||||
utxosUnderAmount,
|
||||
addressesUnderAmount,
|
||||
utxosAmountRange,
|
||||
addressesAmountRange,
|
||||
typeAddressable,
|
||||
typeOther,
|
||||
class: class_,
|
||||
profitabilityRange,
|
||||
profitabilityProfit,
|
||||
profitabilityLoss,
|
||||
};
|
||||
}
|
||||
@@ -1,762 +0,0 @@
|
||||
/**
|
||||
* Holdings section builders
|
||||
*
|
||||
* Supply pattern capabilities by cohort type:
|
||||
* - DeltaHalfInRelTotalPattern2 (STH/LTH): inProfit + inLoss + dominance + share
|
||||
* - SeriesTree_Cohorts_Utxo_All_Supply (All): inProfit + inLoss + share (no dominance)
|
||||
* - DeltaHalfInRelTotalPattern (AgeRange/MaxAge/Epoch): inProfit + inLoss + dominance (no share)
|
||||
* - DeltaHalfInTotalPattern2 (Type.*): inProfit + inLoss (no rel)
|
||||
* - DeltaHalfTotalPattern (Empty/UtxoAmount/AddrAmount): total + half only
|
||||
*/
|
||||
|
||||
import { Unit } from "../../utils/units.js";
|
||||
import {
|
||||
ROLLING_WINDOWS,
|
||||
line,
|
||||
baseline,
|
||||
sumsTreeBaseline,
|
||||
amountSumsTreeBaseline,
|
||||
rollingPercentRatioTree,
|
||||
percentRatio,
|
||||
percentRatioBaseline,
|
||||
chartsFromCount,
|
||||
} from "../series.js";
|
||||
import {
|
||||
amountBaseline,
|
||||
satsBtcUsd,
|
||||
flatMapCohorts,
|
||||
mapCohortsWithAll,
|
||||
flatMapCohortsWithAll,
|
||||
groupedWindowsCumulativeWithAll,
|
||||
} from "../shared.js";
|
||||
import { colors } from "../../utils/colors.js";
|
||||
import { priceLine } from "../constants.js";
|
||||
|
||||
/**
|
||||
* Simple supply series (total + half only, no profit/loss)
|
||||
* @param {{ total: AnyValuePattern }} supply
|
||||
* @returns {AnyFetchedSeriesBlueprint[]}
|
||||
*/
|
||||
function simpleSupplySeries(supply) {
|
||||
return satsBtcUsd({
|
||||
pattern: supply.total,
|
||||
name: "Total",
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param {readonly (UtxoCohortObject | CohortWithoutRelative)[]} list
|
||||
* @param {CohortAll} all
|
||||
* @param {(name: string) => string} title
|
||||
*/
|
||||
function groupedOutputsFolder(list, all, title) {
|
||||
return {
|
||||
name: "Outputs",
|
||||
tree: [
|
||||
{
|
||||
name: "Unspent",
|
||||
tree: [
|
||||
{
|
||||
name: "Count",
|
||||
title: title("UTXO Count"),
|
||||
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
line({ series: tree.outputs.unspentCount.base, name, color, unit: Unit.count }),
|
||||
),
|
||||
},
|
||||
...groupedDeltaItems(list, all, (c) => c.tree.outputs.unspentCount.delta, Unit.count, title, "UTXO Count"),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Spent",
|
||||
tree: groupedWindowsCumulativeWithAll({
|
||||
list, all, title, metricTitle: "Spent UTXO Count",
|
||||
getWindowSeries: (c, key) => c.tree.outputs.spentCount.sum[key],
|
||||
getCumulativeSeries: (c) => c.tree.outputs.spentCount.cumulative,
|
||||
seriesFn: line, unit: Unit.count,
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "Spending Rate",
|
||||
title: title("Spending Rate"),
|
||||
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
line({ series: tree.outputs.spendingRate, name, color, unit: Unit.ratio }),
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{ absolute: { _24h: AnySeriesPattern, _1w: AnySeriesPattern, _1m: AnySeriesPattern, _1y: AnySeriesPattern }, rate: { _24h: { percent: AnySeriesPattern, ratio: AnySeriesPattern }, _1w: { percent: AnySeriesPattern, ratio: AnySeriesPattern }, _1m: { percent: AnySeriesPattern, ratio: AnySeriesPattern }, _1y: { percent: AnySeriesPattern, ratio: AnySeriesPattern } } }} delta
|
||||
* @param {Unit} unit
|
||||
* @param {(name: string) => string} title
|
||||
* @param {string} name
|
||||
* @returns {PartialOptionsTree}
|
||||
*/
|
||||
function singleDeltaItems(delta, unit, title, name) {
|
||||
return [
|
||||
{
|
||||
...sumsTreeBaseline({
|
||||
windows: delta.absolute,
|
||||
title,
|
||||
metric: `${name} Change`,
|
||||
unit,
|
||||
legend: "Change",
|
||||
}),
|
||||
name: "Change",
|
||||
},
|
||||
{
|
||||
...rollingPercentRatioTree({
|
||||
windows: delta.rate,
|
||||
title,
|
||||
metric: `${name} Growth Rate`,
|
||||
}),
|
||||
name: "Growth Rate",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @template {{ name: string, color: Color }} T
|
||||
* @template {{ name: string, color: Color }} A
|
||||
* @param {readonly T[]} list
|
||||
* @param {A} all
|
||||
* @param {(c: T | A) => DeltaPattern} getDelta
|
||||
* @param {Unit} unit
|
||||
* @param {(name: string) => string} title
|
||||
* @param {string} name
|
||||
* @returns {PartialOptionsTree}
|
||||
*/
|
||||
function groupedDeltaItems(list, all, getDelta, unit, title, name) {
|
||||
return [
|
||||
{
|
||||
name: "Change",
|
||||
tree: ROLLING_WINDOWS.map((w) => ({
|
||||
name: w.name,
|
||||
title: title(`${w.title} ${name} Change`),
|
||||
bottom: mapCohortsWithAll(list, all, (c) =>
|
||||
baseline({
|
||||
series: getDelta(c).absolute[w.key],
|
||||
name: c.name,
|
||||
color: c.color,
|
||||
unit,
|
||||
}),
|
||||
),
|
||||
})),
|
||||
},
|
||||
{
|
||||
name: "Growth Rate",
|
||||
tree: ROLLING_WINDOWS.map((w) => ({
|
||||
name: w.name,
|
||||
title: title(`${w.title} ${name} Growth Rate`),
|
||||
bottom: flatMapCohortsWithAll(list, all, (c) =>
|
||||
percentRatioBaseline({
|
||||
pattern: getDelta(c).rate[w.key],
|
||||
name: c.name,
|
||||
color: c.color,
|
||||
}),
|
||||
),
|
||||
})),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Amount-valued single-cohort delta: Change exposes sats + lazy btc per window.
|
||||
* @param {AmountDeltaPattern} delta
|
||||
* @param {(name: string) => string} title
|
||||
* @param {string} name
|
||||
* @returns {PartialOptionsTree}
|
||||
*/
|
||||
function singleAmountDeltaItems(delta, title, name) {
|
||||
return [
|
||||
{
|
||||
...amountSumsTreeBaseline({
|
||||
windows: delta.absolute,
|
||||
title,
|
||||
metric: `${name} Change`,
|
||||
legend: "Change",
|
||||
}),
|
||||
name: "Change",
|
||||
},
|
||||
{
|
||||
...rollingPercentRatioTree({
|
||||
windows: delta.rate,
|
||||
title,
|
||||
metric: `${name} Growth Rate`,
|
||||
}),
|
||||
name: "Growth Rate",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Amount-valued grouped-cohort delta: Change exposes sats + lazy btc per window.
|
||||
* @template {{ name: string, color: Color }} T
|
||||
* @template {{ name: string, color: Color }} A
|
||||
* @param {readonly T[]} list
|
||||
* @param {A} all
|
||||
* @param {(c: T | A) => AmountDeltaPattern} getDelta
|
||||
* @param {(name: string) => string} title
|
||||
* @param {string} name
|
||||
* @returns {PartialOptionsTree}
|
||||
*/
|
||||
function groupedAmountDeltaItems(list, all, getDelta, title, name) {
|
||||
return [
|
||||
{
|
||||
name: "Change",
|
||||
tree: ROLLING_WINDOWS.map((w) => ({
|
||||
name: w.name,
|
||||
title: title(`${w.title} ${name} Change`),
|
||||
bottom: flatMapCohortsWithAll(list, all, (c) =>
|
||||
amountBaseline({
|
||||
pattern: getDelta(c).absolute[w.key],
|
||||
name: c.name,
|
||||
color: c.color,
|
||||
}),
|
||||
),
|
||||
})),
|
||||
},
|
||||
{
|
||||
name: "Growth Rate",
|
||||
tree: ROLLING_WINDOWS.map((w) => ({
|
||||
name: w.name,
|
||||
title: title(`${w.title} ${name} Growth Rate`),
|
||||
bottom: flatMapCohortsWithAll(list, all, (c) =>
|
||||
percentRatioBaseline({
|
||||
pattern: getDelta(c).rate[w.key],
|
||||
name: c.name,
|
||||
color: c.color,
|
||||
}),
|
||||
),
|
||||
})),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Single Cohort Composable Builders
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Amount chart: total + halved + in profit + in loss in sats/btc/usd.
|
||||
* @param {{ total: AnyValuePattern, half: AnyValuePattern, inProfit: AnyValuePattern, inLoss: AnyValuePattern }} supply
|
||||
* @param {(name: string) => string} title
|
||||
* @returns {PartialChartOption}
|
||||
*/
|
||||
function profitabilityAmountChart(supply, title) {
|
||||
return {
|
||||
name: "Amount",
|
||||
title: title("Supply Profitability"),
|
||||
bottom: [
|
||||
...satsBtcUsd({ pattern: supply.total, name: "Total", color: colors.default }),
|
||||
...satsBtcUsd({ pattern: supply.inProfit, name: "In Profit", color: colors.profit }),
|
||||
...satsBtcUsd({ pattern: supply.inLoss, name: "In Loss", color: colors.loss }),
|
||||
...satsBtcUsd({ pattern: supply.half, name: "Halved", color: colors.gray, style: 4 }),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Composition chart: in profit / in loss as % of own supply.
|
||||
* @param {{ inProfit: { share: { percent: AnySeriesPattern, ratio: AnySeriesPattern } }, inLoss: { share: { percent: AnySeriesPattern, ratio: AnySeriesPattern } } }} supply
|
||||
* @param {(name: string) => string} title
|
||||
* @returns {PartialChartOption}
|
||||
*/
|
||||
function profitabilityCompositionChart(supply, title) {
|
||||
return {
|
||||
name: "Composition",
|
||||
title: title("Supply Profitability Composition"),
|
||||
bottom: [
|
||||
...percentRatio({ pattern: supply.inProfit.share, name: "In Profit", color: colors.profit }),
|
||||
...percentRatio({ pattern: supply.inLoss.share, name: "In Loss", color: colors.loss }),
|
||||
priceLine({ number: 100, color: colors.default, style: 0, unit: Unit.percentage }),
|
||||
priceLine({ number: 50, unit: Unit.percentage }),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param {{ dominance: PercentRatioPattern }} supply
|
||||
* @param {Color} color
|
||||
* @param {(name: string) => string} title
|
||||
* @returns {PartialChartOption}
|
||||
*/
|
||||
function dominanceChart(supply, color, title) {
|
||||
return {
|
||||
name: "Dominance",
|
||||
title: title("Supply Dominance"),
|
||||
bottom: percentRatio({ pattern: supply.dominance, name: "Dominance", color }),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {OutputsPattern} outputs
|
||||
* @param {Color} color
|
||||
* @param {(name: string) => string} title
|
||||
* @returns {PartialOptionsGroup}
|
||||
*/
|
||||
function outputsFolder(outputs, color, title) {
|
||||
return {
|
||||
name: "Outputs",
|
||||
tree: [
|
||||
countFolder(outputs.unspentCount, "Unspent", "UTXO Count", color, title),
|
||||
{
|
||||
name: "Spent",
|
||||
tree: chartsFromCount({ pattern: outputs.spentCount, title, metric: "Spent UTXO Count", unit: Unit.count, color }),
|
||||
},
|
||||
{
|
||||
name: "Spending Rate",
|
||||
title: title("Spending Rate"),
|
||||
bottom: [
|
||||
line({ series: outputs.spendingRate, name: "Rate", color, unit: Unit.ratio }),
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{ base: AnySeriesPattern, delta: DeltaPattern }} pattern
|
||||
* @param {string} name
|
||||
* @param {string} chartTitle
|
||||
* @param {Color} color
|
||||
* @param {(name: string) => string} title
|
||||
* @returns {PartialOptionsGroup}
|
||||
*/
|
||||
function countFolder(pattern, name, chartTitle, color, title) {
|
||||
return {
|
||||
name,
|
||||
tree: [
|
||||
{
|
||||
name: "Count",
|
||||
title: title(chartTitle),
|
||||
bottom: [
|
||||
line({
|
||||
series: pattern.base,
|
||||
name: "Count",
|
||||
color,
|
||||
unit: Unit.count,
|
||||
}),
|
||||
],
|
||||
},
|
||||
...singleDeltaItems(pattern.delta, Unit.count, title, chartTitle),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Single Cohort Holdings Sections
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @param {{ cohort: UtxoCohortObject | CohortWithoutRelative, title: (name: string) => string }} args
|
||||
* @returns {PartialOptionsTree}
|
||||
*/
|
||||
export function createHoldingsSection({ cohort, title }) {
|
||||
const { supply } = cohort.tree;
|
||||
return [
|
||||
{
|
||||
name: "Supply",
|
||||
tree: [
|
||||
{
|
||||
name: "Total",
|
||||
title: title("Supply"),
|
||||
bottom: simpleSupplySeries(supply),
|
||||
},
|
||||
dominanceChart(supply, cohort.color, title),
|
||||
...singleAmountDeltaItems(supply.delta, title, "Supply"),
|
||||
],
|
||||
},
|
||||
outputsFolder(cohort.tree.outputs, cohort.color, title),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{ cohort: CohortAll, title: (name: string) => string }} args
|
||||
* @returns {PartialOptionsTree}
|
||||
*/
|
||||
export function createHoldingsSectionAll({ cohort, title }) {
|
||||
const { supply } = cohort.tree;
|
||||
return [
|
||||
{
|
||||
name: "Supply",
|
||||
tree: [
|
||||
{
|
||||
name: "Total",
|
||||
title: title("Supply"),
|
||||
bottom: simpleSupplySeries(supply),
|
||||
},
|
||||
{
|
||||
name: "Profitability",
|
||||
tree: [
|
||||
profitabilityAmountChart(supply, title),
|
||||
profitabilityCompositionChart(supply, title),
|
||||
],
|
||||
},
|
||||
...singleAmountDeltaItems(supply.delta, title, "Supply"),
|
||||
],
|
||||
},
|
||||
outputsFolder(cohort.tree.outputs, cohort.color, title),
|
||||
countFolder(cohort.addressCount, "Addresses", "Address Count", cohort.color, title),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{ cohort: CohortFull | CohortLongTerm, title: (name: string) => string }} args
|
||||
* @returns {PartialOptionsTree}
|
||||
*/
|
||||
export function createHoldingsSectionWithRelative({ cohort, title }) {
|
||||
const { supply } = cohort.tree;
|
||||
return [
|
||||
{
|
||||
name: "Supply",
|
||||
tree: [
|
||||
{
|
||||
name: "Total",
|
||||
title: title("Supply"),
|
||||
bottom: simpleSupplySeries(supply),
|
||||
},
|
||||
dominanceChart(supply, cohort.color, title),
|
||||
{
|
||||
name: "Profitability",
|
||||
tree: [
|
||||
profitabilityAmountChart(supply, title),
|
||||
profitabilityCompositionChart(supply, title),
|
||||
],
|
||||
},
|
||||
...singleAmountDeltaItems(supply.delta, title, "Supply"),
|
||||
],
|
||||
},
|
||||
outputsFolder(cohort.tree.outputs, cohort.color, title),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{ cohort: CohortWithAdjusted | CohortAgeRange, title: (name: string) => string }} args
|
||||
* @returns {PartialOptionsTree}
|
||||
*/
|
||||
export function createHoldingsSectionWithOwnSupply({ cohort, title }) {
|
||||
const { supply } = cohort.tree;
|
||||
return [
|
||||
{
|
||||
name: "Supply",
|
||||
tree: [
|
||||
{
|
||||
name: "Total",
|
||||
title: title("Supply"),
|
||||
bottom: simpleSupplySeries(supply),
|
||||
},
|
||||
dominanceChart(supply, cohort.color, title),
|
||||
{
|
||||
name: "Profitability",
|
||||
tree: [profitabilityAmountChart(supply, title)],
|
||||
},
|
||||
...singleAmountDeltaItems(supply.delta, title, "Supply"),
|
||||
],
|
||||
},
|
||||
outputsFolder(cohort.tree.outputs, cohort.color, title),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{ cohort: CohortWithoutRelative, title: (name: string) => string }} args
|
||||
* @returns {PartialOptionsTree}
|
||||
*/
|
||||
export function createHoldingsSectionWithProfitLoss({ cohort, title }) {
|
||||
const { supply } = cohort.tree;
|
||||
return [
|
||||
{
|
||||
name: "Supply",
|
||||
tree: [
|
||||
{
|
||||
name: "Total",
|
||||
title: title("Supply"),
|
||||
bottom: simpleSupplySeries(supply),
|
||||
},
|
||||
dominanceChart(supply, cohort.color, title),
|
||||
{
|
||||
name: "Profitability",
|
||||
tree: [profitabilityAmountChart(supply, title)],
|
||||
},
|
||||
...singleAmountDeltaItems(supply.delta, title, "Supply"),
|
||||
],
|
||||
},
|
||||
outputsFolder(cohort.tree.outputs, cohort.color, title),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{ cohort: CohortAddr, title: (name: string) => string }} args
|
||||
* @returns {PartialOptionsTree}
|
||||
*/
|
||||
export function createHoldingsSectionAddress({ cohort, title }) {
|
||||
const { supply } = cohort.tree;
|
||||
return [
|
||||
{
|
||||
name: "Supply",
|
||||
tree: [
|
||||
{
|
||||
name: "Total",
|
||||
title: title("Supply"),
|
||||
bottom: simpleSupplySeries(supply),
|
||||
},
|
||||
dominanceChart(supply, cohort.color, title),
|
||||
{
|
||||
name: "Profitability",
|
||||
tree: [profitabilityAmountChart(supply, title)],
|
||||
},
|
||||
...singleAmountDeltaItems(supply.delta, title, "Supply"),
|
||||
],
|
||||
},
|
||||
outputsFolder(cohort.tree.outputs, cohort.color, title),
|
||||
countFolder(cohort.addressCount, "Addresses", "Address Count", cohort.color, title),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{ cohort: AddrCohortObject, title: (name: string) => string }} args
|
||||
* @returns {PartialOptionsTree}
|
||||
*/
|
||||
export function createHoldingsSectionAddressAmount({ cohort, title }) {
|
||||
const { supply } = cohort.tree;
|
||||
return [
|
||||
{
|
||||
name: "Supply",
|
||||
tree: [
|
||||
{
|
||||
name: "Total",
|
||||
title: title("Supply"),
|
||||
bottom: simpleSupplySeries(supply),
|
||||
},
|
||||
dominanceChart(supply, cohort.color, title),
|
||||
...singleAmountDeltaItems(supply.delta, title, "Supply"),
|
||||
],
|
||||
},
|
||||
outputsFolder(cohort.tree.outputs, cohort.color, title),
|
||||
countFolder(cohort.addressCount, "Addresses", "Address Count", cohort.color, title),
|
||||
];
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Grouped Cohort Supply Helpers
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @template {{ name: string, color: Color, tree: { supply: { total: AnyValuePattern } } }} T
|
||||
* @param {readonly T[]} list
|
||||
* @param {CohortAll} all
|
||||
* @param {(name: string) => string} title
|
||||
* @returns {PartialChartOption}
|
||||
*/
|
||||
function groupedSupplyTotal(list, all, title) {
|
||||
return { name: "Total", title: title("Supply"), bottom: flatMapCohortsWithAll(list, all, ({ name, color, tree }) => satsBtcUsd({ pattern: tree.supply.total, name, color })) };
|
||||
}
|
||||
|
||||
/**
|
||||
* @template {{ name: string, color: Color, tree: { supply: { inProfit: AnyValuePattern, inLoss: AnyValuePattern } } }} T
|
||||
* @param {readonly T[]} list
|
||||
* @param {CohortAll} all
|
||||
* @param {(name: string) => string} title
|
||||
* @returns {PartialOptionsTree}
|
||||
*/
|
||||
function groupedSupplyProfitLoss(list, all, title) {
|
||||
return [
|
||||
{ name: "In Profit", title: title("Supply In Profit"), bottom: flatMapCohortsWithAll(list, all, ({ name, color, tree }) => satsBtcUsd({ pattern: tree.supply.inProfit, name, color })) },
|
||||
{ name: "In Loss", title: title("Supply In Loss"), bottom: flatMapCohortsWithAll(list, all, ({ name, color, tree }) => satsBtcUsd({ pattern: tree.supply.inLoss, name, color })) },
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @template {{ name: string, color: Color, tree: { supply: { dominance: PercentRatioPattern } } }} T
|
||||
* @param {readonly T[]} list
|
||||
* @param {(name: string) => string} title
|
||||
* @returns {PartialChartOption}
|
||||
*/
|
||||
function groupedDominanceChart(list, title) {
|
||||
return {
|
||||
name: "Dominance",
|
||||
title: title("Supply Dominance"),
|
||||
bottom: flatMapCohorts(list, ({ name, color, tree }) =>
|
||||
percentRatio({ pattern: tree.supply.dominance, name, color }),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Grouped Cohort Holdings Sections
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @param {{ list: readonly CohortAddr[], all: CohortAll, title: (name: string) => string }} args
|
||||
* @returns {PartialOptionsTree}
|
||||
*/
|
||||
export function createGroupedHoldingsSectionAddress({ list, all, title }) {
|
||||
return [
|
||||
{
|
||||
name: "Supply",
|
||||
tree: [
|
||||
groupedSupplyTotal(list, all, title),
|
||||
groupedDominanceChart(list, title),
|
||||
{
|
||||
name: "Profitability",
|
||||
tree: groupedSupplyProfitLoss(list, all, title),
|
||||
},
|
||||
...groupedAmountDeltaItems(list, all, (c) => c.tree.supply.delta, title, "Supply"),
|
||||
],
|
||||
},
|
||||
groupedOutputsFolder(list, all, title),
|
||||
{
|
||||
name: "Addresses",
|
||||
tree: [
|
||||
{
|
||||
name: "Count",
|
||||
title: title("Address Count"),
|
||||
bottom: mapCohortsWithAll(list, all, ({ name, color, addressCount }) =>
|
||||
line({ series: addressCount.base, name, color, unit: Unit.count }),
|
||||
),
|
||||
},
|
||||
...groupedDeltaItems(list, all, (c) => c.addressCount.delta, Unit.count, title, "Address Count"),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Average Holdings",
|
||||
tree: [
|
||||
{
|
||||
name: "Per UTXO",
|
||||
title: title("Average Holdings per UTXO"),
|
||||
bottom: flatMapCohortsWithAll(list, all, ({ name, color, avgAmount }) =>
|
||||
satsBtcUsd({ pattern: avgAmount.utxo, name, color }),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "Per Address",
|
||||
title: title("Average Holdings per Funded Address"),
|
||||
bottom: flatMapCohortsWithAll(list, all, ({ name, color, avgAmount }) =>
|
||||
satsBtcUsd({ pattern: avgAmount.addr, name, color }),
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Grouped holdings for address amount cohorts (no inProfit/inLoss, has address count)
|
||||
* @param {{ list: readonly AddrCohortObject[], all: CohortAll, title: (name: string) => string }} args
|
||||
* @returns {PartialOptionsTree}
|
||||
*/
|
||||
export function createGroupedHoldingsSectionAddressAmount({ list, all, title }) {
|
||||
return [
|
||||
{
|
||||
name: "Supply",
|
||||
tree: [
|
||||
groupedSupplyTotal(list, all, title),
|
||||
groupedDominanceChart(list, title),
|
||||
...groupedAmountDeltaItems(list, all, (c) => c.tree.supply.delta, title, "Supply"),
|
||||
],
|
||||
},
|
||||
groupedOutputsFolder(list, all, title),
|
||||
{
|
||||
name: "Addresses",
|
||||
tree: [
|
||||
{
|
||||
name: "Count",
|
||||
title: title("Address Count"),
|
||||
bottom: mapCohortsWithAll(list, all, ({ name, color, addressCount }) =>
|
||||
line({ series: addressCount.base, name, color, unit: Unit.count }),
|
||||
),
|
||||
},
|
||||
...groupedDeltaItems(list, all, (c) => c.addressCount.delta, Unit.count, title, "Address Count"),
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** @param {{ list: readonly (UtxoCohortObject | CohortWithoutRelative)[], all: CohortAll, title: (name: string) => string }} args */
|
||||
export function createGroupedHoldingsSection({ list, all, title }) {
|
||||
return [
|
||||
{
|
||||
name: "Supply",
|
||||
tree: [
|
||||
groupedSupplyTotal(list, all, title),
|
||||
groupedDominanceChart(list, title),
|
||||
...groupedAmountDeltaItems(list, all, (c) => c.tree.supply.delta, title, "Supply"),
|
||||
],
|
||||
},
|
||||
groupedOutputsFolder(list, all, title),
|
||||
];
|
||||
}
|
||||
|
||||
/** @param {{ list: readonly CohortWithoutRelative[], all: CohortAll, title: (name: string) => string }} args */
|
||||
export function createGroupedHoldingsSectionWithProfitLoss({ list, all, title }) {
|
||||
return [
|
||||
{
|
||||
name: "Supply",
|
||||
tree: [
|
||||
groupedSupplyTotal(list, all, title),
|
||||
groupedDominanceChart(list, title),
|
||||
{
|
||||
name: "Profitability",
|
||||
tree: groupedSupplyProfitLoss(list, all, title),
|
||||
},
|
||||
...groupedAmountDeltaItems(list, all, (c) => c.tree.supply.delta, title, "Supply"),
|
||||
],
|
||||
},
|
||||
groupedOutputsFolder(list, all, title),
|
||||
];
|
||||
}
|
||||
|
||||
/** @param {{ list: readonly (CohortWithAdjusted | CohortAgeRange)[], all: CohortAll, title: (name: string) => string }} args */
|
||||
export function createGroupedHoldingsSectionWithOwnSupply({ list, all, title }) {
|
||||
return [
|
||||
{
|
||||
name: "Supply",
|
||||
tree: [
|
||||
groupedSupplyTotal(list, all, title),
|
||||
groupedDominanceChart(list, title),
|
||||
{
|
||||
name: "Profitability",
|
||||
tree: groupedSupplyProfitLoss(list, all, title),
|
||||
},
|
||||
...groupedAmountDeltaItems(list, all, (c) => c.tree.supply.delta, title, "Supply"),
|
||||
],
|
||||
},
|
||||
groupedOutputsFolder(list, all, title),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Grouped holdings with full relative series (dominance + share)
|
||||
* For: CohortFull, CohortLongTerm
|
||||
* @param {{ list: readonly (CohortFull | CohortLongTerm)[], all: CohortAll, title: (name: string) => string }} args
|
||||
* @returns {PartialOptionsTree}
|
||||
*/
|
||||
export function createGroupedHoldingsSectionWithRelative({ list, all, title }) {
|
||||
return [
|
||||
{
|
||||
name: "Supply",
|
||||
tree: [
|
||||
groupedSupplyTotal(list, all, title),
|
||||
groupedDominanceChart(list, title),
|
||||
{
|
||||
name: "Profitability",
|
||||
tree: [
|
||||
...groupedSupplyProfitLoss(list, all, title),
|
||||
{
|
||||
name: "Composition",
|
||||
tree: [
|
||||
{ name: "In Profit", title: title("Supply In Profit Composition"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ series: tree.supply.inProfit.share.percent, name, color, unit: Unit.percentage })) },
|
||||
{ name: "In Loss", title: title("Supply In Loss Composition"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ series: tree.supply.inLoss.share.percent, name, color, unit: Unit.percentage })) },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
...groupedAmountDeltaItems(list, all, (c) => c.tree.supply.delta, title, "Supply"),
|
||||
],
|
||||
},
|
||||
groupedOutputsFolder(list, all, title),
|
||||
];
|
||||
}
|
||||
@@ -1,767 +0,0 @@
|
||||
/**
|
||||
* Cohort module - exports all cohort-related functionality
|
||||
*
|
||||
* Folder builders compose sections from building blocks:
|
||||
* - holdings.js: Supply, UTXO Count, Address Count
|
||||
* - valuation.js: Realized Cap, Market Cap, MVRV
|
||||
* - prices.js: Realized Price, ratios
|
||||
* - cost-basis.js: Cost basis percentiles
|
||||
* - profitability.js: Unrealized/Realized P&L, Invested Capital
|
||||
* - activity.js: SOPR, Volume, Lifespan
|
||||
*/
|
||||
|
||||
import {
|
||||
formatCohortTitle,
|
||||
amountBaseline,
|
||||
satsBtcUsd,
|
||||
satsBtcUsdFullTree,
|
||||
avgHoldingsSubtree,
|
||||
exposedSubtree,
|
||||
reusedSubtree,
|
||||
} from "../shared.js";
|
||||
import {
|
||||
ROLLING_WINDOWS,
|
||||
line,
|
||||
percentRatio,
|
||||
amountSumsTreeBaseline,
|
||||
rollingPercentRatioTree,
|
||||
} from "../series.js";
|
||||
import { Unit } from "../../utils/units.js";
|
||||
import { colors } from "../../utils/colors.js";
|
||||
import { brk } from "../../utils/client.js";
|
||||
|
||||
// Section builders
|
||||
import {
|
||||
createHoldingsSection,
|
||||
createHoldingsSectionAll,
|
||||
createHoldingsSectionAddress,
|
||||
createHoldingsSectionAddressAmount,
|
||||
createHoldingsSectionWithProfitLoss,
|
||||
createHoldingsSectionWithRelative,
|
||||
createHoldingsSectionWithOwnSupply,
|
||||
createGroupedHoldingsSection,
|
||||
createGroupedHoldingsSectionAddress,
|
||||
createGroupedHoldingsSectionAddressAmount,
|
||||
createGroupedHoldingsSectionWithRelative,
|
||||
createGroupedHoldingsSectionWithOwnSupply,
|
||||
} from "./holdings.js";
|
||||
import {
|
||||
createValuationSection,
|
||||
createValuationSectionFull,
|
||||
createGroupedValuationSection,
|
||||
createGroupedValuationSectionWithOwnMarketCap,
|
||||
} from "./valuation.js";
|
||||
import {
|
||||
createPricesSectionFull,
|
||||
createPricesSectionBasic,
|
||||
createGroupedPricesSection,
|
||||
createGroupedPricesSectionFull,
|
||||
} from "./prices.js";
|
||||
import {
|
||||
createCostBasisSectionWithPercentiles,
|
||||
createGroupedCostBasisSectionWithPercentiles,
|
||||
} from "./cost-basis.js";
|
||||
import {
|
||||
createProfitabilitySection,
|
||||
createProfitabilitySectionAll,
|
||||
createProfitabilitySectionFull,
|
||||
createProfitabilitySectionWithProfitLoss,
|
||||
createProfitabilitySectionWithInvestedCapitalPct,
|
||||
createProfitabilitySectionLongTerm,
|
||||
createGroupedProfitabilitySection,
|
||||
createGroupedProfitabilitySectionWithProfitLoss,
|
||||
createGroupedProfitabilitySectionWithNupl,
|
||||
createGroupedProfitabilitySectionWithInvestedCapitalPct,
|
||||
} from "./profitability.js";
|
||||
import {
|
||||
createActivitySection,
|
||||
createActivitySectionWithAdjusted,
|
||||
createActivitySectionWithActivity,
|
||||
createGroupedActivitySection,
|
||||
createGroupedActivitySectionWithActivity,
|
||||
createActivitySectionMinimal,
|
||||
createGroupedActivitySectionMinimal,
|
||||
} from "./activity.js";
|
||||
|
||||
// Re-export data builder
|
||||
export { buildCohortData } from "./data.js";
|
||||
|
||||
// ============================================================================
|
||||
// Single Cohort Folder Builders
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* All folder: for the special "All" cohort
|
||||
* @param {CohortAll} cohort
|
||||
* @returns {PartialOptionsGroup}
|
||||
*/
|
||||
export function createCohortFolderAll(cohort) {
|
||||
const title = formatCohortTitle(cohort.title);
|
||||
return {
|
||||
name: cohort.name || "all",
|
||||
tree: [
|
||||
...createHoldingsSectionAll({ cohort, title }),
|
||||
createValuationSectionFull({ cohort, title }),
|
||||
createPricesSectionFull({ cohort, title }),
|
||||
createCostBasisSectionWithPercentiles({ cohort, title }),
|
||||
createProfitabilitySectionAll({ cohort, title }),
|
||||
createActivitySectionWithAdjusted({ cohort, title }),
|
||||
avgHoldingsSubtree(cohort.avgAmount, title),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Full folder: adjustedSopr + percentiles + RelToMarketCap
|
||||
* @param {CohortFull} cohort
|
||||
* @returns {PartialOptionsGroup}
|
||||
*/
|
||||
export function createCohortFolderFull(cohort) {
|
||||
const title = formatCohortTitle(cohort.title);
|
||||
return {
|
||||
name: cohort.name || "all",
|
||||
tree: [
|
||||
...createHoldingsSectionWithRelative({ cohort, title }),
|
||||
createValuationSectionFull({ cohort, title }),
|
||||
createPricesSectionFull({ cohort, title }),
|
||||
createCostBasisSectionWithPercentiles({ cohort, title }),
|
||||
createProfitabilitySectionFull({ cohort, title }),
|
||||
createActivitySectionWithAdjusted({ cohort, title }),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjusted folder: adjustedSopr only, no percentiles
|
||||
* @param {CohortWithAdjusted} cohort
|
||||
* @returns {PartialOptionsGroup}
|
||||
*/
|
||||
export function createCohortFolderWithAdjusted(cohort) {
|
||||
const title = formatCohortTitle(cohort.title);
|
||||
return {
|
||||
name: cohort.name || "all",
|
||||
tree: [
|
||||
...createHoldingsSectionWithOwnSupply({ cohort, title }),
|
||||
createValuationSection({ cohort, title }),
|
||||
createPricesSectionBasic({ cohort, title }),
|
||||
createProfitabilitySectionWithInvestedCapitalPct({ cohort, title }),
|
||||
createActivitySectionWithActivity({ cohort, title }),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Folder for cohorts with nupl + percentiles
|
||||
* @param {CohortWithNuplPercentiles} cohort
|
||||
* @returns {PartialOptionsGroup}
|
||||
*/
|
||||
export function createCohortFolderWithNupl(cohort) {
|
||||
const title = formatCohortTitle(cohort.title);
|
||||
return {
|
||||
name: cohort.name || "all",
|
||||
tree: [
|
||||
...createHoldingsSectionWithRelative({ cohort, title }),
|
||||
createValuationSectionFull({ cohort, title }),
|
||||
createPricesSectionFull({ cohort, title }),
|
||||
createCostBasisSectionWithPercentiles({ cohort, title }),
|
||||
createProfitabilitySection({ cohort, title }),
|
||||
createActivitySection({ cohort, title }),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* LongTerm folder: has own market cap + NUPL + peak regret + P/L ratio
|
||||
* @param {CohortLongTerm} cohort
|
||||
* @returns {PartialOptionsGroup}
|
||||
*/
|
||||
export function createCohortFolderLongTerm(cohort) {
|
||||
const title = formatCohortTitle(cohort.title);
|
||||
return {
|
||||
name: cohort.name || "all",
|
||||
tree: [
|
||||
...createHoldingsSectionWithRelative({ cohort, title }),
|
||||
createValuationSectionFull({ cohort, title }),
|
||||
createPricesSectionFull({ cohort, title }),
|
||||
createCostBasisSectionWithPercentiles({ cohort, title }),
|
||||
createProfitabilitySectionLongTerm({ cohort, title }),
|
||||
createActivitySection({ cohort, title }),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Age range folder: no nupl
|
||||
* @param {CohortAgeRange} cohort
|
||||
* @returns {PartialOptionsGroup}
|
||||
*/
|
||||
export function createCohortFolderAgeRange(cohort) {
|
||||
const title = formatCohortTitle(cohort.title);
|
||||
return {
|
||||
name: cohort.name || "all",
|
||||
tree: [
|
||||
...createHoldingsSectionWithOwnSupply({ cohort, title }),
|
||||
createValuationSection({ cohort, title }),
|
||||
createPricesSectionBasic({ cohort, title }),
|
||||
createProfitabilitySectionWithInvestedCapitalPct({ cohort, title }),
|
||||
createActivitySectionWithActivity({ cohort, title }),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Age range folder with matured supply
|
||||
* @param {CohortAgeRangeWithMatured} cohort
|
||||
* @returns {PartialOptionsGroup}
|
||||
*/
|
||||
export function createCohortFolderAgeRangeWithMatured(cohort) {
|
||||
const folder = createCohortFolderAgeRange(cohort);
|
||||
const title = formatCohortTitle(cohort.title);
|
||||
folder.tree.push({
|
||||
name: "Matured",
|
||||
tree: satsBtcUsdFullTree({
|
||||
pattern: cohort.matured,
|
||||
title,
|
||||
metric: "Matured Supply",
|
||||
}),
|
||||
});
|
||||
return folder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic folder WITH RelToMarketCap
|
||||
* @param {CohortBasicWithMarketCap} cohort
|
||||
* @returns {PartialOptionsGroup}
|
||||
*/
|
||||
export function createCohortFolderBasicWithMarketCap(cohort) {
|
||||
const title = formatCohortTitle(cohort.title);
|
||||
return {
|
||||
name: cohort.name || "all",
|
||||
tree: [
|
||||
...createHoldingsSection({ cohort, title }),
|
||||
createValuationSection({ cohort, title }),
|
||||
createPricesSectionBasic({ cohort, title }),
|
||||
createProfitabilitySection({ cohort, title }),
|
||||
createActivitySectionMinimal({ cohort, title }),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Address folder: like basic but with address count
|
||||
* @param {CohortAddr} cohort
|
||||
* @returns {PartialOptionsGroup}
|
||||
*/
|
||||
export function createCohortFolderAddress(cohort) {
|
||||
const title = formatCohortTitle(cohort.title);
|
||||
return {
|
||||
name: cohort.name || "all",
|
||||
tree: [
|
||||
...createHoldingsSectionAddress({ cohort, title }),
|
||||
createValuationSection({ cohort, title }),
|
||||
createPricesSectionBasic({ cohort, title }),
|
||||
createProfitabilitySectionWithProfitLoss({ cohort, title }),
|
||||
createActivitySectionMinimal({ cohort, title }),
|
||||
avgHoldingsSubtree(cohort.avgAmount, title),
|
||||
reusedSubtree(cohort.reused, cohort.respent, cohort.key, title),
|
||||
exposedSubtree(cohort.exposed, cohort.key, title),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Folder for cohorts WITHOUT relative section
|
||||
* @param {CohortWithoutRelative} cohort
|
||||
* @returns {PartialOptionsGroup}
|
||||
*/
|
||||
export function createCohortFolderWithoutRelative(cohort) {
|
||||
const title = formatCohortTitle(cohort.title);
|
||||
return {
|
||||
name: cohort.name || "all",
|
||||
tree: [
|
||||
...createHoldingsSectionWithProfitLoss({ cohort, title }),
|
||||
createValuationSection({ cohort, title }),
|
||||
createPricesSectionBasic({ cohort, title }),
|
||||
createProfitabilitySectionWithProfitLoss({ cohort, title }),
|
||||
createActivitySectionMinimal({ cohort, title }),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Address amount cohort folder: has NUPL + addrCount
|
||||
* @param {AddrCohortObject} cohort
|
||||
* @returns {PartialOptionsGroup}
|
||||
*/
|
||||
export function createAddressCohortFolder(cohort) {
|
||||
const title = formatCohortTitle(cohort.title);
|
||||
return {
|
||||
name: cohort.name || "all",
|
||||
tree: [
|
||||
...createHoldingsSectionAddressAmount({ cohort, title }),
|
||||
createValuationSection({ cohort, title }),
|
||||
createPricesSectionBasic({ cohort, title }),
|
||||
createProfitabilitySection({ cohort, title }),
|
||||
createActivitySectionMinimal({ cohort, title }),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Grouped Cohort Folder Builders
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @param {CohortGroupWithAdjusted} args
|
||||
* @returns {PartialOptionsGroup}
|
||||
*/
|
||||
export function createGroupedCohortFolderWithAdjusted({
|
||||
name,
|
||||
title: groupTitle,
|
||||
list,
|
||||
all,
|
||||
}) {
|
||||
const title = formatCohortTitle(groupTitle);
|
||||
return {
|
||||
name: name || "all",
|
||||
tree: [
|
||||
...createGroupedHoldingsSectionWithOwnSupply({ list, all, title }),
|
||||
createGroupedValuationSection({ list, all, title }),
|
||||
createGroupedPricesSection({ list, all, title }),
|
||||
createGroupedProfitabilitySectionWithInvestedCapitalPct({
|
||||
list,
|
||||
all,
|
||||
title,
|
||||
}),
|
||||
createGroupedActivitySectionWithActivity({ list, all, title }),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {CohortGroupWithNuplPercentiles} args
|
||||
* @returns {PartialOptionsGroup}
|
||||
*/
|
||||
export function createGroupedCohortFolderWithNupl({
|
||||
name,
|
||||
title: groupTitle,
|
||||
list,
|
||||
all,
|
||||
}) {
|
||||
const title = formatCohortTitle(groupTitle);
|
||||
return {
|
||||
name: name || "all",
|
||||
tree: [
|
||||
...createGroupedHoldingsSectionWithRelative({ list, all, title }),
|
||||
createGroupedValuationSectionWithOwnMarketCap({ list, all, title }),
|
||||
createGroupedPricesSectionFull({ list, all, title }),
|
||||
createGroupedCostBasisSectionWithPercentiles({ list, all, title }),
|
||||
createGroupedProfitabilitySectionWithNupl({ list, all, title }),
|
||||
createGroupedActivitySection({ list, all, title }),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {CohortGroupAgeRange} args
|
||||
* @returns {PartialOptionsGroup}
|
||||
*/
|
||||
export function createGroupedCohortFolderAgeRange({
|
||||
name,
|
||||
title: groupTitle,
|
||||
list,
|
||||
all,
|
||||
}) {
|
||||
const title = formatCohortTitle(groupTitle);
|
||||
return {
|
||||
name: name || "all",
|
||||
tree: [
|
||||
...createGroupedHoldingsSectionWithOwnSupply({ list, all, title }),
|
||||
createGroupedValuationSection({ list, all, title }),
|
||||
createGroupedPricesSection({ list, all, title }),
|
||||
createGroupedProfitabilitySectionWithInvestedCapitalPct({
|
||||
list,
|
||||
all,
|
||||
title,
|
||||
}),
|
||||
createGroupedActivitySectionWithActivity({ list, all, title }),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{ name: string, title: string, list: readonly CohortAgeRangeWithMatured[], all: CohortAll }} args
|
||||
* @returns {PartialOptionsGroup}
|
||||
*/
|
||||
export function createGroupedCohortFolderAgeRangeWithMatured({
|
||||
name,
|
||||
title: groupTitle,
|
||||
list,
|
||||
all,
|
||||
}) {
|
||||
const folder = createGroupedCohortFolderAgeRange({
|
||||
name,
|
||||
title: groupTitle,
|
||||
list,
|
||||
all,
|
||||
});
|
||||
const title = formatCohortTitle(groupTitle);
|
||||
folder.tree.push({
|
||||
name: "Matured",
|
||||
tree: ROLLING_WINDOWS.map((w) => ({
|
||||
name: w.name,
|
||||
title: title(`${w.title} Matured Supply`),
|
||||
bottom: list.flatMap((cohort) =>
|
||||
satsBtcUsd({
|
||||
pattern: cohort.matured.sum[w.key],
|
||||
name: cohort.name,
|
||||
color: cohort.color,
|
||||
}),
|
||||
),
|
||||
})),
|
||||
});
|
||||
return folder;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {CohortGroupBasicWithMarketCap} args
|
||||
* @returns {PartialOptionsGroup}
|
||||
*/
|
||||
export function createGroupedCohortFolderBasicWithMarketCap({
|
||||
name,
|
||||
title: groupTitle,
|
||||
list,
|
||||
all,
|
||||
}) {
|
||||
const title = formatCohortTitle(groupTitle);
|
||||
return {
|
||||
name: name || "all",
|
||||
tree: [
|
||||
...createGroupedHoldingsSection({ list, all, title }),
|
||||
createGroupedValuationSection({ list, all, title }),
|
||||
createGroupedPricesSection({ list, all, title }),
|
||||
createGroupedProfitabilitySection({ list, all, title }),
|
||||
createGroupedActivitySectionMinimal({ list, all, title }),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param {CohortGroupAddr} args
|
||||
* @returns {PartialOptionsGroup}
|
||||
*/
|
||||
export function createGroupedCohortFolderAddress({
|
||||
name,
|
||||
title: groupTitle,
|
||||
list,
|
||||
all,
|
||||
}) {
|
||||
const title = formatCohortTitle(groupTitle);
|
||||
return {
|
||||
name: name || "all",
|
||||
tree: [
|
||||
...createGroupedHoldingsSectionAddress({ list, all, title }),
|
||||
createGroupedValuationSection({ list, all, title }),
|
||||
createGroupedPricesSection({ list, all, title }),
|
||||
createGroupedProfitabilitySectionWithProfitLoss({
|
||||
list,
|
||||
all,
|
||||
title,
|
||||
}),
|
||||
createGroupedActivitySectionMinimal({ list, all, title }),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AddrCohortGroupObject} args
|
||||
* @returns {PartialOptionsGroup}
|
||||
*/
|
||||
export function createGroupedAddressCohortFolder({
|
||||
name,
|
||||
title: groupTitle,
|
||||
list,
|
||||
all,
|
||||
}) {
|
||||
const title = formatCohortTitle(groupTitle);
|
||||
return {
|
||||
name: name || "all",
|
||||
tree: [
|
||||
...createGroupedHoldingsSectionAddressAmount({ list, all, title }),
|
||||
createGroupedValuationSection({ list, all, title }),
|
||||
createGroupedPricesSection({ list, all, title }),
|
||||
createGroupedProfitabilitySection({ list, all, title }),
|
||||
createGroupedActivitySectionMinimal({ list, all, title }),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// UTXO Profitability Folder Builders
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @param {{ name: string, color: Color, pattern: RealizedSupplyPattern }} bucket
|
||||
* @param {string} [parentName]
|
||||
* @returns {PartialOptionsGroup}
|
||||
*/
|
||||
function singleBucketFolder({ name, color, pattern }, parentName) {
|
||||
const title = formatCohortTitle(parentName ? `${parentName} ${name}` : name);
|
||||
return {
|
||||
name,
|
||||
tree: [
|
||||
{
|
||||
name: "Supply",
|
||||
tree: [
|
||||
{
|
||||
name: "Total",
|
||||
title: title("Supply"),
|
||||
bottom: [
|
||||
...satsBtcUsd({ pattern: pattern.supply.all, name: "Total" }),
|
||||
...satsBtcUsd({
|
||||
pattern: pattern.supply.sth,
|
||||
name: "STH",
|
||||
color: colors.term.short,
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
...amountSumsTreeBaseline({
|
||||
windows: pattern.supply.all.delta.absolute,
|
||||
title,
|
||||
metric: "Supply Change",
|
||||
legend: "Change",
|
||||
}),
|
||||
name: "Change",
|
||||
},
|
||||
{
|
||||
...rollingPercentRatioTree({
|
||||
windows: pattern.supply.all.delta.rate,
|
||||
title,
|
||||
metric: "Supply Growth Rate",
|
||||
}),
|
||||
name: "Growth Rate",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Realized Cap",
|
||||
title: title("Realized Cap"),
|
||||
bottom: [
|
||||
line({
|
||||
series: pattern.realizedCap.all,
|
||||
name: "Total",
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
line({
|
||||
series: pattern.realizedCap.sth,
|
||||
name: "STH",
|
||||
color: colors.term.short,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Unrealized PnL",
|
||||
title: title("Unrealized PnL"),
|
||||
bottom: [
|
||||
line({
|
||||
series: pattern.unrealizedPnl.all,
|
||||
name: "Total",
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
line({
|
||||
series: pattern.unrealizedPnl.sth,
|
||||
name: "STH",
|
||||
color: colors.term.short,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "NUPL",
|
||||
title: title("NUPL"),
|
||||
bottom: [
|
||||
line({ series: pattern.nupl.ratio, name, color, unit: Unit.ratio }),
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{ name: string, color: Color, pattern: RealizedSupplyPattern }[]} list
|
||||
* @param {string} groupTitle
|
||||
* @returns {PartialOptionsTree}
|
||||
*/
|
||||
function groupedBucketCharts(list, groupTitle) {
|
||||
const title = formatCohortTitle(groupTitle);
|
||||
return [
|
||||
{
|
||||
name: "Supply",
|
||||
tree: [
|
||||
{
|
||||
name: "All",
|
||||
title: title("Supply"),
|
||||
bottom: list.flatMap(({ name, color, pattern }) =>
|
||||
satsBtcUsd({ pattern: pattern.supply.all, name, color }),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "STH",
|
||||
title: title("STH Supply"),
|
||||
bottom: list.flatMap(({ name, color, pattern }) =>
|
||||
satsBtcUsd({ pattern: pattern.supply.sth, name, color }),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "Change",
|
||||
tree: ROLLING_WINDOWS.map((w) => ({
|
||||
name: w.name,
|
||||
title: title(`${w.title} Supply Change`),
|
||||
bottom: list.flatMap(({ name, color, pattern }) =>
|
||||
amountBaseline({
|
||||
pattern: pattern.supply.all.delta.absolute[w.key],
|
||||
name,
|
||||
color,
|
||||
}),
|
||||
),
|
||||
})),
|
||||
},
|
||||
{
|
||||
name: "Growth Rate",
|
||||
tree: ROLLING_WINDOWS.map((w) => ({
|
||||
name: w.name,
|
||||
title: title(`${w.title} Supply Growth Rate`),
|
||||
bottom: list.flatMap(({ name, color, pattern }) =>
|
||||
percentRatio({
|
||||
pattern: pattern.supply.all.delta.rate[w.key],
|
||||
name,
|
||||
color,
|
||||
}),
|
||||
),
|
||||
})),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Realized Cap",
|
||||
tree: [
|
||||
{
|
||||
name: "All",
|
||||
title: title("Realized Cap"),
|
||||
bottom: list.map(({ name, color, pattern }) =>
|
||||
line({
|
||||
series: pattern.realizedCap.all,
|
||||
name,
|
||||
color,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "STH",
|
||||
title: title("STH Realized Cap"),
|
||||
bottom: list.map(({ name, color, pattern }) =>
|
||||
line({
|
||||
series: pattern.realizedCap.sth,
|
||||
name,
|
||||
color,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Unrealized PnL",
|
||||
tree: [
|
||||
{
|
||||
name: "All",
|
||||
title: title("Unrealized PnL"),
|
||||
bottom: list.map(({ name, color, pattern }) =>
|
||||
line({
|
||||
series: pattern.unrealizedPnl.all,
|
||||
name,
|
||||
color,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "STH",
|
||||
title: title("STH Unrealized PnL"),
|
||||
bottom: list.map(({ name, color, pattern }) =>
|
||||
line({
|
||||
series: pattern.unrealizedPnl.sth,
|
||||
name,
|
||||
color,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "NUPL",
|
||||
title: title("NUPL"),
|
||||
bottom: list.map(({ name, color, pattern }) =>
|
||||
line({ series: pattern.nupl.ratio, name, color, unit: Unit.ratio }),
|
||||
),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{ range: { name: string, color: Color, pattern: RealizedSupplyPattern }[], profit: { name: string, color: Color, pattern: RealizedSupplyPattern }[], loss: { name: string, color: Color, pattern: RealizedSupplyPattern }[] }} args
|
||||
* @returns {PartialOptionsGroup}
|
||||
*/
|
||||
export function createUtxoProfitabilitySection({ range, profit, loss }) {
|
||||
return {
|
||||
name: "UTXO Profitability",
|
||||
tree: [
|
||||
{
|
||||
name: "Range",
|
||||
tree: [
|
||||
{
|
||||
name: "Compare",
|
||||
tree: groupedBucketCharts(range, "Profitability Range"),
|
||||
},
|
||||
...range.map((bucket) => singleBucketFolder(bucket)),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "In Profit",
|
||||
tree: [
|
||||
{ name: "Compare", tree: groupedBucketCharts(profit, "In Profit") },
|
||||
...profit.map((bucket) => singleBucketFolder(bucket, "In Profit")),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "In Loss",
|
||||
tree: [
|
||||
{ name: "Compare", tree: groupedBucketCharts(loss, "In Loss") },
|
||||
...loss.map((bucket) => singleBucketFolder(bucket, "In Loss")),
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Gini leaf for Distribution > Address Balance
|
||||
* @returns {AnyPartialOption}
|
||||
*/
|
||||
export function createAddressBalanceGiniLeaf() {
|
||||
return {
|
||||
name: "Gini",
|
||||
title: "Address Balance Gini Coefficient",
|
||||
bottom: percentRatio({
|
||||
pattern: brk.series.indicators.gini,
|
||||
name: "Gini",
|
||||
color: colors.loss,
|
||||
}),
|
||||
};
|
||||
}
|
||||
@@ -1,173 +0,0 @@
|
||||
/**
|
||||
* Prices section builders
|
||||
*
|
||||
* Structure (single cohort):
|
||||
* - Compare: Both prices on one chart
|
||||
* - Realized: Price + Ratio (MVRV) + Z-Scores (for full cohorts)
|
||||
* - Capitalized: Price + Ratio + Z-Scores (for full cohorts)
|
||||
*
|
||||
* Structure (grouped cohorts):
|
||||
* - Realized: Price + Ratio comparison across cohorts
|
||||
* - Capitalized: Price + Ratio comparison across cohorts
|
||||
*
|
||||
* For cohorts WITHOUT full ratio patterns: basic Price/Ratio charts only (no Z-Scores)
|
||||
*/
|
||||
|
||||
import { colors } from "../../utils/colors.js";
|
||||
import { createPriceRatioCharts, mapCohortsWithAll, priceRatioPercentilesTree } from "../shared.js";
|
||||
import { baseline, price } from "../series.js";
|
||||
import { Unit } from "../../utils/units.js";
|
||||
|
||||
/**
|
||||
* Create prices section for cohorts with full ratio patterns
|
||||
* (CohortAll, CohortFull, CohortLongTerm)
|
||||
* @param {{ cohort: CohortAll | CohortFull | CohortLongTerm, title: (name: string) => string }} args
|
||||
* @returns {PartialOptionsGroup}
|
||||
*/
|
||||
export function createPricesSectionFull({ cohort, title }) {
|
||||
const { tree, color } = cohort;
|
||||
return {
|
||||
name: "Prices",
|
||||
tree: [
|
||||
{
|
||||
name: "Compare",
|
||||
title: title("Realized Prices"),
|
||||
top: [
|
||||
price({ series: tree.realized.price, name: "Realized", color: colors.realized }),
|
||||
price({ series: tree.realized.capitalized.price, name: "Capitalized", color: colors.capitalized }),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Realized",
|
||||
tree: createPriceRatioCharts({
|
||||
context: cohort.title,
|
||||
legend: "Realized",
|
||||
pricePattern: tree.realized.price,
|
||||
ratio: tree.realized.price,
|
||||
color,
|
||||
priceTitle: title("Realized Price"),
|
||||
titlePrefix: "Realized Price",
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "Capitalized",
|
||||
tree: priceRatioPercentilesTree({
|
||||
pattern: tree.realized.capitalized.price,
|
||||
title: title("Capitalized Price"),
|
||||
ratioTitle: title("Capitalized Price Ratio"),
|
||||
legend: "Capitalized",
|
||||
color,
|
||||
}),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create prices section for cohorts with basic ratio patterns only
|
||||
* (CohortWithAdjusted, CohortBasic, CohortAddr, CohortWithoutRelative)
|
||||
* @param {{ cohort: CohortWithAdjusted | CohortBasic | CohortAddr | CohortWithoutRelative | CohortAgeRange, title: (name: string) => string }} args
|
||||
* @returns {PartialOptionsGroup}
|
||||
*/
|
||||
export function createPricesSectionBasic({ cohort, title }) {
|
||||
const { tree, color } = cohort;
|
||||
return {
|
||||
name: "Prices",
|
||||
tree: [
|
||||
{
|
||||
name: "Realized",
|
||||
tree: [
|
||||
{
|
||||
name: "Price",
|
||||
title: title("Realized Price"),
|
||||
top: [price({ series: tree.realized.price, name: "Realized", color })],
|
||||
},
|
||||
{
|
||||
name: "Ratio",
|
||||
title: title("Realized Price Ratio"),
|
||||
bottom: [
|
||||
baseline({
|
||||
series: tree.realized.price.ratio,
|
||||
name: "Ratio",
|
||||
unit: Unit.ratio,
|
||||
base: 1,
|
||||
}),
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create prices section for grouped cohorts
|
||||
* @param {{ list: readonly CohortObject[], all: CohortAll, title: (name: string) => string }} args
|
||||
* @returns {PartialOptionsGroup}
|
||||
*/
|
||||
/**
|
||||
* @param {readonly CohortObject[]} list
|
||||
* @param {CohortAll} all
|
||||
* @param {(name: string) => string} title
|
||||
* @returns {PartialOptionsTree}
|
||||
*/
|
||||
function groupedRealizedPriceItems(list, all, title) {
|
||||
return [
|
||||
{
|
||||
name: "Realized",
|
||||
tree: [
|
||||
{
|
||||
name: "Price",
|
||||
title: title("Realized Price"),
|
||||
top: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
price({ series: tree.realized.price, name, color }),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "Ratio",
|
||||
title: title("Realized Price Ratio"),
|
||||
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
baseline({ series: tree.realized.mvrv, name, color, unit: Unit.ratio, base: 1 }),
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** @param {{ list: readonly CohortObject[], all: CohortAll, title: (name: string) => string }} args */
|
||||
export function createGroupedPricesSection({ list, all, title }) {
|
||||
return {
|
||||
name: "Prices",
|
||||
tree: groupedRealizedPriceItems(list, all, title),
|
||||
};
|
||||
}
|
||||
|
||||
/** @param {{ list: readonly (CohortAll | CohortFull | CohortLongTerm)[], all: CohortAll, title: (name: string) => string }} args */
|
||||
export function createGroupedPricesSectionFull({ list, all, title }) {
|
||||
return {
|
||||
name: "Prices",
|
||||
tree: [
|
||||
...groupedRealizedPriceItems(list, all, title),
|
||||
{
|
||||
name: "Capitalized",
|
||||
tree: [
|
||||
{
|
||||
name: "Price",
|
||||
title: title("Capitalized Price"),
|
||||
top: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
price({ series: tree.realized.capitalized.price, name, color }),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "Ratio",
|
||||
title: title("Capitalized Price Ratio"),
|
||||
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
baseline({ series: tree.realized.capitalized.price.ratio, name, color, unit: Unit.ratio, base: 1 }),
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,193 +0,0 @@
|
||||
/**
|
||||
* Capitalization section builders
|
||||
*/
|
||||
|
||||
import { Unit } from "../../utils/units.js";
|
||||
import { colors } from "../../utils/colors.js";
|
||||
import { ROLLING_WINDOWS, line, baseline, mapWindows, sumsTreeBaseline, rollingPercentRatioTree, percentRatio, percentRatioBaseline } from "../series.js";
|
||||
import { ratioBottomSeries, mapCohortsWithAll, flatMapCohortsWithAll } from "../shared.js";
|
||||
import { priceLine } from "../constants.js";
|
||||
|
||||
// ============================================================================
|
||||
// Shared building blocks
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Single cohort: Change + Growth Rate items (flat)
|
||||
* @param {UtxoCohortObject["tree"]} tree
|
||||
* @param {(name: string) => string} title
|
||||
* @returns {PartialOptionsTree}
|
||||
*/
|
||||
function singleDeltaItems(tree, title) {
|
||||
return [
|
||||
{ ...sumsTreeBaseline({ windows: mapWindows(tree.realized.cap.delta.absolute, (c) => c.usd), title, metric: "Realized Cap Change", unit: Unit.usd, legend: "Change" }), name: "Change" },
|
||||
{ ...rollingPercentRatioTree({ windows: tree.realized.cap.delta.rate, title, metric: "Realized Cap Growth Rate" }), name: "Growth Rate" },
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Grouped: Change + Growth Rate + MVRV items (flat)
|
||||
* @param {readonly (UtxoCohortObject | CohortWithoutRelative)[]} list
|
||||
* @param {CohortAll} all
|
||||
* @param {(name: string) => string} title
|
||||
* @returns {PartialOptionsTree}
|
||||
*/
|
||||
function groupedDeltaAndMvrv(list, all, title) {
|
||||
return [
|
||||
{
|
||||
name: "MVRV",
|
||||
title: title("MVRV"),
|
||||
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
baseline({ series: tree.realized.mvrv, name, color, unit: Unit.ratio, base: 1 }),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "Change",
|
||||
tree: ROLLING_WINDOWS.map((w) => ({
|
||||
name: w.name,
|
||||
title: title(`${w.title} Realized Cap Change`),
|
||||
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
baseline({ series: tree.realized.cap.delta.absolute[w.key].usd, name, color, unit: Unit.usd }),
|
||||
),
|
||||
})),
|
||||
},
|
||||
{
|
||||
name: "Growth Rate",
|
||||
tree: ROLLING_WINDOWS.map((w) => ({
|
||||
name: w.name,
|
||||
title: title(`${w.title} Realized Cap Growth Rate`),
|
||||
bottom: flatMapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
percentRatioBaseline({ pattern: tree.realized.cap.delta.rate[w.key], name, color }),
|
||||
),
|
||||
})),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Single Cohort Sections
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Full capitalization (has invested capital, own market cap ratio, full MVRV)
|
||||
* @param {{ cohort: CohortAll | CohortFull | CohortLongTerm, title: (name: string) => string }} args
|
||||
* @returns {PartialOptionsGroup}
|
||||
*/
|
||||
export function createValuationSectionFull({ cohort, title }) {
|
||||
const { tree, color } = cohort;
|
||||
return {
|
||||
name: "Capitalization",
|
||||
tree: [
|
||||
{ name: "Total", title: title("Realized Cap"), bottom: [line({ series: tree.realized.cap.usd, name: "Realized Cap", color, unit: Unit.usd })] },
|
||||
{
|
||||
name: "Profitability",
|
||||
tree: [
|
||||
{
|
||||
name: "Amount",
|
||||
title: title("Invested Capital"),
|
||||
bottom: [
|
||||
line({ series: tree.realized.cap.usd, name: "Total", color: colors.default, unit: Unit.usd }),
|
||||
line({ series: tree.unrealized.investedCapital.inProfit.usd, name: "In Profit", color: colors.profit, unit: Unit.usd }),
|
||||
line({ series: tree.unrealized.investedCapital.inLoss.usd, name: "In Loss", color: colors.loss, unit: Unit.usd }),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Composition",
|
||||
title: title("Invested Capital Composition"),
|
||||
bottom: [
|
||||
...percentRatio({ pattern: tree.investedCapital.inProfit.share, name: "In Profit", color: colors.profit }),
|
||||
...percentRatio({ pattern: tree.investedCapital.inLoss.share, name: "In Loss", color: colors.loss }),
|
||||
priceLine({ number: 100, color: colors.default, style: 0, unit: Unit.percentage }),
|
||||
priceLine({ number: 50, unit: Unit.percentage }),
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{ name: "MVRV", title: title("MVRV"), bottom: ratioBottomSeries(tree.realized.price) },
|
||||
...singleDeltaItems(tree, title),
|
||||
{ name: "% of Own Market Cap", title: title("Realized Cap (% of Own Market Cap)"), bottom: percentRatioBaseline({ pattern: tree.realized.cap.toOwnMcap, name: "% of Own Market Cap", color }) },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic capitalization (no invested capital, simple MVRV)
|
||||
* @param {{ cohort: CohortWithAdjusted | CohortBasic | CohortAddr | CohortWithoutRelative, title: (name: string) => string }} args
|
||||
* @returns {PartialOptionsGroup}
|
||||
*/
|
||||
export function createValuationSection({ cohort, title }) {
|
||||
const { tree } = cohort;
|
||||
return {
|
||||
name: "Capitalization",
|
||||
tree: [
|
||||
{ name: "Total", title: title("Realized Cap"), bottom: [line({ series: tree.realized.cap.usd, name: "Realized Cap", color: cohort.color, unit: Unit.usd })] },
|
||||
...singleDeltaItems(tree, title),
|
||||
{ name: "MVRV", title: title("MVRV"), bottom: [baseline({ series: tree.realized.mvrv, name: "MVRV", unit: Unit.ratio, base: 1 })] },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Grouped Cohort Sections
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @param {{ list: readonly (UtxoCohortObject | CohortWithoutRelative)[], all: CohortAll, title: (name: string) => string }} args
|
||||
* @returns {PartialOptionsGroup}
|
||||
*/
|
||||
export function createGroupedValuationSection({ list, all, title }) {
|
||||
return {
|
||||
name: "Capitalization",
|
||||
tree: [
|
||||
{
|
||||
name: "Total",
|
||||
title: title("Realized Cap"),
|
||||
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
line({ series: tree.realized.cap.usd, name, color, unit: Unit.usd }),
|
||||
),
|
||||
},
|
||||
...groupedDeltaAndMvrv(list, all, title),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{ list: readonly (CohortAll | CohortFull | CohortLongTerm)[], all: CohortAll, title: (name: string) => string }} args
|
||||
* @returns {PartialOptionsGroup}
|
||||
*/
|
||||
export function createGroupedValuationSectionWithOwnMarketCap({ list, all, title }) {
|
||||
return {
|
||||
name: "Capitalization",
|
||||
tree: [
|
||||
{
|
||||
name: "Total",
|
||||
title: title("Realized Cap"),
|
||||
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
line({ series: tree.realized.cap.usd, name, color, unit: Unit.usd }),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "In Profit",
|
||||
title: title("Invested Capital In Profit"),
|
||||
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
line({ series: tree.unrealized.investedCapital.inProfit.usd, name, color, unit: Unit.usd }),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "In Loss",
|
||||
title: title("Invested Capital In Loss"),
|
||||
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
line({ series: tree.unrealized.investedCapital.inLoss.usd, name, color, unit: Unit.usd }),
|
||||
),
|
||||
},
|
||||
...groupedDeltaAndMvrv(list, all, title),
|
||||
{
|
||||
name: "% of Own Market Cap",
|
||||
title: title("Realized Cap (% of Own Market Cap)"),
|
||||
bottom: flatMapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
percentRatio({ pattern: tree.realized.cap.toOwnMcap, name, color }),
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user