mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-04-24 06:39:58 -07:00
435 lines
14 KiB
JavaScript
435 lines
14 KiB
JavaScript
/**
|
|
* 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, ROLLING_WINDOWS } from "../series.js";
|
|
import {
|
|
satsBtcUsdFullTree,
|
|
mapCohortsWithAll,
|
|
flatMapCohortsWithAll,
|
|
} from "../shared.js";
|
|
import { colors } from "../../utils/colors.js";
|
|
|
|
// ============================================================================
|
|
// Shared Volume Helpers
|
|
// ============================================================================
|
|
|
|
/**
|
|
* @param {{ transferVolume: TransferVolumePattern, coindaysDestroyed: CountPattern<number> }} activity
|
|
* @param {Color} color
|
|
* @param {(name: string) => string} title
|
|
* @returns {PartialOptionsTree}
|
|
*/
|
|
function volumeAndCoinsTree(activity, color, title) {
|
|
return [
|
|
{
|
|
name: "Volume",
|
|
tree: satsBtcUsdFullTree({
|
|
pattern: activity.transferVolume,
|
|
name: "Volume",
|
|
title: title("Sent Volume"),
|
|
color,
|
|
}),
|
|
},
|
|
{
|
|
name: "Coindays Destroyed",
|
|
tree: chartsFromCount({
|
|
pattern: activity.coindaysDestroyed,
|
|
title: title("Coindays Destroyed"),
|
|
unit: Unit.coindays,
|
|
color,
|
|
}),
|
|
},
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Sent in profit/loss breakdown tree (shared by full and mid-level activity)
|
|
* @param {TransferVolumePattern} sent
|
|
* @param {(name: string) => string} title
|
|
* @returns {PartialOptionsTree}
|
|
*/
|
|
function sentProfitLossTree(sent, title) {
|
|
return [
|
|
{
|
|
name: "Sent In Profit",
|
|
tree: satsBtcUsdFullTree({
|
|
pattern: sent.inProfit,
|
|
name: "In Profit",
|
|
title: title("Sent Volume In Profit"),
|
|
color: colors.profit,
|
|
}),
|
|
},
|
|
{
|
|
name: "Sent In Loss",
|
|
tree: satsBtcUsdFullTree({
|
|
pattern: sent.inLoss,
|
|
name: "In Loss",
|
|
title: title("Sent Volume In Loss"),
|
|
color: colors.loss,
|
|
}),
|
|
},
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Volume and coins tree with dormancy, and sent in profit/loss (All/STH/LTH)
|
|
* @param {FullActivityPattern} activity
|
|
* @param {Color} color
|
|
* @param {(name: string) => string} title
|
|
* @returns {PartialOptionsTree}
|
|
*/
|
|
function fullVolumeTree(activity, color, title) {
|
|
return [
|
|
...volumeAndCoinsTree(activity, color, title),
|
|
...sentProfitLossTree(activity.transferVolume, title),
|
|
{
|
|
name: "Dormancy",
|
|
title: title("Dormancy"),
|
|
bottom: [line({ series: activity.dormancy, name: "Dormancy", color, unit: Unit.days })],
|
|
},
|
|
];
|
|
}
|
|
|
|
// ============================================================================
|
|
// 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(`${prefix}SOPR (${w.title})`),
|
|
bottom: [baseline({ series: ratio[w.key], name: "SOPR", unit: Unit.ratio, base: 1 })],
|
|
})),
|
|
];
|
|
}
|
|
|
|
// ============================================================================
|
|
// 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(`Sell Side Risk (${w.title})`),
|
|
bottom: percentRatio({ pattern: sellSideRisk[w.key], name: "Risk" }),
|
|
})),
|
|
];
|
|
}
|
|
|
|
// ============================================================================
|
|
// Single Cohort Activity Sections
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Full activity with adjusted SOPR (All/STH)
|
|
* @param {{ cohort: CohortAll | CohortFull, title: (name: string) => string }} args
|
|
* @returns {PartialOptionsGroup}
|
|
*/
|
|
export function createActivitySectionWithAdjusted({ cohort, title }) {
|
|
const { tree, color } = cohort;
|
|
const r = tree.realized;
|
|
const sopr = r.sopr;
|
|
|
|
return {
|
|
name: "Activity",
|
|
tree: [
|
|
...fullVolumeTree(tree.activity, color, title),
|
|
{
|
|
name: "SOPR",
|
|
tree: [
|
|
...singleRollingSoprTree(sopr.ratio, title),
|
|
{
|
|
name: "Adjusted",
|
|
tree: singleRollingSoprTree(sopr.adjusted.ratio, title, "Adjusted "),
|
|
},
|
|
],
|
|
},
|
|
{ name: "Sell Side Risk", tree: singleSellSideRiskTree(r.sellSideRiskRatio, title) },
|
|
],
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Activity section for cohorts with rolling SOPR + sell side risk (LTH, also CohortFull | CohortLongTerm)
|
|
* @param {{ cohort: CohortFull | CohortLongTerm, title: (name: string) => string }} args
|
|
* @returns {PartialOptionsGroup}
|
|
*/
|
|
export function createActivitySection({ cohort, title }) {
|
|
const { tree, color } = cohort;
|
|
const r = tree.realized;
|
|
const sopr = r.sopr;
|
|
|
|
return {
|
|
name: "Activity",
|
|
tree: [
|
|
...fullVolumeTree(tree.activity, color, title),
|
|
{
|
|
name: "SOPR",
|
|
tree: singleRollingSoprTree(sopr.ratio, title),
|
|
},
|
|
{ name: "Sell Side Risk", tree: singleSellSideRiskTree(r.sellSideRiskRatio, 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: [
|
|
...volumeAndCoinsTree(tree.activity, color, title),
|
|
...sentProfitLossTree(tree.activity.transferVolume, title),
|
|
{
|
|
name: "SOPR",
|
|
title: title("SOPR (24h)"),
|
|
bottom: [dotsBaseline({ series: sopr.ratio._24h, name: "SOPR", unit: Unit.ratio, base: 1 })],
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
|
|
/**
|
|
* 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: chartsFromCount({
|
|
pattern: cohort.tree.realized.sopr.valueCreated,
|
|
title: title("Volume"),
|
|
unit: Unit.usd,
|
|
}),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 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: ROLLING_WINDOWS.map((w) => ({
|
|
name: w.name,
|
|
title: title(`Volume (${w.title})`),
|
|
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
|
line({ series: tree.realized.sopr.valueCreated.sum[w.key], name, color, 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(`${prefix}SOPR (${w.title})`),
|
|
bottom: mapCohortsWithAll(list, all, (c) =>
|
|
baseline({ series: getRatio(c)[w.key], name: c.name, color: c.color, unit: Unit.ratio, base: 1 }),
|
|
),
|
|
}));
|
|
}
|
|
|
|
// ============================================================================
|
|
// Grouped Value/Flow Helpers
|
|
// ============================================================================
|
|
|
|
/**
|
|
* @template {{ color: Color, name: string }} T
|
|
* @template {{ color: Color, name: string }} A
|
|
* @param {readonly T[]} list
|
|
* @param {A} all
|
|
* @param {readonly { name: string, title: string, getCreated: (item: T | A) => AnySeriesPattern, getDestroyed: (item: T | A) => AnySeriesPattern }[]} windows
|
|
* @param {(name: string) => string} title
|
|
* @param {string} [prefix]
|
|
* @returns {PartialOptionsTree}
|
|
*/
|
|
|
|
|
|
// ============================================================================
|
|
// Grouped Activity Sections
|
|
// ============================================================================
|
|
|
|
/**
|
|
* @param {{ list: readonly CohortFull[], all: CohortAll, title: (name: string) => string }} args
|
|
* @returns {PartialOptionsGroup}
|
|
*/
|
|
export function createGroupedActivitySectionWithAdjusted({ list, all, title }) {
|
|
return {
|
|
name: "Activity",
|
|
tree: [
|
|
{
|
|
name: "Volume",
|
|
title: title("Sent Volume"),
|
|
bottom: flatMapCohortsWithAll(list, all, ({ name, color, tree }) => [
|
|
line({ series: tree.activity.transferVolume.sum._24h.sats, name, color, unit: Unit.sats }),
|
|
]),
|
|
},
|
|
{
|
|
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 "),
|
|
},
|
|
],
|
|
},
|
|
{
|
|
name: "Sell Side Risk",
|
|
tree: ROLLING_WINDOWS.map((w) => ({
|
|
name: w.name,
|
|
title: title(`Sell Side Risk (${w.title})`),
|
|
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
|
line({ series: tree.realized.sellSideRiskRatio[w.key].ratio, name, color, unit: Unit.ratio }),
|
|
),
|
|
})),
|
|
},
|
|
{
|
|
name: "Coindays Destroyed",
|
|
title: title("Coindays Destroyed"),
|
|
bottom: flatMapCohortsWithAll(list, all, ({ name, color, tree }) => [
|
|
line({ series: tree.activity.coindaysDestroyed.sum._24h, name, color, unit: Unit.coindays }),
|
|
]),
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Grouped activity for cohorts with rolling SOPR + sell side risk (LTH-like)
|
|
* @param {{ list: readonly (CohortFull | CohortLongTerm)[], all: CohortAll, title: (name: string) => string }} args
|
|
* @returns {PartialOptionsGroup}
|
|
*/
|
|
export function createGroupedActivitySection({ list, all, title }) {
|
|
return {
|
|
name: "Activity",
|
|
tree: [
|
|
{
|
|
name: "Volume",
|
|
title: title("Sent Volume"),
|
|
bottom: flatMapCohortsWithAll(list, all, ({ name, color, tree }) => [
|
|
line({ series: tree.activity.transferVolume.sum._24h.sats, name, color, unit: Unit.sats }),
|
|
]),
|
|
},
|
|
{
|
|
name: "SOPR",
|
|
tree: [
|
|
...groupedSoprCharts(list, all, (c) => c.tree.realized.sopr.ratio, title),
|
|
],
|
|
},
|
|
{
|
|
name: "Sell Side Risk",
|
|
tree: ROLLING_WINDOWS.map((w) => ({
|
|
name: w.name,
|
|
title: title(`Sell Side Risk (${w.title})`),
|
|
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
|
line({ series: tree.realized.sellSideRiskRatio[w.key].ratio, name, color, unit: Unit.ratio }),
|
|
),
|
|
})),
|
|
},
|
|
{
|
|
name: "Coindays Destroyed",
|
|
title: title("Coindays Destroyed"),
|
|
bottom: flatMapCohortsWithAll(list, all, ({ name, color, tree }) => [
|
|
line({ series: tree.activity.coindaysDestroyed.sum._24h, name, color, unit: Unit.coindays }),
|
|
]),
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 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: [
|
|
{
|
|
name: "Volume",
|
|
title: title("Sent Volume"),
|
|
bottom: flatMapCohortsWithAll(list, all, ({ name, color, tree }) => [
|
|
line({ series: tree.activity.transferVolume.sum._24h.sats, name, color, unit: Unit.sats }),
|
|
]),
|
|
},
|
|
{
|
|
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 }),
|
|
),
|
|
},
|
|
{
|
|
name: "Coindays Destroyed",
|
|
title: title("Coindays Destroyed"),
|
|
bottom: flatMapCohortsWithAll(list, all, ({ name, color, tree }) => [
|
|
line({ series: tree.activity.coindaysDestroyed.sum._24h, name, color, unit: Unit.coindays }),
|
|
]),
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|