mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-08-05 07:03:06 -07:00
next: ai part 11
This commit is contained in:
+15
-14
@@ -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
|
||||
|
||||
@@ -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),
|
||||
}),
|
||||
);
|
||||
@@ -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"),
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
+34
-9
@@ -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",
|
||||
@@ -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",
|
||||
|
||||
@@ -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 };
|
||||
}),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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()],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 }),
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user