global: snapshot

This commit is contained in:
nym21
2026-03-15 00:57:53 +01:00
parent 0d177494d9
commit 9e36a4188a
50 changed files with 2765 additions and 1239 deletions

View File

@@ -77,6 +77,127 @@ function volumeAndCoinsTree(activity, color, title) {
];
}
/**
* Sent in profit/loss breakdown tree (shared by full and mid-level activity)
* @param {Brk.BaseCumulativeInSumPattern} sent
* @param {(metric: string) => string} title
* @returns {PartialOptionsTree}
*/
function sentProfitLossTree(sent, title) {
return [
{
name: "Sent In Profit",
tree: [
{
name: "USD",
title: title("Sent Volume In Profit"),
bottom: [
line({ metric: sent.inProfit.base.usd, name: "Base", color: colors.profit, unit: Unit.usd }),
line({ metric: sent.inProfit.sum._24h.usd, name: "24h", color: colors.time._24h, unit: Unit.usd, defaultActive: false }),
line({ metric: sent.inProfit.sum._1w.usd, name: "1w", color: colors.time._1w, unit: Unit.usd, defaultActive: false }),
line({ metric: sent.inProfit.sum._1m.usd, name: "1m", color: colors.time._1m, unit: Unit.usd, defaultActive: false }),
line({ metric: sent.inProfit.sum._1y.usd, name: "1y", color: colors.time._1y, unit: Unit.usd, defaultActive: false }),
],
},
{
name: "BTC",
title: title("Sent Volume In Profit (BTC)"),
bottom: [
line({ metric: sent.inProfit.base.btc, name: "Base", color: colors.profit, unit: Unit.btc }),
line({ metric: sent.inProfit.sum._24h.btc, name: "24h", color: colors.time._24h, unit: Unit.btc, defaultActive: false }),
line({ metric: sent.inProfit.sum._1w.btc, name: "1w", color: colors.time._1w, unit: Unit.btc, defaultActive: false }),
line({ metric: sent.inProfit.sum._1m.btc, name: "1m", color: colors.time._1m, unit: Unit.btc, defaultActive: false }),
line({ metric: sent.inProfit.sum._1y.btc, name: "1y", color: colors.time._1y, unit: Unit.btc, defaultActive: false }),
],
},
{
name: "Sats",
title: title("Sent Volume In Profit (Sats)"),
bottom: [
line({ metric: sent.inProfit.base.sats, name: "Base", color: colors.profit, unit: Unit.sats }),
line({ metric: sent.inProfit.sum._24h.sats, name: "24h", color: colors.time._24h, unit: Unit.sats, defaultActive: false }),
line({ metric: sent.inProfit.sum._1w.sats, name: "1w", color: colors.time._1w, unit: Unit.sats, defaultActive: false }),
line({ metric: sent.inProfit.sum._1m.sats, name: "1m", color: colors.time._1m, unit: Unit.sats, defaultActive: false }),
line({ metric: sent.inProfit.sum._1y.sats, name: "1y", color: colors.time._1y, unit: Unit.sats, defaultActive: false }),
],
},
{ name: "Cumulative", title: title("Cumulative Sent In Profit"), bottom: [
line({ metric: sent.inProfit.cumulative.usd, name: "USD", color: colors.profit, unit: Unit.usd }),
line({ metric: sent.inProfit.cumulative.btc, name: "BTC", color: colors.profit, unit: Unit.btc, defaultActive: false }),
line({ metric: sent.inProfit.cumulative.sats, name: "Sats", color: colors.profit, unit: Unit.sats, defaultActive: false }),
]},
],
},
{
name: "Sent In Loss",
tree: [
{
name: "USD",
title: title("Sent Volume In Loss"),
bottom: [
line({ metric: sent.inLoss.base.usd, name: "Base", color: colors.loss, unit: Unit.usd }),
line({ metric: sent.inLoss.sum._24h.usd, name: "24h", color: colors.time._24h, unit: Unit.usd, defaultActive: false }),
line({ metric: sent.inLoss.sum._1w.usd, name: "1w", color: colors.time._1w, unit: Unit.usd, defaultActive: false }),
line({ metric: sent.inLoss.sum._1m.usd, name: "1m", color: colors.time._1m, unit: Unit.usd, defaultActive: false }),
line({ metric: sent.inLoss.sum._1y.usd, name: "1y", color: colors.time._1y, unit: Unit.usd, defaultActive: false }),
],
},
{
name: "BTC",
title: title("Sent Volume In Loss (BTC)"),
bottom: [
line({ metric: sent.inLoss.base.btc, name: "Base", color: colors.loss, unit: Unit.btc }),
line({ metric: sent.inLoss.sum._24h.btc, name: "24h", color: colors.time._24h, unit: Unit.btc, defaultActive: false }),
line({ metric: sent.inLoss.sum._1w.btc, name: "1w", color: colors.time._1w, unit: Unit.btc, defaultActive: false }),
line({ metric: sent.inLoss.sum._1m.btc, name: "1m", color: colors.time._1m, unit: Unit.btc, defaultActive: false }),
line({ metric: sent.inLoss.sum._1y.btc, name: "1y", color: colors.time._1y, unit: Unit.btc, defaultActive: false }),
],
},
{
name: "Sats",
title: title("Sent Volume In Loss (Sats)"),
bottom: [
line({ metric: sent.inLoss.base.sats, name: "Base", color: colors.loss, unit: Unit.sats }),
line({ metric: sent.inLoss.sum._24h.sats, name: "24h", color: colors.time._24h, unit: Unit.sats, defaultActive: false }),
line({ metric: sent.inLoss.sum._1w.sats, name: "1w", color: colors.time._1w, unit: Unit.sats, defaultActive: false }),
line({ metric: sent.inLoss.sum._1m.sats, name: "1m", color: colors.time._1m, unit: Unit.sats, defaultActive: false }),
line({ metric: sent.inLoss.sum._1y.sats, name: "1y", color: colors.time._1y, unit: Unit.sats, defaultActive: false }),
],
},
{ name: "Cumulative", title: title("Cumulative Sent In Loss"), bottom: [
line({ metric: sent.inLoss.cumulative.usd, name: "USD", color: colors.loss, unit: Unit.usd }),
line({ metric: sent.inLoss.cumulative.btc, name: "BTC", color: colors.loss, unit: Unit.btc, defaultActive: false }),
line({ metric: sent.inLoss.cumulative.sats, name: "Sats", color: colors.loss, unit: Unit.sats, defaultActive: false }),
]},
],
},
];
}
/**
* Volume and coins tree with coinyears, dormancy, and sent in profit/loss (All/STH/LTH)
* @param {Brk.CoindaysCoinyearsDormancySentPattern} activity
* @param {Color} color
* @param {(metric: string) => string} title
* @returns {PartialOptionsTree}
*/
function fullVolumeTree(activity, color, title) {
return [
...volumeAndCoinsTree(activity, color, title),
...sentProfitLossTree(activity.sent, title),
{
name: "Coinyears Destroyed",
title: title("Coinyears Destroyed"),
bottom: [line({ metric: activity.coinyearsDestroyed, name: "CYD", color, unit: Unit.years })],
},
{
name: "Dormancy",
title: title("Dormancy"),
bottom: [line({ metric: activity.dormancy, name: "Dormancy", color, unit: Unit.days })],
},
];
}
// ============================================================================
// Shared SOPR Helpers
// ============================================================================
@@ -349,7 +470,7 @@ export function createActivitySectionWithAdjusted({ cohort, title }) {
return {
name: "Activity",
tree: [
...volumeAndCoinsTree(tree.activity, color, title),
...fullVolumeTree(tree.activity, color, title),
{
name: "SOPR",
tree: [
@@ -400,7 +521,7 @@ export function createActivitySection({ cohort, title }) {
return {
name: "Activity",
tree: [
...volumeAndCoinsTree(tree.activity, color, title),
...fullVolumeTree(tree.activity, color, title),
{
name: "SOPR",
tree: singleRollingSoprTree(sopr.ratio, title),
@@ -430,6 +551,7 @@ export function createActivitySectionWithActivity({ cohort, title }) {
name: "Activity",
tree: [
...volumeAndCoinsTree(tree.activity, color, title),
...sentProfitLossTree(tree.activity.sent, title),
{
name: "SOPR",
title: title("SOPR (24h)"),

View File

@@ -83,6 +83,7 @@ export function buildCohortData() {
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) => ({

View File

@@ -115,6 +115,34 @@ function circulatingSupplyPctSeries(supply) {
];
}
/**
* Ratio of Circulating Supply series (total, profit, loss)
* @param {{ relToCirculating: { ratio: AnyMetricPattern }, inProfit: { relToCirculating: { ratio: AnyMetricPattern } }, inLoss: { relToCirculating: { ratio: AnyMetricPattern } } }} supply
* @returns {AnyFetchedSeriesBlueprint[]}
*/
function circulatingSupplyRatioSeries(supply) {
return [
line({
metric: supply.relToCirculating.ratio,
name: "Total",
color: colors.default,
unit: Unit.ratio,
}),
line({
metric: supply.inProfit.relToCirculating.ratio,
name: "In Profit",
color: colors.profit,
unit: Unit.ratio,
}),
line({
metric: supply.inLoss.relToCirculating.ratio,
name: "In Loss",
color: colors.loss,
unit: Unit.ratio,
}),
];
}
/**
* @param {readonly (UtxoCohortObject | CohortWithoutRelative)[]} list
* @param {CohortAll} all
@@ -157,7 +185,7 @@ function singleDeltaTree(delta, unit, title, name) {
* @template {{ name: string, color: Color }} A
* @param {readonly T[]} list
* @param {A} all
* @param {(c: T | A) => ChangeRatePattern | ChangeRatePattern2} getDelta
* @param {(c: T | A) => DeltaPattern} getDelta
* @param {Unit} unit
* @param {(metric: string) => string} title
* @param {string} name
@@ -309,11 +337,21 @@ export function createHoldingsSectionWithRelative({ cohort, title }) {
tree: [
{
name: "Supply",
title: title("Supply"),
bottom: [
...fullSupplySeries(supply),
...circulatingSupplyPctSeries(supply),
...ownSupplyPctSeries(supply),
tree: [
{
name: "Overview",
title: title("Supply"),
bottom: [
...fullSupplySeries(supply),
...circulatingSupplyPctSeries(supply),
...ownSupplyPctSeries(supply),
],
},
{
name: "Ratio",
title: title("Supply (% of Circulating)"),
bottom: circulatingSupplyRatioSeries(supply),
},
],
},
singleUtxoCountChart(cohort, title),
@@ -341,10 +379,20 @@ export function createHoldingsSectionWithOwnSupply({ cohort, title }) {
tree: [
{
name: "Supply",
title: title("Supply"),
bottom: [
...fullSupplySeries(supply),
...circulatingSupplyPctSeries(supply),
tree: [
{
name: "Overview",
title: title("Supply"),
bottom: [
...fullSupplySeries(supply),
...circulatingSupplyPctSeries(supply),
],
},
{
name: "Ratio",
title: title("Supply (% of Circulating)"),
bottom: circulatingSupplyRatioSeries(supply),
},
],
},
singleUtxoCountChart(cohort, title),
@@ -359,6 +407,33 @@ export function createHoldingsSectionWithOwnSupply({ cohort, title }) {
};
}
/**
* Holdings with inProfit/inLoss (no rel, no address count)
* For: CohortWithoutRelative (p2ms, unknown, empty)
* @param {{ cohort: CohortWithoutRelative, title: (metric: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createHoldingsSectionWithProfitLoss({ cohort, title }) {
return {
name: "Holdings",
tree: [
{
name: "Supply",
title: title("Supply"),
bottom: fullSupplySeries(cohort.tree.supply),
},
singleUtxoCountChart(cohort, title),
{
name: "Change",
tree: [
singleDeltaTree(cohort.tree.supply.delta, Unit.sats, title, "Supply"),
singleDeltaTree(cohort.tree.outputs.unspentCount.delta, Unit.count, title, "UTXO Count"),
],
},
],
};
}
/**
* Holdings for CohortAddress (has inProfit/inLoss but no rel, plus address count)
* @param {{ cohort: CohortAddress, title: (metric: string) => string }} args
@@ -559,6 +634,66 @@ export function createGroupedHoldingsSection({ list, all, title }) {
};
}
/**
* Grouped holdings with inProfit/inLoss (no rel, no address count)
* For: CohortWithoutRelative (p2ms, unknown, empty)
* @param {{ list: readonly CohortWithoutRelative[], all: CohortAll, title: (metric: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createGroupedHoldingsSectionWithProfitLoss({
list,
all,
title,
}) {
return {
name: "Holdings",
tree: [
{
name: "Supply",
tree: [
{
name: "Total",
title: title("Supply"),
bottom: flatMapCohortsWithAll(list, all, ({ name, color, tree }) =>
satsBtcUsd({ pattern: tree.supply.total, name, color }),
),
},
{
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,
}),
),
},
],
},
groupedUtxoCountChart(list, all, title),
{
name: "Change",
tree: [
groupedDeltaTree(list, all, (c) => c.tree.supply.delta, Unit.sats, title, "Supply"),
groupedDeltaTree(list, all, (c) => c.tree.outputs.unspentCount.delta, Unit.count, title, "UTXO Count"),
],
},
],
};
}
/**
* Grouped holdings with inProfit/inLoss + relToCirculating (no relToOwn)
* For: CohortWithAdjusted, CohortAgeRange

View File

@@ -10,7 +10,7 @@
* - activity.js: SOPR, Volume, Lifespan
*/
import { formatCohortTitle } from "../shared.js";
import { formatCohortTitle, satsBtcUsd } from "../shared.js";
// Section builders
import {
@@ -18,9 +18,11 @@ import {
createHoldingsSectionAll,
createHoldingsSectionAddress,
createHoldingsSectionAddressAmount,
createHoldingsSectionWithProfitLoss,
createHoldingsSectionWithRelative,
createHoldingsSectionWithOwnSupply,
createGroupedHoldingsSection,
createGroupedHoldingsSectionWithProfitLoss,
createGroupedHoldingsSectionAddress,
createGroupedHoldingsSectionAddressAmount,
createGroupedHoldingsSectionWithRelative,
@@ -42,14 +44,16 @@ import {
createGroupedCostBasisSectionWithPercentiles,
} from "./cost-basis.js";
import {
createProfitabilitySection,
createProfitabilitySectionAll,
createProfitabilitySectionFull,
createProfitabilitySectionWithNupl,
createProfitabilitySectionWithInvestedCapitalPct,
createProfitabilitySectionBasicWithInvestedCapitalPct,
createProfitabilitySectionAddress,
createProfitabilitySectionWithProfitLoss,
createProfitabilitySectionLongTerm,
createGroupedProfitabilitySection,
createGroupedProfitabilitySectionWithProfitLoss,
createGroupedProfitabilitySectionWithNupl,
createGroupedProfitabilitySectionWithInvestedCapitalPct,
createGroupedProfitabilitySectionBasicWithInvestedCapitalPct,
@@ -126,7 +130,7 @@ export function createCohortFolderWithAdjusted(cohort) {
createHoldingsSectionWithOwnSupply({ cohort, title }),
createValuationSection({ cohort, title }),
createPricesSectionBasic({ cohort, title }),
createProfitabilitySectionWithNupl({ cohort, title }),
createProfitabilitySectionWithInvestedCapitalPct({ cohort, title }),
createActivitySectionWithActivity({ cohort, title }),
],
};
@@ -191,6 +195,22 @@ export function createCohortFolderAgeRange(cohort) {
};
}
/**
* Age range folder with matured supply
* @param {CohortAgeRangeWithMatured} cohort
* @returns {PartialOptionsGroup}
*/
export function createCohortFolderAgeRangeWithMatured(cohort) {
const folder = createCohortFolderAgeRange(cohort);
const title = formatCohortTitle(cohort.name);
folder.tree.push({
name: "Matured",
title: title("Matured Supply"),
bottom: satsBtcUsd({ pattern: cohort.matured, name: cohort.name }),
});
return folder;
}
/**
* Basic folder WITH RelToMarketCap
* @param {CohortBasicWithMarketCap} cohort
@@ -242,7 +262,7 @@ export function createCohortFolderAddress(cohort) {
createHoldingsSectionAddress({ cohort, title }),
createValuationSection({ cohort, title }),
createPricesSectionBasic({ cohort, title }),
createProfitabilitySectionBasicWithInvestedCapitalPct({ cohort, title }),
createProfitabilitySectionAddress({ cohort, title }),
createActivitySectionMinimal({ cohort, title }),
],
};
@@ -258,10 +278,10 @@ export function createCohortFolderWithoutRelative(cohort) {
return {
name: cohort.name || "all",
tree: [
createHoldingsSection({ cohort, title }),
createHoldingsSectionWithProfitLoss({ cohort, title }),
createValuationSection({ cohort, title }),
createPricesSectionBasic({ cohort, title }),
createProfitabilitySection({ cohort, title }),
createProfitabilitySectionWithProfitLoss({ cohort, title }),
createActivitySectionMinimal({ cohort, title }),
],
};
@@ -331,7 +351,11 @@ export function createGroupedCohortFolderWithAdjusted({
createGroupedHoldingsSectionWithOwnSupply({ list, all, title }),
createGroupedValuationSection({ list, all, title }),
createGroupedPricesSection({ list, all, title }),
createGroupedProfitabilitySectionWithInvestedCapitalPct({ list, all, title }),
createGroupedProfitabilitySectionWithInvestedCapitalPct({
list,
all,
title,
}),
createGroupedActivitySectionWithActivity({ list, all, title }),
],
};
@@ -412,6 +436,28 @@ export function createGroupedCohortFolderAgeRange({
};
}
/**
* @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",
title: title("Matured Supply"),
bottom: list.flatMap((cohort) =>
satsBtcUsd({ pattern: cohort.matured, name: cohort.name, color: cohort.color }),
),
});
return folder;
}
/**
* @param {CohortGroupBasicWithMarketCap} args
* @returns {PartialOptionsGroup}
@@ -503,10 +549,10 @@ export function createGroupedCohortFolderWithoutRelative({
return {
name: name || "all",
tree: [
createGroupedHoldingsSection({ list, all, title }),
createGroupedHoldingsSectionWithProfitLoss({ list, all, title }),
createGroupedValuationSection({ list, all, title }),
createGroupedPricesSection({ list, all, title }),
createGroupedProfitabilitySection({ list, all, title }),
createGroupedProfitabilitySectionWithProfitLoss({ list, all, title }),
createGroupedActivitySectionMinimal({ list, all, title }),
],
};

View File

@@ -98,8 +98,8 @@ export function createPricesSectionBasic({ cohort, title }) {
top: [price({ metric: tree.realized.price, name: "Realized", color })],
},
{
name: "Ratio",
title: title("Realized Price Ratio"),
name: "MVRV",
title: title("MVRV"),
bottom: [
baseline({
metric: tree.realized.mvrv,
@@ -109,6 +109,18 @@ export function createPricesSectionBasic({ cohort, title }) {
}),
],
},
{
name: "Price Ratio",
title: title("Realized Price Ratio"),
bottom: [
baseline({
metric: tree.realized.price.ratio,
name: "Price Ratio",
unit: Unit.ratio,
base: 1,
}),
],
},
],
},
],

View File

@@ -105,6 +105,7 @@ function unrealizedPnlTreeAll(u, title) {
{ name: "USD", title: title("Unrealized P&L"), bottom: unrealizedUsdSeries(u) },
relPnlChart(u.profit.relToMcap, u.loss.relToMcap, "% of Mcap", title),
relPnlChart(u.profit.relToOwnGross, u.loss.relToOwnGross, "% of Own P&L", title),
...unrealizedCumulativeRollingTree(u.profit, u.loss, title),
];
}
@@ -120,6 +121,7 @@ function unrealizedPnlTreeFull(u, title) {
relPnlChart(u.profit.relToMcap, u.loss.relToMcap, "% of Mcap", title),
relPnlChart(u.profit.relToOwnMcap, u.loss.relToOwnMcap, "% of Own Mcap", title),
relPnlChart(u.profit.relToOwnGross, u.loss.relToOwnGross, "% of Own P&L", title),
...unrealizedCumulativeRollingTree(u.profit, u.loss, title),
];
}
@@ -139,21 +141,89 @@ function unrealizedPnlTreeLongTerm(u, title) {
},
relPnlChart(u.profit.relToOwnMcap, u.loss.relToOwnMcap, "% of Own Mcap", title),
relPnlChart(u.profit.relToOwnGross, u.loss.relToOwnGross, "% of Own P&L", title),
...unrealizedCumulativeRollingTree(u.profit, u.loss, title),
];
}
/**
* Unrealized P&L (USD only) for mid-tier cohorts (AgeRange/MaxAge)
* Unrealized P&L tree for mid-tier cohorts (AgeRange/MaxAge)
* @param {Brk.LossNetNuplProfitPattern} u
* @returns {AnyFetchedSeriesBlueprint[]}
* @param {(metric: string) => string} title
* @returns {PartialOptionsTree}
*/
function unrealizedMid(u) {
function unrealizedPnlTreeMid(u, title) {
return [
...pnlLines(
{ profit: u.profit.base.usd, loss: u.loss.base.usd, negLoss: u.loss.negative },
Unit.usd,
),
priceLine({ unit: Unit.usd, defaultActive: false }),
{
name: "USD",
title: title("Unrealized P&L"),
bottom: [
...pnlLines(
{ profit: u.profit.base.usd, loss: u.loss.base.usd, negLoss: u.loss.negative },
Unit.usd,
),
priceLine({ unit: Unit.usd, defaultActive: false }),
],
},
...unrealizedCumulativeRollingTree(u.profit, u.loss, title),
];
}
/**
* Unrealized cumulative + rolling P&L tree (profit and loss have cumulative.usd + sum[w].usd)
* @param {{ cumulative: { usd: AnyMetricPattern }, sum: { _24h: { usd: AnyMetricPattern }, _1w: { usd: AnyMetricPattern }, _1m: { usd: AnyMetricPattern }, _1y: { usd: AnyMetricPattern } } }} profit
* @param {{ cumulative: { usd: AnyMetricPattern }, sum: { _24h: { usd: AnyMetricPattern }, _1w: { usd: AnyMetricPattern }, _1m: { usd: AnyMetricPattern }, _1y: { usd: AnyMetricPattern } } }} loss
* @param {(metric: string) => string} title
* @returns {PartialOptionsTree}
*/
function unrealizedCumulativeRollingTree(profit, loss, title) {
return [
{
name: "Cumulative",
title: title("Cumulative Unrealized P&L"),
bottom: [
line({ metric: profit.cumulative.usd, name: "Profit", color: colors.profit, unit: Unit.usd }),
line({ metric: loss.cumulative.usd, name: "Loss", color: colors.loss, unit: Unit.usd }),
],
},
{
name: "Rolling",
tree: [
{
name: "Profit",
tree: [
{
name: "Compare",
title: title("Rolling Unrealized Profit"),
bottom: ROLLING_WINDOWS.map((w) =>
line({ metric: profit.sum[w.key].usd, name: w.name, color: w.color, unit: Unit.usd }),
),
},
...ROLLING_WINDOWS.map((w) => ({
name: w.name,
title: title(`Unrealized Profit (${w.name})`),
bottom: [line({ metric: profit.sum[w.key].usd, name: "Profit", color: colors.profit, unit: Unit.usd })],
})),
],
},
{
name: "Loss",
tree: [
{
name: "Compare",
title: title("Rolling Unrealized Loss"),
bottom: ROLLING_WINDOWS.map((w) =>
line({ metric: loss.sum[w.key].usd, name: w.name, color: w.color, unit: Unit.usd }),
),
},
...ROLLING_WINDOWS.map((w) => ({
name: w.name,
title: title(`Unrealized Loss (${w.name})`),
bottom: [line({ metric: loss.sum[w.key].usd, name: "Loss", color: colors.loss, unit: Unit.usd })],
})),
],
},
],
},
];
}
@@ -320,24 +390,103 @@ function realizedPnlCumulativeTreeFull(r, title) {
}
/**
* Net realized P&L delta tree (absolute + rate across all rolling windows)
* @param {Brk.BaseChangeCumulativeDeltaRelSumPattern | Brk.BaseCumulativeDeltaSumPattern} netPnl
* @param {(metric: string) => string} title
* @returns {PartialOptionsGroup}
*/
function realizedNetPnlDeltaTree(netPnl, title) {
return {
name: "Change",
tree: [
{
name: "Absolute",
tree: [
{
name: "Compare",
title: title("Net Realized P&L Change"),
bottom: ROLLING_WINDOWS.map((w) =>
baseline({ metric: netPnl.delta.absolute[w.key].usd, name: w.name, color: w.color, unit: Unit.usd }),
),
},
...ROLLING_WINDOWS.map((w) => ({
name: w.name,
title: title(`Net Realized P&L Change (${w.name})`),
bottom: [baseline({ metric: netPnl.delta.absolute[w.key].usd, name: "Change", unit: Unit.usd })],
})),
],
},
{
name: "Rate",
tree: [
{
name: "Compare",
title: title("Net Realized P&L Rate"),
bottom: ROLLING_WINDOWS.flatMap((w) =>
percentRatio({ pattern: netPnl.delta.rate[w.key], name: w.name, color: w.color }),
),
},
...ROLLING_WINDOWS.map((w) => ({
name: w.name,
title: title(`Net Realized P&L Rate (${w.name})`),
bottom: percentRatioBaseline({ pattern: netPnl.delta.rate[w.key], name: "Rate" }),
})),
],
},
],
};
}
/**
* Full realized delta tree (absolute + rate + rel to mcap/rcap)
* @param {Brk.CapGrossInvestorLossMvrvNetPeakPriceProfitSellSoprPattern | Brk.MetricsTree_Cohorts_Utxo_Lth_Realized} r
* @param {(metric: string) => string} title
* @returns {PartialOptionsTree}
* @returns {PartialOptionsGroup}
*/
function realized30dChangeTreeFull(r, title) {
return [
{ name: "USD", title: title("Realized P&L 30d Change"), bottom: [baseline({ metric: r.netPnl.delta.absolute._1m.usd, name: "30d Change", unit: Unit.usd })] },
{
name: "% of Mcap",
title: title("Realized 30d Change (% of Mcap)"),
bottom: percentRatioBaseline({ pattern: r.netPnl.change1m.relToMcap, name: "30d Change" }),
},
{
name: "% of Rcap",
title: title("Realized 30d Change (% of Realized Cap)"),
bottom: percentRatioBaseline({ pattern: r.netPnl.change1m.relToRcap, name: "30d Change" }),
},
];
function realizedNetPnlDeltaTreeFull(r, title) {
const base = realizedNetPnlDeltaTree(r.netPnl, title);
return {
...base,
tree: [
...base.tree,
{
name: "% of Mcap",
title: title("Net Realized P&L Change (% of Mcap)"),
bottom: percentRatioBaseline({ pattern: r.netPnl.change1m.relToMcap, name: "30d Change" }),
},
{
name: "% of Rcap",
title: title("Net Realized P&L Change (% of Rcap)"),
bottom: percentRatioBaseline({ pattern: r.netPnl.change1m.relToRcap, name: "30d Change" }),
},
],
};
}
/**
* Rolling net realized P&L tree (reusable by full and mid realized)
* @param {{ sum: { _24h: { usd: AnyMetricPattern }, _1w: { usd: AnyMetricPattern }, _1m: { usd: AnyMetricPattern }, _1y: { usd: AnyMetricPattern } } }} netPnl
* @param {(metric: string) => string} title
* @returns {PartialOptionsGroup}
*/
function rollingNetRealizedTree(netPnl, title) {
return {
name: "Net",
tree: [
{
name: "Compare",
title: title("Rolling Net Realized P&L"),
bottom: ROLLING_WINDOWS.map((w) =>
baseline({ metric: netPnl.sum[w.key].usd, name: w.name, color: w.color, unit: Unit.usd }),
),
},
...ROLLING_WINDOWS.map((w) => ({
name: w.name,
title: title(`Net Realized P&L (${w.name})`),
bottom: [baseline({ metric: netPnl.sum[w.key].usd, name: "Net", unit: Unit.usd })],
})),
],
};
}
/**
@@ -382,23 +531,7 @@ function singleRollingRealizedTreeFull(r, title) {
})),
],
},
{
name: "Net",
tree: [
{
name: "Compare",
title: title("Rolling Net Realized P&L"),
bottom: ROLLING_WINDOWS.map((w) =>
baseline({ metric: r.netPnl.sum[w.key].usd, name: w.name, color: w.color, unit: Unit.usd }),
),
},
...ROLLING_WINDOWS.map((w) => ({
name: w.name,
title: title(`Net Realized P&L (${w.name})`),
bottom: [baseline({ metric: r.netPnl.sum[w.key].usd, name: "Net", unit: Unit.usd })],
})),
],
},
rollingNetRealizedTree(r.netPnl, title),
{
name: "P/L Ratio",
tree: [
@@ -451,6 +584,96 @@ function singleRollingRealizedTreeBasic(profit, loss, title) {
// Realized Subfolder Builders
// ============================================================================
/**
* Value Created/Destroyed tree for a single P&L side (profit or loss)
* @param {CountPattern<number>} valueCreated
* @param {CountPattern<number>} valueDestroyed
* @param {string} label - "Profit" or "Loss"
* @param {(metric: string) => string} title
* @returns {PartialOptionsGroup}
*/
function realizedValueTree(valueCreated, valueDestroyed, label, title) {
return {
name: label,
tree: [
{
name: "Rolling",
tree: [
{
name: "Compare",
title: title(`${label} Value Created vs Destroyed`),
bottom: ROLLING_WINDOWS.flatMap((w) => [
line({ metric: valueCreated.sum[w.key], name: `Created (${w.name})`, color: w.color, unit: Unit.usd }),
line({ metric: valueDestroyed.sum[w.key], name: `Destroyed (${w.name})`, color: w.color, unit: Unit.usd, style: 2 }),
]),
},
...ROLLING_WINDOWS.map((w) => ({
name: w.name,
title: title(`${label} Value (${w.name})`),
bottom: [
line({ metric: valueCreated.sum[w.key], name: "Created", color: colors.profit, unit: Unit.usd }),
line({ metric: valueDestroyed.sum[w.key], name: "Destroyed", color: colors.loss, unit: Unit.usd }),
],
})),
],
},
{
name: "Cumulative",
title: title(`Cumulative ${label} Value`),
bottom: [
line({ metric: valueCreated.cumulative, name: "Created", color: colors.profit, unit: Unit.usd }),
line({ metric: valueDestroyed.cumulative, name: "Destroyed", color: colors.loss, unit: Unit.usd }),
],
},
],
};
}
/**
* Investor price percentiles tree (pct1/2/5/95/98/99)
* @param {InvestorPercentilesPattern} percentiles
* @param {(metric: string) => string} title
* @returns {PartialOptionsGroup}
*/
function investorPricePercentilesTree(percentiles, title) {
/** @type {readonly [InvestorPercentileEntry, string, Color][]} */
const pcts = [
[percentiles.pct99, "p99", colors.stat.max],
[percentiles.pct98, "p98", colors.stat.pct90],
[percentiles.pct95, "p95", colors.stat.pct75],
[percentiles.pct5, "p5", colors.stat.pct25],
[percentiles.pct2, "p2", colors.stat.pct10],
[percentiles.pct1, "p1", colors.stat.min],
];
return {
name: "Percentiles",
tree: [
{
name: "USD",
title: title("Investor Price Percentiles"),
bottom: pcts.map(([p, name, color]) =>
line({ metric: p.price.usd, name, color, unit: Unit.usd }),
),
},
{
name: "Sats",
title: title("Investor Price Percentiles (Sats)"),
bottom: pcts.map(([p, name, color]) =>
line({ metric: p.price.sats, name, color, unit: Unit.sats }),
),
},
{
name: "Ratio",
title: title("Investor Price Percentile Ratios"),
bottom: pcts.map(([p, name, color]) =>
baseline({ metric: p.ratio, name, color, unit: Unit.ratio }),
),
},
],
};
}
/**
* Full realized subfolder (All/STH/LTH)
* @param {Brk.CapGrossInvestorLossMvrvNetPeakPriceProfitSellSoprPattern | Brk.MetricsTree_Cohorts_Utxo_Lth_Realized} r
@@ -463,7 +686,7 @@ function realizedSubfolderFull(r, title) {
tree: [
{ name: "P&L", tree: realizedPnlSumTreeFull(r, title) },
{ name: "Net", tree: realizedNetPnlSumTreeFull(r, title) },
{ name: "30d Change", tree: realized30dChangeTreeFull(r, title) },
realizedNetPnlDeltaTreeFull(r, title),
{
name: "Gross P&L",
tree: [
@@ -488,6 +711,13 @@ function realizedSubfolderFull(r, title) {
{ name: "Cumulative", title: title("Total Realized P&L"), bottom: [line({ metric: r.grossPnl.cumulative.usd, name: "Total", unit: Unit.usd, color: colors.bitcoin })] },
],
},
{
name: "Value",
tree: [
realizedValueTree(r.profit.valueCreated, r.profit.valueDestroyed, "Profit", title),
realizedValueTree(r.loss.valueCreated, r.loss.valueDestroyed, "Loss", title),
],
},
{
name: "P/L Ratio",
title: title("Realized Profit/Loss Ratio"),
@@ -498,6 +728,12 @@ function realizedSubfolderFull(r, title) {
title: title("Realized Peak Regret"),
bottom: [line({ metric: r.peakRegret.base, name: "Peak Regret", unit: Unit.usd })],
},
{
name: "Investor Price",
tree: [
investorPricePercentilesTree(r.investor.price.percentiles, title),
],
},
{ name: "Rolling", tree: singleRollingRealizedTreeFull(r, title) },
{
name: "Cumulative",
@@ -555,12 +791,14 @@ function realizedSubfolderMid(r, title) {
title: title("Net Realized P&L"),
bottom: [dotsBaseline({ metric: r.netPnl.base.usd, name: "Net", unit: Unit.usd })],
},
realizedNetPnlDeltaTree(r.netPnl, title),
{
name: "30d Change",
title: title("Realized P&L 30d Change"),
bottom: [baseline({ metric: r.netPnl.delta.absolute._1m.usd, name: "30d Change", unit: Unit.usd })],
name: "Rolling",
tree: [
...singleRollingRealizedTreeBasic(r.profit, r.loss, title),
rollingNetRealizedTree(r.netPnl, title),
],
},
{ name: "Rolling", tree: singleRollingRealizedTreeBasic(r.profit, r.loss, title) },
{
name: "Cumulative",
tree: [
@@ -620,7 +858,7 @@ function realizedSubfolderBasic(r, title) {
/**
* Basic profitability section (NUPL only unrealized, basic realized)
* @param {{ cohort: UtxoCohortObject | CohortWithoutRelative, title: (metric: string) => string }} args
* @param {{ cohort: UtxoCohortObject, title: (metric: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createProfitabilitySection({ cohort, title }) {
@@ -639,6 +877,42 @@ export function createProfitabilitySection({ cohort, title }) {
};
}
/**
* Profitability section with unrealized P&L + NUPL (no netPnl, no rel)
* For: CohortWithoutRelative (p2ms, unknown, empty)
* @param {{ cohort: CohortWithoutRelative, title: (metric: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createProfitabilitySectionWithProfitLoss({ cohort, title }) {
const u = cohort.tree.unrealized;
return {
name: "Profitability",
tree: [
{
name: "Unrealized",
tree: [
{
name: "P&L",
tree: [
{
name: "USD",
title: title("Unrealized P&L"),
bottom: [
...pnlLines({ profit: u.profit.base.usd, loss: u.loss.base.usd, negLoss: u.loss.negative }, Unit.usd),
priceLine({ unit: Unit.usd, defaultActive: false }),
],
},
...unrealizedCumulativeRollingTree(u.profit, u.loss, title),
],
},
{ name: "NUPL", title: title("NUPL"), bottom: nuplSeries(u.nupl) },
],
},
realizedSubfolderBasic(cohort.tree.realized, title),
],
};
}
/**
* Section for All cohort
* @param {{ cohort: CohortAll, title: (metric: string) => string }} args
@@ -764,7 +1038,7 @@ export function createProfitabilitySectionWithInvestedCapitalPct({ cohort, title
{
name: "Unrealized",
tree: [
{ name: "P&L", title: title("Unrealized P&L"), bottom: unrealizedMid(u) },
{ name: "P&L", tree: unrealizedPnlTreeMid(u, title) },
{ name: "Net P&L", title: title("Net Unrealized P&L"), bottom: netUnrealizedMid(u) },
{ name: "NUPL", title: title("NUPL"), bottom: nuplSeries(u.nupl) },
],
@@ -795,6 +1069,41 @@ export function createProfitabilitySectionBasicWithInvestedCapitalPct({ cohort,
};
}
/**
* Section for CohortAddress (has unrealized profit/loss + NUPL, basic realized)
* @param {{ cohort: CohortAddress, title: (metric: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createProfitabilitySectionAddress({ cohort, title }) {
const u = cohort.tree.unrealized;
return {
name: "Profitability",
tree: [
{
name: "Unrealized",
tree: [
{
name: "P&L",
tree: [
{
name: "USD",
title: title("Unrealized P&L"),
bottom: [
...pnlLines({ profit: u.profit.base.usd, loss: u.loss.base.usd, negLoss: u.loss.negative }, Unit.usd),
priceLine({ unit: Unit.usd, defaultActive: false }),
],
},
...unrealizedCumulativeRollingTree(u.profit, u.loss, title),
],
},
{ name: "NUPL", title: title("NUPL"), bottom: nuplSeries(u.nupl) },
],
},
realizedSubfolderBasic(cohort.tree.realized, title),
],
};
}
// ============================================================================
// Grouped Cohort Helpers
// ============================================================================
@@ -945,6 +1254,63 @@ function groupedRealizedSubfolder(list, all, title) {
};
}
/**
* Grouped net realized P&L delta (Absolute + Rate with all rolling windows)
* @param {readonly (CohortAll | CohortFull | CohortLongTerm)[]} list
* @param {CohortAll} all
* @param {(metric: string) => string} title
* @returns {PartialOptionsGroup}
*/
function groupedRealizedNetPnlDeltaTree(list, all, title) {
return {
name: "Change",
tree: [
{
name: "Absolute",
tree: [
{
name: "Compare",
title: title("Net Realized P&L Change"),
bottom: ROLLING_WINDOWS.flatMap((w) =>
mapCohortsWithAll(list, all, ({ name, tree }) =>
baseline({ metric: tree.realized.netPnl.delta.absolute[w.key].usd, name: `${name} (${w.name})`, color: w.color, unit: Unit.usd }),
),
),
},
...ROLLING_WINDOWS.map((w) => ({
name: w.name,
title: title(`Net Realized P&L Change (${w.name})`),
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
baseline({ metric: tree.realized.netPnl.delta.absolute[w.key].usd, name, color, unit: Unit.usd }),
),
})),
],
},
{
name: "Rate",
tree: [
{
name: "Compare",
title: title("Net Realized P&L Rate"),
bottom: ROLLING_WINDOWS.flatMap((w) =>
flatMapCohortsWithAll(list, all, ({ name, tree }) =>
percentRatio({ pattern: tree.realized.netPnl.delta.rate[w.key], name: `${name} (${w.name})`, color: w.color }),
),
),
},
...ROLLING_WINDOWS.map((w) => ({
name: w.name,
title: title(`Net Realized P&L Rate (${w.name})`),
bottom: flatMapCohortsWithAll(list, all, ({ name, color, tree }) =>
percentRatio({ pattern: tree.realized.netPnl.delta.rate[w.key], name, color }),
),
})),
],
},
],
};
}
/**
* Grouped realized subfolder for full cohorts
* @param {readonly (CohortAll | CohortFull | CohortLongTerm)[]} list
@@ -964,13 +1330,7 @@ function groupedRealizedSubfolderFull(list, all, title) {
baseline({ metric: tree.realized.netPnl.base.usd, name, color, unit: Unit.usd }),
),
},
{
name: "30d Change",
title: title("Realized P&L 30d Change"),
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
baseline({ metric: tree.realized.netPnl.delta.absolute._1m.usd, name, color, unit: Unit.usd }),
),
},
groupedRealizedNetPnlDeltaTree(list, all, title),
{ name: "Rolling", tree: groupedRollingRealizedChartsFull(list, all, title) },
{
name: "Cumulative",
@@ -1265,6 +1625,41 @@ export function createGroupedProfitabilitySection({ list, all, title }) {
};
}
/**
* Grouped profitability with unrealized profit/loss + NUPL
* For: CohortWithoutRelative (p2ms, unknown, empty)
* @param {{ list: readonly CohortWithoutRelative[], all: CohortAll, title: (metric: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createGroupedProfitabilitySectionWithProfitLoss({ list, all, title }) {
return {
name: "Profitability",
tree: [
{
name: "Unrealized",
tree: [
{
name: "Profit",
title: title("Unrealized Profit"),
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
line({ metric: tree.unrealized.profit.base.usd, name, color, unit: Unit.usd }),
),
},
{
name: "Loss",
title: title("Unrealized Loss"),
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
line({ metric: tree.unrealized.loss.base.usd, name, color, unit: Unit.usd }),
),
},
...groupedNuplCharts(list, all, title),
],
},
groupedRealizedSubfolder(list, all, title),
],
};
}
/**
* Grouped section with invested capital % (basic cohorts — uses NUPL only)
* @param {{ list: readonly CohortBasicWithoutMarketCap[], all: CohortAll, title: (metric: string) => string }} args

View File

@@ -0,0 +1,89 @@
/** UTXO Profitability section — range bands, cumulative profit/loss thresholds */
import { colors } from "../../utils/colors.js";
import { entries } from "../../utils/array.js";
import { Unit } from "../../utils/units.js";
import { line, price } from "../series.js";
import { brk } from "../../client.js";
import { satsBtcUsd } from "../shared.js";
/**
* @param {{ name: string, color: Color, pattern: RealizedSupplyPattern }[]} list
* @param {string} titlePrefix
* @returns {PartialOptionsTree}
*/
function bucketCharts(list, titlePrefix) {
return [
{
name: "Supply",
title: `${titlePrefix}: Supply`,
bottom: list.flatMap(({ name, color, pattern }) =>
satsBtcUsd({ pattern: pattern.supply, name, color }),
),
},
{
name: "Realized Cap",
title: `${titlePrefix}: Realized Cap`,
bottom: list.map(({ name, color, pattern }) =>
line({ metric: pattern.realizedCap, name, color, unit: Unit.usd }),
),
},
{
name: "Realized Price",
title: `${titlePrefix}: Realized Price`,
top: list.map(({ name, color, pattern }) =>
price({ metric: pattern.realizedPrice, name, color }),
),
},
];
}
/**
* @returns {PartialOptionsGroup}
*/
export function createUtxoProfitabilitySection() {
const { range, profit, loss } = brk.metrics.cohorts.utxo.profitability;
const {
PROFITABILITY_RANGE_NAMES,
PROFIT_NAMES,
LOSS_NAMES,
} = brk;
const rangeList = entries(PROFITABILITY_RANGE_NAMES).map(
([key, names], i, arr) => ({
name: names.short,
color: colors.at(i, arr.length),
pattern: range[key],
}),
);
const profitList = entries(PROFIT_NAMES).map(([key, names], i, arr) => ({
name: names.short,
color: colors.at(i, arr.length),
pattern: profit[key],
}));
const lossList = entries(LOSS_NAMES).map(([key, names], i, arr) => ({
name: names.short,
color: colors.at(i, arr.length),
pattern: loss[key],
}));
return {
name: "UTXO Profitability",
tree: [
{
name: "Range",
tree: bucketCharts(rangeList, "Profitability Range"),
},
{
name: "In Profit",
tree: bucketCharts(profitList, "In Profit"),
},
{
name: "In Loss",
tree: bucketCharts(lossList, "In Loss"),
},
],
};
}

View File

@@ -36,6 +36,18 @@ import { periodIdToName } from "./utils.js";
* @property {Brk.BpsCentsRatioSatsUsdPattern} ratio
*/
/**
* Create index (percent) + ratio line pair from a BpsPercentRatioPattern
* @param {{ pattern: { percent: AnyMetricPattern, ratio: AnyMetricPattern }, name: string, color?: Color, defaultActive?: boolean }} args
* @returns {AnyFetchedSeriesBlueprint[]}
*/
function indexRatio({ pattern, name, color, defaultActive }) {
return [
line({ metric: pattern.percent, name, color, defaultActive, unit: Unit.index }),
line({ metric: pattern.ratio, name, color, defaultActive, unit: Unit.ratio }),
];
}
const commonMaIds = /** @type {const} */ ([
"1w",
"1m",
@@ -671,166 +683,43 @@ export function createMarketSection() {
name: "Compare",
title: "RSI Comparison",
bottom: [
line({
metric: technical.rsi._24h.rsi.percent,
name: "1d",
color: colors.time._24h,
unit: Unit.index,
}),
line({
metric: technical.rsi._1w.rsi.percent,
name: "1w",
color: colors.time._1w,
unit: Unit.index,
}),
line({
metric: technical.rsi._1m.rsi.percent,
name: "1m",
color: colors.time._1m,
unit: Unit.index,
}),
line({
metric: technical.rsi._1y.rsi.percent,
name: "1y",
color: colors.time._1y,
unit: Unit.index,
}),
...ROLLING_WINDOWS.flatMap((w) =>
indexRatio({ pattern: technical.rsi[w.key].rsi, name: w.name, color: w.color }),
),
priceLine({ unit: Unit.index, number: 70 }),
priceLine({ unit: Unit.index, number: 30 }),
],
},
{
name: "1 Day",
title: "RSI (1d)",
bottom: [
line({
metric: technical.rsi._24h.rsi.percent,
name: "RSI",
color: colors.indicator.main,
unit: Unit.index,
}),
line({
metric: technical.rsi._24h.rsiMax.percent,
name: "Max",
color: colors.stat.max,
defaultActive: false,
unit: Unit.index,
}),
line({
metric: technical.rsi._24h.rsiMin.percent,
name: "Min",
color: colors.stat.min,
defaultActive: false,
unit: Unit.index,
}),
priceLine({ unit: Unit.index, number: 70 }),
priceLine({
unit: Unit.index,
number: 50,
defaultActive: false,
}),
priceLine({ unit: Unit.index, number: 30 }),
],
},
{
name: "1 Week",
title: "RSI (1w)",
bottom: [
line({
metric: technical.rsi._1w.rsi.percent,
name: "RSI",
color: colors.indicator.main,
unit: Unit.index,
}),
line({
metric: technical.rsi._1w.rsiMax.percent,
name: "Max",
color: colors.stat.max,
defaultActive: false,
unit: Unit.index,
}),
line({
metric: technical.rsi._1w.rsiMin.percent,
name: "Min",
color: colors.stat.min,
defaultActive: false,
unit: Unit.index,
}),
priceLine({ unit: Unit.index, number: 70 }),
priceLine({
unit: Unit.index,
number: 50,
defaultActive: false,
}),
priceLine({ unit: Unit.index, number: 30 }),
],
},
{
name: "1 Month",
title: "RSI (1m)",
bottom: [
line({
metric: technical.rsi._1m.rsi.percent,
name: "RSI",
color: colors.indicator.main,
unit: Unit.index,
}),
line({
metric: technical.rsi._1m.rsiMax.percent,
name: "Max",
color: colors.stat.max,
defaultActive: false,
unit: Unit.index,
}),
line({
metric: technical.rsi._1m.rsiMin.percent,
name: "Min",
color: colors.stat.min,
defaultActive: false,
unit: Unit.index,
}),
priceLine({ unit: Unit.index, number: 70 }),
priceLine({
unit: Unit.index,
number: 50,
defaultActive: false,
}),
priceLine({ unit: Unit.index, number: 30 }),
],
},
{
name: "1 Year",
title: "RSI (1y)",
bottom: [
line({
metric: technical.rsi._1y.rsi.percent,
name: "RSI",
color: colors.indicator.main,
unit: Unit.index,
}),
line({
metric: technical.rsi._1y.rsiMax.percent,
name: "Max",
color: colors.stat.max,
defaultActive: false,
unit: Unit.index,
}),
line({
metric: technical.rsi._1y.rsiMin.percent,
name: "Min",
color: colors.stat.min,
defaultActive: false,
unit: Unit.index,
}),
priceLine({ unit: Unit.index, number: 70 }),
priceLine({
unit: Unit.index,
number: 50,
defaultActive: false,
}),
priceLine({ unit: Unit.index, number: 30 }),
],
},
...ROLLING_WINDOWS.map((w) => {
const rsi = technical.rsi[w.key];
return {
name: w.name,
tree: [
{
name: "Value",
title: `RSI (${w.name})`,
bottom: [
...indexRatio({ pattern: rsi.rsi, name: "RSI", color: colors.indicator.main }),
...indexRatio({ pattern: rsi.rsiMax, name: "Max", color: colors.stat.max, defaultActive: false }),
...indexRatio({ pattern: rsi.rsiMin, name: "Min", color: colors.stat.min, defaultActive: false }),
priceLine({ unit: Unit.index, number: 70 }),
priceLine({ unit: Unit.index, number: 50, defaultActive: false }),
priceLine({ unit: Unit.index, number: 30 }),
],
},
{
name: "Components",
title: `RSI Components (${w.name})`,
bottom: [
line({ metric: rsi.averageGain, name: "Avg Gain", color: colors.profit, unit: Unit.usd }),
line({ metric: rsi.averageLoss, name: "Avg Loss", color: colors.loss, unit: Unit.usd }),
line({ metric: rsi.gains, name: "Gains", color: colors.profit, defaultActive: false, unit: Unit.usd }),
line({ metric: rsi.losses, name: "Losses", color: colors.loss, defaultActive: false, unit: Unit.usd }),
],
},
],
};
}),
],
},
{
@@ -840,127 +729,33 @@ export function createMarketSection() {
name: "Compare",
title: "Stochastic RSI Comparison",
bottom: [
line({
metric: technical.rsi._24h.stochRsiK.percent,
name: "1d K",
color: colors.time._24h,
unit: Unit.index,
}),
line({
metric: technical.rsi._1w.stochRsiK.percent,
name: "1w K",
color: colors.time._1w,
unit: Unit.index,
}),
line({
metric: technical.rsi._1m.stochRsiK.percent,
name: "1m K",
color: colors.time._1m,
unit: Unit.index,
}),
line({
metric: technical.rsi._1y.stochRsiK.percent,
name: "1y K",
color: colors.time._1y,
unit: Unit.index,
}),
...priceLines({ unit: Unit.index, numbers: [80, 20] }),
],
},
{
name: "1 Day",
title: "Stochastic RSI (1d)",
bottom: [
line({
metric: technical.rsi._24h.stochRsiK.percent,
name: "K",
color: colors.indicator.fast,
unit: Unit.index,
}),
line({
metric: technical.rsi._24h.stochRsiD.percent,
name: "D",
color: colors.indicator.slow,
unit: Unit.index,
}),
...priceLines({ unit: Unit.index, numbers: [80, 20] }),
],
},
{
name: "1 Week",
title: "Stochastic RSI (1w)",
bottom: [
line({
metric: technical.rsi._1w.stochRsiK.percent,
name: "K",
color: colors.indicator.fast,
unit: Unit.index,
}),
line({
metric: technical.rsi._1w.stochRsiD.percent,
name: "D",
color: colors.indicator.slow,
unit: Unit.index,
}),
...priceLines({ unit: Unit.index, numbers: [80, 20] }),
],
},
{
name: "1 Month",
title: "Stochastic RSI (1m)",
bottom: [
line({
metric: technical.rsi._1m.stochRsiK.percent,
name: "K",
color: colors.indicator.fast,
unit: Unit.index,
}),
line({
metric: technical.rsi._1m.stochRsiD.percent,
name: "D",
color: colors.indicator.slow,
unit: Unit.index,
}),
...priceLines({ unit: Unit.index, numbers: [80, 20] }),
],
},
{
name: "1 Year",
title: "Stochastic RSI (1y)",
bottom: [
line({
metric: technical.rsi._1y.stochRsiK.percent,
name: "K",
color: colors.indicator.fast,
unit: Unit.index,
}),
line({
metric: technical.rsi._1y.stochRsiD.percent,
name: "D",
color: colors.indicator.slow,
unit: Unit.index,
}),
...ROLLING_WINDOWS.flatMap((w) =>
indexRatio({ pattern: technical.rsi[w.key].stochRsiK, name: `${w.name} K`, color: w.color }),
),
...priceLines({ unit: Unit.index, numbers: [80, 20] }),
],
},
...ROLLING_WINDOWS.map((w) => {
const rsi = technical.rsi[w.key];
return {
name: w.name,
title: `Stochastic RSI (${w.name})`,
bottom: [
...indexRatio({ pattern: rsi.stochRsi, name: "Raw", color: colors.indicator.main, defaultActive: false }),
...indexRatio({ pattern: rsi.stochRsiK, name: "K", color: colors.indicator.fast }),
...indexRatio({ pattern: rsi.stochRsiD, name: "D", color: colors.indicator.slow }),
...priceLines({ unit: Unit.index, numbers: [80, 20] }),
],
};
}),
],
},
{
name: "Stochastic",
title: "Stochastic Oscillator",
bottom: [
line({
metric: technical.stochK.percent,
name: "K",
color: colors.indicator.fast,
unit: Unit.index,
}),
line({
metric: technical.stochD.percent,
name: "D",
color: colors.indicator.slow,
unit: Unit.index,
}),
...indexRatio({ pattern: technical.stochK, name: "K", color: colors.indicator.fast }),
...indexRatio({ pattern: technical.stochD, name: "D", color: colors.indicator.slow }),
...priceLines({ unit: Unit.index, numbers: [80, 20] }),
],
},
@@ -970,32 +765,9 @@ export function createMarketSection() {
{
name: "Compare",
title: "MACD Comparison",
bottom: [
line({
metric: technical.macd._24h.line,
name: "1d",
color: colors.time._24h,
unit: Unit.usd,
}),
line({
metric: technical.macd._1w.line,
name: "1w",
color: colors.time._1w,
unit: Unit.usd,
}),
line({
metric: technical.macd._1m.line,
name: "1m",
color: colors.time._1m,
unit: Unit.usd,
}),
line({
metric: technical.macd._1y.line,
name: "1y",
color: colors.time._1y,
unit: Unit.usd,
}),
],
bottom: ROLLING_WINDOWS.map((w) =>
line({ metric: technical.macd[w.key].line, name: w.name, color: w.color, unit: Unit.usd }),
),
},
...ROLLING_WINDOWS.map((w) => ({
name: w.name,

View File

@@ -118,7 +118,6 @@ export function createNetworkSection() {
},
]);
const countTypes = /** @type {const} */ ([
{
name: "Funded",
@@ -568,7 +567,7 @@ export function createNetworkSection() {
name: "Base",
title: "OP_RETURN Burned",
bottom: satsBtcUsd({
pattern: supply.burned.opReturn.base,
pattern: scripts.value.opReturn.base,
name: "sum",
}),
},
@@ -580,7 +579,7 @@ export function createNetworkSection() {
title: "OP_RETURN Burned Rolling",
bottom: ROLLING_WINDOWS.flatMap((w) =>
satsBtcUsd({
pattern: supply.burned.opReturn.sum[w.key],
pattern: scripts.value.opReturn.sum[w.key],
name: w.name,
color: w.color,
}),
@@ -590,7 +589,7 @@ export function createNetworkSection() {
name: w.name,
title: `OP_RETURN Burned ${w.name}`,
bottom: satsBtcUsd({
pattern: supply.burned.opReturn.sum[w.key],
pattern: scripts.value.opReturn.sum[w.key],
name: w.name,
color: w.color,
}),
@@ -601,7 +600,7 @@ export function createNetworkSection() {
name: "Cumulative",
title: "OP_RETURN Burned (Total)",
bottom: satsBtcUsd({
pattern: supply.burned.opReturn.cumulative,
pattern: scripts.value.opReturn.cumulative,
name: "all-time",
}),
},
@@ -1074,7 +1073,8 @@ export function createNetworkSection() {
title: "UTXO Count 30d Change",
bottom: [
baseline({
metric: cohorts.utxo.all.outputs.unspentCount.delta.absolute._1m,
metric:
cohorts.utxo.all.outputs.unspentCount.delta.absolute._1m,
name: "30d Change",
unit: Unit.count,
}),

View File

@@ -6,7 +6,7 @@ import {
createCohortFolderFull,
createCohortFolderWithAdjusted,
createCohortFolderLongTerm,
createCohortFolderAgeRange,
createCohortFolderAgeRangeWithMatured,
createCohortFolderBasicWithMarketCap,
createCohortFolderBasicWithoutMarketCap,
createCohortFolderWithoutRelative,
@@ -14,12 +14,13 @@ import {
createAddressCohortFolder,
createGroupedCohortFolderWithAdjusted,
createGroupedCohortFolderWithNupl,
createGroupedCohortFolderAgeRange,
createGroupedCohortFolderAgeRangeWithMatured,
createGroupedCohortFolderBasicWithMarketCap,
createGroupedCohortFolderBasicWithoutMarketCap,
createGroupedCohortFolderAddress,
createGroupedAddressCohortFolder,
} from "./distribution/index.js";
import { createUtxoProfitabilitySection } from "./distribution/utxo-profitability.js";
import { createMarketSection } from "./market.js";
import { createNetworkSection } from "./network.js";
import { createMiningSection } from "./mining.js";
@@ -110,26 +111,26 @@ export function createPartialOptions() {
{
name: "Older Than",
tree: [
createGroupedCohortFolderBasicWithMarketCap({
createGroupedCohortFolderWithAdjusted({
name: "Compare",
title: "Over Age",
list: overAge,
all: cohortAll,
}),
...overAge.map(createCohortFolderBasicWithMarketCap),
...overAge.map(createCohortFolderWithAdjusted),
],
},
// Range
{
name: "Range",
tree: [
createGroupedCohortFolderAgeRange({
createGroupedCohortFolderAgeRangeWithMatured({
name: "Compare",
title: "Age Ranges",
list: ageRange,
all: cohortAll,
}),
...ageRange.map(createCohortFolderAgeRange),
...ageRange.map(createCohortFolderAgeRangeWithMatured),
],
},
],
@@ -246,13 +247,13 @@ export function createPartialOptions() {
{
name: "Epochs",
tree: [
createGroupedCohortFolderBasicWithoutMarketCap({
createGroupedCohortFolderWithAdjusted({
name: "Compare",
title: "Epochs",
list: epoch,
all: cohortAll,
}),
...epoch.map(createCohortFolderBasicWithoutMarketCap),
...epoch.map(createCohortFolderWithAdjusted),
],
},
@@ -260,15 +261,18 @@ export function createPartialOptions() {
{
name: "Years",
tree: [
createGroupedCohortFolderBasicWithoutMarketCap({
createGroupedCohortFolderWithAdjusted({
name: "Compare",
title: "Years",
list: class_,
all: cohortAll,
}),
...class_.map(createCohortFolderBasicWithoutMarketCap),
...class_.map(createCohortFolderWithAdjusted),
],
},
// UTXO Profitability bands
createUtxoProfitabilitySection(),
],
},

View File

@@ -233,6 +233,9 @@
* @property {Color} color
* @property {AgeRangePattern} tree
*
* Age range cohort with matured supply
* @typedef {CohortAgeRange & { matured: AnyValuePattern }} CohortAgeRangeWithMatured
*
* Basic cohort WITH RelToMarketCap (geAmount.*, ltAmount.*)
* @typedef {Object} CohortBasicWithMarketCap
* @property {string} name