mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-06-14 00:33:36 -07:00
global: snapshot
This commit is contained in:
@@ -33,7 +33,6 @@ import { style } from "../utils/elements.js";
|
||||
/**
|
||||
* @template T
|
||||
* @typedef {Object} Series
|
||||
* @property {string} key
|
||||
* @property {string} id
|
||||
* @property {number} paneIndex
|
||||
* @property {PersistedValue<boolean>} active
|
||||
@@ -448,16 +447,12 @@ export function createChart({ parent, id: chartId, brk, fitContent }) {
|
||||
};
|
||||
|
||||
const serieses = {
|
||||
/** @type {Map<string, PersistedValue<boolean>>} */
|
||||
activeStates: new Map(),
|
||||
/** @type {Map<string, Set<AnySeries>>} */
|
||||
byKey: new Map(),
|
||||
/** @type {Set<AnySeries>} */
|
||||
all: new Set(),
|
||||
|
||||
refreshAll() {
|
||||
serieses.byKey.forEach((set) => {
|
||||
set.forEach((s) => {
|
||||
if (s.active.value) s.fetch?.();
|
||||
});
|
||||
serieses.all.forEach((s) => {
|
||||
if (s.active.value) s.fetch?.();
|
||||
});
|
||||
},
|
||||
|
||||
@@ -503,17 +498,12 @@ export function createChart({ parent, id: chartId, brk, fitContent }) {
|
||||
const key = customKey ?? stringToId(name);
|
||||
const id = `${unit.id}-${key}`;
|
||||
|
||||
// Reuse existing state if same name (links legends across panes, regardless of unit)
|
||||
const existingActive = serieses.activeStates.get(key);
|
||||
const active =
|
||||
existingActive ??
|
||||
createPersistedValue({
|
||||
defaultValue: defaultActive ?? true,
|
||||
storageKey: key,
|
||||
urlKey: key,
|
||||
...serdeBool,
|
||||
});
|
||||
if (!existingActive) serieses.activeStates.set(key, active);
|
||||
const active = createPersistedValue({
|
||||
defaultValue: defaultActive ?? true,
|
||||
storageKey: `${chartId}-p${paneIndex}-${key}`,
|
||||
urlKey: `${paneIndex === 0 ? "t" : "b"}-${key}`,
|
||||
...serdeBool,
|
||||
});
|
||||
|
||||
setOrder(-order);
|
||||
|
||||
@@ -533,18 +523,9 @@ export function createChart({ parent, id: chartId, brk, fitContent }) {
|
||||
setActive(value) {
|
||||
const wasActive = active.value;
|
||||
active.set(value);
|
||||
const linkedSeries = serieses.byKey.get(key);
|
||||
linkedSeries?.forEach((s) => {
|
||||
value ? s.show() : s.hide();
|
||||
});
|
||||
document.querySelectorAll(`[data-series="${key}"]`).forEach((el) => {
|
||||
if (el instanceof HTMLInputElement && el.type === "checkbox") {
|
||||
el.checked = value;
|
||||
}
|
||||
});
|
||||
// Fetch data for ALL linked series, not just this one
|
||||
value ? show() : hide();
|
||||
if (value && !wasActive) {
|
||||
linkedSeries?.forEach((s) => s.fetch?.());
|
||||
_fetch?.();
|
||||
}
|
||||
panes.updateVisibility();
|
||||
},
|
||||
@@ -555,7 +536,6 @@ export function createChart({ parent, id: chartId, brk, fitContent }) {
|
||||
tame,
|
||||
hasData: () => hasData,
|
||||
fetch: () => _fetch?.(),
|
||||
key,
|
||||
id,
|
||||
paneIndex,
|
||||
url: null,
|
||||
@@ -563,18 +543,12 @@ export function createChart({ parent, id: chartId, brk, fitContent }) {
|
||||
update,
|
||||
remove() {
|
||||
onRemove();
|
||||
serieses.byKey.get(key)?.delete(series);
|
||||
serieses.all.delete(series);
|
||||
panes.seriesByHome.get(paneIndex)?.delete(series);
|
||||
},
|
||||
};
|
||||
|
||||
// Register series for cross-pane linking (by name only)
|
||||
let keySet = serieses.byKey.get(key);
|
||||
if (!keySet) {
|
||||
keySet = new Set();
|
||||
serieses.byKey.set(key, keySet);
|
||||
}
|
||||
keySet.add(series);
|
||||
serieses.all.add(series);
|
||||
|
||||
/** @param {ChartableIndex} idx */
|
||||
function setupIndexEffect(idx) {
|
||||
@@ -1312,35 +1286,28 @@ export function createChart({ parent, id: chartId, brk, fitContent }) {
|
||||
deserialize: (s) => /** @type {"lin" | "log"} */ (s),
|
||||
});
|
||||
|
||||
/** @param {"lin" | "log"} value */
|
||||
const applyScale = (value) => {
|
||||
panes.whenReady(paneIndex, () => {
|
||||
try {
|
||||
ichart
|
||||
.panes()
|
||||
.at(paneIndex)
|
||||
?.priceScale("right")
|
||||
.applyOptions({
|
||||
mode: value === "lin" ? 0 : 1,
|
||||
});
|
||||
} catch {}
|
||||
});
|
||||
/** @param {IPaneApi<Time>} pane @param {"lin" | "log"} value */
|
||||
const applyScale = (pane, value) => {
|
||||
try {
|
||||
pane.priceScale("right").applyOptions({
|
||||
mode: value === "lin" ? 0 : 1,
|
||||
});
|
||||
} catch {}
|
||||
};
|
||||
|
||||
applyScale(persisted.value);
|
||||
|
||||
fieldsets.addIfNeeded({
|
||||
id,
|
||||
paneIndex,
|
||||
position: "sw",
|
||||
createChild() {
|
||||
createChild(pane) {
|
||||
applyScale(pane, persisted.value);
|
||||
return createRadios({
|
||||
choices: /** @type {const} */ (["lin", "log"]),
|
||||
id: stringToId(`${id} ${paneIndex}`),
|
||||
initialValue: persisted.value,
|
||||
onChange(value) {
|
||||
persisted.set(value);
|
||||
applyScale(value);
|
||||
applyScale(pane, value);
|
||||
},
|
||||
});
|
||||
},
|
||||
@@ -1472,12 +1439,7 @@ export function createChart({ parent, id: chartId, brk, fitContent }) {
|
||||
// Remove old series AFTER adding new ones to prevent pane collapse
|
||||
oldSeries.forEach((s) => s.remove());
|
||||
|
||||
// Ensure other pane's series are in their correct pane before applying scale
|
||||
// (they may have been collapsed when this pane was empty)
|
||||
const otherPaneIndex = paneIndex === 0 ? 1 : 0;
|
||||
panes.moveTo(otherPaneIndex, otherPaneIndex);
|
||||
|
||||
// Apply scale after series are created and panes are properly separated
|
||||
// Store scale config - it will be applied when createForPane runs after updateVisibility
|
||||
applyScaleForUnit(paneIndex, unit);
|
||||
},
|
||||
|
||||
|
||||
@@ -50,19 +50,17 @@ export function createLegend() {
|
||||
}
|
||||
legends[order] = div;
|
||||
|
||||
const { input, label } = createLabeledInput({
|
||||
const { label } = createLabeledInput({
|
||||
inputId: stringToId(`legend-${series.id}`),
|
||||
inputName: stringToId(`selected-${series.id}`),
|
||||
inputValue: "value",
|
||||
title: "Click to toggle",
|
||||
inputChecked: series.active.value,
|
||||
onClick: (event) => {
|
||||
event.preventDefault();
|
||||
onClick: () => {
|
||||
series.setActive(!series.active.value);
|
||||
},
|
||||
type: "checkbox",
|
||||
});
|
||||
input.dataset.series = series.key;
|
||||
|
||||
const spanMain = window.document.createElement("span");
|
||||
spanMain.classList.add("main");
|
||||
@@ -93,7 +91,7 @@ export function createLegend() {
|
||||
anchor.href = series.url;
|
||||
anchor.target = "_blank";
|
||||
anchor.rel = "noopener noreferrer";
|
||||
anchor.title = "Click to view data";
|
||||
anchor.title = "Open the metric data in a new tab";
|
||||
div.append(anchor);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -45,14 +45,13 @@ export function createChainSection(ctx) {
|
||||
const {
|
||||
colors,
|
||||
brk,
|
||||
fromSizePattern,
|
||||
fromFullnessPattern,
|
||||
fromDollarsPattern,
|
||||
fromFeeRatePattern,
|
||||
fromSumStatsPattern,
|
||||
fromBaseStatsPattern,
|
||||
fromFullStatsPattern,
|
||||
fromStatsPattern,
|
||||
fromCoinbasePattern,
|
||||
fromValuePattern,
|
||||
fromBlockCountWithUnit,
|
||||
fromIntervalPattern,
|
||||
fromCountPattern,
|
||||
fromSupplyPattern,
|
||||
} = ctx;
|
||||
const {
|
||||
@@ -132,19 +131,19 @@ export function createChainSection(ctx) {
|
||||
{
|
||||
name: "New",
|
||||
title: `${titlePrefix}New Address Count`,
|
||||
bottom: fromDollarsPattern(distribution.newAddrCount[key], Unit.count),
|
||||
bottom: fromFullStatsPattern(distribution.newAddrCount[key], Unit.count),
|
||||
},
|
||||
{
|
||||
name: "Growth Rate",
|
||||
title: `${titlePrefix}Address Growth Rate`,
|
||||
bottom: fromFullnessPattern(distribution.growthRate[key], Unit.ratio),
|
||||
bottom: fromBaseStatsPattern(distribution.growthRate[key], Unit.ratio),
|
||||
},
|
||||
{
|
||||
name: "Activity",
|
||||
tree: activityTypes.map((a) => ({
|
||||
name: a.name,
|
||||
title: `${titlePrefix}${a.name} Address Count`,
|
||||
bottom: fromFullnessPattern(
|
||||
bottom: fromBaseStatsPattern(
|
||||
distribution.addressActivity[key][a.key],
|
||||
Unit.count,
|
||||
),
|
||||
@@ -297,7 +296,7 @@ export function createChainSection(ctx) {
|
||||
name: "Count",
|
||||
title: "Block Count",
|
||||
bottom: [
|
||||
...fromBlockCountWithUnit(blocks.count.blockCount, Unit.count),
|
||||
...fromCountPattern(blocks.count.blockCount, Unit.count),
|
||||
line({
|
||||
metric: blocks.count.blockCountTarget,
|
||||
name: "Target",
|
||||
@@ -339,7 +338,7 @@ export function createChainSection(ctx) {
|
||||
name: "Interval",
|
||||
title: "Block Interval",
|
||||
bottom: [
|
||||
...fromIntervalPattern(blocks.interval, Unit.secs),
|
||||
...fromBaseStatsPattern(blocks.interval, Unit.secs, "", { avgActive: false }),
|
||||
priceLine({ ctx, unit: Unit.secs, name: "Target", number: 600 }),
|
||||
],
|
||||
},
|
||||
@@ -347,7 +346,7 @@ export function createChainSection(ctx) {
|
||||
name: "Size",
|
||||
title: "Block Size",
|
||||
bottom: [
|
||||
...fromSizePattern(blocks.size, Unit.bytes),
|
||||
...fromSumStatsPattern(blocks.size, Unit.bytes),
|
||||
line({
|
||||
metric: blocks.totalSize,
|
||||
name: "Total",
|
||||
@@ -355,8 +354,8 @@ export function createChainSection(ctx) {
|
||||
unit: Unit.bytes,
|
||||
defaultActive: false,
|
||||
}),
|
||||
...fromFullnessPattern(blocks.vbytes, Unit.vb),
|
||||
...fromFullnessPattern(blocks.weight, Unit.wu),
|
||||
...fromBaseStatsPattern(blocks.vbytes, Unit.vb),
|
||||
...fromBaseStatsPattern(blocks.weight, Unit.wu),
|
||||
line({
|
||||
metric: blocks.weight.sum,
|
||||
name: "Sum",
|
||||
@@ -376,7 +375,7 @@ export function createChainSection(ctx) {
|
||||
{
|
||||
name: "Fullness",
|
||||
title: "Block Fullness",
|
||||
bottom: fromFullnessPattern(blocks.fullness, Unit.percentage),
|
||||
bottom: fromBaseStatsPattern(blocks.fullness, Unit.percentage),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -388,7 +387,7 @@ export function createChainSection(ctx) {
|
||||
{
|
||||
name: "Count",
|
||||
title: "Transaction Count",
|
||||
bottom: fromDollarsPattern(transactions.count.txCount, Unit.count),
|
||||
bottom: fromFullStatsPattern(transactions.count.txCount, Unit.count),
|
||||
},
|
||||
{
|
||||
name: "Speed",
|
||||
@@ -426,34 +425,34 @@ export function createChainSection(ctx) {
|
||||
name: "Size",
|
||||
title: "Transaction Size",
|
||||
bottom: [
|
||||
...fromFeeRatePattern(transactions.size.weight, Unit.wu),
|
||||
...fromFeeRatePattern(transactions.size.vsize, Unit.vb),
|
||||
...fromStatsPattern(transactions.size.weight, Unit.wu),
|
||||
...fromStatsPattern(transactions.size.vsize, Unit.vb),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Fee Rate",
|
||||
title: "Fee Rate",
|
||||
bottom: fromFeeRatePattern(transactions.fees.feeRate, Unit.feeRate),
|
||||
bottom: fromStatsPattern(transactions.fees.feeRate, Unit.feeRate),
|
||||
},
|
||||
{
|
||||
name: "Versions",
|
||||
title: "Transaction Versions",
|
||||
bottom: [
|
||||
...fromBlockCountWithUnit(
|
||||
...fromCountPattern(
|
||||
transactions.versions.v1,
|
||||
Unit.count,
|
||||
"v1",
|
||||
colors.orange,
|
||||
colors.red,
|
||||
),
|
||||
...fromBlockCountWithUnit(
|
||||
...fromCountPattern(
|
||||
transactions.versions.v2,
|
||||
Unit.count,
|
||||
"v2",
|
||||
colors.cyan,
|
||||
colors.blue,
|
||||
),
|
||||
...fromBlockCountWithUnit(
|
||||
...fromCountPattern(
|
||||
transactions.versions.v3,
|
||||
Unit.count,
|
||||
"v3",
|
||||
@@ -489,12 +488,12 @@ export function createChainSection(ctx) {
|
||||
{
|
||||
name: "Input Count",
|
||||
title: "Input Count",
|
||||
bottom: [...fromSizePattern(inputs.count, Unit.count)],
|
||||
bottom: [...fromSumStatsPattern(inputs.count, Unit.count)],
|
||||
},
|
||||
{
|
||||
name: "Output Count",
|
||||
title: "Output Count",
|
||||
bottom: [...fromSizePattern(outputs.count.totalCount, Unit.count)],
|
||||
bottom: [...fromSumStatsPattern(outputs.count.totalCount, Unit.count)],
|
||||
},
|
||||
{
|
||||
name: "Inputs/sec",
|
||||
@@ -546,12 +545,12 @@ export function createChainSection(ctx) {
|
||||
{
|
||||
name: "P2PKH",
|
||||
title: "P2PKH Output Count",
|
||||
bottom: fromDollarsPattern(scripts.count.p2pkh, Unit.count),
|
||||
bottom: fromFullStatsPattern(scripts.count.p2pkh, Unit.count),
|
||||
},
|
||||
{
|
||||
name: "P2PK33",
|
||||
title: "P2PK33 Output Count",
|
||||
bottom: fromDollarsPattern(
|
||||
bottom: fromFullStatsPattern(
|
||||
scripts.count.p2pk33,
|
||||
Unit.count,
|
||||
),
|
||||
@@ -559,7 +558,7 @@ export function createChainSection(ctx) {
|
||||
{
|
||||
name: "P2PK65",
|
||||
title: "P2PK65 Output Count",
|
||||
bottom: fromDollarsPattern(
|
||||
bottom: fromFullStatsPattern(
|
||||
scripts.count.p2pk65,
|
||||
Unit.count,
|
||||
),
|
||||
@@ -573,12 +572,12 @@ export function createChainSection(ctx) {
|
||||
{
|
||||
name: "P2SH",
|
||||
title: "P2SH Output Count",
|
||||
bottom: fromDollarsPattern(scripts.count.p2sh, Unit.count),
|
||||
bottom: fromFullStatsPattern(scripts.count.p2sh, Unit.count),
|
||||
},
|
||||
{
|
||||
name: "P2MS",
|
||||
title: "P2MS Output Count",
|
||||
bottom: fromDollarsPattern(scripts.count.p2ms, Unit.count),
|
||||
bottom: fromFullStatsPattern(scripts.count.p2ms, Unit.count),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -589,7 +588,7 @@ export function createChainSection(ctx) {
|
||||
{
|
||||
name: "All SegWit",
|
||||
title: "SegWit Output Count",
|
||||
bottom: fromDollarsPattern(
|
||||
bottom: fromFullStatsPattern(
|
||||
scripts.count.segwit,
|
||||
Unit.count,
|
||||
),
|
||||
@@ -597,7 +596,7 @@ export function createChainSection(ctx) {
|
||||
{
|
||||
name: "P2WPKH",
|
||||
title: "P2WPKH Output Count",
|
||||
bottom: fromDollarsPattern(
|
||||
bottom: fromFullStatsPattern(
|
||||
scripts.count.p2wpkh,
|
||||
Unit.count,
|
||||
),
|
||||
@@ -605,7 +604,7 @@ export function createChainSection(ctx) {
|
||||
{
|
||||
name: "P2WSH",
|
||||
title: "P2WSH Output Count",
|
||||
bottom: fromDollarsPattern(scripts.count.p2wsh, Unit.count),
|
||||
bottom: fromFullStatsPattern(scripts.count.p2wsh, Unit.count),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -616,12 +615,12 @@ export function createChainSection(ctx) {
|
||||
{
|
||||
name: "P2TR",
|
||||
title: "P2TR Output Count",
|
||||
bottom: fromDollarsPattern(scripts.count.p2tr, Unit.count),
|
||||
bottom: fromFullStatsPattern(scripts.count.p2tr, Unit.count),
|
||||
},
|
||||
{
|
||||
name: "P2A",
|
||||
title: "P2A Output Count",
|
||||
bottom: fromDollarsPattern(scripts.count.p2a, Unit.count),
|
||||
bottom: fromFullStatsPattern(scripts.count.p2a, Unit.count),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -632,7 +631,7 @@ export function createChainSection(ctx) {
|
||||
{
|
||||
name: "OP_RETURN",
|
||||
title: "OP_RETURN Output Count",
|
||||
bottom: fromDollarsPattern(
|
||||
bottom: fromFullStatsPattern(
|
||||
scripts.count.opreturn,
|
||||
Unit.count,
|
||||
),
|
||||
@@ -640,7 +639,7 @@ export function createChainSection(ctx) {
|
||||
{
|
||||
name: "Empty",
|
||||
title: "Empty Output Count",
|
||||
bottom: fromDollarsPattern(
|
||||
bottom: fromFullStatsPattern(
|
||||
scripts.count.emptyoutput,
|
||||
Unit.count,
|
||||
),
|
||||
@@ -648,7 +647,7 @@ export function createChainSection(ctx) {
|
||||
{
|
||||
name: "Unknown",
|
||||
title: "Unknown Output Count",
|
||||
bottom: fromDollarsPattern(
|
||||
bottom: fromFullStatsPattern(
|
||||
scripts.count.unknownoutput,
|
||||
Unit.count,
|
||||
),
|
||||
@@ -793,9 +792,9 @@ export function createChainSection(ctx) {
|
||||
name: "Fee",
|
||||
title: "Transaction Fees",
|
||||
bottom: [
|
||||
...fromSizePattern(transactions.fees.fee.bitcoin, Unit.btc),
|
||||
...fromSizePattern(transactions.fees.fee.sats, Unit.sats),
|
||||
...fromSizePattern(transactions.fees.fee.dollars, Unit.usd),
|
||||
...fromSumStatsPattern(transactions.fees.fee.bitcoin, Unit.btc),
|
||||
...fromSumStatsPattern(transactions.fees.fee.sats, Unit.sats),
|
||||
...fromSumStatsPattern(transactions.fees.fee.dollars, Unit.usd),
|
||||
line({
|
||||
metric: blocks.rewards.feeDominance,
|
||||
name: "Dominance",
|
||||
|
||||
@@ -1,37 +1,6 @@
|
||||
import { Unit } from "../utils/units.js";
|
||||
import { line, price } from "./series.js";
|
||||
import {
|
||||
satsBtcUsd,
|
||||
createRatioChart,
|
||||
createZScoresFolder,
|
||||
formatCohortTitle,
|
||||
} from "./shared.js";
|
||||
|
||||
/**
|
||||
* Create price with ratio options for cointime prices
|
||||
* @param {PartialContext} ctx
|
||||
* @param {Object} args
|
||||
* @param {string} args.title
|
||||
* @param {string} args.legend
|
||||
* @param {AnyPricePattern} args.pricePattern
|
||||
* @param {ActivePriceRatioPattern} args.ratio
|
||||
* @param {Color} args.color
|
||||
* @returns {PartialOptionsTree}
|
||||
*/
|
||||
function createCointimePriceWithRatioOptions(
|
||||
ctx,
|
||||
{ title, legend, pricePattern, ratio, color },
|
||||
) {
|
||||
return [
|
||||
{
|
||||
name: "Price",
|
||||
title,
|
||||
top: [price({ metric: pricePattern, name: legend, color })],
|
||||
},
|
||||
createRatioChart(ctx, { title: formatCohortTitle(title), pricePattern, ratio, color }),
|
||||
createZScoresFolder(ctx, { title, legend, pricePattern, ratio, color }),
|
||||
];
|
||||
}
|
||||
import { satsBtcUsd, createPriceRatioCharts } from "./shared.js";
|
||||
|
||||
/**
|
||||
* Create Cointime section
|
||||
@@ -134,12 +103,12 @@ export function createCointimeSection(ctx) {
|
||||
},
|
||||
...cointimePrices.map(({ pricePattern, ratio, name, color, title }) => ({
|
||||
name,
|
||||
tree: createCointimePriceWithRatioOptions(ctx, {
|
||||
tree: createPriceRatioCharts(ctx, {
|
||||
context: title,
|
||||
legend: name,
|
||||
pricePattern,
|
||||
ratio,
|
||||
legend: name,
|
||||
color,
|
||||
title,
|
||||
}),
|
||||
})),
|
||||
],
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import {
|
||||
fromSizePattern,
|
||||
fromFullnessPattern,
|
||||
fromDollarsPattern,
|
||||
fromFeeRatePattern,
|
||||
fromSumStatsPattern,
|
||||
fromBaseStatsPattern,
|
||||
fromFullStatsPattern,
|
||||
fromStatsPattern,
|
||||
fromCoinbasePattern,
|
||||
fromValuePattern,
|
||||
fromBitcoinPatternWithUnit,
|
||||
fromBlockCountWithUnit,
|
||||
fromIntervalPattern,
|
||||
fromCountPattern,
|
||||
fromSupplyPattern,
|
||||
} from "./series.js";
|
||||
import { colors } from "../chart/colors.js";
|
||||
@@ -39,15 +38,14 @@ export function createContext({ brk }) {
|
||||
return {
|
||||
colors,
|
||||
brk,
|
||||
fromSizePattern: bind(fromSizePattern),
|
||||
fromFullnessPattern: bind(fromFullnessPattern),
|
||||
fromDollarsPattern: bind(fromDollarsPattern),
|
||||
fromFeeRatePattern: bind(fromFeeRatePattern),
|
||||
fromSumStatsPattern: bind(fromSumStatsPattern),
|
||||
fromBaseStatsPattern: bind(fromBaseStatsPattern),
|
||||
fromFullStatsPattern: bind(fromFullStatsPattern),
|
||||
fromStatsPattern: bind(fromStatsPattern),
|
||||
fromCoinbasePattern: bind(fromCoinbasePattern),
|
||||
fromValuePattern: bind(fromValuePattern),
|
||||
fromBitcoinPatternWithUnit: bind(fromBitcoinPatternWithUnit),
|
||||
fromBlockCountWithUnit: bind(fromBlockCountWithUnit),
|
||||
fromIntervalPattern: bind(fromIntervalPattern),
|
||||
fromCountPattern: bind(fromCountPattern),
|
||||
fromSupplyPattern,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -26,7 +26,13 @@ import {
|
||||
createSingleSupplyRelativeOptions,
|
||||
createSingleSellSideRiskSeries,
|
||||
createSingleValueCreatedDestroyedSeries,
|
||||
createSingleValueFlowBreakdownSeries,
|
||||
createSingleCapitulationProfitFlowSeries,
|
||||
createSingleSoprSeries,
|
||||
createSingleInvestorPriceSeries,
|
||||
createSingleInvestorPriceRatioSeries,
|
||||
createInvestorPriceSeries,
|
||||
createInvestorPriceRatioSeries,
|
||||
} from "./shared.js";
|
||||
|
||||
/**
|
||||
@@ -41,7 +47,7 @@ export function createAddressCohortFolder(ctx, args) {
|
||||
const useGroupName = "list" in args;
|
||||
const isSingle = !("list" in args);
|
||||
|
||||
const title = formatCohortTitle(args.title);
|
||||
const title = formatCohortTitle(args.name);
|
||||
|
||||
return {
|
||||
name: args.name || "all",
|
||||
@@ -96,6 +102,21 @@ export function createAddressCohortFolder(ctx, args) {
|
||||
title: title("Realized Price Ratio"),
|
||||
bottom: createRealizedPriceRatioSeries(list),
|
||||
},
|
||||
{
|
||||
name: "Investor Price",
|
||||
tree: [
|
||||
{
|
||||
name: "Price",
|
||||
title: title("Investor Price"),
|
||||
top: createInvestorPriceSeries(list),
|
||||
},
|
||||
{
|
||||
name: "Ratio",
|
||||
title: title("Investor Price Ratio"),
|
||||
bottom: createInvestorPriceRatioSeries(list),
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
: createRealizedPriceOptions(
|
||||
/** @type {AddressCohortObject} */ (args),
|
||||
@@ -161,6 +182,21 @@ function createRealizedPriceOptions(args, title) {
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Investor Price",
|
||||
tree: [
|
||||
{
|
||||
name: "Price",
|
||||
title: title("Investor Price"),
|
||||
top: createSingleInvestorPriceSeries(tree, color),
|
||||
},
|
||||
{
|
||||
name: "Ratio",
|
||||
title: title("Investor Price Ratio"),
|
||||
bottom: createSingleInvestorPriceRatioSeries(tree, color),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -365,8 +401,23 @@ function createRealizedPnlSection(ctx, args, title) {
|
||||
},
|
||||
{
|
||||
name: "Value",
|
||||
title: title("Value Created & Destroyed"),
|
||||
bottom: createSingleValueCreatedDestroyedSeries(colors, args.tree),
|
||||
tree: [
|
||||
{
|
||||
name: "Created & Destroyed",
|
||||
title: title("Value Created & Destroyed"),
|
||||
bottom: createSingleValueCreatedDestroyedSeries(colors, args.tree),
|
||||
},
|
||||
{
|
||||
name: "Breakdown",
|
||||
title: title("Value Flow Breakdown"),
|
||||
bottom: createSingleValueFlowBreakdownSeries(colors, args.tree),
|
||||
},
|
||||
{
|
||||
name: "Flow",
|
||||
title: title("Capitulation & Profit Flow"),
|
||||
bottom: createSingleCapitulationProfitFlowSeries(colors, args.tree),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -434,6 +485,35 @@ function createUnrealizedSection(ctx, list, useGroupName, title) {
|
||||
}),
|
||||
]),
|
||||
},
|
||||
{
|
||||
name: "Invested Capital",
|
||||
tree: [
|
||||
{
|
||||
name: "In Profit",
|
||||
title: title("Invested Capital In Profit"),
|
||||
bottom: list.flatMap(({ color, name, tree }) => [
|
||||
line({
|
||||
metric: tree.unrealized.investedCapitalInProfit,
|
||||
name: useGroupName ? name : "In Profit",
|
||||
color: useGroupName ? color : colors.green,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
]),
|
||||
},
|
||||
{
|
||||
name: "In Loss",
|
||||
title: title("Invested Capital In Loss"),
|
||||
bottom: list.flatMap(({ color, name, tree }) => [
|
||||
line({
|
||||
metric: tree.unrealized.investedCapitalInLoss,
|
||||
name: useGroupName ? name : "In Loss",
|
||||
color: useGroupName ? color : colors.red,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
]),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Relative",
|
||||
tree: [
|
||||
@@ -498,6 +578,30 @@ function createUnrealizedSection(ctx, list, useGroupName, title) {
|
||||
}),
|
||||
]),
|
||||
},
|
||||
{
|
||||
name: "Invested Capital In Profit",
|
||||
title: title("Invested Capital In Profit"),
|
||||
bottom: list.flatMap(({ color, name, tree }) => [
|
||||
baseline({
|
||||
metric: tree.relative.investedCapitalInProfitPct,
|
||||
name: useGroupName ? name : "In Profit",
|
||||
color: useGroupName ? color : colors.green,
|
||||
unit: Unit.pctRcap,
|
||||
}),
|
||||
]),
|
||||
},
|
||||
{
|
||||
name: "Invested Capital In Loss",
|
||||
title: title("Invested Capital In Loss"),
|
||||
bottom: list.flatMap(({ color, name, tree }) => [
|
||||
baseline({
|
||||
metric: tree.relative.investedCapitalInLossPct,
|
||||
name: useGroupName ? name : "In Loss",
|
||||
color: useGroupName ? color : colors.red,
|
||||
unit: Unit.pctRcap,
|
||||
}),
|
||||
]),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -14,6 +14,7 @@ export {
|
||||
createCohortFolderAgeRange,
|
||||
createCohortFolderBasicWithMarketCap,
|
||||
createCohortFolderBasicWithoutMarketCap,
|
||||
createCohortFolderWithoutRelative,
|
||||
createCohortFolderAddress,
|
||||
} from "./utxo.js";
|
||||
export { createAddressCohortFolder } from "./address.js";
|
||||
|
||||
@@ -3,31 +3,148 @@
|
||||
import { Unit } from "../../utils/units.js";
|
||||
import { priceLine } from "../constants.js";
|
||||
import { baseline, dots, line, price } from "../series.js";
|
||||
import { satsBtcUsd } from "../shared.js";
|
||||
import { satsBtcUsd, createPriceRatioCharts, formatCohortTitle } from "../shared.js";
|
||||
|
||||
// ============================================================================
|
||||
// Generic Price Helpers
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Create supply section for a single cohort
|
||||
* Create price folder (price + ratio + z-scores wrapped in folder)
|
||||
* For cohorts with full extended ratio metrics (ActivePriceRatioPattern)
|
||||
* @param {PartialContext} ctx
|
||||
* @param {CohortObject} cohort
|
||||
* @param {Object} [options]
|
||||
* @param {AnyFetchedSeriesBlueprint[]} [options.supplyRelative] - Supply relative to circulating supply metrics
|
||||
* @param {AnyFetchedSeriesBlueprint[]} [options.pnlRelative] - Supply in profit/loss relative to circulating supply metrics
|
||||
* @param {{ name: string, cohortTitle?: string, priceMetric: ActivePricePattern, ratioPattern: AnyRatioPattern, color: Color }} args
|
||||
* @returns {PartialOptionsGroup}
|
||||
*/
|
||||
export function createPriceFolder(ctx, { name, cohortTitle, priceMetric, ratioPattern, color }) {
|
||||
const context = cohortTitle ? `${cohortTitle} ${name}` : name;
|
||||
return {
|
||||
name,
|
||||
tree: createPriceRatioCharts(ctx, {
|
||||
context,
|
||||
legend: name,
|
||||
pricePattern: priceMetric,
|
||||
ratio: ratioPattern,
|
||||
color,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create basic price charts (price + ratio only, no z-scores) - flat array
|
||||
* For cohorts with basic ratio metrics (only .ratio field)
|
||||
* @template {AnyMetricPattern} R
|
||||
* @param {{ name: string, context: string, priceMetric: ActivePricePattern, ratioMetric: R, color: Color }} args
|
||||
* @returns {PartialOptionsTree}
|
||||
*/
|
||||
export function createBasicPriceCharts({ name, context, priceMetric, ratioMetric, color }) {
|
||||
return [
|
||||
{
|
||||
name: "Price",
|
||||
title: context,
|
||||
top: [price({ metric: priceMetric, name, color })],
|
||||
},
|
||||
{
|
||||
name: "Ratio",
|
||||
title: formatCohortTitle(context)("Ratio"),
|
||||
bottom: [
|
||||
baseline({
|
||||
metric: ratioMetric,
|
||||
name: "Ratio",
|
||||
color,
|
||||
unit: Unit.ratio,
|
||||
base: 1,
|
||||
}),
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create basic price folder (price + ratio wrapped in folder, no z-scores)
|
||||
* For cohorts with basic ratio metrics (only .ratio field)
|
||||
* @template {AnyMetricPattern} R
|
||||
* @param {{ name: string, cohortTitle?: string, priceMetric: ActivePricePattern, ratioMetric: R, color: Color }} args
|
||||
* @returns {PartialOptionsGroup}
|
||||
*/
|
||||
export function createBasicPriceFolder({ name, cohortTitle, priceMetric, ratioMetric, color }) {
|
||||
const context = cohortTitle ? `${cohortTitle} ${name}` : name;
|
||||
return {
|
||||
name,
|
||||
tree: createBasicPriceCharts({ name, context, priceMetric, ratioMetric, color }),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create grouped price charts (price + ratio) - flat array, no z-scores
|
||||
* @template {{ color: Color, name: string, tree: { realized: AnyRealizedPattern } }} T
|
||||
* @param {{ name: string, title: (metric: string) => string, list: readonly T[], getPrice: (tree: T['tree']) => ActivePricePattern, getRatio: (tree: T['tree']) => AnyMetricPattern }} args
|
||||
* @returns {PartialOptionsTree}
|
||||
*/
|
||||
export function createGroupedPriceCharts({ name, title, list, getPrice, getRatio }) {
|
||||
return [
|
||||
{
|
||||
name: "Price",
|
||||
title: title(name),
|
||||
top: list.map(({ color, name: cohortName, tree }) =>
|
||||
price({ metric: getPrice(tree), name: cohortName, color }),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "Ratio",
|
||||
title: title(`${name} Ratio`),
|
||||
bottom: list.map(({ color, name: cohortName, tree }) =>
|
||||
baseline({ metric: getRatio(tree), name: cohortName, color, unit: Unit.ratio, base: 1 }),
|
||||
),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create grouped price folder (price + ratio wrapped in folder)
|
||||
* @template {{ color: Color, name: string, tree: { realized: AnyRealizedPattern } }} T
|
||||
* @param {{ name: string, title: (metric: string) => string, list: readonly T[], getPrice: (tree: T['tree']) => ActivePricePattern, getRatio: (tree: T['tree']) => AnyMetricPattern }} args
|
||||
* @returns {PartialOptionsGroup}
|
||||
*/
|
||||
export function createGroupedPriceFolder({ name, title, list, getPrice, getRatio }) {
|
||||
return {
|
||||
name,
|
||||
tree: createGroupedPriceCharts({ name, title, list, getPrice, getRatio }),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create base supply series (without relative metrics)
|
||||
* @param {PartialContext} ctx
|
||||
* @param {CohortObject | CohortWithoutRelative} cohort
|
||||
* @returns {AnyFetchedSeriesBlueprint[]}
|
||||
*/
|
||||
export function createSingleSupplySeries(ctx, cohort, { supplyRelative = [], pnlRelative = [] } = {}) {
|
||||
function createSingleSupplySeriesBase(ctx, cohort) {
|
||||
const { colors } = ctx;
|
||||
const { tree } = cohort;
|
||||
|
||||
return [
|
||||
...satsBtcUsd(tree.supply.total, "Supply", colors.default),
|
||||
...supplyRelative,
|
||||
...satsBtcUsd(tree.unrealized.supplyInProfit, "In Profit", colors.green),
|
||||
...satsBtcUsd(tree.unrealized.supplyInLoss, "In Loss", colors.red),
|
||||
...satsBtcUsd(tree.supply.halved, "half", colors.gray).map((s) => ({
|
||||
...s,
|
||||
options: { lineStyle: 4 },
|
||||
})),
|
||||
...pnlRelative,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create supply relative to own supply metrics
|
||||
* @param {PartialContext} ctx
|
||||
* @param {UtxoCohortObject | AddressCohortObject} cohort
|
||||
* @returns {AnyFetchedSeriesBlueprint[]}
|
||||
*/
|
||||
function createSingleSupplyRelativeToOwnMetrics(ctx, cohort) {
|
||||
const { colors } = ctx;
|
||||
const { tree } = cohort;
|
||||
|
||||
return [
|
||||
line({
|
||||
metric: tree.relative.supplyInProfitRelToOwnSupply,
|
||||
name: "In Profit",
|
||||
@@ -51,6 +168,34 @@ export function createSingleSupplySeries(ctx, cohort, { supplyRelative = [], pnl
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create supply section for a single cohort (with relative metrics)
|
||||
* @param {PartialContext} ctx
|
||||
* @param {UtxoCohortObject | AddressCohortObject} cohort
|
||||
* @param {Object} [options]
|
||||
* @param {AnyFetchedSeriesBlueprint[]} [options.supplyRelative] - Supply relative to circulating supply metrics
|
||||
* @param {AnyFetchedSeriesBlueprint[]} [options.pnlRelative] - Supply in profit/loss relative to circulating supply metrics
|
||||
* @returns {AnyFetchedSeriesBlueprint[]}
|
||||
*/
|
||||
export function createSingleSupplySeries(ctx, cohort, { supplyRelative = [], pnlRelative = [] } = {}) {
|
||||
return [
|
||||
...createSingleSupplySeriesBase(ctx, cohort),
|
||||
...supplyRelative,
|
||||
...pnlRelative,
|
||||
...createSingleSupplyRelativeToOwnMetrics(ctx, cohort),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create supply series for cohorts WITHOUT relative metrics
|
||||
* @param {PartialContext} ctx
|
||||
* @param {CohortWithoutRelative} cohort
|
||||
* @returns {AnyFetchedSeriesBlueprint[]}
|
||||
*/
|
||||
export function createSingleSupplySeriesWithoutRelative(ctx, cohort) {
|
||||
return createSingleSupplySeriesBase(ctx, cohort);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create supply total series for grouped cohorts
|
||||
* @template {readonly CohortObject[]} T
|
||||
@@ -98,7 +243,7 @@ export function createGroupedSupplyInLossSeries(list, { relativeMetrics } = {})
|
||||
|
||||
/**
|
||||
* Create supply section for grouped cohorts
|
||||
* @template {readonly CohortObject[]} T
|
||||
* @template {readonly (CohortObject | CohortWithoutRelative)[]} T
|
||||
* @param {T} list
|
||||
* @param {(metric: string) => string} title
|
||||
* @param {Object} [options]
|
||||
@@ -365,6 +510,67 @@ export function createCostBasisPercentilesSeries(colors, list, useGroupName) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create invested capital percentile series (only for cohorts with CostBasisPattern2)
|
||||
* Shows invested capital at each percentile level
|
||||
* @param {Colors} colors
|
||||
* @param {readonly CohortWithCostBasisPercentiles[]} list
|
||||
* @param {boolean} useGroupName
|
||||
* @returns {FetchedPriceSeriesBlueprint[]}
|
||||
*/
|
||||
export function createInvestedCapitalPercentilesSeries(colors, list, useGroupName) {
|
||||
return list.flatMap(({ name, tree }) => {
|
||||
const ic = tree.costBasis.investedCapital;
|
||||
const n = (/** @type {number} */ pct) => (useGroupName ? `${name} p${pct}` : `p${pct}`);
|
||||
return [
|
||||
price({ metric: ic.pct95, name: n(95), color: colors.fuchsia, defaultActive: false }),
|
||||
price({ metric: ic.pct90, name: n(90), color: colors.pink, defaultActive: false }),
|
||||
price({ metric: ic.pct85, name: n(85), color: colors.pink, defaultActive: false }),
|
||||
price({ metric: ic.pct80, name: n(80), color: colors.rose, defaultActive: false }),
|
||||
price({ metric: ic.pct75, name: n(75), color: colors.red, defaultActive: false }),
|
||||
price({ metric: ic.pct70, name: n(70), color: colors.orange, defaultActive: false }),
|
||||
price({ metric: ic.pct65, name: n(65), color: colors.amber, defaultActive: false }),
|
||||
price({ metric: ic.pct60, name: n(60), color: colors.yellow, defaultActive: false }),
|
||||
price({ metric: ic.pct55, name: n(55), color: colors.yellow, defaultActive: false }),
|
||||
price({ metric: ic.pct50, name: n(50), color: colors.avocado }),
|
||||
price({ metric: ic.pct45, name: n(45), color: colors.lime, defaultActive: false }),
|
||||
price({ metric: ic.pct40, name: n(40), color: colors.green, defaultActive: false }),
|
||||
price({ metric: ic.pct35, name: n(35), color: colors.emerald, defaultActive: false }),
|
||||
price({ metric: ic.pct30, name: n(30), color: colors.teal, defaultActive: false }),
|
||||
price({ metric: ic.pct25, name: n(25), color: colors.teal, defaultActive: false }),
|
||||
price({ metric: ic.pct20, name: n(20), color: colors.cyan, defaultActive: false }),
|
||||
price({ metric: ic.pct15, name: n(15), color: colors.sky, defaultActive: false }),
|
||||
price({ metric: ic.pct10, name: n(10), color: colors.blue, defaultActive: false }),
|
||||
price({ metric: ic.pct05, name: n(5), color: colors.indigo, defaultActive: false }),
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create spot percentile series (shows current percentile of price relative to cost basis/invested capital)
|
||||
* @param {Colors} colors
|
||||
* @param {readonly CohortWithCostBasisPercentiles[]} list
|
||||
* @param {boolean} useGroupName
|
||||
* @returns {FetchedBaselineSeriesBlueprint[]}
|
||||
*/
|
||||
export function createSpotPercentileSeries(colors, list, useGroupName) {
|
||||
return list.flatMap(({ name, color, tree }) => [
|
||||
baseline({
|
||||
metric: tree.costBasis.spotCostBasisPercentile,
|
||||
name: useGroupName ? `${name} Cost Basis` : "Cost Basis",
|
||||
color: useGroupName ? color : colors.default,
|
||||
unit: Unit.ratio,
|
||||
}),
|
||||
baseline({
|
||||
metric: tree.costBasis.spotInvestedCapitalPercentile,
|
||||
name: useGroupName ? `${name} Invested Capital` : "Invested Capital",
|
||||
color: useGroupName ? color : colors.orange,
|
||||
unit: Unit.ratio,
|
||||
defaultActive: false,
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Activity Section Helpers
|
||||
// ============================================================================
|
||||
@@ -613,6 +819,67 @@ export function createSingleValueCreatedDestroyedSeries(colors, tree) {
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create profit/loss value breakdown series for single cohort
|
||||
* Shows profit value created/destroyed and loss value created/destroyed
|
||||
* @param {Colors} colors
|
||||
* @param {{ realized: AnyRealizedPattern }} tree
|
||||
* @returns {AnyFetchedSeriesBlueprint[]}
|
||||
*/
|
||||
export function createSingleValueFlowBreakdownSeries(colors, tree) {
|
||||
return [
|
||||
line({
|
||||
metric: tree.realized.profitValueCreated,
|
||||
name: "Profit Created",
|
||||
color: colors.green,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
line({
|
||||
metric: tree.realized.profitValueDestroyed,
|
||||
name: "Profit Destroyed",
|
||||
color: colors.lime,
|
||||
unit: Unit.usd,
|
||||
defaultActive: false,
|
||||
}),
|
||||
line({
|
||||
metric: tree.realized.lossValueCreated,
|
||||
name: "Loss Created",
|
||||
color: colors.orange,
|
||||
unit: Unit.usd,
|
||||
defaultActive: false,
|
||||
}),
|
||||
line({
|
||||
metric: tree.realized.lossValueDestroyed,
|
||||
name: "Loss Destroyed",
|
||||
color: colors.red,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create capitulation & profit flow series for single cohort
|
||||
* @param {Colors} colors
|
||||
* @param {{ realized: AnyRealizedPattern }} tree
|
||||
* @returns {AnyFetchedSeriesBlueprint[]}
|
||||
*/
|
||||
export function createSingleCapitulationProfitFlowSeries(colors, tree) {
|
||||
return [
|
||||
line({
|
||||
metric: tree.realized.profitFlow,
|
||||
name: "Profit Flow",
|
||||
color: colors.green,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
line({
|
||||
metric: tree.realized.capitulationFlow,
|
||||
name: "Capitulation Flow",
|
||||
color: colors.red,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// SOPR Helpers
|
||||
// ============================================================================
|
||||
@@ -649,3 +916,278 @@ export function createSingleSoprSeries(colors, tree) {
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Investor Price Helpers
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Create investor price series for single cohort
|
||||
* @param {{ realized: AnyRealizedPattern }} tree
|
||||
* @param {Color} color
|
||||
* @returns {FetchedPriceSeriesBlueprint[]}
|
||||
*/
|
||||
export function createSingleInvestorPriceSeries(tree, color) {
|
||||
return [
|
||||
price({
|
||||
metric: tree.realized.investorPrice,
|
||||
name: "Investor",
|
||||
color,
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create investor price ratio series for single cohort
|
||||
* @param {{ realized: AnyRealizedPattern }} tree
|
||||
* @param {Color} color
|
||||
* @returns {AnyFetchedSeriesBlueprint[]}
|
||||
*/
|
||||
export function createSingleInvestorPriceRatioSeries(tree, color) {
|
||||
return [
|
||||
baseline({
|
||||
metric: tree.realized.investorPriceExtra.ratio,
|
||||
name: "Investor Ratio",
|
||||
color,
|
||||
unit: Unit.ratio,
|
||||
base: 1,
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create investor price series for grouped cohorts
|
||||
* @param {readonly CohortObject[]} list
|
||||
* @returns {FetchedPriceSeriesBlueprint[]}
|
||||
*/
|
||||
export function createInvestorPriceSeries(list) {
|
||||
return list.map(({ color, name, tree }) =>
|
||||
price({ metric: tree.realized.investorPrice, name, color }),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create investor price ratio series for grouped cohorts
|
||||
* @param {readonly CohortObject[]} list
|
||||
* @returns {AnyFetchedSeriesBlueprint[]}
|
||||
*/
|
||||
export function createInvestorPriceRatioSeries(list) {
|
||||
return list.map(({ name, tree }) =>
|
||||
baseline({
|
||||
metric: tree.realized.investorPriceExtra.ratio,
|
||||
name,
|
||||
unit: Unit.ratio,
|
||||
base: 1,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create investor price folder for extended cohorts (with full Z-scores)
|
||||
* For cohorts with ActivePriceRatioPattern (all, term.*, ageRange.* UTXO cohorts)
|
||||
* @param {PartialContext} ctx
|
||||
* @param {{ tree: { realized: RealizedWithExtras }, color: Color }} cohort
|
||||
* @param {string} [cohortTitle] - Cohort title (e.g., "STH")
|
||||
* @returns {PartialOptionsGroup}
|
||||
*/
|
||||
export function createInvestorPriceFolderFull(ctx, cohort, cohortTitle) {
|
||||
const { tree, color } = cohort;
|
||||
return createPriceFolder(ctx, {
|
||||
name: "Investor Price",
|
||||
cohortTitle,
|
||||
priceMetric: tree.realized.investorPrice,
|
||||
ratioPattern: tree.realized.investorPriceExtra,
|
||||
color,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create investor price folder for basic cohorts (price + ratio only)
|
||||
* For cohorts with InvestorPriceExtraPattern (only .ratio field)
|
||||
* @param {{ tree: { realized: AnyRealizedPattern }, color: Color }} cohort
|
||||
* @param {string} [cohortTitle] - Cohort title (e.g., "STH")
|
||||
* @returns {PartialOptionsGroup}
|
||||
*/
|
||||
export function createInvestorPriceFolderBasic(cohort, cohortTitle) {
|
||||
const { tree, color } = cohort;
|
||||
return createBasicPriceFolder({
|
||||
name: "Investor Price",
|
||||
cohortTitle,
|
||||
priceMetric: tree.realized.investorPrice,
|
||||
ratioMetric: tree.realized.investorPriceExtra.ratio,
|
||||
color,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create investor price folder for grouped cohorts
|
||||
* @param {readonly CohortObject[]} list
|
||||
* @param {(metric: string) => string} title
|
||||
* @returns {PartialOptionsGroup}
|
||||
*/
|
||||
export function createGroupedInvestorPriceFolder(list, title) {
|
||||
return createGroupedPriceFolder({
|
||||
name: "Investor Price",
|
||||
title,
|
||||
list,
|
||||
getPrice: (tree) => tree.realized.investorPrice,
|
||||
getRatio: (tree) => tree.realized.investorPriceExtra.ratio,
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// ATH Regret Helpers
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Create realized ATH regret series for single cohort
|
||||
* @param {{ realized: AnyRealizedPattern }} tree
|
||||
* @param {Color} color
|
||||
* @returns {AnyFetchedSeriesBlueprint[]}
|
||||
*/
|
||||
export function createSingleRealizedAthRegretSeries(tree, color) {
|
||||
return [
|
||||
line({
|
||||
metric: tree.realized.athRegret.sum,
|
||||
name: "ATH Regret",
|
||||
color,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
line({
|
||||
metric: tree.realized.athRegret.cumulative,
|
||||
name: "Cumulative",
|
||||
color,
|
||||
unit: Unit.usd,
|
||||
defaultActive: false,
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create unrealized ATH regret series for single cohort
|
||||
* @param {{ unrealized: UnrealizedPattern }} tree
|
||||
* @param {Color} color
|
||||
* @returns {AnyFetchedSeriesBlueprint[]}
|
||||
*/
|
||||
export function createSingleUnrealizedAthRegretSeries(tree, color) {
|
||||
return [
|
||||
line({
|
||||
metric: tree.unrealized.athRegret,
|
||||
name: "ATH Regret",
|
||||
color,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create realized ATH regret series for grouped cohorts
|
||||
* @param {readonly CohortObject[]} list
|
||||
* @returns {AnyFetchedSeriesBlueprint[]}
|
||||
*/
|
||||
export function createGroupedRealizedAthRegretSeries(list) {
|
||||
return list.flatMap(({ color, name, tree }) => [
|
||||
line({
|
||||
metric: tree.realized.athRegret.sum,
|
||||
name,
|
||||
color,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create unrealized ATH regret series for grouped cohorts
|
||||
* @param {readonly { color: Color, name: string, tree: { unrealized: UnrealizedPattern } }[]} list
|
||||
* @returns {AnyFetchedSeriesBlueprint[]}
|
||||
*/
|
||||
export function createGroupedUnrealizedAthRegretSeries(list) {
|
||||
return list.flatMap(({ color, name, tree }) => [
|
||||
line({
|
||||
metric: tree.unrealized.athRegret,
|
||||
name,
|
||||
color,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Sentiment Helpers (greedIndex, painIndex, netSentiment)
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Create sentiment series for single cohort
|
||||
* @param {Colors} colors
|
||||
* @param {{ unrealized: UnrealizedPattern }} tree
|
||||
* @returns {AnyFetchedSeriesBlueprint[]}
|
||||
*/
|
||||
export function createSingleSentimentSeries(colors, tree) {
|
||||
return [
|
||||
baseline({
|
||||
metric: tree.unrealized.netSentiment,
|
||||
name: "Net Sentiment",
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
line({
|
||||
metric: tree.unrealized.greedIndex,
|
||||
name: "Greed Index",
|
||||
color: colors.green,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
line({
|
||||
metric: tree.unrealized.painIndex,
|
||||
name: "Pain Index",
|
||||
color: colors.red,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create net sentiment series for grouped cohorts
|
||||
* @param {readonly { color: Color, name: string, tree: { unrealized: UnrealizedPattern } }[]} list
|
||||
* @returns {AnyFetchedSeriesBlueprint[]}
|
||||
*/
|
||||
export function createGroupedNetSentimentSeries(list) {
|
||||
return list.flatMap(({ color, name, tree }) => [
|
||||
baseline({
|
||||
metric: tree.unrealized.netSentiment,
|
||||
name,
|
||||
color,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create greed index series for grouped cohorts
|
||||
* @param {readonly { color: Color, name: string, tree: { unrealized: UnrealizedPattern } }[]} list
|
||||
* @returns {AnyFetchedSeriesBlueprint[]}
|
||||
*/
|
||||
export function createGroupedGreedIndexSeries(list) {
|
||||
return list.flatMap(({ color, name, tree }) => [
|
||||
line({
|
||||
metric: tree.unrealized.greedIndex,
|
||||
name,
|
||||
color,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create pain index series for grouped cohorts
|
||||
* @param {readonly { color: Color, name: string, tree: { unrealized: UnrealizedPattern } }[]} list
|
||||
* @returns {AnyFetchedSeriesBlueprint[]}
|
||||
*/
|
||||
export function createGroupedPainIndexSeries(list) {
|
||||
return list.flatMap(({ color, name, tree }) => [
|
||||
line({
|
||||
metric: tree.unrealized.painIndex,
|
||||
name,
|
||||
color,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+81
-115
@@ -24,7 +24,6 @@ export function initOptions(brk) {
|
||||
const savedPath = /** @type {string[]} */ (
|
||||
JSON.parse(readStored(LS_SELECTED_KEY) || "[]") || []
|
||||
).filter((v) => v);
|
||||
console.log(savedPath);
|
||||
|
||||
const partialOptions = createPartialOptions({
|
||||
brk,
|
||||
@@ -83,91 +82,80 @@ export function initOptions(brk) {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a metric is an ActivePricePattern (has dollars and sats sub-metrics)
|
||||
* @param {any} metric
|
||||
* @returns {metric is ActivePricePattern}
|
||||
*/
|
||||
function isActivePricePattern(metric) {
|
||||
return (
|
||||
metric &&
|
||||
typeof metric === "object" &&
|
||||
"dollars" in metric &&
|
||||
"sats" in metric &&
|
||||
metric.dollars?.by &&
|
||||
metric.sats?.by
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {(AnyFetchedSeriesBlueprint | FetchedPriceSeriesBlueprint)[]} [arr]
|
||||
*/
|
||||
function arrayToMap(arr = []) {
|
||||
function arrayToMap(arr) {
|
||||
/** @type {Map<Unit, AnyFetchedSeriesBlueprint[]>} */
|
||||
const map = new Map();
|
||||
/** @type {Map<Unit, Set<number>>} */
|
||||
const priceLines = new Map();
|
||||
|
||||
for (const blueprint of arr || []) {
|
||||
if (!blueprint.metric) {
|
||||
throw new Error(
|
||||
`Blueprint missing metric: ${JSON.stringify(blueprint)}`,
|
||||
);
|
||||
}
|
||||
if (!arr) return map;
|
||||
|
||||
// Auto-expand ActivePricePattern into USD and sats versions
|
||||
if (isActivePricePattern(blueprint.metric)) {
|
||||
const pricePattern = /** @type {AnyPricePattern} */ (blueprint.metric);
|
||||
// Cache arrays for common units outside loop
|
||||
/** @type {AnyFetchedSeriesBlueprint[] | undefined} */
|
||||
let usdArr;
|
||||
/** @type {AnyFetchedSeriesBlueprint[] | undefined} */
|
||||
let satsArr;
|
||||
|
||||
// USD version
|
||||
markUsed(pricePattern.dollars);
|
||||
if (!map.has(Unit.usd)) map.set(Unit.usd, []);
|
||||
map.get(Unit.usd)?.push({ ...blueprint, metric: pricePattern.dollars, unit: Unit.usd });
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
const blueprint = arr[i];
|
||||
|
||||
// Sats version
|
||||
markUsed(pricePattern.sats);
|
||||
if (!map.has(Unit.sats)) map.set(Unit.sats, []);
|
||||
map.get(Unit.sats)?.push({ ...blueprint, metric: pricePattern.sats, unit: Unit.sats });
|
||||
// Check for price pattern blueprint (has dollars/sats sub-metrics)
|
||||
// Use unknown cast for safe property access check
|
||||
const maybePriceMetric = /** @type {{ dollars?: AnyMetricPattern, sats?: AnyMetricPattern }} */ (
|
||||
/** @type {unknown} */ (blueprint.metric)
|
||||
);
|
||||
if (maybePriceMetric.dollars?.by && maybePriceMetric.sats?.by) {
|
||||
const { dollars, sats } = maybePriceMetric;
|
||||
markUsed(dollars);
|
||||
if (!usdArr) map.set(Unit.usd, (usdArr = []));
|
||||
usdArr.push({ ...blueprint, metric: dollars, unit: Unit.usd });
|
||||
|
||||
markUsed(sats);
|
||||
if (!satsArr) map.set(Unit.sats, (satsArr = []));
|
||||
satsArr.push({ ...blueprint, metric: sats, unit: Unit.sats });
|
||||
continue;
|
||||
}
|
||||
|
||||
// At this point, blueprint is definitely an AnyFetchedSeriesBlueprint (not a price pattern)
|
||||
// After continue, we know this is a regular metric blueprint
|
||||
const regularBlueprint = /** @type {AnyFetchedSeriesBlueprint} */ (blueprint);
|
||||
|
||||
if (!regularBlueprint.unit) {
|
||||
throw new Error(`Blueprint missing unit: ${regularBlueprint.title}`);
|
||||
}
|
||||
markUsed(regularBlueprint.metric);
|
||||
const metric = regularBlueprint.metric;
|
||||
const unit = regularBlueprint.unit;
|
||||
if (!map.has(unit)) {
|
||||
map.set(unit, []);
|
||||
}
|
||||
map.get(unit)?.push(regularBlueprint);
|
||||
if (!unit) continue;
|
||||
|
||||
markUsed(metric);
|
||||
let unitArr = map.get(unit);
|
||||
if (!unitArr) map.set(unit, (unitArr = []));
|
||||
unitArr.push(regularBlueprint);
|
||||
|
||||
// Track baseline base values for auto price lines
|
||||
if (regularBlueprint.type === "Baseline") {
|
||||
const baseValue = regularBlueprint.options?.baseValue?.price ?? 0;
|
||||
if (!priceLines.has(unit)) priceLines.set(unit, new Set());
|
||||
priceLines.get(unit)?.add(baseValue);
|
||||
}
|
||||
|
||||
// Remove from set if manual price line already exists
|
||||
// Note: line() doesn't set type, so undefined means Line
|
||||
if (regularBlueprint.type === "Line" || regularBlueprint.type === undefined) {
|
||||
const path = Object.values(regularBlueprint.metric.by)[0]?.path ?? "";
|
||||
if (path.includes("constant_")) {
|
||||
priceLines.get(unit)?.delete(parseFloat(regularBlueprint.title));
|
||||
const type = regularBlueprint.type;
|
||||
if (type === "Baseline") {
|
||||
let priceSet = priceLines.get(unit);
|
||||
if (!priceSet) priceLines.set(unit, (priceSet = new Set()));
|
||||
priceSet.add(regularBlueprint.options?.baseValue?.price ?? 0);
|
||||
} else if (!type || type === "Line") {
|
||||
// Check if manual price line - avoid Object.values() array allocation
|
||||
const by = metric.by;
|
||||
for (const k in by) {
|
||||
if (by[/** @type {Index} */ (k)]?.path?.includes("constant_")) {
|
||||
priceLines.get(unit)?.delete(parseFloat(regularBlueprint.title));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add price lines at end for remaining values
|
||||
for (const [unit, values] of priceLines) {
|
||||
const arr = map.get(unit);
|
||||
if (!arr) continue;
|
||||
for (const baseValue of values) {
|
||||
const metric = getConstant(brk.metrics.constants, baseValue);
|
||||
markUsed(metric);
|
||||
map.get(unit)?.push({
|
||||
arr.push({
|
||||
metric,
|
||||
title: `${baseValue}`,
|
||||
color: colors.gray,
|
||||
@@ -238,30 +226,33 @@ export function initOptions(brk) {
|
||||
* @typedef {ProcessedGroup | ProcessedOption} ProcessedNode
|
||||
*/
|
||||
|
||||
// Pre-compute path strings for faster comparison
|
||||
const urlPathStr = urlPath?.join("/");
|
||||
const savedPathStr = savedPath?.join("/");
|
||||
|
||||
/**
|
||||
* @param {PartialOptionsTree} partialTree
|
||||
* @param {string[]} parentPath
|
||||
* @returns {ProcessedNode[]}
|
||||
* @param {string} parentPathStr
|
||||
* @returns {{ nodes: ProcessedNode[], count: number }}
|
||||
*/
|
||||
function processPartialTree(partialTree, parentPath = []) {
|
||||
function processPartialTree(partialTree, parentPath = [], parentPathStr = "") {
|
||||
/** @type {ProcessedNode[]} */
|
||||
const nodes = [];
|
||||
let totalCount = 0;
|
||||
|
||||
for (const anyPartial of partialTree) {
|
||||
for (let i = 0; i < partialTree.length; i++) {
|
||||
const anyPartial = partialTree[i];
|
||||
if ("tree" in anyPartial) {
|
||||
const serName = stringToId(anyPartial.name);
|
||||
const path = [...parentPath, serName];
|
||||
const children = processPartialTree(anyPartial.tree, path);
|
||||
|
||||
// Compute count from children
|
||||
const count = children.reduce(
|
||||
(sum, child) => sum + (child.type === "group" ? child.count : 1),
|
||||
0,
|
||||
);
|
||||
const pathStr = parentPathStr ? `${parentPathStr}/${serName}` : serName;
|
||||
const path = parentPath.concat(serName);
|
||||
const { nodes: children, count } = processPartialTree(anyPartial.tree, path, pathStr);
|
||||
|
||||
// Skip groups with no children
|
||||
if (count === 0) continue;
|
||||
|
||||
totalCount += count;
|
||||
nodes.push({
|
||||
type: "group",
|
||||
name: anyPartial.name,
|
||||
@@ -273,39 +264,23 @@ export function initOptions(brk) {
|
||||
} else {
|
||||
const option = /** @type {Option} */ (anyPartial);
|
||||
const name = option.name;
|
||||
const path = [...parentPath, stringToId(option.name)];
|
||||
const serName = stringToId(name);
|
||||
const pathStr = parentPathStr ? `${parentPathStr}/${serName}` : serName;
|
||||
const path = parentPath.concat(serName);
|
||||
|
||||
// Transform partial to full option
|
||||
if ("kind" in anyPartial && anyPartial.kind === "explorer") {
|
||||
Object.assign(
|
||||
option,
|
||||
/** @satisfies {ExplorerOption} */ ({
|
||||
kind: anyPartial.kind,
|
||||
path,
|
||||
name,
|
||||
title: option.title,
|
||||
}),
|
||||
);
|
||||
option.kind = anyPartial.kind;
|
||||
option.path = path;
|
||||
option.name = name;
|
||||
} else if ("kind" in anyPartial && anyPartial.kind === "table") {
|
||||
Object.assign(
|
||||
option,
|
||||
/** @satisfies {TableOption} */ ({
|
||||
kind: anyPartial.kind,
|
||||
path,
|
||||
name,
|
||||
title: option.title,
|
||||
}),
|
||||
);
|
||||
option.kind = anyPartial.kind;
|
||||
option.path = path;
|
||||
option.name = name;
|
||||
} else if ("kind" in anyPartial && anyPartial.kind === "simulation") {
|
||||
Object.assign(
|
||||
option,
|
||||
/** @satisfies {SimulationOption} */ ({
|
||||
kind: anyPartial.kind,
|
||||
path,
|
||||
name,
|
||||
title: anyPartial.title,
|
||||
}),
|
||||
);
|
||||
option.kind = anyPartial.kind;
|
||||
option.path = path;
|
||||
option.name = name;
|
||||
} else if ("url" in anyPartial) {
|
||||
Object.assign(
|
||||
option,
|
||||
@@ -319,7 +294,7 @@ export function initOptions(brk) {
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
const title = option.title || option.name;
|
||||
const title = option.title || name;
|
||||
Object.assign(
|
||||
option,
|
||||
/** @satisfies {ChartOption} */ ({
|
||||
@@ -334,22 +309,13 @@ export function initOptions(brk) {
|
||||
}
|
||||
|
||||
list.push(option);
|
||||
totalCount++;
|
||||
|
||||
// Check if this matches URL or saved path
|
||||
if (urlPath) {
|
||||
const sameAsURLPath =
|
||||
urlPath.length === path.length &&
|
||||
urlPath.every((val, i) => val === path[i]);
|
||||
if (sameAsURLPath) {
|
||||
selected.set(option);
|
||||
}
|
||||
} else if (savedPath) {
|
||||
const sameAsSavedPath =
|
||||
savedPath.length === path.length &&
|
||||
savedPath.every((val, i) => val === path[i]);
|
||||
if (sameAsSavedPath) {
|
||||
savedOption = option;
|
||||
}
|
||||
// Check if this matches URL or saved path (string comparison is faster)
|
||||
if (urlPathStr && pathStr === urlPathStr) {
|
||||
selected.set(option);
|
||||
} else if (savedPathStr && pathStr === savedPathStr) {
|
||||
savedOption = option;
|
||||
}
|
||||
|
||||
nodes.push({
|
||||
@@ -360,10 +326,10 @@ export function initOptions(brk) {
|
||||
}
|
||||
}
|
||||
|
||||
return nodes;
|
||||
return { nodes, count: totalCount };
|
||||
}
|
||||
|
||||
const processedTree = processPartialTree(partialOptions);
|
||||
const { nodes: processedTree } = processPartialTree(partialOptions);
|
||||
logUnused();
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/** Moving averages section */
|
||||
|
||||
import { price } from "../series.js";
|
||||
import { createRatioChart, createZScoresFolder, formatCohortTitle } from "../shared.js";
|
||||
import { createPriceRatioCharts } from "../shared.js";
|
||||
import { periodIdToName } from "./utils.js";
|
||||
|
||||
/**
|
||||
@@ -66,39 +66,6 @@ function buildEmaAverages(colors, ma) {
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create price with ratio options (for moving averages)
|
||||
* @param {PartialContext} ctx
|
||||
* @param {Object} args
|
||||
* @param {string} args.title
|
||||
* @param {string} args.legend
|
||||
* @param {EmaRatioPattern} args.ratio
|
||||
* @param {Color} args.color
|
||||
* @returns {PartialOptionsTree}
|
||||
*/
|
||||
export function createPriceWithRatioOptions(
|
||||
ctx,
|
||||
{ title, legend, ratio, color },
|
||||
) {
|
||||
const pricePattern = ratio.price;
|
||||
|
||||
return [
|
||||
{
|
||||
name: "Price",
|
||||
title,
|
||||
top: [price({ metric: pricePattern, name: legend, color })],
|
||||
},
|
||||
createRatioChart(ctx, { title: formatCohortTitle(title), pricePattern, ratio, color }),
|
||||
createZScoresFolder(ctx, {
|
||||
title,
|
||||
legend,
|
||||
pricePattern,
|
||||
ratio,
|
||||
color,
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
/** Common period IDs to show at top level */
|
||||
const COMMON_PERIODS = ["1w", "1m", "200d", "1y", "200w", "4y"];
|
||||
|
||||
@@ -176,10 +143,11 @@ export function createAveragesSection(ctx, movingAverage) {
|
||||
// Common periods at top level
|
||||
...commonAverages.map(({ name, color, ratio }) => ({
|
||||
name,
|
||||
tree: createPriceWithRatioOptions(ctx, {
|
||||
ratio,
|
||||
title: `${name} ${label}`,
|
||||
tree: createPriceRatioCharts(ctx, {
|
||||
context: `${name} ${label}`,
|
||||
legend: "average",
|
||||
pricePattern: ratio.price,
|
||||
ratio,
|
||||
color,
|
||||
}),
|
||||
})),
|
||||
@@ -188,10 +156,11 @@ export function createAveragesSection(ctx, movingAverage) {
|
||||
name: "More...",
|
||||
tree: moreAverages.map(({ name, color, ratio }) => ({
|
||||
name,
|
||||
tree: createPriceWithRatioOptions(ctx, {
|
||||
ratio,
|
||||
title: `${name} ${label}`,
|
||||
tree: createPriceRatioCharts(ctx, {
|
||||
context: `${name} ${label}`,
|
||||
legend: "average",
|
||||
pricePattern: ratio.price,
|
||||
ratio,
|
||||
color,
|
||||
}),
|
||||
})),
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
/** Market section - Main entry point */
|
||||
|
||||
import { localhost } from "../../utils/env.js";
|
||||
import { Unit } from "../../utils/units.js";
|
||||
import { candlestick, line, price } from "../series.js";
|
||||
import { line, price } from "../series.js";
|
||||
import { createAveragesSection } from "./averages.js";
|
||||
import { createReturnsSection } from "./performance.js";
|
||||
import { createMomentumSection } from "./momentum.js";
|
||||
@@ -21,7 +20,7 @@ import {
|
||||
*/
|
||||
export function createMarketSection(ctx) {
|
||||
const { colors, brk } = ctx;
|
||||
const { market, supply, price: priceMetrics } = brk.metrics;
|
||||
const { market, supply } = brk.metrics;
|
||||
const {
|
||||
movingAverage,
|
||||
ath,
|
||||
|
||||
@@ -52,7 +52,7 @@ export function createDcaVsLumpSumSection(ctx, { dca, lookback, returns }) {
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {ShortPeriodKey | LongPeriodKey} key
|
||||
* @param {AllPeriodKey} key
|
||||
*/
|
||||
const costBasisChart = (name, key) => ({
|
||||
name: "Cost Basis",
|
||||
@@ -71,7 +71,10 @@ export function createDcaVsLumpSumSection(ctx, { dca, lookback, returns }) {
|
||||
],
|
||||
});
|
||||
|
||||
/** @param {string} name @param {ShortPeriodKey | LongPeriodKey} key */
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {AllPeriodKey} key
|
||||
*/
|
||||
const daysInProfitChart = (name, key) => ({
|
||||
name: "Days in Profit",
|
||||
title: `${name} Days in Profit`,
|
||||
@@ -85,7 +88,10 @@ export function createDcaVsLumpSumSection(ctx, { dca, lookback, returns }) {
|
||||
],
|
||||
});
|
||||
|
||||
/** @param {string} name @param {ShortPeriodKey | LongPeriodKey} key */
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {AllPeriodKey} key
|
||||
*/
|
||||
const daysInLossChart = (name, key) => ({
|
||||
name: "Days in Loss",
|
||||
title: `${name} Days in Loss`,
|
||||
@@ -99,7 +105,10 @@ export function createDcaVsLumpSumSection(ctx, { dca, lookback, returns }) {
|
||||
],
|
||||
});
|
||||
|
||||
/** @param {string} name @param {ShortPeriodKey | LongPeriodKey} key */
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {AllPeriodKey} key
|
||||
*/
|
||||
const maxDrawdownChart = (name, key) => ({
|
||||
name: "Max Drawdown",
|
||||
title: `${name} Max Drawdown`,
|
||||
@@ -113,7 +122,10 @@ export function createDcaVsLumpSumSection(ctx, { dca, lookback, returns }) {
|
||||
],
|
||||
});
|
||||
|
||||
/** @param {string} name @param {ShortPeriodKey | LongPeriodKey} key */
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {AllPeriodKey} key
|
||||
*/
|
||||
const maxReturnChart = (name, key) => ({
|
||||
name: "Max Return",
|
||||
title: `${name} Max Return`,
|
||||
@@ -129,7 +141,7 @@ export function createDcaVsLumpSumSection(ctx, { dca, lookback, returns }) {
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {ShortPeriodKey | LongPeriodKey} key
|
||||
* @param {AllPeriodKey} key
|
||||
*/
|
||||
const stackChart = (name, key) => ({
|
||||
name: "Stack",
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
createCohortFolderAgeRange,
|
||||
createCohortFolderBasicWithMarketCap,
|
||||
createCohortFolderBasicWithoutMarketCap,
|
||||
createCohortFolderWithoutRelative,
|
||||
createCohortFolderAddress,
|
||||
createAddressCohortFolder,
|
||||
} from "./distribution/index.js";
|
||||
@@ -64,6 +65,9 @@ export function createPartialOptions({ brk }) {
|
||||
/** @param {CohortBasicWithoutMarketCap} cohort */
|
||||
const mapBasicWithoutMarketCap = (cohort) =>
|
||||
createCohortFolderBasicWithoutMarketCap(ctx, cohort);
|
||||
/** @param {CohortWithoutRelative} cohort */
|
||||
const mapWithoutRelative = (cohort) =>
|
||||
createCohortFolderWithoutRelative(ctx, cohort);
|
||||
/** @param {CohortAddress} cohort */
|
||||
const mapAddress = (cohort) => createCohortFolderAddress(ctx, cohort);
|
||||
/** @param {AddressCohortObject} cohort */
|
||||
@@ -107,7 +111,7 @@ export function createPartialOptions({ brk }) {
|
||||
// STH vs LTH - Direct comparison
|
||||
createCohortFolderWithNupl(ctx, {
|
||||
name: "STH vs LTH",
|
||||
title: "Holders",
|
||||
title: "STH vs LTH",
|
||||
list: [termShort, termLong],
|
||||
}),
|
||||
|
||||
@@ -121,7 +125,7 @@ export function createPartialOptions({ brk }) {
|
||||
tree: [
|
||||
createCohortFolderWithAdjusted(ctx, {
|
||||
name: "Compare",
|
||||
title: "Age Younger Than",
|
||||
title: "Max Age",
|
||||
list: upToDate,
|
||||
}),
|
||||
...upToDate.map(mapWithAdjusted),
|
||||
@@ -133,7 +137,7 @@ export function createPartialOptions({ brk }) {
|
||||
tree: [
|
||||
createCohortFolderBasicWithMarketCap(ctx, {
|
||||
name: "Compare",
|
||||
title: "Age Older Than",
|
||||
title: "Min Age",
|
||||
list: fromDate,
|
||||
}),
|
||||
...fromDate.map(mapBasicWithMarketCap),
|
||||
@@ -145,7 +149,7 @@ export function createPartialOptions({ brk }) {
|
||||
tree: [
|
||||
createCohortFolderAgeRange(ctx, {
|
||||
name: "Compare",
|
||||
title: "Age Range",
|
||||
title: "Age Ranges",
|
||||
list: dateRange,
|
||||
}),
|
||||
...dateRange.map(mapAgeRange),
|
||||
@@ -164,7 +168,7 @@ export function createPartialOptions({ brk }) {
|
||||
tree: [
|
||||
createCohortFolderBasicWithMarketCap(ctx, {
|
||||
name: "Compare",
|
||||
title: "Size Less Than",
|
||||
title: "Max Size",
|
||||
list: utxosUnderAmount,
|
||||
}),
|
||||
...utxosUnderAmount.map(mapBasicWithMarketCap),
|
||||
@@ -176,7 +180,7 @@ export function createPartialOptions({ brk }) {
|
||||
tree: [
|
||||
createCohortFolderBasicWithMarketCap(ctx, {
|
||||
name: "Compare",
|
||||
title: "Size More Than",
|
||||
title: "Min Size",
|
||||
list: utxosAboveAmount,
|
||||
}),
|
||||
...utxosAboveAmount.map(mapBasicWithMarketCap),
|
||||
@@ -188,7 +192,7 @@ export function createPartialOptions({ brk }) {
|
||||
tree: [
|
||||
createCohortFolderBasicWithoutMarketCap(ctx, {
|
||||
name: "Compare",
|
||||
title: "Size Range",
|
||||
title: "Size Ranges",
|
||||
list: utxosAmountRanges,
|
||||
}),
|
||||
...utxosAmountRanges.map(mapBasicWithoutMarketCap),
|
||||
@@ -207,7 +211,7 @@ export function createPartialOptions({ brk }) {
|
||||
tree: [
|
||||
createAddressCohortFolder(ctx, {
|
||||
name: "Compare",
|
||||
title: "Balance Less Than",
|
||||
title: "Max Balance",
|
||||
list: addressesUnderAmount,
|
||||
}),
|
||||
...addressesUnderAmount.map(mapAddressCohorts),
|
||||
@@ -219,7 +223,7 @@ export function createPartialOptions({ brk }) {
|
||||
tree: [
|
||||
createAddressCohortFolder(ctx, {
|
||||
name: "Compare",
|
||||
title: "Balance More Than",
|
||||
title: "Min Balance",
|
||||
list: addressesAboveAmount,
|
||||
}),
|
||||
...addressesAboveAmount.map(mapAddressCohorts),
|
||||
@@ -231,7 +235,7 @@ export function createPartialOptions({ brk }) {
|
||||
tree: [
|
||||
createAddressCohortFolder(ctx, {
|
||||
name: "Compare",
|
||||
title: "Balance Range",
|
||||
title: "Balance Ranges",
|
||||
list: addressesAmountRanges,
|
||||
}),
|
||||
...addressesAmountRanges.map(mapAddressCohorts),
|
||||
@@ -246,11 +250,11 @@ export function createPartialOptions({ brk }) {
|
||||
tree: [
|
||||
createCohortFolderAddress(ctx, {
|
||||
name: "Compare",
|
||||
title: "Script Type",
|
||||
title: "Script Types",
|
||||
list: typeAddressable,
|
||||
}),
|
||||
...typeAddressable.map(mapAddress),
|
||||
...typeOther.map(mapBasicWithoutMarketCap),
|
||||
...typeOther.map(mapWithoutRelative),
|
||||
],
|
||||
},
|
||||
|
||||
@@ -260,7 +264,7 @@ export function createPartialOptions({ brk }) {
|
||||
tree: [
|
||||
createCohortFolderBasicWithoutMarketCap(ctx, {
|
||||
name: "Compare",
|
||||
title: "Epoch",
|
||||
title: "Epochs",
|
||||
list: epoch,
|
||||
}),
|
||||
...epoch.map(mapBasicWithoutMarketCap),
|
||||
@@ -273,7 +277,7 @@ export function createPartialOptions({ brk }) {
|
||||
tree: [
|
||||
createCohortFolderBasicWithoutMarketCap(ctx, {
|
||||
name: "Compare",
|
||||
title: "Year",
|
||||
title: "Years",
|
||||
list: year,
|
||||
}),
|
||||
...year.map(mapBasicWithoutMarketCap),
|
||||
|
||||
@@ -46,9 +46,8 @@ export function price({
|
||||
|
||||
/**
|
||||
* Create percentile series (max/min/median/pct75/pct25/pct90/pct10) from any stats pattern
|
||||
* Works with FullnessPattern, FeeRatePattern, AnyStatsPattern, DollarsPattern, etc.
|
||||
* @param {Colors} colors
|
||||
* @param {FullnessPattern<any> | FeeRatePattern<any> | AnyStatsPattern | DollarsPattern<any>} pattern
|
||||
* @param {StatsPattern<any> | BaseStatsPattern<any> | FullStatsPattern<any> | AnyStatsPattern} pattern
|
||||
* @param {Unit} unit
|
||||
* @param {string} title
|
||||
* @param {{ type?: "Dots" }} [options]
|
||||
@@ -289,14 +288,14 @@ export function histogram({
|
||||
}
|
||||
|
||||
/**
|
||||
* Create series from a SizePattern ({ average, sum, cumulative, min, max, percentiles })
|
||||
* Create series from patterns with sum + cumulative + percentiles (NO base)
|
||||
* @param {Colors} colors
|
||||
* @param {AnyStatsPattern} pattern
|
||||
* @param {Unit} unit
|
||||
* @param {string} [title]
|
||||
* @returns {AnyFetchedSeriesBlueprint[]}
|
||||
*/
|
||||
export function fromSizePattern(colors, pattern, unit, title = "") {
|
||||
export function fromSumStatsPattern(colors, pattern, unit, title = "") {
|
||||
const { stat } = colors;
|
||||
return [
|
||||
{ metric: pattern.average, title: `${title} avg`.trim(), unit },
|
||||
@@ -319,36 +318,39 @@ export function fromSizePattern(colors, pattern, unit, title = "") {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create series from a FullnessPattern ({ base, average, sum, cumulative, min, max, percentiles })
|
||||
* Create series from a BaseStatsPattern (base + avg + percentiles, NO sum)
|
||||
* @param {Colors} colors
|
||||
* @param {FullnessPattern<any>} pattern
|
||||
* @param {BaseStatsPattern<any>} pattern
|
||||
* @param {Unit} unit
|
||||
* @param {string} [title]
|
||||
* @param {{ baseColor?: Color, avgActive?: boolean }} [options]
|
||||
* @returns {AnyFetchedSeriesBlueprint[]}
|
||||
*/
|
||||
export function fromFullnessPattern(colors, pattern, unit, title = "") {
|
||||
export function fromBaseStatsPattern(colors, pattern, unit, title = "", options) {
|
||||
const { stat } = colors;
|
||||
const { baseColor, avgActive = true } = options || {};
|
||||
return [
|
||||
{ metric: pattern.base, title: title || "base", unit },
|
||||
{ metric: pattern.base, title: title || "base", color: baseColor, unit },
|
||||
{
|
||||
metric: pattern.average,
|
||||
title: `${title} avg`.trim(),
|
||||
color: stat.avg,
|
||||
unit,
|
||||
defaultActive: avgActive,
|
||||
},
|
||||
...percentileSeries(colors, pattern, unit, title),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create series from a DollarsPattern ({ base, sum, cumulative, average, min, max, percentiles })
|
||||
* Create series from a FullStatsPattern (base + sum + cumulative + avg + percentiles)
|
||||
* @param {Colors} colors
|
||||
* @param {DollarsPattern<any>} pattern
|
||||
* @param {FullStatsPattern<any>} pattern
|
||||
* @param {Unit} unit
|
||||
* @param {string} [title]
|
||||
* @returns {AnyFetchedSeriesBlueprint[]}
|
||||
*/
|
||||
export function fromDollarsPattern(colors, pattern, unit, title = "") {
|
||||
export function fromFullStatsPattern(colors, pattern, unit, title = "") {
|
||||
const { stat } = colors;
|
||||
return [
|
||||
{ metric: pattern.base, title: title || "base", unit },
|
||||
@@ -377,14 +379,14 @@ export function fromDollarsPattern(colors, pattern, unit, title = "") {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create series from a FeeRatePattern ({ average, min, max, percentiles })
|
||||
* Create series from a StatsPattern ({ average, min, max, percentiles })
|
||||
* @param {Colors} colors
|
||||
* @param {FeeRatePattern<any>} pattern
|
||||
* @param {StatsPattern<any>} pattern
|
||||
* @param {Unit} unit
|
||||
* @param {string} [title]
|
||||
* @returns {AnyFetchedSeriesBlueprint[]}
|
||||
*/
|
||||
export function fromFeeRatePattern(colors, pattern, unit, title = "") {
|
||||
export function fromStatsPattern(colors, pattern, unit, title = "") {
|
||||
return [
|
||||
{
|
||||
type: "Dots",
|
||||
@@ -397,14 +399,14 @@ export function fromFeeRatePattern(colors, pattern, unit, title = "") {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create series from a pattern with sum and cumulative (fullness stats + sum + cumulative)
|
||||
* Create series from AnyFullStatsPattern (base + sum + cumulative + avg + percentiles)
|
||||
* @param {Colors} colors
|
||||
* @param {FullnessPatternWithSumCumulative} pattern
|
||||
* @param {AnyFullStatsPattern} pattern
|
||||
* @param {Unit} unit
|
||||
* @param {string} [title]
|
||||
* @returns {AnyFetchedSeriesBlueprint[]}
|
||||
*/
|
||||
export function fromFullnessPatternWithSumCumulative(
|
||||
export function fromAnyFullStatsPattern(
|
||||
colors,
|
||||
pattern,
|
||||
unit,
|
||||
@@ -412,7 +414,7 @@ export function fromFullnessPatternWithSumCumulative(
|
||||
) {
|
||||
const { stat } = colors;
|
||||
return [
|
||||
...fromFullnessPattern(colors, pattern, unit, title),
|
||||
...fromBaseStatsPattern(colors, pattern, unit, title),
|
||||
{
|
||||
metric: pattern.sum,
|
||||
title: `${title} sum`.trim(),
|
||||
@@ -438,19 +440,19 @@ export function fromFullnessPatternWithSumCumulative(
|
||||
*/
|
||||
export function fromCoinbasePattern(colors, pattern, title = "") {
|
||||
return [
|
||||
...fromFullnessPatternWithSumCumulative(
|
||||
...fromAnyFullStatsPattern(
|
||||
colors,
|
||||
pattern.bitcoin,
|
||||
Unit.btc,
|
||||
title,
|
||||
),
|
||||
...fromFullnessPatternWithSumCumulative(
|
||||
...fromAnyFullStatsPattern(
|
||||
colors,
|
||||
pattern.sats,
|
||||
Unit.sats,
|
||||
title,
|
||||
),
|
||||
...fromFullnessPatternWithSumCumulative(
|
||||
...fromAnyFullStatsPattern(
|
||||
colors,
|
||||
pattern.dollars,
|
||||
Unit.usd,
|
||||
@@ -460,7 +462,7 @@ export function fromCoinbasePattern(colors, pattern, title = "") {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create series from a ValuePattern ({ sats, bitcoin, dollars } each as BlockCountPattern with sum + cumulative)
|
||||
* Create series from a ValuePattern ({ sats, bitcoin, dollars } each as CountPattern with sum + cumulative)
|
||||
* @param {Colors} colors
|
||||
* @param {ValuePattern} pattern
|
||||
* @param {string} [title]
|
||||
@@ -557,16 +559,16 @@ export function fromBitcoinPatternWithUnit(
|
||||
}
|
||||
|
||||
/**
|
||||
* Create sum/cumulative series from a BlockCountPattern with explicit unit and colors
|
||||
* Create sum/cumulative series from a CountPattern with explicit unit and colors
|
||||
* @param {Colors} colors
|
||||
* @param {BlockCountPattern<any>} pattern
|
||||
* @param {CountPattern<any>} pattern
|
||||
* @param {Unit} unit
|
||||
* @param {string} [title]
|
||||
* @param {Color} [sumColor]
|
||||
* @param {Color} [cumulativeColor]
|
||||
* @returns {AnyFetchedSeriesBlueprint[]}
|
||||
*/
|
||||
export function fromBlockCountWithUnit(
|
||||
export function fromCountPattern(
|
||||
colors,
|
||||
pattern,
|
||||
unit,
|
||||
@@ -591,30 +593,6 @@ export function fromBlockCountWithUnit(
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create series from an IntervalPattern (base + average/min/max/median/percentiles, no sum/cumulative)
|
||||
* @param {Colors} colors
|
||||
* @param {IntervalPattern} pattern
|
||||
* @param {Unit} unit
|
||||
* @param {string} [title]
|
||||
* @param {Color} [color]
|
||||
* @returns {AnyFetchedSeriesBlueprint[]}
|
||||
*/
|
||||
export function fromIntervalPattern(colors, pattern, unit, title = "", color) {
|
||||
const { stat } = colors;
|
||||
return [
|
||||
{ metric: pattern.base, title: title ?? "base", color, unit },
|
||||
{
|
||||
metric: pattern.average,
|
||||
title: `${title} avg`.trim(),
|
||||
color: stat.avg,
|
||||
unit,
|
||||
defaultActive: false,
|
||||
},
|
||||
...percentileSeries(colors, pattern, unit, title),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create series from a SupplyPattern (sats/bitcoin/dollars, no sum/cumulative)
|
||||
* @param {SupplyPattern} pattern
|
||||
|
||||
@@ -44,7 +44,7 @@ export function satsBtcUsd(pattern, name, color, options) {
|
||||
/**
|
||||
* Build percentile USD mappings from a ratio pattern
|
||||
* @param {Colors} colors
|
||||
* @param {ActivePriceRatioPattern} ratio
|
||||
* @param {AnyRatioPattern} ratio
|
||||
*/
|
||||
export function percentileUsdMap(colors, ratio) {
|
||||
return /** @type {const} */ ([
|
||||
@@ -60,7 +60,7 @@ export function percentileUsdMap(colors, ratio) {
|
||||
/**
|
||||
* Build percentile ratio mappings from a ratio pattern
|
||||
* @param {Colors} colors
|
||||
* @param {ActivePriceRatioPattern} ratio
|
||||
* @param {AnyRatioPattern} ratio
|
||||
*/
|
||||
export function percentileMap(colors, ratio) {
|
||||
return /** @type {const} */ ([
|
||||
@@ -75,7 +75,7 @@ export function percentileMap(colors, ratio) {
|
||||
|
||||
/**
|
||||
* Build SD patterns from a ratio pattern
|
||||
* @param {ActivePriceRatioPattern} ratio
|
||||
* @param {AnyRatioPattern} ratio
|
||||
*/
|
||||
export function sdPatterns(ratio) {
|
||||
return /** @type {const} */ ([
|
||||
@@ -135,7 +135,7 @@ export function sdBandsRatio(colors, sd) {
|
||||
/**
|
||||
* Build ratio SMA series from a ratio pattern
|
||||
* @param {Colors} colors
|
||||
* @param {ActivePriceRatioPattern} ratio
|
||||
* @param {AnyRatioPattern} ratio
|
||||
*/
|
||||
export function ratioSmas(colors, ratio) {
|
||||
return /** @type {const} */ ([
|
||||
@@ -154,7 +154,7 @@ export function ratioSmas(colors, ratio) {
|
||||
* @param {Object} args
|
||||
* @param {(metric: string) => string} args.title
|
||||
* @param {AnyPricePattern} args.pricePattern - The price pattern to show in top pane
|
||||
* @param {ActivePriceRatioPattern} args.ratio - The ratio pattern
|
||||
* @param {AnyRatioPattern} args.ratio - The ratio pattern
|
||||
* @param {Color} args.color
|
||||
* @param {string} [args.name] - Optional name override (default: "ratio")
|
||||
* @returns {PartialChartOption}
|
||||
@@ -205,16 +205,16 @@ export function createRatioChart(ctx, { title, pricePattern, ratio, color, name
|
||||
* Create ZScores folder from ActivePriceRatioPattern
|
||||
* @param {PartialContext} ctx
|
||||
* @param {Object} args
|
||||
* @param {string} args.title
|
||||
* @param {(suffix: string) => string} args.formatTitle - Function that takes metric suffix and returns full title
|
||||
* @param {string} args.legend
|
||||
* @param {AnyPricePattern} args.pricePattern - The price pattern to show in top pane
|
||||
* @param {ActivePriceRatioPattern} args.ratio - The ratio pattern
|
||||
* @param {AnyRatioPattern} args.ratio - The ratio pattern
|
||||
* @param {Color} args.color
|
||||
* @returns {PartialOptionsGroup}
|
||||
*/
|
||||
export function createZScoresFolder(
|
||||
ctx,
|
||||
{ title, legend, pricePattern, ratio, color },
|
||||
{ formatTitle, legend, pricePattern, ratio, color },
|
||||
) {
|
||||
const { colors } = ctx;
|
||||
const sdPats = sdPatterns(ratio);
|
||||
@@ -224,7 +224,7 @@ export function createZScoresFolder(
|
||||
tree: [
|
||||
{
|
||||
name: "Compare",
|
||||
title: `${title} Z-Scores`,
|
||||
title: formatTitle("Z-Scores"),
|
||||
top: [
|
||||
price({ metric: pricePattern, name: legend, color }),
|
||||
price({
|
||||
@@ -287,7 +287,7 @@ export function createZScoresFolder(
|
||||
},
|
||||
...sdPats.map(({ nameAddon, titleAddon, sd }) => ({
|
||||
name: nameAddon,
|
||||
title: `${title} ${titleAddon} Z-Score`,
|
||||
title: formatTitle(`${titleAddon ? `${titleAddon} ` : ""}Z-Score`),
|
||||
top: [
|
||||
price({ metric: pricePattern, name: legend, color }),
|
||||
...sdBandsUsd(colors, sd).map(
|
||||
@@ -343,3 +343,44 @@ export function createZScoresFolder(
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create price + ratio + z-scores charts - flat array
|
||||
* Unified helper for averages, distribution, and other price-based metrics
|
||||
* @param {PartialContext} ctx
|
||||
* @param {Object} args
|
||||
* @param {string} args.context - Context string for ratio/z-scores titles (e.g., "1 Week SMA", "STH")
|
||||
* @param {string} args.legend - Legend name for the price series
|
||||
* @param {AnyPricePattern} args.pricePattern - The price pattern
|
||||
* @param {AnyRatioPattern} args.ratio - The ratio pattern
|
||||
* @param {Color} args.color
|
||||
* @param {string} [args.ratioName] - Optional custom name for ratio chart (default: "ratio")
|
||||
* @param {string} [args.priceTitle] - Optional override for price chart title (default: context)
|
||||
* @param {string} [args.zScoresSuffix] - Optional suffix appended to context for z-scores (e.g., "MVRV" gives "2y Z-Score: STH MVRV")
|
||||
* @returns {PartialOptionsTree}
|
||||
*/
|
||||
export function createPriceRatioCharts(ctx, { context, legend, pricePattern, ratio, color, ratioName, priceTitle, zScoresSuffix }) {
|
||||
const titleFn = formatCohortTitle(context);
|
||||
const zScoresTitleFn = zScoresSuffix ? formatCohortTitle(`${context} ${zScoresSuffix}`) : titleFn;
|
||||
return [
|
||||
{
|
||||
name: "Price",
|
||||
title: priceTitle ?? context,
|
||||
top: [price({ metric: pricePattern, name: legend, color })],
|
||||
},
|
||||
createRatioChart(ctx, {
|
||||
title: titleFn,
|
||||
pricePattern,
|
||||
ratio,
|
||||
color,
|
||||
name: ratioName,
|
||||
}),
|
||||
createZScoresFolder(ctx, {
|
||||
formatTitle: zScoresTitleFn,
|
||||
legend,
|
||||
pricePattern,
|
||||
ratio,
|
||||
color,
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -160,6 +160,10 @@
|
||||
* - EpochPattern (epoch.*, amountRange.*, year.*, type.*)
|
||||
* @typedef {EpochPattern} PatternBasicWithoutMarketCap
|
||||
*
|
||||
* Patterns without relative section entirely (edge case output types):
|
||||
* - EmptyPattern (type.empty, type.p2ms, type.unknown)
|
||||
* @typedef {EmptyPattern} PatternWithoutRelative
|
||||
*
|
||||
* Union of basic patterns (for backwards compat)
|
||||
* @typedef {PatternBasicWithMarketCap | PatternBasicWithoutMarketCap} PatternBasic
|
||||
*
|
||||
@@ -224,6 +228,13 @@
|
||||
* @property {Color} color
|
||||
* @property {PatternBasicWithoutMarketCap} tree
|
||||
*
|
||||
* Cohort without relative section (edge case types: empty, p2ms, unknown)
|
||||
* @typedef {Object} CohortWithoutRelative
|
||||
* @property {string} name
|
||||
* @property {string} title
|
||||
* @property {Color} color
|
||||
* @property {PatternWithoutRelative} tree
|
||||
*
|
||||
* Union of basic cohort types
|
||||
* @typedef {CohortBasicWithMarketCap | CohortBasicWithoutMarketCap} CohortBasic
|
||||
*
|
||||
@@ -273,6 +284,11 @@
|
||||
* @property {string} title
|
||||
* @property {readonly CohortBasicWithoutMarketCap[]} list
|
||||
*
|
||||
* @typedef {Object} CohortGroupWithoutRelative
|
||||
* @property {string} name
|
||||
* @property {string} title
|
||||
* @property {readonly CohortWithoutRelative[]} list
|
||||
*
|
||||
* Union of basic cohort group types
|
||||
* @typedef {CohortGroupBasicWithMarketCap | CohortGroupBasicWithoutMarketCap} CohortGroupBasic
|
||||
*
|
||||
@@ -287,7 +303,7 @@
|
||||
* @property {Color} color
|
||||
* @property {AddressCohortPattern} tree
|
||||
*
|
||||
* @typedef {UtxoCohortObject | AddressCohortObject} CohortObject
|
||||
* @typedef {UtxoCohortObject | AddressCohortObject | CohortWithoutRelative} CohortObject
|
||||
*
|
||||
*
|
||||
* @typedef {Object} AddressCohortGroupObject
|
||||
|
||||
@@ -44,8 +44,10 @@ function walk(node, map, path) {
|
||||
kn.startsWith("satblocks") ||
|
||||
kn.startsWith("satdays") ||
|
||||
kn.endsWith("state") ||
|
||||
kn.endsWith("cents") ||
|
||||
kn.endsWith("index") ||
|
||||
kn.endsWith("indexes") ||
|
||||
kn.endsWith("raw") ||
|
||||
kn.endsWith("bytes") ||
|
||||
(kn.startsWith("_") && kn.endsWith("start"))
|
||||
)
|
||||
|
||||
+72
-51
@@ -14,7 +14,7 @@
|
||||
*
|
||||
* @import { WebSockets } from "./utils/ws.js"
|
||||
*
|
||||
* @import { Option, PartialChartOption, ChartOption, AnyPartialOption, ProcessedOptionAddons, OptionsTree, SimulationOption, AnySeriesBlueprint, SeriesType, AnyFetchedSeriesBlueprint, TableOption, ExplorerOption, UrlOption, PartialOptionsGroup, OptionsGroup, PartialOptionsTree, UtxoCohortObject, AddressCohortObject, CohortObject, CohortGroupObject, FetchedLineSeriesBlueprint, FetchedBaselineSeriesBlueprint, FetchedHistogramSeriesBlueprint, PartialContext, PatternAll, PatternFull, PatternWithAdjusted, PatternWithPercentiles, PatternBasic, PatternBasicWithMarketCap, PatternBasicWithoutMarketCap, CohortAll, CohortFull, CohortWithAdjusted, CohortWithPercentiles, CohortBasic, CohortBasicWithMarketCap, CohortBasicWithoutMarketCap, CohortAddress, CohortLongTerm, CohortAgeRange, CohortGroupFull, CohortGroupWithAdjusted, CohortGroupWithPercentiles, CohortGroupLongTerm, CohortGroupAgeRange, CohortGroupBasic, CohortGroupBasicWithMarketCap, CohortGroupBasicWithoutMarketCap, CohortGroupAddress, UtxoCohortGroupObject, AddressCohortGroupObject, FetchedDotsSeriesBlueprint, FetchedCandlestickSeriesBlueprint, FetchedPriceSeriesBlueprint, AnyPricePattern } from "./options/partial.js"
|
||||
* @import { Option, PartialChartOption, ChartOption, AnyPartialOption, ProcessedOptionAddons, OptionsTree, SimulationOption, AnySeriesBlueprint, SeriesType, AnyFetchedSeriesBlueprint, TableOption, ExplorerOption, UrlOption, PartialOptionsGroup, OptionsGroup, PartialOptionsTree, UtxoCohortObject, AddressCohortObject, CohortObject, CohortGroupObject, FetchedLineSeriesBlueprint, FetchedBaselineSeriesBlueprint, FetchedHistogramSeriesBlueprint, PartialContext, PatternAll, PatternFull, PatternWithAdjusted, PatternWithPercentiles, PatternBasic, PatternBasicWithMarketCap, PatternBasicWithoutMarketCap, PatternWithoutRelative, CohortAll, CohortFull, CohortWithAdjusted, CohortWithPercentiles, CohortBasic, CohortBasicWithMarketCap, CohortBasicWithoutMarketCap, CohortWithoutRelative, CohortAddress, CohortLongTerm, CohortAgeRange, CohortGroupFull, CohortGroupWithAdjusted, CohortGroupWithPercentiles, CohortGroupLongTerm, CohortGroupAgeRange, CohortGroupBasic, CohortGroupBasicWithMarketCap, CohortGroupBasicWithoutMarketCap, CohortGroupWithoutRelative, CohortGroupAddress, UtxoCohortGroupObject, AddressCohortGroupObject, FetchedDotsSeriesBlueprint, FetchedCandlestickSeriesBlueprint, FetchedPriceSeriesBlueprint, AnyPricePattern } from "./options/partial.js"
|
||||
*
|
||||
*
|
||||
* @import { UnitObject as Unit } from "./utils/units.js"
|
||||
@@ -31,74 +31,91 @@
|
||||
* @typedef {ISeriesMarkersPluginApi<Time>} SeriesMarkersPlugin
|
||||
* @typedef {SeriesMarker<Time>} TimeSeriesMarker
|
||||
*
|
||||
* Brk type aliases
|
||||
* Brk tree types (stable across regenerations)
|
||||
* @typedef {Brk.MetricsTree_Distribution_UtxoCohorts} UtxoCohortTree
|
||||
* @typedef {Brk.MetricsTree_Distribution_AddressCohorts} AddressCohortTree
|
||||
* @typedef {Brk.MetricsTree_Distribution_UtxoCohorts_All} AllUtxoPattern
|
||||
* @typedef {Brk.MetricsTree_Distribution_UtxoCohorts_Term_Short} ShortTermPattern
|
||||
* @typedef {Brk.MetricsTree_Distribution_UtxoCohorts_Term_Long} LongTermPattern
|
||||
* @typedef {Brk._10yPattern} MaxAgePattern
|
||||
* @typedef {Brk._10yTo12yPattern} AgeRangePattern
|
||||
* @typedef {Brk._0satsPattern2} UtxoAmountPattern
|
||||
* @typedef {Brk._0satsPattern} AddressAmountPattern
|
||||
* @typedef {Brk._100btcPattern} BasicUtxoPattern
|
||||
* @typedef {Brk._0satsPattern2} EpochPattern
|
||||
* @typedef {Brk.Ratio1ySdPattern} Ratio1ySdPattern
|
||||
* @typedef {Brk.Dollars} Dollars
|
||||
* @typedef {Brk.Price111dSmaPattern} EmaRatioPattern
|
||||
* @typedef {Brk.CoinbasePattern} CoinbasePattern
|
||||
* @typedef {Brk.ActivePriceRatioPattern} ActivePriceRatioPattern
|
||||
* @typedef {Brk.UnclaimedRewardsPattern} ValuePattern
|
||||
* @typedef {Brk.AnyMetricPattern} AnyMetricPattern
|
||||
* @typedef {Brk.ActivePricePattern} ActivePricePattern
|
||||
* @typedef {Brk.AnyMetricEndpointBuilder} AnyMetricEndpoint
|
||||
* @typedef {Brk.AnyMetricData} AnyMetricData
|
||||
* @typedef {Brk.AddrCountPattern} AddrCountPattern
|
||||
* @typedef {Brk.MetricsTree_Distribution_UtxoCohorts_All_Relative} AllRelativePattern
|
||||
* @typedef {Brk.MetricsTree_Supply_Circulating} SupplyPattern
|
||||
* @typedef {Brk.MetricsTree_Blocks_Size} BlockSizePattern
|
||||
* @typedef {keyof Brk.MetricsTree_Distribution_UtxoCohorts_Type} SpendableType
|
||||
* @typedef {keyof Brk.MetricsTree_Distribution_AnyAddressIndexes} AddressableType
|
||||
* @typedef {FullnessPattern<any>} IntervalPattern
|
||||
* @typedef {Brk.MetricsTree_Supply_Circulating} SupplyPattern
|
||||
* @typedef {Brk.RelativePattern} GlobalRelativePattern
|
||||
* @typedef {Brk.RelativePattern2} OwnRelativePattern
|
||||
* @typedef {Brk.RelativePattern5} FullRelativePattern
|
||||
* @typedef {Brk.MetricsTree_Distribution_UtxoCohorts_All_Relative} AllRelativePattern
|
||||
* @typedef {Brk.UnrealizedPattern} UnrealizedPattern
|
||||
*
|
||||
* Brk pattern types (using new pattern names)
|
||||
* @typedef {Brk.ActivityCostOutputsRealizedRelativeSupplyUnrealizedPattern5} MaxAgePattern
|
||||
* @typedef {Brk.ActivityCostOutputsRealizedRelativeSupplyUnrealizedPattern} AgeRangePattern
|
||||
* @typedef {Brk.ActivityCostOutputsRealizedRelativeSupplyUnrealizedPattern3} UtxoAmountPattern
|
||||
* @typedef {Brk.ActivityAddrCostOutputsRealizedRelativeSupplyUnrealizedPattern} AddressAmountPattern
|
||||
* @typedef {Brk.ActivityCostOutputsRealizedRelativeSupplyUnrealizedPattern4} BasicUtxoPattern
|
||||
* @typedef {Brk.ActivityCostOutputsRealizedRelativeSupplyUnrealizedPattern3} EpochPattern
|
||||
* @typedef {Brk.ActivityCostOutputsRealizedSupplyUnrealizedPattern} EmptyPattern
|
||||
* @typedef {Brk._0sdM0M1M1sdM2M2sdM3sdP0P1P1sdP2P2sdP3sdSdSmaZscorePattern} Ratio1ySdPattern
|
||||
* @typedef {Brk.Dollars} Dollars
|
||||
* CoinbasePattern: patterns with bitcoin/sats/dollars each having fullness + sum + cumulative
|
||||
* @typedef {Brk.BitcoinDollarsSatsPattern2} CoinbasePattern
|
||||
* ActivePriceRatioPattern: ratio pattern with price (extended)
|
||||
* @typedef {Brk.PriceRatioPattern} ActivePriceRatioPattern
|
||||
* AnyRatioPattern: full ratio patterns (with or without price) - has ratio, percentiles, z-scores
|
||||
* @typedef {Brk.RatioPattern | Brk.PriceRatioPattern} AnyRatioPattern
|
||||
* ValuePattern: patterns with {bitcoin.sum, bitcoin.cumulative, sats.sum, sats.cumulative, dollars.sum, dollars.cumulative}
|
||||
* @typedef {Brk.BitcoinDollarsSatsPattern6 | Brk.BitcoinDollarsSatsPattern3 | Brk.BitcoinDollarsSatsPattern2} ValuePattern
|
||||
* @typedef {Brk.AnyMetricPattern} AnyMetricPattern
|
||||
* @typedef {Brk.DollarsSatsPattern} ActivePricePattern
|
||||
* @typedef {Brk.AnyMetricEndpointBuilder} AnyMetricEndpoint
|
||||
* @typedef {Brk.AnyMetricData} AnyMetricData
|
||||
* @typedef {Brk.AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern} AddrCountPattern
|
||||
* Relative patterns by capability:
|
||||
* - BasicRelativePattern: minimal relative (investedCapitalIn*Pct, supplyIn*RelToOwnSupply only)
|
||||
* - GlobalRelativePattern: has RelToMarketCap metrics (netUnrealizedPnlRelToMarketCap, etc)
|
||||
* - OwnRelativePattern: has RelToOwnMarketCap metrics (netUnrealizedPnlRelToOwnMarketCap, etc)
|
||||
* - FullRelativePattern: has BOTH RelToMarketCap AND RelToOwnMarketCap
|
||||
* @typedef {Brk.InvestedSupplyPattern} BasicRelativePattern
|
||||
* @typedef {Brk.InvestedNegNetNuplSupplyUnrealizedPattern} GlobalRelativePattern
|
||||
* @typedef {Brk.InvestedNegNetSupplyUnrealizedPattern} OwnRelativePattern
|
||||
* @typedef {Brk.InvestedNegNetNuplSupplyUnrealizedPattern2} FullRelativePattern
|
||||
* @typedef {Brk.AthGreedInvestedInvestorNegNetPainSupplyTotalUnrealizedPattern} UnrealizedPattern
|
||||
*
|
||||
* Realized patterns
|
||||
* @typedef {Brk.AthCapCapitulationInvestorLossMvrvNegNetProfitRealizedSellSoprTotalValuePattern} RealizedPattern
|
||||
* @typedef {Brk.AthCapCapitulationInvestorLossMvrvNegNetProfitRealizedSellSoprTotalValuePattern2} RealizedPattern2
|
||||
* @typedef {Brk.AdjustedAthCapCapitulationInvestorLossMvrvNegNetProfitRealizedSellSoprTotalValuePattern} RealizedPattern3
|
||||
* @typedef {Brk.AdjustedAthCapCapitulationInvestorLossMvrvNegNetProfitRealizedSellSoprTotalValuePattern2} RealizedPattern4
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @typedef {Brk.BlockCountPattern<T>} BlockCountPattern
|
||||
*/
|
||||
/**
|
||||
* @template T
|
||||
* @typedef {Brk.FullnessPattern<T>} FullnessPattern
|
||||
*/
|
||||
/**
|
||||
* @template T
|
||||
* @typedef {Brk.FeeRatePattern<T>} FeeRatePattern
|
||||
*/
|
||||
/**
|
||||
* @template T
|
||||
* @typedef {Brk.MetricEndpointBuilder<T>} MetricEndpoint
|
||||
*/
|
||||
/**
|
||||
* Stats pattern: average, min, max, percentiles (NO base)
|
||||
* @template T
|
||||
* @typedef {Brk.DollarsPattern<T>} SizePattern
|
||||
* @typedef {Brk.AverageMaxMedianMinPct10Pct25Pct75Pct90TxindexPattern<T>} StatsPattern
|
||||
*/
|
||||
/**
|
||||
* Base stats pattern: base, average, min, max, percentiles (NO sum/cumulative)
|
||||
* @template T
|
||||
* @typedef {Brk.DollarsPattern<T>} DollarsPattern
|
||||
* @typedef {Brk.AverageBaseMaxMedianMinPct10Pct25Pct75Pct90Pattern<T>} BaseStatsPattern
|
||||
*/
|
||||
/**
|
||||
* Full stats pattern: base, average, sum, cumulative, min, max, percentiles
|
||||
* @template T
|
||||
* @typedef {Brk.CountPattern2<T>} CountStatsPattern
|
||||
* @typedef {Brk.AverageBaseCumulativeMaxMedianMinPct10Pct25Pct75Pct90SumPattern2<T>} FullStatsPattern
|
||||
*/
|
||||
/**
|
||||
* @typedef {Brk.MetricsTree_Blocks_Size} BlockSizePattern
|
||||
* Sum stats pattern: average, sum, cumulative, percentiles (NO base)
|
||||
* @template T
|
||||
* @typedef {Brk.AverageCumulativeMaxMedianMinPct10Pct25Pct75Pct90SumPattern2<T>} SumStatsPattern
|
||||
*/
|
||||
/**
|
||||
* Stats pattern union - accepts both CountStatsPattern and BlockSizePattern
|
||||
* @typedef {CountStatsPattern<any> | BlockSizePattern} AnyStatsPattern
|
||||
* Count pattern: sum and cumulative only
|
||||
* @template T
|
||||
* @typedef {Brk.CumulativeSumPattern<T>} CountPattern
|
||||
*/
|
||||
/**
|
||||
* Any stats pattern union - patterns with sum/cumulative + percentiles
|
||||
* @typedef {SumStatsPattern<any> | FullStatsPattern<any> | BlockSizePattern} AnyStatsPattern
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -112,12 +129,14 @@
|
||||
* @typedef {Brk.MetricsTree_Market} Market
|
||||
* @typedef {Brk.MetricsTree_Market_MovingAverage} MarketMovingAverage
|
||||
* @typedef {Brk.MetricsTree_Market_Dca} MarketDca
|
||||
* @typedef {Brk.PeriodCagrPattern} PeriodCagrPattern
|
||||
* @typedef {Brk.BitcoinPattern | Brk.DollarsPattern<any>} FullnessPatternWithSumCumulative
|
||||
* @typedef {Brk._10y2y3y4y5y6y8yPattern} PeriodCagrPattern
|
||||
* Full stats pattern union (both generic and non-generic variants)
|
||||
* @typedef {Brk.AverageBaseCumulativeMaxMedianMinPct10Pct25Pct75Pct90SumPattern | FullStatsPattern<any>} AnyFullStatsPattern
|
||||
*
|
||||
* DCA period keys
|
||||
* DCA period keys - derived from pattern types
|
||||
* @typedef {keyof Brk._10y2y3y4y5y6y8yPattern} LongPeriodKey
|
||||
* @typedef {"_1w" | "_1m" | "_3m" | "_6m" | "_1y"} ShortPeriodKey
|
||||
* @typedef {keyof PeriodCagrPattern} LongPeriodKey
|
||||
* @typedef {ShortPeriodKey | LongPeriodKey} AllPeriodKey
|
||||
*
|
||||
* Pattern unions by cohort type
|
||||
* @typedef {AllUtxoPattern | AgeRangePattern | UtxoAmountPattern} UtxoCohortPattern
|
||||
@@ -129,12 +148,14 @@
|
||||
* @typedef {OwnRelativePattern | FullRelativePattern} RelativeWithOwnMarketCap
|
||||
* @typedef {OwnRelativePattern | FullRelativePattern | AllRelativePattern} RelativeWithOwnPnl
|
||||
* @typedef {GlobalRelativePattern | FullRelativePattern} RelativeWithNupl
|
||||
* @typedef {BasicRelativePattern | GlobalRelativePattern | OwnRelativePattern | FullRelativePattern | AllRelativePattern} RelativeWithInvestedCapitalPct
|
||||
*
|
||||
* Realized pattern capability types (RealizedPattern2 and RealizedPattern3 have extra metrics)
|
||||
* @typedef {Brk.RealizedPattern2 | Brk.RealizedPattern3} RealizedWithExtras
|
||||
* Realized pattern capability types
|
||||
* RealizedWithExtras: patterns with realizedCapRelToOwnMarketCap + realizedProfitToLossRatio
|
||||
* @typedef {RealizedPattern2 | RealizedPattern3} RealizedWithExtras
|
||||
*
|
||||
* Any realized pattern (all have sellSideRiskRatio, valueCreated, valueDestroyed, etc.)
|
||||
* @typedef {Brk.RealizedPattern | Brk.RealizedPattern2 | Brk.RealizedPattern3 | Brk.RealizedPattern4} AnyRealizedPattern
|
||||
* @typedef {RealizedPattern | RealizedPattern2 | RealizedPattern3 | RealizedPattern4} AnyRealizedPattern
|
||||
*
|
||||
* Capability-based pattern groupings (patterns that have specific properties)
|
||||
* @typedef {AllUtxoPattern | AgeRangePattern | UtxoAmountPattern} PatternWithRealizedPrice
|
||||
|
||||
@@ -153,10 +153,11 @@ export function createLabeledInput({
|
||||
if (title) {
|
||||
label.title = title;
|
||||
}
|
||||
label.htmlFor = inputId;
|
||||
|
||||
if (onClick) {
|
||||
label.addEventListener("click", onClick);
|
||||
input.addEventListener("click", onClick);
|
||||
} else {
|
||||
label.htmlFor = inputId;
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user