next: ai part 11

This commit is contained in:
nym21
2026-07-29 10:50:14 +02:00
parent eccbda34ee
commit 1cdee36f2e
56 changed files with 2269 additions and 1037 deletions
+15 -14
View File
@@ -42,7 +42,7 @@
* @typedef {Brk.BtcCentsSatsUsdPattern} SupplyPattern
* @typedef {Brk.AverageBlockCumulativeMaxMedianMinPct10Pct25Pct75Pct90SumPattern} BlockSizePattern
* @typedef {keyof Brk.SeriesTree_Cohorts_Utxo_Type} SpendableType
* @typedef {Brk.SpendingSpentUnspentPattern} OutputsPattern
* @typedef {Brk.SpentUnspentUtxoPattern} OutputsPattern
* @typedef {keyof Brk.SeriesTree_Addrs_Raw} AddressableType
*
* Brk pattern types (using new pattern names)
@@ -69,11 +69,11 @@
* @typedef {Brk.MempoolBlock} MempoolBlock
* @typedef {Brk.NextBlockHash} NextBlockHash
* ActivePriceRatioPattern: ratio pattern with price (extended)
* @typedef {Brk.BpsPriceRatioPattern} ActivePriceRatioPattern
* @typedef {Brk.PriceRatioPattern} ActivePriceRatioPattern
* PriceRatioPercentilesPattern: price pattern with ratio + percentiles (no SMAs/stdDev)
* @typedef {Brk.BpsCentsPercentilesRatioSatsUsdPattern} PriceRatioPercentilesPattern
* @typedef {Brk.CentsPercentilesPpmRatioSatsUsdPattern} PriceRatioPercentilesPattern
* AnyRatioPattern: full ratio pattern with percentiles, SMAs, and std dev bands
* @typedef {Brk.BpsCentsPercentilesRatioSatsSmaStdUsdPattern} AnyRatioPattern
* @typedef {Brk.CentsPercentilesPpmRatioSatsSmaStdUsdPattern} AnyRatioPattern
* FullValuePattern: block + cumulative + sum + average rolling windows (sats/btc/cents/usd)
* @typedef {Brk.AverageBlockCumulativeSumPattern3} FullValuePattern
* RollingWindowSlot: a single rolling window with stats (pct10, pct25, median, pct75, pct90, max, min) per unit
@@ -92,6 +92,7 @@
*
* Realized pattern (full: cap + gross + capitalized + loss + mvrv + net + peak + price + profit + sell + sopr)
* @typedef {Brk.CapCapitalizedGrossLossMvrvNetPeakPriceProfitSellSoprPattern} RealizedPattern
* @typedef {Omit<RealizedPattern, "sopr">} FullRealizedProfitabilityPattern
*
* Transfer volume pattern (block + cumulative + inProfit/inLoss + sum windows)
* @typedef {Brk.AverageBlockCumulativeInSumPattern} TransferVolumePattern
@@ -103,14 +104,14 @@
* @typedef {Brk.CoindaysCoinyearsDormancyTransferPattern} FullActivityPattern
*
*
* BPS + percent + ratio pattern
* @typedef {Brk.BpsPercentRatioPattern2} PercentRatioPattern
* PPM + percent + ratio pattern
* @typedef {Brk.PercentPpmRatioPattern2} PercentRatioPattern
*
* Percent + ratio per window + cumulative (mirrors CountPattern but for percent)
* @typedef {Brk._1m1w1y24hBpsPercentRatioPattern} PercentRatioCumulativePattern
* @typedef {Brk._1m1w1y24hPercentPpmRatioPattern} PercentRatioCumulativePattern
*
* BPS + ratio pattern (for NUPL and similar)
* @typedef {Brk.BpsRatioPattern} NuplPattern
* PPM + ratio pattern (for NUPL and similar)
* @typedef {Brk.PpmRatioPattern} NuplPattern
*
* LTH realized tree
* @typedef {Brk.SeriesTree_Cohorts_Utxo_Lth_Realized} LthRealizedPattern
@@ -127,8 +128,8 @@
* Basic realized pattern (cap + loss + MVRV + price + profit, no net/sopr)
* @typedef {Brk.CapLossMvrvPriceProfitPattern} BasicRealizedPattern
*
* Moving average price ratio pattern (bps + cents + ratio + sats + usd)
* @typedef {Brk.BpsCentsRatioSatsUsdPattern} MaPriceRatioPattern
* Moving average price ratio pattern (ppm + cents + ratio + sats + usd)
* @typedef {Brk.CentsPpmRatioSatsUsdPattern} MaPriceRatioPattern
*
* Address count pattern (base + delta with absolute + rate)
* @typedef {Brk.BaseDeltaPattern} AddrCountPattern
@@ -189,7 +190,7 @@
*/
/**
* Dominance pattern: percent/ratio at top level + per rolling window
* @typedef {Brk._1m1w1y24hBpsPercentRatioPattern} DominancePattern
* @typedef {Brk._1m1w1y24hPercentPpmRatioPattern} DominancePattern
*/
/**
@@ -243,7 +244,7 @@
* @typedef {{ name: string, title: string, color: Color, tree: PatternWithActivity }} CohortWithActivity
* @typedef {{ name: string, title: string, color: Color, tree: PatternWithCostBasisPercentiles }} CohortWithCostBasisPercentiles
*
* Cohorts with nupl + percentiles (CohortFull and CohortLongTerm both have nupl and percentiles)
* Cohorts with full NUPL and cost-basis percentiles.
* @typedef {CohortFull | CohortLongTerm} CohortWithNuplPercentiles
* @typedef {{ name: string, title: string, list: readonly CohortWithNuplPercentiles[], all: CohortAll }} CohortGroupWithNuplPercentiles
*
@@ -255,7 +256,7 @@
*
* Capitalized price percentiles (pct1/2/5/95/98/99)
* @typedef {Brk.Pct0Pct1Pct2Pct5Pct95Pct98Pct99Pattern} CapitalizedPercentilesPattern
* @typedef {Brk.BpsPriceRatioPattern} CapitalizedPercentileEntry
* @typedef {Brk.PriceRatioPattern} CapitalizedPercentileEntry
*
* Generic tree node type for walking
* @typedef {AnySeriesPattern | Record<string, unknown>} TreeNode
+18
View File
@@ -0,0 +1,18 @@
import { entries } from "../utils/array.js";
import { brk } from "../utils/client.js";
import { colors } from "../utils/colors.js";
/**
* Shared display metadata for every UTXO age range.
*
* Consumers attach their own tree because Distribution, Cointime, and
* Coinflow expose different metrics for the same ranges.
*/
export const ageRanges = entries(brk.AGE_RANGE_NAMES).map(
([key, names], index, all) => ({
key,
name: names.short,
title: `UTXOs ${names.long}`,
color: colors.at(index, all.length),
}),
);
+10 -8
View File
@@ -1,6 +1,7 @@
import { colors } from "../../utils/colors.js";
import { entries } from "../../utils/array.js";
import { brk } from "../../utils/client.js";
import { ageRanges } from "../age-ranges.js";
/** @type {readonly AddressableType[]} */
const ADDRESSABLE_TYPES = [
@@ -14,9 +15,13 @@ const ADDRESSABLE_TYPES = [
"p2pk65",
];
/** @type {(key: SpendableType) => key is AddressableType} */
const isAddressable = (key) =>
/** @type {readonly string[]} */ (ADDRESSABLE_TYPES).includes(key);
/**
* @param {SpendableType} key
* @returns {key is AddressableType}
*/
function isAddressable(key) {
return /** @type {readonly string[]} */ (ADDRESSABLE_TYPES).includes(key);
}
export function buildCohortData() {
const utxoCohorts = brk.series.cohorts.utxo;
@@ -27,7 +32,6 @@ export function buildCohortData() {
EPOCH_NAMES,
UNDER_AGE_NAMES,
OVER_AGE_NAMES,
AGE_RANGE_NAMES,
OVER_AMOUNT_NAMES,
UNDER_AMOUNT_NAMES,
AMOUNT_RANGE_NAMES,
@@ -83,10 +87,8 @@ export function buildCohortData() {
tree: utxoCohorts.overAge[key],
}));
const ageRange = entries(AGE_RANGE_NAMES).map(([key, names], i, arr) => ({
name: names.short,
title: `UTXOs ${names.long}`,
color: colors.at(i, arr.length),
const ageRange = ageRanges.map(({ key, ...range }) => ({
...range,
tree: utxoCohorts.ageRange[key],
matured: utxoCohorts.matured[key],
}));
@@ -77,10 +77,15 @@ function groupedOutputsFolder(list, all, title) {
}),
},
{
name: "Spending Rate",
title: title("Spending Rate"),
name: "1Y Turnover",
title: title("1 Year UTXO Turnover"),
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
line({ series: tree.outputs.spendingRate, name, color, unit: Unit.ratio }),
line({
series: tree.outputs.utxoTurnover1y,
name,
color,
unit: Unit.ratio,
}),
),
},
],
@@ -308,10 +313,15 @@ function outputsFolder(outputs, color, title) {
tree: chartsFromCount({ pattern: outputs.spentCount, title, metric: "Spent UTXO Count", unit: Unit.count, color }),
},
{
name: "Spending Rate",
title: title("Spending Rate"),
name: "1Y Turnover",
title: title("1 Year UTXO Turnover"),
bottom: [
line({ series: outputs.spendingRate, name: "Rate", color, unit: Unit.ratio }),
line({
series: outputs.utxoTurnover1y,
name: "Turnover",
color,
unit: Unit.ratio,
}),
],
},
],
@@ -501,7 +501,7 @@ function realizedOverviewFolder({
/**
* Full realized subfolder (All/STH/LTH)
* @param {RealizedPattern | LthRealizedPattern} r
* @param {FullRealizedProfitabilityPattern} r
* @param {(name: string) => string} title
* @returns {PartialOptionsGroup}
*/
@@ -809,7 +809,8 @@ export function createProfitabilitySectionAll({ cohort, title }) {
/**
* Section for cohorts with full realized and unrealized profitability data.
* @param {{ cohort: { tree: { unrealized: FullRelativePattern, realized: RealizedPattern } }, title: (name: string) => string }} args
* This section does not use SOPR, whose shape differs between STH and Entry.
* @param {{ cohort: { tree: { unrealized: FullRelativePattern, realized: FullRealizedProfitabilityPattern } }, title: (name: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createProfitabilitySectionFull({ cohort, title }) {
@@ -0,0 +1,186 @@
import { brk } from "../../utils/client.js";
import { colors } from "../../utils/colors.js";
import { Unit } from "../../utils/units.js";
import { ageRanges } from "../age-ranges.js";
import { line, price } from "../series.js";
import { satsBtcUsd } from "../shared.js";
/**
* @typedef {Object} CoinflowAgeRange
* @property {string} name
* @property {Color} color
* @property {{
* mobility: AnySeriesPattern,
* spendingRate: AnySeriesPattern,
* spendingExposure: AnySeriesPattern,
* supply: { mobile: AnyValuePattern, immobile: AnyValuePattern },
* }} tree
*/
/**
* @param {readonly CoinflowAgeRange[]} ranges
* @param {"mobility" | "spendingRate" | "spendingExposure"} key
* @param {string} name
* @returns {PartialChartOption}
*/
function ageRangeRatioChart(ranges, key, name) {
return {
name,
title: `${name} by UTXO Age`,
bottom: ranges.map((range) =>
line({
series: range.tree[key],
name: range.name,
color: range.color,
unit: Unit.ratio,
}),
),
};
}
/**
* @param {readonly CoinflowAgeRange[]} ranges
* @param {"mobile" | "immobile"} key
* @param {string} name
* @returns {PartialChartOption}
*/
function ageRangeSupplyChart(ranges, key, name) {
return {
name,
title: `${name} Supply by UTXO Age`,
bottom: ranges.flatMap((range) =>
satsBtcUsd({
pattern: range.tree.supply[key],
name: range.name,
color: range.color,
}),
),
};
}
/**
* Create Coinflow section.
* @returns {PartialOptionsGroup}
*/
export function createCoinflowSection() {
const { coinflow } = brk.series;
const ranges = ageRanges.map(({ key, ...range }) => ({
...range,
tree: coinflow.ageRange[key],
}));
const horizons = /** @type {const} */ ([
{ key: "_8y", name: "8Y" },
{ key: "_4y", name: "4Y" },
{ key: "_2y", name: "2Y" },
{ key: "_1y", name: "1Y" },
{ key: "_6m", name: "6M" },
{ key: "_3m", name: "3M" },
{ key: "_1m", name: "1M" },
]).map((horizon, index, all) => ({
...horizon,
color: colors.at(index, all.length),
}));
return {
name: "Coinflow",
tree: [
{
name: "Supply",
tree: [
{
name: "Breakdown",
title: "Mobile vs Immobile Supply",
bottom: [
...satsBtcUsd({
pattern: coinflow.supply.mobile,
name: "Mobile",
color: colors.mobile,
}),
...satsBtcUsd({
pattern: coinflow.supply.immobile,
name: "Immobile",
color: colors.immobile,
}),
],
},
{
name: "Mobile in Loss",
title: "Mobile Supply in Loss",
bottom: [
line({
series: coinflow.supply.mobile.inLoss.share,
name: "Lifetime",
color: colors.loss,
unit: Unit.ratio,
}),
],
},
{
name: "In Loss by Horizon",
title: "Mobile Supply in Loss by Horizon",
bottom: horizons.map((horizon) =>
line({
series:
coinflow.horizon[horizon.key].supply.inLoss.share,
name: horizon.name,
color: horizon.color,
unit: Unit.ratio,
}),
),
},
],
},
{
name: "Cap",
title: "Coinflow Cap",
bottom: [
line({
series: coinflow.cap.usd,
name: "Coinflow",
color: colors.coinflow,
unit: Unit.usd,
}),
],
},
{
name: "Price",
title: "Coinflow Price",
top: [
price({
series: coinflow.price,
name: "Coinflow",
color: colors.coinflow,
}),
],
bottom: [
line({
series: coinflow.price.ratio,
name: "Spot / Coinflow",
color: colors.coinflow,
unit: Unit.ratio,
}),
],
},
{
name: "Age Range",
tree: [
ageRangeRatioChart(ranges, "mobility", "Mobility"),
ageRangeRatioChart(ranges, "spendingRate", "Spending Rate"),
ageRangeRatioChart(
ranges,
"spendingExposure",
"Spending Exposure",
),
{
name: "Supply",
tree: [
ageRangeSupplyChart(ranges, "mobile", "Mobile"),
ageRangeSupplyChart(ranges, "immobile", "Immobile"),
],
},
],
},
],
};
}
@@ -0,0 +1,162 @@
import { Unit } from "../../../utils/units.js";
import { line, ROLLING_WINDOWS } from "../../series.js";
import { satsBtcUsd } from "../../shared.js";
/**
* @typedef {{
* average: Record<"_24h" | "_1w" | "_1m" | "_1y", AnySeriesPattern>,
* sum: Record<"_24h" | "_1w" | "_1m" | "_1y", AnySeriesPattern>,
* cumulative: AnySeriesPattern,
* }} CointimeAgeRangeCoindays
*
* @typedef {Object} CointimeAgeRange
* @property {string} name
* @property {Color} color
* @property {{
* coindaysCreated: CointimeAgeRangeCoindays,
* coindaysConsumed: CointimeAgeRangeCoindays,
* coindaysStored: CointimeAgeRangeCoindays,
* liveliness: AnySeriesPattern,
* vaultedness: AnySeriesPattern,
* ratio: AnySeriesPattern,
* supply: { active: AnyValuePattern, vaulted: AnyValuePattern },
* }} tree
*/
/**
* @param {readonly CointimeAgeRange[]} ranges
* @param {"liveliness" | "vaultedness" | "ratio"} key
* @param {string} name
* @param {string} legend
* @returns {PartialChartOption}
*/
function activityChart(ranges, key, name, legend) {
return {
name,
title: `${legend} by UTXO Age`,
bottom: ranges.map((range) =>
line({
series: range.tree[key],
name: range.name,
color: range.color,
unit: Unit.ratio,
}),
),
};
}
/**
* @param {readonly CointimeAgeRange[]} ranges
* @param {"active" | "vaulted"} key
* @param {string} name
* @returns {PartialChartOption}
*/
function supplyChart(ranges, key, name) {
return {
name,
title: `${name} Supply by UTXO Age`,
bottom: ranges.flatMap((range) =>
satsBtcUsd({
pattern: range.tree.supply[key],
name: range.name,
color: range.color,
}),
),
};
}
/**
* @param {readonly CointimeAgeRange[]} ranges
* @param {"coindaysCreated" | "coindaysConsumed" | "coindaysStored"} key
* @param {string} name
* @returns {PartialOptionsGroup}
*/
function coindaysTree(ranges, key, name) {
return {
name,
tree: [
{
name: "Average",
tree: ROLLING_WINDOWS.map((window) => ({
name: window.name,
title: `${window.title} Average ${name} by UTXO Age`,
bottom: ranges.map((range) =>
line({
series: range.tree[key].average[window.key],
name: range.name,
color: range.color,
unit: Unit.coindays,
}),
),
})),
},
{
name: "Sum",
tree: ROLLING_WINDOWS.map((window) => ({
name: window.name,
title: `${window.title} ${name} by UTXO Age`,
bottom: ranges.map((range) =>
line({
series: range.tree[key].sum[window.key],
name: range.name,
color: range.color,
unit: Unit.coindays,
}),
),
})),
},
{
name: "Cumulative",
title: `Cumulative ${name} by UTXO Age`,
bottom: ranges.map((range) =>
line({
series: range.tree[key].cumulative,
name: range.name,
color: range.color,
unit: Unit.coindays,
}),
),
},
],
};
}
/**
* @param {readonly CointimeAgeRange[]} ranges
* @returns {PartialOptionsGroup}
*/
export function createCointimeAgeRangeSection(ranges) {
return {
name: "Age Range",
tree: [
{
name: "Supply",
tree: [
supplyChart(ranges, "active", "Active"),
supplyChart(ranges, "vaulted", "Vaulted"),
],
},
{
name: "Activity",
tree: [
activityChart(ranges, "liveliness", "Liveliness", "Liveliness"),
activityChart(ranges, "vaultedness", "Vaultedness", "Vaultedness"),
activityChart(
ranges,
"ratio",
"Activity Ratio",
"Liveliness / Vaultedness",
),
],
},
{
name: "Coindays",
tree: [
coindaysTree(ranges, "coindaysCreated", "Coindays Created"),
coindaysTree(ranges, "coindaysConsumed", "Coindays Consumed"),
coindaysTree(ranges, "coindaysStored", "Coindays Stored"),
],
},
],
};
}
@@ -1,6 +1,6 @@
import { colors } from "../utils/colors.js";
import { brk } from "../utils/client.js";
import { Unit } from "../utils/units.js";
import { colors } from "../../../utils/colors.js";
import { brk } from "../../../utils/client.js";
import { Unit } from "../../../utils/units.js";
import {
dots,
line,
@@ -8,8 +8,10 @@ import {
multiSeriesTree,
percentRatioDots,
sumsAndAveragesCumulative,
} from "./series.js";
import { satsBtcUsd, priceRatioPercentilesTree } from "./shared.js";
} from "../../series.js";
import { ageRanges } from "../../age-ranges.js";
import { satsBtcUsd, priceRatioPercentilesTree } from "../../shared.js";
import { createCointimeAgeRangeSection } from "./age-range.js";
/**
* Create Cointime section
@@ -27,6 +29,10 @@ export function createCointimeSection() {
value,
} = cointime;
const { all } = cohorts.utxo;
const cointimeAgeRanges = ageRanges.map(({ key, ...range }) => ({
...range,
tree: cointime.ageRange[key],
}));
// Reference lines for cap comparisons
const capReferenceLines = /** @type {const} */ ([
@@ -247,12 +253,31 @@ export function createCointimeSection() {
{
name: "Supply",
title: "Active vs Vaulted Supply",
bottom: supplyBreakdown.flatMap(({ pattern, name, color }) =>
satsBtcUsd({ pattern, name, color }),
),
tree: [
{
name: "Breakdown",
title: "Active vs Vaulted Supply",
bottom: supplyBreakdown.flatMap(({ pattern, name, color }) =>
satsBtcUsd({ pattern, name, color }),
),
},
{
name: "Active in Loss",
title: "Active Supply in Loss",
bottom: [
line({
series: cointimeSupply.active.inLoss.share,
name: "Share",
color: colors.loss,
unit: Unit.ratio,
}),
],
},
],
},
createCointimeAgeRangeSection(cointimeAgeRanges),
{
name: "Activity",
title: "Liveliness & Vaultedness",
+1 -42
View File
@@ -17,7 +17,7 @@ import {
ROLLING_WINDOWS,
ROLLING_WINDOWS_TO_1M,
} from "./series.js";
import { simplePriceRatioTree, percentileBands, priceBands } from "./shared.js";
import { simplePriceRatioTree } from "./shared.js";
/**
* @typedef {Object} Period
@@ -921,47 +921,6 @@ export function createMarketSection() {
{
name: "Indicators",
tree: [
{
name: "Rarity Meter",
tree: /** @type {const} */ ([
{ key: "full", name: "Full", title: "Rarity Meter" },
{ key: "local", name: "Local", title: "Local Rarity Meter" },
{ key: "cycle", name: "Cycle", title: "Cycle Rarity Meter" },
]).map((v) => {
const m = indicators.rarityMeter[v.key];
return {
name: v.name,
title: v.title,
top: priceBands(percentileBands(m), { defaultActive: true }),
bottom: [
histogram({
series: m.index,
name: "Index",
unit: Unit.count,
colorFn: (v) =>
/** @type {const} */ ([
colors.ratioPct._0_5,
colors.ratioPct._1,
colors.ratioPct._2,
colors.ratioPct._5,
colors.transparent,
colors.ratioPct._95,
colors.ratioPct._98,
colors.ratioPct._99,
colors.ratioPct._99_5,
])[v + 4],
}),
baseline({
series: m.score,
name: "Score",
unit: Unit.count,
color: [colors.ratioPct._99, colors.ratioPct._1],
defaultActive: false,
}),
],
};
}),
},
{
name: "NVT",
title: "NVT Ratio",
+136
View File
@@ -0,0 +1,136 @@
import { brk } from "../../utils/client.js";
import { colors } from "../../utils/colors.js";
import { Unit } from "../../utils/units.js";
import { line } from "../series.js";
const FLOOR_PERCENTILES = /** @type {const} */ ([
{ key: "pct95", name: "P95" },
{ key: "pct98", name: "P98" },
{ key: "pct99", name: "P99" },
{ key: "pct995", name: "P99.5" },
{ key: "pct999", name: "P99.9" },
]);
const LEVEL_PERCENTILES = /** @type {const} */ ([
{ key: "pct10", name: "P10" },
{ key: "pct20", name: "P20" },
{ key: "pct30", name: "P30" },
{ key: "pct40", name: "P40" },
{ key: "pct50", name: "P50" },
{ key: "pct60", name: "P60" },
{ key: "pct70", name: "P70" },
{ key: "pct80", name: "P80" },
{ key: "pct90", name: "P90" },
]);
/**
* @typedef {Object} BedrockMode
* @property {string} name
* @property {AnySeriesPattern} inLoss
* @property {{
* floor: Record<string, AnySeriesPattern>,
* level: Record<string, AnySeriesPattern>,
* lossThreshold: Record<string, AnySeriesPattern>,
* }} tree
*/
/**
* @param {BedrockMode} mode
* @param {AnySeriesPattern} ath
* @returns {PartialChartOption}
*/
function modeChart(mode, ath) {
return {
name: mode.name,
title: `Bitcoin Bedrock Model: ${mode.name}`,
top: [
...FLOOR_PERCENTILES.map((percentile, index) =>
line({
series: mode.tree.floor[percentile.key],
name: percentile.name,
color: colors.bedrock.percentiles[index],
unit: Unit.usd,
}),
),
...LEVEL_PERCENTILES.map((percentile, index) =>
line({
series: mode.tree.level[percentile.key],
name: `L${percentile.name.slice(1)}`,
color: colors.bedrock.levels[index],
unit: Unit.usd,
style: 1,
}),
),
line({
series: ath,
name: "L100",
color: colors.bedrock.levels[9],
unit: Unit.usd,
style: 1,
}),
],
bottom: [
line({
series: mode.inLoss,
name: "Loss",
color: colors.default,
defaultActive: false,
unit: Unit.ratio,
}),
...FLOOR_PERCENTILES.map((percentile, index) =>
line({
series: mode.tree.lossThreshold[percentile.key],
name: percentile.name,
color: colors.bedrock.percentiles[index],
defaultActive: false,
unit: Unit.ratio,
}),
),
],
};
}
/**
* Create Bedrock model section.
* @returns {PartialOptionsGroup}
*/
export function createBedrockSection() {
const { bedrock, market, cohorts, cointime, coinflow } = brk.series;
const horizonModes = /** @type {const} */ ([
{ key: "coinflow8y", horizon: "_8y", name: "Coinflow 8Y" },
{ key: "coinflow4y", horizon: "_4y", name: "Coinflow 4Y" },
{ key: "coinflow2y", horizon: "_2y", name: "Coinflow 2Y" },
{ key: "coinflow1y", horizon: "_1y", name: "Coinflow 1Y" },
{ key: "coinflow6m", horizon: "_6m", name: "Coinflow 6M" },
{ key: "coinflow3m", horizon: "_3m", name: "Coinflow 3M" },
{ key: "coinflow1m", horizon: "_1m", name: "Coinflow 1M" },
]).map((mode) => ({
name: mode.name,
tree: bedrock[mode.key],
inLoss: coinflow.horizon[mode.horizon].supply.inLoss.share,
}));
const modes = /** @type {readonly BedrockMode[]} */ ([
{
name: "Raw",
tree: bedrock.raw,
inLoss: cohorts.utxo.all.supply.inLoss.share.ratio,
},
{
name: "Cointime",
tree: bedrock.cointime,
inLoss: cointime.supply.active.inLoss.share,
},
{
name: "Coinflow",
tree: bedrock.coinflow,
inLoss: coinflow.supply.mobile.inLoss.share,
},
...horizonModes,
]);
return {
name: "Bedrock",
tree: modes.map((mode) => modeChart(mode, market.ath.high.usd)),
};
}
@@ -0,0 +1,118 @@
import { brk } from "../../utils/client.js";
import { colors } from "../../utils/colors.js";
import { Unit } from "../../utils/units.js";
import { baseline, histogram } from "../series.js";
import {
percentileBands,
priceBands,
priceRatioPercentilesTree,
} from "../shared.js";
/**
* Create Rarity Meter model section.
* @returns {PartialOptionsGroup}
*/
export function createRarityMeterSection() {
const { rarityMeter } = brk.series.indicators;
const { all, sth, lth } = brk.series.cohorts.utxo;
return {
name: "Rarity Meter",
tree: [
.../** @type {const} */ ([
{ key: "full", name: "Full", title: "Bitcoin Rarity Meter: Full" },
{ key: "local", name: "Local", title: "Bitcoin Rarity Meter: Local" },
{ key: "cycle", name: "Cycle", title: "Bitcoin Rarity Meter: Cycle" },
]).map((variant) => {
const meter = rarityMeter[variant.key];
return {
name: variant.name,
title: variant.title,
top: priceBands(percentileBands(meter), { defaultActive: true }),
bottom: [
histogram({
series: meter.index,
name: "Index",
unit: Unit.count,
colorFn: (value) =>
/** @type {const} */ ([
colors.ratioPct._0_5,
colors.ratioPct._1,
colors.ratioPct._2,
colors.ratioPct._5,
colors.transparent,
colors.ratioPct._95,
colors.ratioPct._98,
colors.ratioPct._99,
colors.ratioPct._99_5,
])[value + 4],
}),
baseline({
series: meter.score,
name: "Score",
unit: Unit.count,
color: [colors.ratioPct._99, colors.ratioPct._1],
defaultActive: false,
}),
],
};
}),
{
name: "Components",
tree: [
{
name: "Realized Price",
title: "Realized Price",
pattern: all.realized.price,
legend: "Realized",
color: colors.realized,
},
{
name: "Capitalized Price",
title: "Capitalized Price",
pattern: all.realized.capitalized.price,
legend: "Capitalized",
color: colors.capitalized,
},
{
name: "STH RP",
title: "STH Realized Price",
pattern: sth.realized.price,
legend: "Realized",
color: colors.realized,
},
{
name: "STH CP",
title: "STH Capitalized Price",
pattern: sth.realized.capitalized.price,
legend: "Capitalized",
color: colors.capitalized,
},
{
name: "LTH RP",
title: "LTH Realized Price",
pattern: lth.realized.price,
legend: "Realized",
color: colors.realized,
},
{
name: "LTH CP",
title: "LTH Capitalized Price",
pattern: lth.realized.capitalized.price,
legend: "Capitalized",
color: colors.capitalized,
},
].map((component) => {
const [, ratioChart] = priceRatioPercentilesTree({
pattern: component.pattern,
title: component.title,
legend: component.legend,
color: component.color,
defaultActivePercentiles: true,
});
return { ...ratioChart, name: component.name };
}),
},
],
};
}
+74 -102
View File
@@ -32,6 +32,7 @@ import {
groupedWindowsCumulative,
avgHoldingsSubtree,
exposedSubtree,
reusedCountTree,
reusedSubtree,
} from "./shared.js";
import { createOpReturnSection } from "./network/op-return.js";
@@ -111,50 +112,6 @@ export function createNetworkSection() {
const respent = addrs.respent;
const key = /** @type {const} */ ("all");
/**
* Windowed sums + cumulative, overlaying reused (primary) and respent (gray).
* @param {CountPattern<number>} reusedPattern
* @param {CountPattern<number>} respentPattern
* @param {string} metric
* @returns {PartialOptionsTree}
*/
const countPair = (reusedPattern, respentPattern, metric) => [
...ROLLING_WINDOWS.map((w) => ({
name: w.name,
title: title(`${w.title} ${metric}`),
bottom: [
line({
series: reusedPattern.sum[w.key],
name: "2+ Funded",
unit: Unit.count,
}),
line({
series: respentPattern.sum[w.key],
name: "2+ Spent",
color: colors.gray,
unit: Unit.count,
}),
],
})),
{
name: "Cumulative",
title: title(`Cumulative ${metric}`),
bottom: [
line({
series: reusedPattern.cumulative,
name: "2+ Funded",
unit: Unit.count,
}),
line({
series: respentPattern.cumulative,
name: "2+ Spent",
color: colors.gray,
unit: Unit.count,
}),
],
},
];
return {
name: "Reused",
tree: [
@@ -242,9 +199,10 @@ export function createNetworkSection() {
tree: [
{
name: "Count",
tree: countPair(
tree: reusedCountTree(
reused.events.outputToReusedAddrCount[key],
respent.events.outputToReusedAddrCount[key],
title,
"Transaction Outputs to Reused Addresses",
),
},
@@ -296,9 +254,10 @@ export function createNetworkSection() {
tree: [
{
name: "Count",
tree: countPair(
tree: reusedCountTree(
reused.events.inputFromReusedAddrCount[key],
respent.events.inputFromReusedAddrCount[key],
title,
"Transaction Inputs from Reused Addresses",
),
},
@@ -855,40 +814,75 @@ export function createNetworkSection() {
defaultActive: false,
},
]);
/**
* @param {Object} args
* @param {Readonly<Record<string, CountPattern<number>>>} args.patterns
* @param {(window: (typeof ROLLING_WINDOWS)[number], average: boolean) => string} args.windowTitle
* @param {string} args.cumulativeTitle
* @returns {PartialOptionsTree}
*/
const countComparisonTree = ({
patterns,
windowTitle,
cumulativeTitle,
}) => [
...ROLLING_WINDOWS.map((window) => ({
name: window.name,
title: windowTitle(window, false),
bottom: countTypes.map((type) =>
line({
series: patterns[type.key].sum[window.key],
name: type.name,
color: type.color,
unit: Unit.count,
defaultActive: type.defaultActive,
}),
),
})),
{
name: "Average",
tree: ROLLING_WINDOWS.map((window) => ({
name: window.name,
title: windowTitle(window, true),
bottom: countTypes.map((type) =>
line({
series: patterns[type.key].average[window.key],
name: type.name,
color: type.color,
unit: Unit.count,
defaultActive: type.defaultActive,
}),
),
})),
},
{
name: "Cumulative",
title: cumulativeTitle,
bottom: countTypes.map((type) =>
line({
series: patterns[type.key].cumulative,
name: type.name,
color: type.color,
unit: Unit.count,
defaultActive: type.defaultActive,
}),
),
},
];
return [
{
name: "Compare",
tree: [
{
name: "Count",
tree: [
...ROLLING_WINDOWS.map((w) => ({
name: w.name,
title: `${w.title} ${label} Count by Type`,
bottom: countTypes.map((t) =>
line({
series: count[t.key].sum[w.key],
name: t.name,
color: t.color,
unit: Unit.count,
defaultActive: t.defaultActive,
}),
),
})),
{
name: "Cumulative",
title: `Cumulative ${label} Count by Type`,
bottom: countTypes.map((t) =>
line({
series: count[t.key].cumulative,
name: t.name,
color: t.color,
unit: Unit.count,
defaultActive: t.defaultActive,
}),
),
},
],
tree: countComparisonTree({
patterns: count,
windowTitle: (window, average) =>
`${window.title}${average ? " Average" : ""} ${label} Count by Type`,
cumulativeTitle: `Cumulative ${label} Count by Type`,
}),
},
{
name: "Share",
@@ -904,34 +898,12 @@ export function createNetworkSection() {
},
{
name: "Transaction Count",
tree: [
...ROLLING_WINDOWS.map((w) => ({
name: w.name,
title: `${w.title} Transactions by ${label} Type`,
bottom: countTypes.map((t) =>
line({
series: txCount[t.key].sum[w.key],
name: t.name,
color: t.color,
unit: Unit.count,
defaultActive: t.defaultActive,
}),
),
})),
{
name: "Cumulative",
title: `Cumulative Transactions by ${label} Type`,
bottom: countTypes.map((t) =>
line({
series: txCount[t.key].cumulative,
name: t.name,
color: t.color,
unit: Unit.count,
defaultActive: t.defaultActive,
}),
),
},
],
tree: countComparisonTree({
patterns: txCount,
windowTitle: (window, average) =>
`${window.title}${average ? " Average" : ""} Transactions by ${label} Type`,
cumulativeTitle: `Cumulative Transactions by ${label} Type`,
}),
},
{
name: "Transaction Share",
+10 -2
View File
@@ -24,7 +24,10 @@ import {
import { createMarketSection } from "./market.js";
import { createNetworkSection } from "./network.js";
import { createMiningSection } from "./mining.js";
import { createCointimeSection } from "./cointime.js";
import { createCointimeSection } from "./frameworks/cointime/index.js";
import { createCoinflowSection } from "./frameworks/coinflow.js";
import { createBedrockSection } from "./models/bedrock.js";
import { createRarityMeterSection } from "./models/rarity-meter.js";
import { createInvestingSection } from "./investing.js";
import {
oracleOutputsHeatmapOption,
@@ -316,7 +319,12 @@ export function createPartialOptions() {
{
name: "Frameworks",
tree: [createCointimeSection()],
tree: [createCointimeSection(), createCoinflowSection()],
},
{
name: "Models",
tree: [createRarityMeterSection(), createBedrockSection()],
},
],
},
+12 -3
View File
@@ -1256,7 +1256,7 @@ export function chartsFromPercentCumulativeEntries({
}
/**
* Windowed sums + cumulative for multiple named entries (e.g. transaction versions)
* Windowed sums + optional averages + cumulative for multiple named entries.
* @param {Object} args
* @param {Array<[string, CountPattern<number>]>} args.entries
* @param {(metric: string) => string} [args.title]
@@ -1269,15 +1269,24 @@ export function chartsFromCountEntries({ entries, title = (s) => s, metric, unit
name,
color: colors.at(i, arr.length),
sum: data.sum,
average: data.average,
cumulative: data.cumulative,
}));
return [
...ROLLING_WINDOWS.map((w) => ({
name: w.name,
title: title(`${w.title} ${metric}`),
bottom: items.map((e) =>
bottom: items.flatMap((e) => [
line({ series: e.sum[w.key], name: e.name, color: e.color, unit }),
),
line({
series: e.average[w.key],
name: `${e.name} Avg`,
color: e.color,
unit,
defaultActive: false,
style: 1,
}),
]),
})),
{
name: "Cumulative",
+77 -44
View File
@@ -363,6 +363,68 @@ export function exposedSubtree(exposed, key, title) {
};
}
/**
* Windowed reused/respent counts with sums and optional averages.
* @param {CountPattern<number>} reused
* @param {CountPattern<number>} respent
* @param {(name: string) => string} title
* @param {string} metric
* @returns {PartialOptionsTree}
*/
export function reusedCountTree(reused, respent, title, metric) {
return [
...ROLLING_WINDOWS.map((window) => ({
name: window.name,
title: title(`${window.title} ${metric}`),
bottom: [
line({
series: reused.sum[window.key],
name: "2+ Funded",
unit: Unit.count,
}),
line({
series: respent.sum[window.key],
name: "2+ Spent",
color: colors.gray,
unit: Unit.count,
}),
line({
series: reused.average[window.key],
name: "2+ Funded Avg",
unit: Unit.count,
defaultActive: false,
style: 1,
}),
line({
series: respent.average[window.key],
name: "2+ Spent Avg",
color: colors.gray,
unit: Unit.count,
defaultActive: false,
style: 1,
}),
],
})),
{
name: "Cumulative",
title: title(`Cumulative ${metric}`),
bottom: [
line({
series: reused.cumulative,
name: "2+ Funded",
unit: Unit.count,
}),
line({
series: respent.cumulative,
name: "2+ Spent",
color: colors.gray,
unit: Unit.count,
}),
],
},
];
}
/**
* "Reused" subtree (per-type / per-cohort, no "Active" window since that
* data is only tracked globally). Respent (addresses whose outputs have
@@ -376,42 +438,6 @@ export function exposedSubtree(exposed, key, title) {
* @returns {PartialOptionsGroup}
*/
export function reusedSubtree(reused, respent, key, title) {
/**
* Windowed sums + cumulative, overlaying reused (primary) and respent (gray).
* @param {CountPattern<number>} reusedPattern
* @param {CountPattern<number>} respentPattern
* @param {string} metric
* @returns {PartialOptionsTree}
*/
const countPair = (reusedPattern, respentPattern, metric) => [
...ROLLING_WINDOWS.map((w) => ({
name: w.name,
title: title(`${w.title} ${metric}`),
bottom: [
line({ series: reusedPattern.sum[w.key], name: "2+ Funded", unit: Unit.count }),
line({
series: respentPattern.sum[w.key],
name: "2+ Spent",
color: colors.gray,
unit: Unit.count,
}),
],
})),
{
name: "Cumulative",
title: title(`Cumulative ${metric}`),
bottom: [
line({ series: reusedPattern.cumulative, name: "2+ Funded", unit: Unit.count }),
line({
series: respentPattern.cumulative,
name: "2+ Spent",
color: colors.gray,
unit: Unit.count,
}),
],
},
];
return {
name: "Reused",
tree: [
@@ -446,9 +472,10 @@ export function reusedSubtree(reused, respent, key, title) {
tree: [
{
name: "Count",
tree: countPair(
tree: reusedCountTree(
reused.events.outputToReusedAddrCount[key],
respent.events.outputToReusedAddrCount[key],
title,
"Transaction Outputs to Reused Addresses",
),
},
@@ -477,9 +504,10 @@ export function reusedSubtree(reused, respent, key, title) {
tree: [
{
name: "Count",
tree: countPair(
tree: reusedCountTree(
reused.events.inputFromReusedAddrCount[key],
respent.events.inputFromReusedAddrCount[key],
title,
"Transaction Inputs from Reused Addresses",
),
},
@@ -645,14 +673,17 @@ export function priceBands(bands, opts) {
);
}
/** @param {{ name: string, prop: AnySeriesPattern, color: Color }[]} bands */
function ratioBands(bands) {
/**
* @param {{ name: string, prop: AnySeriesPattern, color: Color }[]} bands
* @param {{ defaultActive?: boolean }} [opts]
*/
function ratioBands(bands, opts) {
return bands.map(({ name, prop, color }) =>
line({
series: prop,
name,
color,
defaultActive: false,
defaultActive: opts?.defaultActive ?? false,
unit: Unit.ratio,
options: { lineStyle: 1 },
}),
@@ -668,6 +699,7 @@ function ratioBands(bands) {
* @param {Color} [args.color]
* @param {string} [args.ratioTitle]
* @param {FetchedPriceSeriesBlueprint[]} [args.priceReferences]
* @param {boolean} [args.defaultActivePercentiles]
* @returns {PartialOptionsTree}
*/
export function priceRatioPercentilesTree({
@@ -677,6 +709,7 @@ export function priceRatioPercentilesTree({
color,
ratioTitle,
priceReferences,
defaultActivePercentiles,
}) {
const p = pattern.percentiles;
const pctUsd = percentileBandsWith(p, (e) => e.price);
@@ -688,7 +721,7 @@ export function priceRatioPercentilesTree({
top: [
price({ series: pattern, name: legend, color }),
...(priceReferences ?? []),
...priceBands(pctUsd),
...priceBands(pctUsd, { defaultActive: defaultActivePercentiles }),
],
},
{
@@ -696,7 +729,7 @@ export function priceRatioPercentilesTree({
title: ratioTitle ?? `${title} Ratio`,
top: [
price({ series: pattern, name: legend, color }),
...priceBands(pctUsd),
...priceBands(pctUsd, { defaultActive: defaultActivePercentiles }),
],
bottom: [
baseline({
@@ -705,7 +738,7 @@ export function priceRatioPercentilesTree({
unit: Unit.ratio,
base: 1,
}),
...ratioBands(pctRatio),
...ratioBands(pctRatio, { defaultActive: defaultActivePercentiles }),
],
},
];
+1 -1
View File
@@ -93,7 +93,7 @@
* @typedef {Object} PartialChartOptionSpecific
* @property {"chart"} [kind]
* @property {string} title
* @property {FetchedPriceSeriesBlueprint[]} [top]
* @property {(FetchedPriceSeriesBlueprint | AnyFetchedSeriesBlueprint)[]} [top]
* @property {AnyFetchedSeriesBlueprint[]} [bottom]
*
* @typedef {PartialOption & PartialChartOptionSpecific} PartialChartOption
+1 -3
View File
@@ -35,9 +35,7 @@ function walkSeries(node, map, path) {
const joined = newPath.join(".");
if (
joined.endsWith(".count.total.average") ||
joined.endsWith(".versions.v1.average") ||
joined.endsWith(".versions.v2.average") ||
joined.endsWith(".versions.v3.average")
joined === "cohorts.utxo.all.supply.dominance"
)
continue;
walkSeries(/** @type {TreeNode | null | undefined} */ (value), map, newPath);
+6 -6
View File
@@ -64,10 +64,10 @@ import { Unit } from "../units.js";
*
* @typedef {Object} Legend
* @property {HTMLLegendElement} element
* @property {function(HTMLElement): void} setPrefix
* @property {function(): void} clearPrefix
* @property {function({ series: AnySeries, name: string, order: number, colors: Color[] }): void} addOrReplace
* @property {function(number): void} removeFrom
* @property {(element: HTMLElement) => void} setPrefix
* @property {() => void} clearPrefix
* @property {(args: { series: AnySeries, name: string, order: number, colors: Color[] }) => void} addOrReplace
* @property {(index: number) => void} removeFrom
*/
const lineWidth = /** @type {1} */ (/** @type {unknown} */ (1.5));
@@ -234,7 +234,7 @@ export function createChart({ parent, brk, fitContent }) {
const chartEl = document.createElement("div");
root.append(chartEl);
const ichart = /** @type {CreateLCChart} */ (untypedLcCreateChart)(
const ichart = /** @type {typeof CreateLCChart} */ (untypedLcCreateChart)(
chartEl,
/** @satisfies {DeepPartial<ChartOptions>} */ ({
autoSize: true,
@@ -1706,5 +1706,5 @@ export function createChart({ parent, brk, fitContent }) {
/**
* @typedef {typeof createChart} CreateChart
* @typedef {ReturnType<createChart>} Chart
* @typedef {ReturnType<typeof createChart>} Chart
*/
+25
View File
@@ -152,6 +152,9 @@ export const colors = {
active: palette.rose,
activity: palette.purple,
cointime: palette.yellow,
coinflow: palette.blue,
mobile: palette.rose,
immobile: palette.lime,
destroyed: palette.red,
created: palette.orange,
stored: palette.green,
@@ -227,6 +230,28 @@ export const colors = {
_0_5: palette.indigo,
},
bedrock: {
levels: [
palette.purple,
palette.violet,
palette.indigo,
palette.blue,
palette.sky,
palette.cyan,
palette.teal,
palette.emerald,
palette.green,
palette.lime,
],
percentiles: [
palette.yellow,
palette.amber,
palette.orange,
palette.rose,
palette.red,
],
},
// Standard deviation bands (warm = positive, cool = negative)
sd: {
_0: palette.lime,