website: snapshot

This commit is contained in:
nym21
2026-01-25 12:42:16 +01:00
parent 7cdf47a9e4
commit c6f63fd4a2
24 changed files with 2262 additions and 1818 deletions

View File

@@ -1,13 +1,9 @@
import { Unit } from "../utils/units.js";
import { priceLine, priceLines } from "./constants.js";
import { line, baseline } from "./series.js";
import { line } from "./series.js";
import {
satsBtcUsd,
percentileUsdMap,
percentileMap,
sdPatterns,
sdBandsUsd,
sdBandsRatio,
createRatioChart,
createZScoresFolder,
} from "./shared.js";
/**
@@ -18,229 +14,21 @@ import {
* @param {string} args.legend
* @param {AnyMetricPattern} args.price
* @param {ActivePriceRatioPattern} args.ratio
* @param {Color} [args.color]
* @param {Color} args.color
* @returns {PartialOptionsTree}
*/
function createCointimePriceWithRatioOptions(
ctx,
{ title, legend, price, ratio, color },
) {
const { colors } = ctx;
const pctUsdMap = percentileUsdMap(colors, ratio);
const pctMap = percentileMap(colors, ratio);
const sdPats = sdPatterns(ratio);
return [
{
name: "price",
title,
top: [line({ metric: price, name: legend, color, unit: Unit.usd })],
},
{
name: "Ratio",
title: `${title} Ratio`,
top: [
line({ metric: price, name: legend, color, unit: Unit.usd }),
...pctUsdMap.map(({ name: pctName, prop, color: pctColor }) =>
line({
metric: prop,
name: pctName,
color: pctColor,
defaultActive: false,
unit: Unit.usd,
options: { lineStyle: 1 },
}),
),
],
bottom: [
baseline({
metric: ratio.ratio,
name: "Ratio",
unit: Unit.ratio,
base: 1,
}),
line({
metric: ratio.ratio1wSma,
name: "1w SMA",
color: colors.lime,
unit: Unit.ratio,
defaultActive: false,
}),
line({
metric: ratio.ratio1mSma,
name: "1m SMA",
color: colors.teal,
unit: Unit.ratio,
defaultActive: false,
}),
line({
metric: ratio.ratio1ySd.sma,
name: "1y SMA",
color: colors.sky,
unit: Unit.ratio,
defaultActive: false,
}),
line({
metric: ratio.ratio2ySd.sma,
name: "2y SMA",
color: colors.indigo,
unit: Unit.ratio,
defaultActive: false,
}),
line({
metric: ratio.ratio4ySd.sma,
name: "4y SMA",
color: colors.purple,
unit: Unit.ratio,
defaultActive: false,
}),
line({
metric: ratio.ratioSd.sma,
name: "All SMA",
color: colors.rose,
unit: Unit.ratio,
defaultActive: false,
}),
...pctMap.map(({ name: pctName, prop, color: pctColor }) =>
line({
metric: prop,
name: pctName,
color: pctColor,
defaultActive: false,
unit: Unit.ratio,
options: { lineStyle: 1 },
}),
),
priceLine({ ctx, unit: Unit.ratio, number: 1 }),
],
},
{
name: "ZScores",
tree: [
// Compare all Z-Scores
{
name: "Compare",
title: `${title} Z-Scores`,
top: [
line({ metric: price, name: legend, color, unit: Unit.usd }),
line({
metric: ratio.ratio1ySd._0sdUsd,
name: "1y 0σ",
color: colors.orange,
defaultActive: false,
unit: Unit.usd,
}),
line({
metric: ratio.ratio2ySd._0sdUsd,
name: "2y 0σ",
color: colors.yellow,
defaultActive: false,
unit: Unit.usd,
}),
line({
metric: ratio.ratio4ySd._0sdUsd,
name: "4y 0σ",
color: colors.lime,
defaultActive: false,
unit: Unit.usd,
}),
line({
metric: ratio.ratioSd._0sdUsd,
name: "all 0σ",
color: colors.blue,
defaultActive: false,
unit: Unit.usd,
}),
],
bottom: [
line({
metric: ratio.ratioSd.zscore,
name: "all",
color: colors.blue,
unit: Unit.sd,
}),
line({
metric: ratio.ratio4ySd.zscore,
name: "4y",
color: colors.lime,
unit: Unit.sd,
}),
line({
metric: ratio.ratio2ySd.zscore,
name: "2y",
color: colors.yellow,
unit: Unit.sd,
}),
line({
metric: ratio.ratio1ySd.zscore,
name: "1y",
color: colors.orange,
unit: Unit.sd,
}),
...priceLines({
ctx,
unit: Unit.sd,
numbers: [0, 1, -1, 2, -2, 3, -3, 4, -4],
defaultActive: false,
}),
],
},
// Individual Z-Score charts
...sdPats.map(({ nameAddon, titleAddon, sd }) => ({
name: nameAddon,
title: `${title} ${titleAddon} Z-Score`,
top: [
line({ metric: price, name: legend, color, unit: Unit.usd }),
...sdBandsUsd(colors, sd).map(
({ name: bandName, prop, color: bandColor }) =>
line({
metric: prop,
name: bandName,
color: bandColor,
unit: Unit.usd,
defaultActive: false,
}),
),
],
bottom: [
baseline({
metric: sd.zscore,
name: "Z-Score",
unit: Unit.sd,
}),
baseline({
metric: ratio.ratio,
name: "Ratio",
unit: Unit.ratio,
base: 1,
}),
line({
metric: sd.sd,
name: "Volatility",
color: colors.gray,
unit: Unit.percentage,
}),
...sdBandsRatio(colors, sd).map(
({ name: bandName, prop, color: bandColor }) =>
line({
metric: prop,
name: bandName,
color: bandColor,
unit: Unit.ratio,
defaultActive: false,
}),
),
...priceLines({
ctx,
unit: Unit.sd,
numbers: [0, 1, -1, 2, -2, 3, -3],
defaultActive: false,
}),
],
})),
],
},
createRatioChart(ctx, { title, price, ratio, color }),
createZScoresFolder(ctx, { title, legend, price, ratio, color }),
];
}
@@ -252,7 +40,7 @@ function createCointimePriceWithRatioOptions(
export function createCointimeSection(ctx) {
const { colors, brk } = ctx;
const { cointime, distribution, supply } = brk.metrics;
const { pricing, cap, activity, supply: cointimeSupply, adjusted } = cointime;
const { pricing, cap, activity, supply: cointimeSupply, adjusted, reserveRisk, value } = cointime;
const { all } = distribution.utxoCohorts;
// Cointime prices data
@@ -481,6 +269,132 @@ export function createCointimeSection(ctx) {
],
},
// Reserve Risk
{
name: "Reserve Risk",
tree: [
{
name: "reserve risk",
title: "Reserve Risk",
bottom: [
line({
metric: reserveRisk.reserveRisk,
name: "Reserve Risk",
color: colors.orange,
unit: Unit.ratio,
}),
],
},
{
name: "hodl bank",
title: "HODL Bank",
bottom: [
line({
metric: reserveRisk.hodlBank,
name: "HODL Bank",
color: colors.blue,
unit: Unit.ratio,
}),
],
},
{
name: "vocdd 365d sma",
title: "VOCDD 365d SMA",
bottom: [
line({
metric: reserveRisk.vocdd365dSma,
name: "VOCDD 365d SMA",
color: colors.purple,
unit: Unit.ratio,
}),
],
},
],
},
// Cointime Value
{
name: "Value",
tree: [
{
name: "created",
title: "Cointime Value Created",
bottom: [
line({
metric: value.cointimeValueCreated.sum,
name: "Created",
color: colors.green,
unit: Unit.usd,
}),
line({
metric: value.cointimeValueCreated.cumulative,
name: "Cumulative",
color: colors.green,
unit: Unit.usd,
defaultActive: false,
}),
],
},
{
name: "destroyed",
title: "Cointime Value Destroyed",
bottom: [
line({
metric: value.cointimeValueDestroyed.sum,
name: "Destroyed",
color: colors.red,
unit: Unit.usd,
}),
line({
metric: value.cointimeValueDestroyed.cumulative,
name: "Cumulative",
color: colors.red,
unit: Unit.usd,
defaultActive: false,
}),
],
},
{
name: "stored",
title: "Cointime Value Stored",
bottom: [
line({
metric: value.cointimeValueStored.sum,
name: "Stored",
color: colors.blue,
unit: Unit.usd,
}),
line({
metric: value.cointimeValueStored.cumulative,
name: "Cumulative",
color: colors.blue,
unit: Unit.usd,
defaultActive: false,
}),
],
},
{
name: "vocdd",
title: "VOCDD (Value of Coin Days Destroyed)",
bottom: [
line({
metric: value.vocdd.sum,
name: "VOCDD",
color: colors.orange,
unit: Unit.usd,
}),
line({
metric: value.vocdd.cumulative,
name: "Cumulative",
color: colors.orange,
unit: Unit.usd,
defaultActive: false,
}),
],
},
],
},
// Adjusted metrics
{
name: "Adjusted",

View File

@@ -60,21 +60,8 @@ export function createContext({ brk }) {
fromValuePattern: (pattern, title, sumColor, cumulativeColor) =>
fromValuePattern(colors, pattern, title, sumColor, cumulativeColor),
/** @type {OmitFirstArg<typeof fromBitcoinPatternWithUnit>} */
fromBitcoinPatternWithUnit: (
pattern,
title,
unit,
sumColor,
cumulativeColor,
) =>
fromBitcoinPatternWithUnit(
colors,
pattern,
title,
unit,
sumColor,
cumulativeColor,
),
fromBitcoinPatternWithUnit: (pattern, unit, title, sumColor, cumulativeColor) =>
fromBitcoinPatternWithUnit(colors, pattern, unit, title, sumColor, cumulativeColor),
/** @type {OmitFirstArg<typeof fromBlockCountWithUnit>} */
fromBlockCountWithUnit: (pattern, unit, title, sumColor, cumulativeColor) =>
fromBlockCountWithUnit(

View File

@@ -16,6 +16,15 @@ import {
createAddressCountSeries,
createRealizedPriceSeries,
createRealizedPriceRatioSeries,
createSingleCoinsDestroyedSeries,
createGroupedCoinblocksDestroyedSeries,
createGroupedCoindaysDestroyedSeries,
createGroupedSatblocksDestroyedSeries,
createGroupedSatdaysDestroyedSeries,
createSingleSentSeries,
createGroupedSentSatsSeries,
createGroupedSentBitcoinSeries,
createGroupedSentDollarsSeries,
} from "./shared.js";
/**
@@ -123,7 +132,7 @@ export function createAddressCohortFolder(ctx, args) {
...createCostBasisSection(list, useGroupName, title),
// Activity section
...createActivitySection(list, useGroupName, title),
...createActivitySection(args, title),
],
};
}
@@ -208,20 +217,25 @@ function createRealizedPnlSection(ctx, args, title) {
color: colors.green,
unit: Unit.usd,
}),
line({
metric: realized.realizedProfit.cumulative,
name: "Profit Cumulative",
color: colors.green,
unit: Unit.usd,
defaultActive: false,
}),
line({
metric: realized.realizedLoss.sum,
name: "Loss",
color: colors.red,
unit: Unit.usd,
defaultActive: false,
}),
// RealizedPattern (address cohorts) doesn't have realizedProfitToLossRatio
line({
metric: realized.totalRealizedPnl,
name: "Total",
color: colors.default,
defaultActive: false,
metric: realized.realizedLoss.cumulative,
name: "Loss Cumulative",
color: colors.red,
unit: Unit.usd,
defaultActive: false,
}),
line({
metric: realized.negRealizedLoss.sum,
@@ -231,7 +245,176 @@ function createRealizedPnlSection(ctx, args, title) {
}),
line({
metric: realized.negRealizedLoss.cumulative,
name: "Negative Loss",
name: "Negative Loss Cumulative",
color: colors.red,
unit: Unit.usd,
defaultActive: false,
}),
line({
metric: realized.totalRealizedPnl,
name: "Total",
color: colors.default,
unit: Unit.usd,
defaultActive: false,
}),
baseline({
metric: realized.realizedProfitRelToRealizedCap.sum,
name: "Profit",
color: colors.green,
unit: Unit.pctRcap,
}),
baseline({
metric: realized.realizedProfitRelToRealizedCap.cumulative,
name: "Profit Cumulative",
color: colors.green,
unit: Unit.pctRcap,
defaultActive: false,
}),
baseline({
metric: realized.realizedLossRelToRealizedCap.sum,
name: "Loss",
color: colors.red,
unit: Unit.pctRcap,
}),
baseline({
metric: realized.realizedLossRelToRealizedCap.cumulative,
name: "Loss Cumulative",
color: colors.red,
unit: Unit.pctRcap,
defaultActive: false,
}),
],
},
{
name: "Net pnl",
title: `Net Realized P&L ${title}`,
bottom: [
baseline({
metric: realized.netRealizedPnl.sum,
name: "Net",
unit: Unit.usd,
}),
baseline({
metric: realized.netRealizedPnl.cumulative,
name: "Net Cumulative",
unit: Unit.usd,
defaultActive: false,
}),
baseline({
metric: realized.netRealizedPnlCumulative30dDelta,
name: "Cumulative 30d change",
unit: Unit.usd,
defaultActive: false,
}),
baseline({
metric: realized.netRealizedPnlRelToRealizedCap.sum,
name: "Net",
unit: Unit.pctRcap,
}),
baseline({
metric: realized.netRealizedPnlRelToRealizedCap.cumulative,
name: "Net Cumulative",
unit: Unit.pctRcap,
defaultActive: false,
}),
baseline({
metric: realized.netRealizedPnlCumulative30dDeltaRelToRealizedCap,
name: "Cumulative 30d change",
unit: Unit.pctRcap,
defaultActive: false,
}),
baseline({
metric: realized.netRealizedPnlCumulative30dDeltaRelToMarketCap,
name: "Cumulative 30d change",
unit: Unit.pctMcap,
}),
priceLine({
ctx,
unit: Unit.usd,
number: 1,
}),
priceLine({
ctx,
unit: Unit.pctMcap,
}),
priceLine({
ctx,
unit: Unit.pctRcap,
}),
],
},
{
name: "sopr",
title: `SOPR ${title}`,
bottom: [
baseline({
metric: realized.sopr,
name: "SOPR",
unit: Unit.ratio,
base: 1,
}),
baseline({
metric: realized.sopr7dEma,
name: "7d EMA",
color: [colors.lime, colors.rose],
unit: Unit.ratio,
defaultActive: false,
base: 1,
}),
baseline({
metric: realized.sopr30dEma,
name: "30d EMA",
color: [colors.avocado, colors.pink],
unit: Unit.ratio,
defaultActive: false,
base: 1,
}),
priceLine({
ctx,
unit: Unit.ratio,
number: 1,
}),
],
},
{
name: "Sell Side Risk",
title: `Sell Side Risk Ratio ${title}`,
bottom: [
line({
metric: realized.sellSideRiskRatio,
name: "Raw",
color: colors.orange,
unit: Unit.ratio,
}),
line({
metric: realized.sellSideRiskRatio7dEma,
name: "7d EMA",
color: colors.red,
unit: Unit.ratio,
defaultActive: false,
}),
line({
metric: realized.sellSideRiskRatio30dEma,
name: "30d EMA",
color: colors.rose,
unit: Unit.ratio,
defaultActive: false,
}),
],
},
{
name: "value",
title: `Value Created & Destroyed ${title}`,
bottom: [
line({
metric: realized.valueCreated,
name: "Created",
color: colors.emerald,
unit: Unit.usd,
}),
line({
metric: realized.valueDestroyed,
name: "Destroyed",
color: colors.red,
unit: Unit.usd,
}),
@@ -255,19 +438,6 @@ function createUnrealizedSection(ctx, list, useGroupName, title) {
{
name: "Unrealized",
tree: [
{
name: "nupl",
title: `Net Unrealized P&L ${title}`,
bottom: list.flatMap(({ color, name, tree }) => [
baseline({
metric: tree.unrealized.netUnrealizedPnl,
name: useGroupName ? name : "NUPL",
color: useGroupName ? color : [colors.red, colors.green],
unit: Unit.ratio,
options: { baseValue: { price: 0 } },
}),
]),
},
{
name: "profit",
title: `Unrealized Profit ${title}`,
@@ -275,7 +445,7 @@ function createUnrealizedSection(ctx, list, useGroupName, title) {
line({
metric: tree.unrealized.unrealizedProfit,
name: useGroupName ? name : "Profit",
color,
color: useGroupName ? color : colors.green,
unit: Unit.usd,
}),
]),
@@ -287,11 +457,117 @@ function createUnrealizedSection(ctx, list, useGroupName, title) {
line({
metric: tree.unrealized.unrealizedLoss,
name: useGroupName ? name : "Loss",
color,
color: useGroupName ? color : colors.red,
unit: Unit.usd,
}),
]),
},
{
name: "total pnl",
title: `Total Unrealized P&L ${title}`,
bottom: list.flatMap(({ color, name, tree }) => [
baseline({
metric: tree.unrealized.totalUnrealizedPnl,
name: useGroupName ? name : "Total",
color: useGroupName ? color : undefined,
unit: Unit.usd,
}),
]),
},
{
name: "negative loss",
title: `Negative Unrealized Loss ${title}`,
bottom: list.flatMap(({ color, name, tree }) => [
line({
metric: tree.unrealized.negUnrealizedLoss,
name: useGroupName ? name : "Negative Loss",
color: useGroupName ? color : colors.red,
unit: Unit.usd,
}),
]),
},
{
name: "Relative",
tree: [
{
name: "nupl",
title: `NUPL (Rel to Market Cap) ${title}`,
bottom: list.flatMap(({ color, name, tree }) => [
baseline({
metric: tree.relative.nupl,
name: useGroupName ? name : "NUPL",
color: useGroupName ? color : undefined,
unit: Unit.ratio,
options: { baseValue: { price: 0 } },
}),
]),
},
{
name: "profit",
title: `Unrealized Profit (% of Market Cap) ${title}`,
bottom: list.flatMap(({ color, name, tree }) => [
line({
metric: tree.relative.unrealizedProfitRelToMarketCap,
name: useGroupName ? name : "Profit",
color: useGroupName ? color : colors.green,
unit: Unit.pctMcap,
}),
]),
},
{
name: "loss",
title: `Unrealized Loss (% of Market Cap) ${title}`,
bottom: list.flatMap(({ color, name, tree }) => [
line({
metric: tree.relative.unrealizedLossRelToMarketCap,
name: useGroupName ? name : "Loss",
color: useGroupName ? color : colors.red,
unit: Unit.pctMcap,
}),
]),
},
{
name: "net pnl",
title: `Net Unrealized P&L (% of Market Cap) ${title}`,
bottom: list.flatMap(({ color, name, tree }) => [
baseline({
metric: tree.relative.netUnrealizedPnlRelToMarketCap,
name: useGroupName ? name : "Net",
color: useGroupName ? color : undefined,
unit: Unit.pctMcap,
}),
]),
},
{
name: "negative loss",
title: `Negative Unrealized Loss (% of Market Cap) ${title}`,
bottom: list.flatMap(({ color, name, tree }) => [
line({
metric: tree.relative.negUnrealizedLossRelToMarketCap,
name: useGroupName ? name : "Negative Loss",
color: useGroupName ? color : colors.red,
unit: Unit.pctMcap,
}),
]),
},
],
},
{
name: "nupl",
title: `Net Unrealized P&L ${title}`,
bottom: list.flatMap(({ color, name, tree }) => [
baseline({
metric: tree.unrealized.netUnrealizedPnl,
name: useGroupName ? name : "NUPL",
color: useGroupName ? color : undefined,
unit: Unit.ratio,
}),
priceLine({
ctx,
unit: Unit.ratio,
}),
]),
},
],
},
];
@@ -340,12 +616,37 @@ function createCostBasisSection(list, useGroupName, title) {
/**
* Create activity section
* @param {readonly AddressCohortObject[]} list
* @param {boolean} useGroupName
* @param {AddressCohortObject | AddressCohortGroupObject} args
* @param {string} title
* @returns {PartialOptionsTree}
*/
function createActivitySection(list, useGroupName, title) {
function createActivitySection(args, title) {
const list = "list" in args ? args.list : [args];
const isSingle = !("list" in args);
// Single cohort: all metrics on one chart
if (isSingle) {
const cohort = /** @type {AddressCohortObject} */ (args);
return [
{
name: "Activity",
tree: [
{
name: "Coins Destroyed",
title: `Coins Destroyed ${title}`,
bottom: createSingleCoinsDestroyedSeries(cohort),
},
{
name: "Sent",
title: `Sent ${title}`,
bottom: createSingleSentSeries(cohort),
},
],
},
];
}
// Grouped cohorts: split charts for comparison
return [
{
name: "Activity",
@@ -353,38 +654,42 @@ function createActivitySection(list, useGroupName, title) {
{
name: "coinblocks destroyed",
title: `Coinblocks Destroyed ${title}`,
bottom: list.flatMap(({ color, name, tree }) => [
line({
metric: tree.activity.coinblocksDestroyed.sum,
name: useGroupName ? name : "Coinblocks",
color,
unit: Unit.coinblocks,
}),
line({
metric: tree.activity.coinblocksDestroyed.cumulative,
name: useGroupName ? name : "Coinblocks",
color,
unit: Unit.coinblocks,
}),
]),
bottom: createGroupedCoinblocksDestroyedSeries(list),
},
{
name: "coindays destroyed",
title: `Coindays Destroyed ${title}`,
bottom: list.flatMap(({ color, name, tree }) => [
line({
metric: tree.activity.coindaysDestroyed.sum,
name: useGroupName ? name : "Coindays",
color,
unit: Unit.coindays,
}),
line({
metric: tree.activity.coindaysDestroyed.cumulative,
name: useGroupName ? name : "Coindays",
color,
unit: Unit.coindays,
}),
]),
bottom: createGroupedCoindaysDestroyedSeries(list),
},
{
name: "satblocks destroyed",
title: `Satblocks Destroyed ${title}`,
bottom: createGroupedSatblocksDestroyedSeries(list),
},
{
name: "satdays destroyed",
title: `Satdays Destroyed ${title}`,
bottom: createGroupedSatdaysDestroyedSeries(list),
},
{
name: "Sent",
tree: [
{
name: "sats",
title: `Sent (Sats) ${title}`,
bottom: createGroupedSentSatsSeries(list),
},
{
name: "bitcoin",
title: `Sent (BTC) ${title}`,
bottom: createGroupedSentBitcoinSeries(list),
},
{
name: "dollars",
title: `Sent ($) ${title}`,
bottom: createGroupedSentDollarsSeries(list),
},
],
},
],
},

View File

@@ -92,8 +92,8 @@ export function buildCohortData(colors, brk) {
};
});
// Min age cohorts (from X time) - CohortBasic (neither adjustedSopr nor percentiles)
/** @type {readonly CohortBasic[]} */
// Min age cohorts (from X time) - CohortBasicWithMarketCap (has RelToMarketCap)
/** @type {readonly CohortBasicWithMarketCap[]} */
const fromDate = entries(utxoCohorts.minAge).map(([key, tree]) => {
const names = MIN_AGE_NAMES[key];
return {
@@ -116,8 +116,8 @@ export function buildCohortData(colors, brk) {
};
});
// Epoch cohorts - CohortBasic (neither adjustedSopr nor percentiles)
/** @type {readonly CohortBasic[]} */
// Epoch cohorts - CohortBasicWithoutMarketCap (no RelToMarketCap)
/** @type {readonly CohortBasicWithoutMarketCap[]} */
const epoch = entries(utxoCohorts.epoch).map(([key, tree]) => {
const names = EPOCH_NAMES[key];
return {
@@ -128,8 +128,8 @@ export function buildCohortData(colors, brk) {
};
});
// UTXOs above amount - CohortBasic (neither adjustedSopr nor percentiles)
/** @type {readonly CohortBasic[]} */
// UTXOs above amount - CohortBasicWithMarketCap (has RelToMarketCap)
/** @type {readonly CohortBasicWithMarketCap[]} */
const utxosAboveAmount = entries(utxoCohorts.geAmount).map(([key, tree]) => {
const names = GE_AMOUNT_NAMES[key];
return {
@@ -154,8 +154,8 @@ export function buildCohortData(colors, brk) {
},
);
// UTXOs under amount - CohortBasic (neither adjustedSopr nor percentiles)
/** @type {readonly CohortBasic[]} */
// UTXOs under amount - CohortBasicWithMarketCap (has RelToMarketCap)
/** @type {readonly CohortBasicWithMarketCap[]} */
const utxosUnderAmount = entries(utxoCohorts.ltAmount).map(([key, tree]) => {
const names = LT_AMOUNT_NAMES[key];
return {
@@ -180,8 +180,8 @@ export function buildCohortData(colors, brk) {
},
);
// UTXOs amount ranges - CohortBasic (neither adjustedSopr nor percentiles)
/** @type {readonly CohortBasic[]} */
// UTXOs amount ranges - CohortBasicWithoutMarketCap (no RelToMarketCap)
/** @type {readonly CohortBasicWithoutMarketCap[]} */
const utxosAmountRanges = entries(utxoCohorts.amountRange).map(
([key, tree]) => {
const names = AMOUNT_RANGE_NAMES[key];
@@ -221,7 +221,7 @@ export function buildCohortData(colors, brk) {
};
});
/** @type {readonly CohortBasic[]} */
/** @type {readonly CohortBasicWithoutMarketCap[]} */
const typeOther = entries(utxoCohorts.type)
.filter(([key]) => !isAddressable(key))
.map(([key, tree]) => {
@@ -234,8 +234,8 @@ export function buildCohortData(colors, brk) {
};
});
// Year cohorts - CohortBasic (neither adjustedSopr nor percentiles)
/** @type {readonly CohortBasic[]} */
// Year cohorts - CohortBasicWithoutMarketCap (no RelToMarketCap)
/** @type {readonly CohortBasicWithoutMarketCap[]} */
const year = entries(utxoCohorts.year).map(([key, tree]) => {
const names = YEAR_NAMES[key];
return {

View File

@@ -11,7 +11,8 @@ export {
createCohortFolderFull,
createCohortFolderWithAdjusted,
createCohortFolderWithPercentiles,
createCohortFolderBasic,
createCohortFolderBasicWithMarketCap,
createCohortFolderBasicWithoutMarketCap,
createCohortFolderAddress,
} from "./utxo.js";
export { createAddressCohortFolder } from "./address.js";
@@ -27,6 +28,5 @@ export {
createRealizedPriceSeries,
createRealizedPriceRatioSeries,
createRealizedCapSeries,
createCostBasisMinMaxSeries,
createCostBasisPercentilesSeries,
} from "./shared.js";

View File

@@ -221,72 +221,256 @@ export function createRealizedCapSeries(list, useGroupName) {
}
/**
* Create cost basis min/max series (available on all cohorts)
* @param {readonly CohortObject[]} list
* Create cost basis percentile series (only for cohorts with CostBasisPattern2)
* Includes min (p0) and max (p100) with full rainbow coloring
* @param {Colors} colors
* @param {readonly CohortWithCostBasisPercentiles[]} list
* @param {boolean} useGroupName
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createCostBasisMinMaxSeries(list, useGroupName) {
return list.flatMap(({ color, name, tree }) => [
export function createCostBasisPercentilesSeries(colors, list, useGroupName) {
return list.flatMap(({ name, tree }) => {
const cb = tree.costBasis;
const p = cb.percentiles;
const n = (/** @type {number} */ pct) => (useGroupName ? `${name} p${pct}` : `p${pct}`);
return [
line({ metric: cb.max, name: n(100), color: colors.purple, unit: Unit.usd, defaultActive: false }),
line({ metric: p.pct95, name: n(95), color: colors.fuchsia, unit: Unit.usd, defaultActive: false }),
line({ metric: p.pct90, name: n(90), color: colors.pink, unit: Unit.usd, defaultActive: false }),
line({ metric: p.pct85, name: n(85), color: colors.pink, unit: Unit.usd, defaultActive: false }),
line({ metric: p.pct80, name: n(80), color: colors.rose, unit: Unit.usd, defaultActive: false }),
line({ metric: p.pct75, name: n(75), color: colors.red, unit: Unit.usd, defaultActive: false }),
line({ metric: p.pct70, name: n(70), color: colors.orange, unit: Unit.usd, defaultActive: false }),
line({ metric: p.pct65, name: n(65), color: colors.amber, unit: Unit.usd, defaultActive: false }),
line({ metric: p.pct60, name: n(60), color: colors.yellow, unit: Unit.usd, defaultActive: false }),
line({ metric: p.pct55, name: n(55), color: colors.yellow, unit: Unit.usd, defaultActive: false }),
line({ metric: p.pct50, name: n(50), color: colors.avocado, unit: Unit.usd }),
line({ metric: p.pct45, name: n(45), color: colors.lime, unit: Unit.usd, defaultActive: false }),
line({ metric: p.pct40, name: n(40), color: colors.green, unit: Unit.usd, defaultActive: false }),
line({ metric: p.pct35, name: n(35), color: colors.emerald, unit: Unit.usd, defaultActive: false }),
line({ metric: p.pct30, name: n(30), color: colors.teal, unit: Unit.usd, defaultActive: false }),
line({ metric: p.pct25, name: n(25), color: colors.teal, unit: Unit.usd, defaultActive: false }),
line({ metric: p.pct20, name: n(20), color: colors.cyan, unit: Unit.usd, defaultActive: false }),
line({ metric: p.pct15, name: n(15), color: colors.sky, unit: Unit.usd, defaultActive: false }),
line({ metric: p.pct10, name: n(10), color: colors.blue, unit: Unit.usd, defaultActive: false }),
line({ metric: p.pct05, name: n(5), color: colors.indigo, unit: Unit.usd, defaultActive: false }),
line({ metric: cb.min, name: n(0), color: colors.violet, unit: Unit.usd, defaultActive: false }),
];
});
}
// ============================================================================
// Activity Section Helpers
// ============================================================================
/**
* Create coins destroyed series (coinblocks, coindays, satblocks, satdays) for single cohort
* All metrics on one chart
* @param {CohortObject} cohort
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createSingleCoinsDestroyedSeries(cohort) {
const { tree, color } = cohort;
return [
line({
metric: tree.costBasis.min,
name: useGroupName ? `${name} min` : "Min",
metric: tree.activity.coinblocksDestroyed.sum,
name: "Coinblocks",
color,
unit: Unit.usd,
unit: Unit.coinblocks,
}),
line({
metric: tree.costBasis.max,
name: useGroupName ? `${name} max` : "Max",
metric: tree.activity.coinblocksDestroyed.cumulative,
name: "Coinblocks Cumulative",
color,
unit: Unit.usd,
unit: Unit.coinblocks,
defaultActive: false,
}),
line({
metric: tree.activity.coindaysDestroyed.sum,
name: "Coindays",
color,
unit: Unit.coindays,
}),
line({
metric: tree.activity.coindaysDestroyed.cumulative,
name: "Coindays Cumulative",
color,
unit: Unit.coindays,
defaultActive: false,
}),
line({
metric: tree.activity.satblocksDestroyed,
name: "Satblocks",
color,
unit: Unit.satblocks,
}),
line({
metric: tree.activity.satdaysDestroyed,
name: "Satdays",
color,
unit: Unit.satdays,
}),
];
}
/**
* Create coinblocks destroyed series for grouped cohorts (comparison)
* @param {readonly CohortObject[]} list
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createGroupedCoinblocksDestroyedSeries(list) {
return list.flatMap(({ color, name, tree }) => [
line({
metric: tree.activity.coinblocksDestroyed.sum,
name,
color,
unit: Unit.coinblocks,
}),
]);
}
/**
* Create cost basis percentile series (only for cohorts with CostBasisPattern2)
* @param {readonly CohortWithCostBasisPercentiles[]} list
* @param {boolean} useGroupName
* Create coindays destroyed series for grouped cohorts (comparison)
* @param {readonly CohortObject[]} list
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createCostBasisPercentilesSeries(list, useGroupName) {
return list.flatMap(({ color, name, tree }) => {
const percentiles = tree.costBasis.percentiles;
return [
line({
metric: percentiles.pct10,
name: useGroupName ? `${name} p10` : "p10",
color,
unit: Unit.usd,
defaultActive: false,
}),
line({
metric: percentiles.pct25,
name: useGroupName ? `${name} p25` : "p25",
color,
unit: Unit.usd,
defaultActive: false,
}),
line({
metric: percentiles.pct50,
name: useGroupName ? `${name} p50` : "p50",
color,
unit: Unit.usd,
}),
line({
metric: percentiles.pct75,
name: useGroupName ? `${name} p75` : "p75",
color,
unit: Unit.usd,
defaultActive: false,
}),
line({
metric: percentiles.pct90,
name: useGroupName ? `${name} p90` : "p90",
color,
unit: Unit.usd,
defaultActive: false,
}),
];
});
export function createGroupedCoindaysDestroyedSeries(list) {
return list.flatMap(({ color, name, tree }) => [
line({
metric: tree.activity.coindaysDestroyed.sum,
name,
color,
unit: Unit.coindays,
}),
]);
}
/**
* Create satblocks destroyed series for grouped cohorts (comparison)
* @param {readonly CohortObject[]} list
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createGroupedSatblocksDestroyedSeries(list) {
return list.flatMap(({ color, name, tree }) => [
line({
metric: tree.activity.satblocksDestroyed,
name,
color,
unit: Unit.satblocks,
}),
]);
}
/**
* Create satdays destroyed series for grouped cohorts (comparison)
* @param {readonly CohortObject[]} list
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createGroupedSatdaysDestroyedSeries(list) {
return list.flatMap(({ color, name, tree }) => [
line({
metric: tree.activity.satdaysDestroyed,
name,
color,
unit: Unit.satdays,
}),
]);
}
/**
* Create sent series (sats, btc, usd) for single cohort - all on one chart
* @param {CohortObject} cohort
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createSingleSentSeries(cohort) {
const { tree, color } = cohort;
return [
line({
metric: tree.activity.sent.sats.sum,
name: "Sent",
color,
unit: Unit.sats,
}),
line({
metric: tree.activity.sent.sats.cumulative,
name: "Cumulative",
color,
unit: Unit.sats,
defaultActive: false,
}),
line({
metric: tree.activity.sent.bitcoin.sum,
name: "Sent",
color,
unit: Unit.btc,
}),
line({
metric: tree.activity.sent.bitcoin.cumulative,
name: "Cumulative",
color,
unit: Unit.btc,
defaultActive: false,
}),
line({
metric: tree.activity.sent.dollars.sum,
name: "Sent",
color,
unit: Unit.usd,
}),
line({
metric: tree.activity.sent.dollars.cumulative,
name: "Cumulative",
color,
unit: Unit.usd,
defaultActive: false,
}),
];
}
/**
* Create sent (sats) series for grouped cohorts (comparison)
* @param {readonly CohortObject[]} list
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createGroupedSentSatsSeries(list) {
return list.flatMap(({ color, name, tree }) => [
line({
metric: tree.activity.sent.sats.sum,
name,
color,
unit: Unit.sats,
}),
]);
}
/**
* Create sent (bitcoin) series for grouped cohorts (comparison)
* @param {readonly CohortObject[]} list
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createGroupedSentBitcoinSeries(list) {
return list.flatMap(({ color, name, tree }) => [
line({
metric: tree.activity.sent.bitcoin.sum,
name,
color,
unit: Unit.btc,
}),
]);
}
/**
* Create sent (dollars) series for grouped cohorts (comparison)
* @param {readonly CohortObject[]} list
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createGroupedSentDollarsSeries(list) {
return list.flatMap(({ color, name, tree }) => [
line({
metric: tree.activity.sent.dollars.sum,
name,
color,
unit: Unit.usd,
}),
]);
}

View File

@@ -58,7 +58,7 @@ export function createCohortFolderAll(ctx, args) {
createSingleAddrCountChart(ctx, args, title),
createSingleRealizedSectionFull(ctx, args, title),
createSingleUnrealizedSectionAll(ctx, args, title),
createSingleCostBasisSectionWithPercentiles(args, title),
createSingleCostBasisSectionWithPercentiles(ctx, args, title),
createSingleActivitySectionWithAdjusted(ctx, args, title),
],
};
@@ -81,7 +81,7 @@ export function createCohortFolderFull(ctx, args) {
createGroupedUtxoCountChart(list, title),
createGroupedRealizedSectionWithAdjusted(ctx, list, title),
createGroupedUnrealizedSectionFull(ctx, list, title),
createGroupedCostBasisSectionWithPercentiles(list, title),
createGroupedCostBasisSectionWithPercentiles(ctx, list, title),
createGroupedActivitySectionWithAdjusted(list, title),
],
};
@@ -94,7 +94,7 @@ export function createCohortFolderFull(ctx, args) {
createSingleUtxoCountChart(args, title),
createSingleRealizedSectionFull(ctx, args, title),
createSingleUnrealizedSectionFull(ctx, args, title),
createSingleCostBasisSectionWithPercentiles(args, title),
createSingleCostBasisSectionWithPercentiles(ctx, args, title),
createSingleActivitySectionWithAdjusted(ctx, args, title),
],
};
@@ -153,7 +153,7 @@ export function createCohortFolderWithPercentiles(ctx, args) {
createGroupedUtxoCountChart(list, title),
createGroupedRealizedSectionBasic(ctx, list, title),
createGroupedUnrealizedSectionWithOwnCaps(ctx, list, title),
createGroupedCostBasisSectionWithPercentiles(list, title),
createGroupedCostBasisSectionWithPercentiles(ctx, list, title),
createGroupedActivitySectionBasic(list, title),
],
};
@@ -166,19 +166,55 @@ export function createCohortFolderWithPercentiles(ctx, args) {
createSingleUtxoCountChart(args, title),
createSingleRealizedSectionWithPercentiles(ctx, args, title),
createSingleUnrealizedSectionWithOwnCaps(ctx, args, title),
createSingleCostBasisSectionWithPercentiles(args, title),
createSingleCostBasisSectionWithPercentiles(ctx, args, title),
createSingleActivitySectionBasic(ctx, args, title),
],
};
}
/**
* Basic folder: no adjustedSopr, no percentiles (minAge.*, epoch.*, amount cohorts)
* Basic folder WITH RelToMarketCap (minAge.*, geAmount.*, ltAmount.*, type.*)
* @param {PartialContext} ctx
* @param {CohortBasic | CohortGroupBasic} args
* @param {CohortBasicWithMarketCap | CohortGroupBasicWithMarketCap} args
* @returns {PartialOptionsGroup}
*/
export function createCohortFolderBasic(ctx, args) {
export function createCohortFolderBasicWithMarketCap(ctx, args) {
if ("list" in args) {
const { list } = args;
const title = args.title ? `by ${args.title}` : "";
return {
name: args.name || "all",
tree: [
createGroupedSupplySection(list, title),
createGroupedUtxoCountChart(list, title),
createGroupedRealizedSectionBasic(ctx, list, title),
createGroupedUnrealizedSectionWithMarketCapOnly(ctx, list, title),
createGroupedCostBasisSection(list, title),
createGroupedActivitySectionBasic(list, title),
],
};
}
const title = args.title ? `of ${args.title}` : "";
return {
name: args.name || "all",
tree: [
createSingleSupplyChart(ctx, args, title),
createSingleUtxoCountChart(args, title),
createSingleRealizedSectionBasic(ctx, args, title),
createSingleUnrealizedSectionWithMarketCapOnly(ctx, args, title),
createSingleCostBasisSection(args, title),
createSingleActivitySectionBasic(ctx, args, title),
],
};
}
/**
* Basic folder WITHOUT RelToMarketCap (epoch.*, amountRange.*, year.*)
* @param {PartialContext} ctx
* @param {CohortBasicWithoutMarketCap | CohortGroupBasicWithoutMarketCap} args
* @returns {PartialOptionsGroup}
*/
export function createCohortFolderBasicWithoutMarketCap(ctx, args) {
if ("list" in args) {
const { list } = args;
const title = args.title ? `by ${args.title}` : "";
@@ -217,6 +253,7 @@ export function createCohortFolderBasic(ctx, args) {
/**
* Address folder: like basic but with address count (addressable type cohorts)
* Uses base unrealized section (no RelToMarketCap since it extends CohortBasicWithoutMarketCap)
* @param {PartialContext} ctx
* @param {CohortAddress | CohortGroupAddress} args
* @returns {PartialOptionsGroup}
@@ -468,7 +505,7 @@ function createSingleRealizedSectionWithPercentiles(ctx, cohort, title) {
/**
* Create realized section for CohortBasic (no adjustedSopr, partial ratio)
* @param {PartialContext} ctx
* @param {CohortBasic} cohort
* @param {CohortBasic | CohortAddress} cohort
* @param {string} title
* @returns {PartialOptionsGroup}
*/
@@ -491,7 +528,7 @@ function createSingleRealizedSectionBasic(ctx, cohort, title) {
/**
* Create realized section without adjusted SOPR for grouped cohorts
* @param {PartialContext} ctx
* @param {readonly (CohortWithPercentiles | CohortBasic)[]} list
* @param {readonly (CohortWithPercentiles | CohortBasic | CohortAddress)[]} list
* @param {string} title
* @returns {PartialOptionsGroup}
*/
@@ -558,10 +595,11 @@ function createSingleRealizedPriceChartsWithRatio(ctx, cohort, title) {
return [
createSingleRealizedPriceChart(cohort, title),
createRatioChart(ctx, {
title: `Realized Price ${title}`,
title,
price: tree.realized.realizedPrice,
ratio,
color,
name: "MVRV",
}),
createZScoresFolder(ctx, {
title: `Realized Price ${title}`,
@@ -577,7 +615,7 @@ function createSingleRealizedPriceChartsWithRatio(ctx, cohort, title) {
* Create realized price and basic ratio charts for cohorts with RealizedPriceExtraPattern
* (CohortWithAdjusted, CohortBasic have RealizedPattern/4 which has RealizedPriceExtraPattern)
* @param {PartialContext} ctx
* @param {CohortWithAdjusted | CohortBasic} cohort
* @param {CohortWithAdjusted | CohortBasic | CohortAddress} cohort
* @param {string} title
* @returns {PartialChartOption[]}
*/
@@ -617,6 +655,13 @@ function createSingleRealizedCapSeries(ctx, cohort) {
color,
unit: Unit.usd,
}),
line({
metric: tree.realized.realizedValue,
name: "Value",
color,
unit: Unit.usd,
defaultActive: false,
}),
baseline({
metric: tree.realized.realizedCap30dDelta,
name: "30d change",
@@ -689,8 +734,8 @@ function createSingleRealizedPnlSection(ctx, cohort, title) {
),
...fromBitcoinPatternWithUnit(
tree.realized.negRealizedLoss,
"Negative Loss",
Unit.usd,
"Negative Loss",
colors.red,
),
...("realizedProfitToLossRatio" in tree.realized
@@ -716,12 +761,26 @@ function createSingleRealizedPnlSection(ctx, cohort, title) {
color: colors.green,
unit: Unit.pctRcap,
}),
baseline({
metric: tree.realized.realizedProfitRelToRealizedCap.cumulative,
name: "Profit Cumulative",
color: colors.green,
unit: Unit.pctRcap,
defaultActive: false,
}),
baseline({
metric: tree.realized.realizedLossRelToRealizedCap.sum,
name: "Loss",
color: colors.red,
unit: Unit.pctRcap,
}),
baseline({
metric: tree.realized.realizedLossRelToRealizedCap.cumulative,
name: "Loss Cumulative",
color: colors.red,
unit: Unit.pctRcap,
defaultActive: false,
}),
priceLine({ ctx, unit: Unit.pctRcap }),
priceLine({ ctx, unit: Unit.usd, defaultActive: false }),
],
@@ -746,6 +805,12 @@ function createSingleRealizedPnlSection(ctx, cohort, title) {
name: "Net",
unit: Unit.pctRcap,
}),
baseline({
metric: tree.realized.netRealizedPnlRelToRealizedCap.cumulative,
name: "Net Cumulative",
unit: Unit.pctRcap,
defaultActive: false,
}),
baseline({
metric:
tree.realized.netRealizedPnlCumulative30dDeltaRelToRealizedCap,
@@ -900,7 +965,6 @@ function createGroupedRealizedPnlSections(ctx, list, title) {
name,
color,
unit: Unit.usd,
defaultActive: false,
}),
]),
priceLine({ ctx, unit: Unit.usd }),
@@ -980,7 +1044,7 @@ function createSingleBaseSoprChart(ctx, cohort, title) {
metric: tree.realized.sopr,
name: "SOPR",
unit: Unit.ratio,
options: { baseValue: { price: 1 } },
base: 1,
}),
baseline({
metric: tree.realized.sopr7dEma,
@@ -988,7 +1052,7 @@ function createSingleBaseSoprChart(ctx, cohort, title) {
color: [colors.lime, colors.rose],
unit: Unit.ratio,
defaultActive: false,
options: { baseValue: { price: 1 } },
base: 1,
}),
baseline({
metric: tree.realized.sopr30dEma,
@@ -996,7 +1060,7 @@ function createSingleBaseSoprChart(ctx, cohort, title) {
color: [colors.avocado, colors.pink],
unit: Unit.ratio,
defaultActive: false,
options: { baseValue: { price: 1 } },
base: 1,
}),
priceLine({ ctx, number: 1, unit: Unit.ratio }),
],
@@ -1064,7 +1128,7 @@ function createGroupedBaseSoprChart(ctx, list, title) {
name,
color,
unit: Unit.ratio,
options: { baseValue: { price: 1 } },
base: 1,
}),
baseline({
metric: tree.realized.sopr7dEma,
@@ -1072,7 +1136,7 @@ function createGroupedBaseSoprChart(ctx, list, title) {
color,
unit: Unit.ratio,
defaultActive: false,
options: { baseValue: { price: 1 } },
base: 1,
}),
baseline({
metric: tree.realized.sopr30dEma,
@@ -1080,7 +1144,7 @@ function createGroupedBaseSoprChart(ctx, list, title) {
color,
unit: Unit.ratio,
defaultActive: false,
options: { baseValue: { price: 1 } },
base: 1,
}),
]),
priceLine({ ctx, number: 1, unit: Unit.ratio }),
@@ -1171,7 +1235,7 @@ function createGroupedSoprSectionWithAdjusted(ctx, list, title) {
/**
* Create SOPR section without adjusted SOPR (for cohorts with RealizedPattern/2)
* @param {PartialContext} ctx
* @param {CohortWithPercentiles | CohortBasic} cohort
* @param {CohortWithPercentiles | CohortBasic | CohortAddress} cohort
* @param {string} title
* @returns {PartialOptionsGroup}
*/
@@ -1185,7 +1249,7 @@ function createSingleSoprSectionBasic(ctx, cohort, title) {
/**
* Create grouped SOPR section without adjusted SOPR
* @param {PartialContext} ctx
* @param {readonly (CohortWithPercentiles | CohortBasic)[]} list
* @param {readonly (CohortWithPercentiles | CohortBasic | CohortAddress)[]} list
* @param {string} title
* @returns {PartialOptionsGroup}
*/
@@ -1378,8 +1442,7 @@ function createNetUnrealizedPnlBaseMetric(tree) {
return baseline({
metric: tree.unrealized.netUnrealizedPnl,
name: "Net",
unit: Unit.ratio,
options: { baseValue: { price: 0 } },
unit: Unit.usd,
});
}
@@ -1457,6 +1520,18 @@ function createSingleUnrealizedSectionFull(ctx, cohort, title) {
priceLine({ ctx, unit: Unit.pctMcap }),
],
},
{
name: "nupl",
title: `NUPL ${title}`,
bottom: [
baseline({
metric: tree.relative.nupl,
name: "NUPL",
unit: Unit.ratio,
}),
priceLine({ ctx, unit: Unit.ratio }),
],
},
],
};
}
@@ -1493,6 +1568,22 @@ function createSingleUnrealizedSectionWithMarketCap(ctx, cohort, title) {
priceLine({ ctx, unit: Unit.pctMcap }),
],
},
...("nupl" in tree.relative
? [
{
name: "nupl",
title: `NUPL ${title}`,
bottom: [
baseline({
metric: tree.relative.nupl,
name: "NUPL",
unit: Unit.ratio,
}),
priceLine({ ctx, unit: Unit.ratio }),
],
},
]
: []),
],
};
}
@@ -1530,20 +1621,91 @@ function createSingleUnrealizedSectionWithOwnCaps(ctx, cohort, title) {
priceLine({ ctx, unit: Unit.usd }),
],
},
...("nupl" in tree.relative
? [
{
name: "nupl",
title: `NUPL ${title}`,
bottom: [
baseline({
metric: tree.relative.nupl,
name: "NUPL",
unit: Unit.ratio,
}),
priceLine({ ctx, unit: Unit.ratio }),
],
},
]
: []),
],
};
}
/**
* Unrealized section with only base metrics (no relative unrealized)
* Used by: Epoch cohorts (RelativePattern4)
* Unrealized section WITH RelToMarketCap metrics (for CohortBasicWithMarketCap)
* Used by: minAge.*, geAmount.*, ltAmount.*
* @param {PartialContext} ctx
* @param {{ tree: { unrealized: PatternAll["unrealized"] } }} cohort
* @param {CohortBasicWithMarketCap} cohort
* @param {string} title
* @returns {PartialOptionsGroup}
*/
function createSingleUnrealizedSectionWithMarketCapOnly(ctx, cohort, title) {
const { tree } = cohort;
return {
name: "Unrealized",
tree: [
{
name: "pnl",
title: `Unrealized P&L ${title}`,
bottom: [
...createUnrealizedPnlBaseMetrics(ctx, tree),
...createUnrealizedPnlRelToMarketCapMetrics(ctx, tree.relative),
priceLine({ ctx, unit: Unit.usd, defaultActive: false }),
priceLine({ ctx, unit: Unit.pctMcap, defaultActive: false }),
],
},
{
name: "Net pnl",
title: `Net Unrealized P&L ${title}`,
bottom: [
createNetUnrealizedPnlBaseMetric(tree),
...createNetUnrealizedPnlRelToMarketCapMetrics(tree.relative),
priceLine({ ctx, unit: Unit.usd }),
priceLine({ ctx, unit: Unit.pctMcap }),
],
},
...("nupl" in tree.relative
? [
{
name: "nupl",
title: `NUPL ${title}`,
bottom: [
baseline({
metric: tree.relative.nupl,
name: "NUPL",
unit: Unit.ratio,
}),
priceLine({ ctx, unit: Unit.ratio }),
],
},
]
: []),
],
};
}
/**
* Unrealized section with only base metrics (no RelToMarketCap)
* Used by: epoch.*, amountRange.*, year.*
* @param {PartialContext} ctx
* @param {CohortBasicWithoutMarketCap} cohort
* @param {string} title
* @returns {PartialOptionsGroup}
*/
function createSingleUnrealizedSectionBase(ctx, cohort, title) {
const { tree } = cohort;
return {
name: "Unrealized",
tree: [
@@ -1634,7 +1796,7 @@ function createGroupedUnrealizedSectionFull(ctx, list, title) {
metric: tree.unrealized.netUnrealizedPnl,
name,
color,
unit: Unit.ratio,
unit: Unit.usd,
}),
baseline({
metric: tree.relative.netUnrealizedPnlRelToMarketCap,
@@ -1661,6 +1823,21 @@ function createGroupedUnrealizedSectionFull(ctx, list, title) {
priceLine({ ctx, unit: Unit.pctOwnPnl }),
],
},
{
name: "nupl",
title: `NUPL ${title}`,
bottom: [
...list.flatMap(({ color, name, tree }) => [
baseline({
metric: tree.relative.nupl,
name,
color,
unit: Unit.ratio,
}),
]),
priceLine({ ctx, unit: Unit.ratio }),
],
},
],
};
}
@@ -1686,7 +1863,7 @@ function createGroupedUnrealizedSectionWithMarketCap(ctx, list, title) {
metric: tree.unrealized.netUnrealizedPnl,
name,
color,
unit: Unit.ratio,
unit: Unit.usd,
}),
baseline({
metric: tree.relative.netUnrealizedPnlRelToMarketCap,
@@ -1699,6 +1876,21 @@ function createGroupedUnrealizedSectionWithMarketCap(ctx, list, title) {
priceLine({ ctx, unit: Unit.pctMcap }),
],
},
{
name: "nupl",
title: `NUPL ${title}`,
bottom: [
...list.flatMap(({ color, name, tree }) => [
baseline({
metric: tree.relative.nupl,
name,
color,
unit: Unit.ratio,
}),
]),
priceLine({ ctx, unit: Unit.ratio }),
],
},
],
};
}
@@ -1711,6 +1903,7 @@ function createGroupedUnrealizedSectionWithMarketCap(ctx, list, title) {
* @returns {PartialOptionsGroup}
*/
function createGroupedUnrealizedSectionWithOwnCaps(ctx, list, title) {
const cohortsWithNupl = list.filter(({ tree }) => "nupl" in tree.relative);
return {
name: "Unrealized",
tree: [
@@ -1724,7 +1917,7 @@ function createGroupedUnrealizedSectionWithOwnCaps(ctx, list, title) {
metric: tree.unrealized.netUnrealizedPnl,
name,
color,
unit: Unit.ratio,
unit: Unit.usd,
}),
baseline({
metric: tree.relative.netUnrealizedPnlRelToOwnMarketCap,
@@ -1744,14 +1937,89 @@ function createGroupedUnrealizedSectionWithOwnCaps(ctx, list, title) {
priceLine({ ctx, unit: Unit.pctOwnPnl }),
],
},
...(cohortsWithNupl.length > 0
? [
{
name: "nupl",
title: `NUPL ${title}`,
bottom: [
...cohortsWithNupl.flatMap(({ color, name, tree }) => [
baseline({
metric: /** @type {{ nupl: AnyMetricPattern }} */ (
tree.relative
).nupl,
name,
color,
unit: Unit.ratio,
}),
]),
priceLine({ ctx, unit: Unit.ratio }),
],
},
]
: []),
],
};
}
/**
* Grouped unrealized section for Basic cohorts (base only)
* Grouped unrealized section WITH RelToMarketCap (for CohortBasicWithMarketCap)
* Used by: minAge.*, geAmount.*, ltAmount.*
* @param {PartialContext} ctx
* @param {readonly CohortBasic[]} list
* @param {readonly CohortBasicWithMarketCap[]} list
* @param {string} title
* @returns {PartialOptionsGroup}
*/
function createGroupedUnrealizedSectionWithMarketCapOnly(ctx, list, title) {
return {
name: "Unrealized",
tree: [
...createGroupedUnrealizedBaseCharts(list, title),
{
name: "Net pnl",
title: `Net Unrealized P&L ${title}`,
bottom: [
...list.flatMap(({ color, name, tree }) => [
baseline({
metric: tree.unrealized.netUnrealizedPnl,
name,
color,
unit: Unit.usd,
}),
baseline({
metric: tree.relative.netUnrealizedPnlRelToMarketCap,
name,
color,
unit: Unit.pctMcap,
}),
]),
priceLine({ ctx, unit: Unit.usd }),
priceLine({ ctx, unit: Unit.pctMcap }),
],
},
{
name: "nupl",
title: `NUPL ${title}`,
bottom: [
...list.flatMap(({ color, name, tree }) => [
baseline({
metric: tree.relative.nupl,
name,
color,
unit: Unit.ratio,
}),
]),
priceLine({ ctx, unit: Unit.ratio }),
],
},
],
};
}
/**
* Grouped unrealized section without RelToMarketCap (for CohortBasicWithoutMarketCap)
* @param {PartialContext} ctx
* @param {readonly CohortBasicWithoutMarketCap[]} list
* @param {string} title
* @returns {PartialOptionsGroup}
*/
@@ -1769,7 +2037,7 @@ function createGroupedUnrealizedSectionBase(ctx, list, title) {
metric: tree.unrealized.netUnrealizedPnl,
name,
color,
unit: Unit.ratio,
unit: Unit.usd,
}),
]),
priceLine({ ctx, unit: Unit.usd }),
@@ -1781,11 +2049,13 @@ function createGroupedUnrealizedSectionBase(ctx, list, title) {
/**
* Create cost basis section for single cohort WITH percentiles
* @param {PartialContext} ctx
* @param {CohortAll | CohortFull | CohortWithPercentiles} cohort
* @param {string} title
* @returns {PartialOptionsGroup}
*/
function createSingleCostBasisSectionWithPercentiles(cohort, title) {
function createSingleCostBasisSectionWithPercentiles(ctx, cohort, title) {
const { colors } = ctx;
const { color, tree } = cohort;
return {
@@ -1819,7 +2089,7 @@ function createSingleCostBasisSectionWithPercentiles(cohort, title) {
{
name: "percentiles",
title: `Cost Basis Percentiles ${title}`,
top: createCostBasisPercentilesSeries([cohort], false),
top: createCostBasisPercentilesSeries(colors, [cohort], false),
},
],
};
@@ -1827,11 +2097,13 @@ function createSingleCostBasisSectionWithPercentiles(cohort, title) {
/**
* Create cost basis section for grouped cohorts WITH percentiles
* @param {PartialContext} ctx
* @param {readonly (CohortFull | CohortWithPercentiles)[]} list
* @param {string} title
* @returns {PartialOptionsGroup}
*/
function createGroupedCostBasisSectionWithPercentiles(list, title) {
function createGroupedCostBasisSectionWithPercentiles(ctx, list, title) {
const { colors } = ctx;
return {
name: "Cost Basis",
tree: [
@@ -1861,13 +2133,18 @@ function createGroupedCostBasisSectionWithPercentiles(list, title) {
line({ metric: tree.costBasis.max, name, color, unit: Unit.usd }),
),
},
{
name: "percentiles",
title: `Cost Basis Percentiles ${title}`,
top: createCostBasisPercentilesSeries(colors, list, true),
},
],
};
}
/**
* Create cost basis section for single cohort (no percentiles)
* @param {CohortWithAdjusted | CohortBasic} cohort
* @param {CohortWithAdjusted | CohortBasic | CohortAddress} cohort
* @param {string} title
* @returns {PartialOptionsGroup}
*/
@@ -1908,7 +2185,7 @@ function createSingleCostBasisSection(cohort, title) {
/**
* Create cost basis section for grouped cohorts (no percentiles)
* @param {readonly (CohortWithAdjusted | CohortBasic)[]} list
* @param {readonly (CohortWithAdjusted | CohortBasic | CohortAddress)[]} list
* @param {string} title
* @returns {PartialOptionsGroup}
*/
@@ -1954,12 +2231,36 @@ function createGroupedCostBasisSection(list, title) {
* @returns {PartialOptionsGroup}
*/
function createSingleActivitySectionWithAdjusted(ctx, cohort, title) {
const { colors } = ctx;
const { colors, fromBlockCountWithUnit, fromBitcoinPatternWithUnit } = ctx;
const { tree, color } = cohort;
return {
name: "Activity",
tree: [
{
name: "Sent",
title: `Sent ${title}`,
bottom: [
...fromBlockCountWithUnit(
tree.activity.sent.sats,
Unit.sats,
undefined,
color,
),
...fromBitcoinPatternWithUnit(
tree.activity.sent.bitcoin,
Unit.btc,
undefined,
color,
),
...fromBlockCountWithUnit(
tree.activity.sent.dollars,
Unit.usd,
undefined,
color,
),
],
},
{
name: "Sell Side Risk",
title: `Sell Side Risk Ratio ${title}`,
@@ -2046,6 +2347,18 @@ function createSingleActivitySectionWithAdjusted(ctx, cohort, title) {
unit: Unit.coindays,
defaultActive: false,
}),
line({
metric: tree.activity.satblocksDestroyed,
name: "Satblocks",
color,
unit: Unit.satblocks,
}),
line({
metric: tree.activity.satdaysDestroyed,
name: "Satdays",
color,
unit: Unit.satdays,
}),
],
},
],
@@ -2055,17 +2368,41 @@ function createSingleActivitySectionWithAdjusted(ctx, cohort, title) {
/**
* Create activity section without adjusted values (for cohorts with RealizedPattern/2)
* @param {PartialContext} ctx
* @param {CohortWithPercentiles | CohortBasic} cohort
* @param {CohortWithPercentiles | CohortBasic | CohortAddress} cohort
* @param {string} title
* @returns {PartialOptionsGroup}
*/
function createSingleActivitySectionBasic(ctx, cohort, title) {
const { colors } = ctx;
const { colors, fromBlockCountWithUnit, fromBitcoinPatternWithUnit } = ctx;
const { tree, color } = cohort;
return {
name: "Activity",
tree: [
{
name: "Sent",
title: `Sent ${title}`,
bottom: [
...fromBlockCountWithUnit(
tree.activity.sent.sats,
Unit.sats,
undefined,
color,
),
...fromBitcoinPatternWithUnit(
tree.activity.sent.bitcoin,
Unit.btc,
undefined,
color,
),
...fromBlockCountWithUnit(
tree.activity.sent.dollars,
Unit.usd,
undefined,
color,
),
],
},
{
name: "Sell Side Risk",
title: `Sell Side Risk Ratio ${title}`,
@@ -2140,6 +2477,18 @@ function createSingleActivitySectionBasic(ctx, cohort, title) {
unit: Unit.coindays,
defaultActive: false,
}),
line({
metric: tree.activity.satblocksDestroyed,
name: "Satblocks",
color,
unit: Unit.satblocks,
}),
line({
metric: tree.activity.satdaysDestroyed,
name: "Satdays",
color,
unit: Unit.satdays,
}),
],
},
],
@@ -2278,7 +2627,7 @@ function createGroupedActivitySectionWithAdjusted(list, title) {
/**
* Create activity section for grouped cohorts without adjusted values (for cohorts with RealizedPattern/2)
* @param {readonly (CohortWithPercentiles | CohortBasic)[]} list
* @param {readonly (CohortWithPercentiles | CohortBasic | CohortAddress)[]} list
* @param {string} title
* @returns {PartialOptionsGroup}
*/

View File

@@ -1,45 +1,69 @@
/** Moving averages section */
import { Unit } from "../../utils/units.js";
import { priceLine, priceLines } from "../constants.js";
import { line, baseline } from "../series.js";
import {
percentileUsdMap,
percentileMap,
sdPatterns,
sdBandsUsd,
sdBandsRatio,
} from "../shared.js";
import { line } from "../series.js";
import { createRatioChart, createZScoresFolder } from "../shared.js";
import { periodIdToName } from "./utils.js";
/**
* Build averages data array from market patterns
* @param {Colors} colors
* @param {MarketMovingAverage} ma
*/
export function buildAverages(colors, ma) {
function buildSmaAverages(colors, ma) {
return /** @type {const} */ ([
["1w", 7, "red", ma.price1wSma, ma.price1wEma],
["8d", 8, "orange", ma.price8dSma, ma.price8dEma],
["13d", 13, "amber", ma.price13dSma, ma.price13dEma],
["21d", 21, "yellow", ma.price21dSma, ma.price21dEma],
["1m", 30, "lime", ma.price1mSma, ma.price1mEma],
["34d", 34, "green", ma.price34dSma, ma.price34dEma],
["55d", 55, "emerald", ma.price55dSma, ma.price55dEma],
["89d", 89, "teal", ma.price89dSma, ma.price89dEma],
["144d", 144, "cyan", ma.price144dSma, ma.price144dEma],
["200d", 200, "sky", ma.price200dSma, ma.price200dEma],
["1y", 365, "blue", ma.price1ySma, ma.price1yEma],
["2y", 730, "indigo", ma.price2ySma, ma.price2yEma],
["200w", 1400, "violet", ma.price200wSma, ma.price200wEma],
["4y", 1460, "purple", ma.price4ySma, ma.price4yEma],
]).map(([id, days, colorKey, sma, ema]) => ({
["1w", 7, "red", ma.price1wSma],
["8d", 8, "orange", ma.price8dSma],
["13d", 13, "amber", ma.price13dSma],
["21d", 21, "yellow", ma.price21dSma],
["1m", 30, "lime", ma.price1mSma],
["34d", 34, "green", ma.price34dSma],
["55d", 55, "emerald", ma.price55dSma],
["89d", 89, "teal", ma.price89dSma],
["111d", 111, "cyan", ma.price111dSma],
["144d", 144, "sky", ma.price144dSma],
["200d", 200, "blue", ma.price200dSma],
["350d", 350, "indigo", ma.price350dSma],
["1y", 365, "violet", ma.price1ySma],
["2y", 730, "purple", ma.price2ySma],
["200w", 1400, "fuchsia", ma.price200wSma],
["4y", 1460, "pink", ma.price4ySma],
]).map(([id, days, colorKey, ratio]) => ({
id,
name: periodIdToName(id, true),
days,
color: colors[colorKey],
sma,
ema,
ratio,
}));
}
/**
* @param {Colors} colors
* @param {MarketMovingAverage} ma
*/
function buildEmaAverages(colors, ma) {
return /** @type {const} */ ([
["1w", 7, "red", ma.price1wEma],
["8d", 8, "orange", ma.price8dEma],
["12d", 12, "amber", ma.price12dEma],
["13d", 13, "yellow", ma.price13dEma],
["21d", 21, "lime", ma.price21dEma],
["26d", 26, "green", ma.price26dEma],
["1m", 30, "emerald", ma.price1mEma],
["34d", 34, "teal", ma.price34dEma],
["55d", 55, "cyan", ma.price55dEma],
["89d", 89, "sky", ma.price89dEma],
["144d", 144, "blue", ma.price144dEma],
["200d", 200, "indigo", ma.price200dEma],
["1y", 365, "violet", ma.price1yEma],
["2y", 730, "purple", ma.price2yEma],
["200w", 1400, "fuchsia", ma.price200wEma],
["4y", 1460, "pink", ma.price4yEma],
]).map(([id, days, colorKey, ratio]) => ({
id,
name: periodIdToName(id, true),
days,
color: colors[colorKey],
ratio,
}));
}
@@ -50,271 +74,77 @@ export function buildAverages(colors, ma) {
* @param {string} args.title
* @param {string} args.legend
* @param {EmaRatioPattern} args.ratio
* @param {Color} [args.color]
* @param {Color} args.color
* @returns {PartialOptionsTree}
*/
export function createPriceWithRatioOptions(
ctx,
{ title, legend, ratio, color },
) {
const { colors } = ctx;
const priceMetric = ratio.price;
const pctUsdMap = percentileUsdMap(colors, ratio);
const pctMap = percentileMap(colors, ratio);
const sdPats = sdPatterns(ratio);
return [
{
name: "price",
title,
top: [line({ metric: priceMetric, name: legend, color, unit: Unit.usd })],
},
{
name: "Ratio",
title: `${title} Ratio`,
top: [
line({ metric: priceMetric, name: legend, color, unit: Unit.usd }),
...pctUsdMap.map(({ name: pctName, prop, color: pctColor }) =>
line({
metric: prop,
name: pctName,
color: pctColor,
defaultActive: false,
unit: Unit.usd,
options: { lineStyle: 1 },
}),
),
],
bottom: [
baseline({
metric: ratio.ratio,
name: "Ratio",
base: 1,
unit: Unit.ratio,
}),
.../** @type {const} */ ([
{
metric: ratio.ratio1wSma,
name: "1w SMA",
color: colors.lime,
},
{
metric: ratio.ratio1mSma,
name: "1m SMA",
color: colors.teal,
},
{
metric: ratio.ratio1ySd.sma,
name: "1y SMA",
color: colors.sky,
},
{
metric: ratio.ratio2ySd.sma,
name: "2y SMA",
color: colors.indigo,
},
{
metric: ratio.ratio4ySd.sma,
name: "4y SMA",
color: colors.purple,
},
{
metric: ratio.ratioSd.sma,
name: "All SMA",
color: colors.rose,
},
]).map(({ metric, name, color }) =>
line({
metric,
name,
color,
unit: Unit.ratio,
defaultActive: false,
style: 1,
}),
),
...pctMap.map(({ name: pctName, prop, color: pctColor }) =>
line({
metric: prop,
name: pctName,
color: pctColor,
defaultActive: false,
unit: Unit.ratio,
style: 1,
}),
),
priceLine({ ctx, unit: Unit.ratio, number: 1 }),
],
},
{
name: "ZScores",
tree: [
{
name: "Compare",
title: `${title} Z-Scores`,
top: [
line({ metric: priceMetric, name: legend, color, unit: Unit.usd }),
line({
metric: ratio.ratio1ySd._0sdUsd,
name: "1y 0σ",
color: colors.orange,
defaultActive: false,
unit: Unit.usd,
}),
line({
metric: ratio.ratio2ySd._0sdUsd,
name: "2y 0σ",
color: colors.yellow,
defaultActive: false,
unit: Unit.usd,
}),
line({
metric: ratio.ratio4ySd._0sdUsd,
name: "4y 0σ",
color: colors.lime,
defaultActive: false,
unit: Unit.usd,
}),
line({
metric: ratio.ratioSd._0sdUsd,
name: "all 0σ",
color: colors.blue,
defaultActive: false,
unit: Unit.usd,
}),
],
bottom: [
line({
metric: ratio.ratioSd.zscore,
name: "all",
color: colors.blue,
unit: Unit.sd,
}),
line({
metric: ratio.ratio4ySd.zscore,
name: "4y",
color: colors.lime,
unit: Unit.sd,
}),
line({
metric: ratio.ratio2ySd.zscore,
name: "2y",
color: colors.yellow,
unit: Unit.sd,
}),
line({
metric: ratio.ratio1ySd.zscore,
name: "1y",
color: colors.orange,
unit: Unit.sd,
}),
...priceLines({
ctx,
unit: Unit.sd,
numbers: [0, 1, -1, 2, -2, 3, -3],
defaultActive: false,
}),
],
},
...sdPats.map(({ nameAddon, titleAddon, sd }) => ({
name: nameAddon,
title: `${title} ${titleAddon} Z-Score`,
top: [
line({ metric: priceMetric, name: legend, color, unit: Unit.usd }),
...sdBandsUsd(colors, sd).map(
({ name: bandName, prop, color: bandColor }) =>
line({
metric: prop,
name: bandName,
color: bandColor,
unit: Unit.usd,
defaultActive: false,
}),
),
],
bottom: [
baseline({
metric: sd.zscore,
name: "Z-Score",
unit: Unit.sd,
}),
baseline({
metric: ratio.ratio,
name: "Ratio",
unit: Unit.ratio,
base: 1,
}),
line({
metric: sd.sd,
name: "Volatility",
color: colors.gray,
unit: Unit.percentage,
}),
...sdBandsRatio(colors, sd).map(
({ name: bandName, prop, color: bandColor }) =>
line({
metric: prop,
name: bandName,
color: bandColor,
unit: Unit.ratio,
defaultActive: false,
}),
),
priceLine({ ctx, unit: Unit.ratio, number: 1 }),
priceLine({
ctx,
unit: Unit.sd,
}),
...priceLines({
ctx,
unit: Unit.sd,
numbers: [1, -1, 2, -2, 3, -3],
defaultActive: false,
}),
],
})),
],
},
createRatioChart(ctx, { title, price: priceMetric, ratio, color }),
createZScoresFolder(ctx, {
title,
legend,
price: priceMetric,
ratio,
color,
}),
];
}
/**
* Create Averages section
* @param {PartialContext} ctx
* @param {ReturnType<typeof buildAverages>} averages
* @param {MarketMovingAverage} movingAverage
*/
export function createAveragesSection(ctx, averages) {
export function createAveragesSection(ctx, movingAverage) {
const { colors } = ctx;
const smaAverages = buildSmaAverages(colors, movingAverage);
const emaAverages = buildEmaAverages(colors, movingAverage);
/**
* @param {string} label
* @param {ReturnType<typeof buildSmaAverages> | ReturnType<typeof buildEmaAverages>} averages
*/
const createSubSection = (label, averages) => ({
name: label,
tree: [
{
name: "Compare",
title: `Price ${label}s`,
top: averages.map(({ id, color, ratio }) =>
line({
metric: ratio.price,
name: id,
color,
unit: Unit.usd,
}),
),
},
...averages.map(({ name, color, ratio }) => ({
name,
tree: createPriceWithRatioOptions(ctx, {
ratio,
title: `${name} ${label}`,
legend: "average",
color,
}),
})),
],
});
return {
name: "Averages",
tree: [
{ nameAddon: "Simple", metricAddon: /** @type {const} */ ("sma") },
{ nameAddon: "Exponential", metricAddon: /** @type {const} */ ("ema") },
].map(({ nameAddon, metricAddon }) => ({
name: nameAddon,
tree: [
{
name: "Compare",
title: `Price ${metricAddon.toUpperCase()}s`,
top: averages.map(({ id, color, sma, ema }) =>
line({
metric: (metricAddon === "sma" ? sma : ema).price,
name: id,
color,
unit: Unit.usd,
}),
),
},
...averages.map(({ name, color, sma, ema }) => ({
name,
tree: createPriceWithRatioOptions(ctx, {
ratio: metricAddon === "sma" ? sma : ema,
title: `${name} ${metricAddon.toUpperCase()}`,
legend: "average",
color,
}),
})),
],
})),
createSubSection("SMA", smaAverages),
createSubSection("EMA", emaAverages),
],
};
}

View File

@@ -3,7 +3,7 @@
import { localhost } from "../../utils/env.js";
import { Unit } from "../../utils/units.js";
import { candlestick, line } from "../series.js";
import { buildAverages, createAveragesSection } from "./averages.js";
import { createAveragesSection } from "./averages.js";
import { createPerformanceSection } from "./performance.js";
import { createIndicatorsSection } from "./indicators/index.js";
import { createInvestingSection } from "./investing.js";
@@ -27,7 +27,6 @@ export function createMarketSection(ctx) {
indicators,
} = market;
const averages = buildAverages(colors, movingAverage);
return {
name: "Market",
@@ -163,7 +162,7 @@ export function createMarketSection(ctx) {
},
// Averages
createAveragesSection(ctx, averages),
createAveragesSection(ctx, movingAverage),
// Performance
createPerformanceSection(ctx, returns),

View File

@@ -7,7 +7,8 @@ import {
createCohortFolderFull,
createCohortFolderWithAdjusted,
createCohortFolderWithPercentiles,
createCohortFolderBasic,
createCohortFolderBasicWithMarketCap,
createCohortFolderBasicWithoutMarketCap,
createCohortFolderAddress,
createAddressCohortFolder,
} from "./distribution/index.js";
@@ -57,8 +58,12 @@ export function createPartialOptions({ brk }) {
/** @param {CohortWithPercentiles} cohort */
const mapWithPercentiles = (cohort) =>
createCohortFolderWithPercentiles(ctx, cohort);
/** @param {CohortBasic} cohort */
const mapBasic = (cohort) => createCohortFolderBasic(ctx, cohort);
/** @param {CohortBasicWithMarketCap} cohort */
const mapBasicWithMarketCap = (cohort) =>
createCohortFolderBasicWithMarketCap(ctx, cohort);
/** @param {CohortBasicWithoutMarketCap} cohort */
const mapBasicWithoutMarketCap = (cohort) =>
createCohortFolderBasicWithoutMarketCap(ctx, cohort);
/** @param {CohortAddress} cohort */
const mapAddress = (cohort) => createCohortFolderAddress(ctx, cohort);
/** @param {AddressCohortObject} cohort */
@@ -119,7 +124,7 @@ export function createPartialOptions({ brk }) {
list: typeAddressable,
}),
...typeAddressable.map(mapAddress),
...typeOther.map(mapBasic),
...typeOther.map(mapBasicWithoutMarketCap),
],
},
@@ -143,12 +148,12 @@ export function createPartialOptions({ brk }) {
{
name: "At Least",
tree: [
createCohortFolderBasic(ctx, {
createCohortFolderBasicWithMarketCap(ctx, {
name: "Compare",
title: "Age At Least",
list: fromDate,
}),
...fromDate.map(mapBasic),
...fromDate.map(mapBasicWithMarketCap),
],
},
// Range
@@ -174,36 +179,36 @@ export function createPartialOptions({ brk }) {
{
name: "Under",
tree: [
createCohortFolderBasic(ctx, {
createCohortFolderBasicWithMarketCap(ctx, {
name: "Compare",
title: "Amount Under",
list: utxosUnderAmount,
}),
...utxosUnderAmount.map(mapBasic),
...utxosUnderAmount.map(mapBasicWithMarketCap),
],
},
// Above (≥ X sats)
{
name: "Above",
tree: [
createCohortFolderBasic(ctx, {
createCohortFolderBasicWithMarketCap(ctx, {
name: "Compare",
title: "Amount Above",
list: utxosAboveAmount,
}),
...utxosAboveAmount.map(mapBasic),
...utxosAboveAmount.map(mapBasicWithMarketCap),
],
},
// Range
{
name: "Range",
tree: [
createCohortFolderBasic(ctx, {
createCohortFolderBasicWithoutMarketCap(ctx, {
name: "Compare",
title: "Amount Range",
list: utxosAmountRanges,
}),
...utxosAmountRanges.map(mapBasic),
...utxosAmountRanges.map(mapBasicWithoutMarketCap),
],
},
],
@@ -252,29 +257,29 @@ export function createPartialOptions({ brk }) {
],
},
// Epochs - CohortBasic
// Epochs - CohortBasicWithoutMarketCap (no RelToMarketCap)
{
name: "Epochs",
tree: [
createCohortFolderBasic(ctx, {
createCohortFolderBasicWithoutMarketCap(ctx, {
name: "Compare",
title: "Epoch",
list: epoch,
}),
...epoch.map(mapBasic),
...epoch.map(mapBasicWithoutMarketCap),
],
},
// Years - CohortBasic
// Years - CohortBasicWithoutMarketCap (no RelToMarketCap)
{
name: "Years",
tree: [
createCohortFolderBasic(ctx, {
createCohortFolderBasicWithoutMarketCap(ctx, {
name: "Compare",
title: "Year",
list: year,
}),
...year.map(mapBasic),
...year.map(mapBasicWithoutMarketCap),
],
},
],

View File

@@ -718,8 +718,8 @@ export function fromValuePattern(
* Create sum/cumulative series from a BitcoinPattern ({ sum, cumulative }) with explicit unit and colors
* @param {Colors} colors
* @param {{ sum: AnyMetricPattern, cumulative: AnyMetricPattern }} pattern
* @param {string} title
* @param {Unit} unit
* @param {string} [title]
* @param {Color} [sumColor]
* @param {Color} [cumulativeColor]
* @returns {AnyFetchedSeriesBlueprint[]}
@@ -727,21 +727,21 @@ export function fromValuePattern(
export function fromBitcoinPatternWithUnit(
colors,
pattern,
title,
unit,
title = "",
sumColor,
cumulativeColor,
) {
return [
{
metric: pattern.sum,
title: `${title} sum`,
title: `${title} sum`.trim(),
color: sumColor,
unit,
},
{
metric: pattern.cumulative,
title: `${title} cumulative`,
title: `${title} cumulative`.trim(),
color: cumulativeColor ?? colors.stat.cumulative,
unit,
defaultActive: false,
@@ -763,7 +763,7 @@ export function fromBlockCountWithUnit(
colors,
pattern,
unit,
title,
title = "",
sumColor,
cumulativeColor,
) {

View File

@@ -148,14 +148,15 @@ export function ratioSmas(colors, ratio) {
* @param {AnyMetricPattern} args.price - The price metric to show in top pane
* @param {ActivePriceRatioPattern} args.ratio - The ratio pattern
* @param {Color} args.color
* @param {string} [args.name] - Optional name override (default: "ratio")
* @returns {PartialChartOption}
*/
export function createRatioChart(ctx, { title, price, ratio, color }) {
export function createRatioChart(ctx, { title, price, ratio, color, name }) {
const { colors } = ctx;
return {
name: "ratio",
title: `${title} Ratio`,
name: name ?? "ratio",
title: name ? (title ? `${name} - ${title}` : name) : `${title} Ratio`,
top: [
line({ metric: price, name: "price", color, unit: Unit.usd }),
...percentileUsdMap(colors, ratio).map(({ name, prop, color }) =>

View File

@@ -145,10 +145,16 @@
* - AgeRangePattern (ageRange.*)
* @typedef {LongTermPattern | AgeRangePattern} PatternWithPercentiles
*
* Patterns with neither (RealizedPattern/2, CostBasisPattern):
* Patterns with RelToMarketCap in relative (RelativePattern):
* - BasicUtxoPattern (minAge.*, geAmount.*, ltAmount.*)
* - EpochPattern (epoch.*)
* @typedef {BasicUtxoPattern | EpochPattern} PatternBasic
* @typedef {BasicUtxoPattern} PatternBasicWithMarketCap
*
* Patterns without RelToMarketCap in relative (RelativePattern4):
* - EpochPattern (epoch.*, amountRange.*, year.*, type.*)
* @typedef {EpochPattern} PatternBasicWithoutMarketCap
*
* Union of basic patterns (for backwards compat)
* @typedef {PatternBasicWithMarketCap | PatternBasicWithoutMarketCap} PatternBasic
*
* ============================================================================
* Cohort Object Types (by capability)
@@ -183,19 +189,29 @@
* @property {Color} color
* @property {PatternWithPercentiles} tree
*
* Basic cohort: neither (minAge.*, epoch.*, amount cohorts)
* @typedef {Object} CohortBasic
* Basic cohort WITH RelToMarketCap (minAge.*, geAmount.*, ltAmount.*)
* @typedef {Object} CohortBasicWithMarketCap
* @property {string} name
* @property {string} title
* @property {Color} color
* @property {PatternBasic} tree
* @property {PatternBasicWithMarketCap} tree
*
* Basic cohort WITHOUT RelToMarketCap (epoch.*, amountRange.*, year.*, type.*)
* @typedef {Object} CohortBasicWithoutMarketCap
* @property {string} name
* @property {string} title
* @property {Color} color
* @property {PatternBasicWithoutMarketCap} tree
*
* Union of basic cohort types
* @typedef {CohortBasicWithMarketCap | CohortBasicWithoutMarketCap} CohortBasic
*
* ============================================================================
* Extended Cohort Types (with address count)
* ============================================================================
*
* Basic cohort with address count (for "type" cohorts)
* @typedef {CohortBasic & { addrCount: Brk.MetricPattern1<Brk.StoredU64> }} CohortAddress
* Addressable cohort with address count (for "type" cohorts - no RelToMarketCap)
* @typedef {CohortBasicWithoutMarketCap & { addrCount: Brk.MetricPattern1<Brk.StoredU64> }} CohortAddress
*
* ============================================================================
* Cohort Group Types (by capability)
@@ -216,10 +232,18 @@
* @property {string} title
* @property {readonly CohortWithPercentiles[]} list
*
* @typedef {Object} CohortGroupBasic
* @typedef {Object} CohortGroupBasicWithMarketCap
* @property {string} name
* @property {string} title
* @property {readonly CohortBasic[]} list
* @property {readonly CohortBasicWithMarketCap[]} list
*
* @typedef {Object} CohortGroupBasicWithoutMarketCap
* @property {string} name
* @property {string} title
* @property {readonly CohortBasicWithoutMarketCap[]} list
*
* Union of basic cohort group types
* @typedef {CohortGroupBasicWithMarketCap | CohortGroupBasicWithoutMarketCap} CohortGroupBasic
*
* @typedef {Object} UtxoCohortGroupObject
* @property {string} name

View File

@@ -13,6 +13,34 @@ function walk(node, map, path) {
map.set(/** @type {AnyMetricPattern} */ (node), path);
} else if (node && typeof node === "object") {
for (const [key, value] of Object.entries(node)) {
const kn = key.toLowerCase();
if (
kn === "mvrv" ||
kn === "time" ||
kn === "height" ||
kn === "constants" ||
kn === "oracle" ||
kn === "split" ||
kn === "outpoint" ||
kn === "positions" ||
kn === "outputtype" ||
kn === "heighttopool" ||
kn.endsWith("index") ||
kn.endsWith("indexes") ||
kn.endsWith("bytes") ||
(kn.startsWith("_") && kn.endsWith("start"))
)
continue;
// if (
// kn === "mvrv" ||
// kn.endsWith("index") ||
// kn.endsWith("indexes") ||
// kn.endsWith("start") ||
// kn.endsWith("hash") ||
// kn.endsWith("data") ||
// kn.endsWith("constants")
// )
// return;
walk(/** @type {TreeNode | null | undefined} */ (value), map, [
...path,
key,