global: MASSIVE snapshot

This commit is contained in:
nym21
2026-01-02 19:08:20 +01:00
parent ac6175688d
commit 3e9b1cc2b2
462 changed files with 34975 additions and 20072 deletions
+7
View File
@@ -0,0 +1,7 @@
# Types
To check types run:
```sh
npx --package typescript tsc --noEmit --pretty false | grep -v "modules/"
```
@@ -1,4 +1,4 @@
/** @import { IChartApi, ISeriesApi as _ISeriesApi, SeriesDefinition, SingleValueData as _SingleValueData, CandlestickData as _CandlestickData, BaselineData as _BaselineData, HistogramData as _HistogramData, SeriesType, IPaneApi, LineSeriesPartialOptions as _LineSeriesPartialOptions, HistogramSeriesPartialOptions as _HistogramSeriesPartialOptions, BaselineSeriesPartialOptions as _BaselineSeriesPartialOptions, CandlestickSeriesPartialOptions as _CandlestickSeriesPartialOptions, WhitespaceData, DeepPartial, ChartOptions, Time, LineData as _LineData, createChart as CreateChart } from '../../modules/lightweight-charts/5.0.9/dist/typings' */
/** @import { IChartApi, ISeriesApi as _ISeriesApi, SeriesDefinition, SingleValueData as _SingleValueData, CandlestickData as _CandlestickData, BaselineData as _BaselineData, HistogramData as _HistogramData, SeriesType, IPaneApi, LineSeriesPartialOptions as _LineSeriesPartialOptions, HistogramSeriesPartialOptions as _HistogramSeriesPartialOptions, BaselineSeriesPartialOptions as _BaselineSeriesPartialOptions, CandlestickSeriesPartialOptions as _CandlestickSeriesPartialOptions, WhitespaceData, DeepPartial, ChartOptions, Time, LineData as _LineData, createChart as CreateChart } from '../modules/lightweight-charts/5.0.9/dist/typings' */
import {
createChart as _createChart,
@@ -7,7 +7,7 @@ import {
LineSeries,
BaselineSeries,
// } from "../modules/lightweight-charts/5.0.9/dist/lightweight-charts.standalone.development.mjs";
} from "../../modules/lightweight-charts/5.0.9/dist/lightweight-charts.standalone.production.mjs";
} from "../modules/lightweight-charts/5.0.9/dist/lightweight-charts.standalone.production.mjs";
const createChart = /** @type {CreateChart} */ (_createChart);
@@ -15,12 +15,12 @@ import {
createHorizontalChoiceField,
createLabeledInput,
createSpanName,
} from "../dom";
} from "../utils/dom";
import { createOklchToRGBA } from "./oklch";
import { throttle } from "../timing";
import { serdeBool } from "../serde";
import { stringToId } from "../format";
import { style } from "../elements";
import { throttle } from "../utils/timing";
import { serdeBool } from "../utils/serde";
import { stringToId } from "../utils/format";
import { style } from "../utils/elements";
/**
* @typedef {Object} Valued
@@ -68,6 +68,7 @@ const lineWidth = /** @type {any} */ (1.5);
* @param {Signals} args.signals
* @param {Colors} args.colors
* @param {Resources} args.resources
* @param {BrkClient} args.brk
* @param {Accessor<IndexName>} args.index
* @param {((unknownTimeScaleCallback: VoidFunction) => void)} [args.timeScaleSetCallback]
* @param {true} [args.fitContent]
@@ -80,6 +81,7 @@ function createChartElement({
id: chartId,
index,
resources,
brk,
timeScaleSetCallback,
fitContent,
config,
@@ -204,7 +206,9 @@ function createChartElement({
});
});
const activeResources = /** @type {Set<MetricResource>} */ (new Set());
const activeResources = /** @type {Set<MetricResource<unknown>>} */ (
new Set()
);
ichart.subscribeCrosshairMove(
throttle(() => {
activeResources.forEach((v) => {
@@ -312,7 +316,7 @@ function createChartElement({
* @param {number} args.order
* @param {Color[]} args.colors
* @param {SeriesType} args.seriesType
* @param {Metric} [args.metric]
* @param {MetricAccessor<unknown>} [args.metric]
* @param {SetDataCallback} [args.setDataCallback]
* @param {Accessor<WhitespaceData<number>[]>} [args.data]
* @param {number} args.paneIndex
@@ -353,7 +357,7 @@ function createChartElement({
iseries.setSeriesOrder(order);
/** @type {MetricResource | undefined} */
/** @type {MetricResource<unknown> | undefined} */
let _valuesResource;
/** @type {Series} */
@@ -375,27 +379,35 @@ function createChartElement({
if (metric) {
signals.createEffect(index, (index) => {
const timeResource = resources.metrics.getOrCreate(
index === "height" ? "timestamp_fixed" : "timestamp",
index,
);
timeResource.fetch();
// Get timestamp metric from tree based on index type
// timestampFixed has height only, timestamp has date-based indexes
const timeMetric =
index === "height"
? brk.tree.computed.blocks.time.timestampFixed
: brk.tree.computed.blocks.time.timestamp;
const timeNode = timeMetric.by[index];
const valuesNode = metric.by[index];
if (!timeNode || !valuesNode)
throw new Error(`Missing node for index: ${index}`);
const valuesResource = resources.metrics.getOrCreate(metric, index);
const timeResource = resources.useMetricNode(timeNode);
const valuesResource = resources.useMetricNode(valuesNode);
_valuesResource = valuesResource;
series.url.set(() => valuesResource.url);
series.url.set(() => `${brk.baseUrl}${valuesResource.path}`);
signals.createEffect(active, (active) => {
if (active) {
timeResource.fetch();
valuesResource.fetch();
activeResources.add(valuesResource);
const fetchedKey = resources.metrics.genKey();
const timeRange = timeResource.range();
const valuesRange = valuesResource.range();
signals.createEffect(
() => ({
_indexes: timeResource.fetched().get(fetchedKey)?.data(),
values: valuesResource.fetched().get(fetchedKey)?.data(),
_indexes: timeRange.response()?.data,
values: valuesRange.response()?.data,
}),
({ _indexes, values }) => {
if (!_indexes?.length || !values?.length) return;
@@ -434,6 +446,8 @@ function createChartElement({
// if (sameTime) {
// console.log(data[offsetedI]);
// }
if (!Array.isArray(v) || v.length !== 4)
throw new Error(`Expected OHLC tuple, got: ${v}`);
let [open, high, low, close] = v;
data[offsetedI] = {
time,
@@ -545,7 +559,7 @@ function createChartElement({
* @param {string} args.name
* @param {Unit} args.unit
* @param {number} args.order
* @param {Metric} [args.metric]
* @param {MetricAccessor<unknown>} [args.metric]
* @param {Accessor<CandlestickData[]>} [args.data]
* @param {number} [args.paneIndex]
* @param {boolean} [args.defaultActive]
@@ -605,7 +619,7 @@ function createChartElement({
* @param {Unit} args.unit
* @param {number} args.order
* @param {Color} args.color
* @param {Metric} [args.metric]
* @param {MetricAccessor<unknown>} [args.metric]
* @param {Accessor<HistogramData[]>} [args.data]
* @param {number} [args.paneIndex]
* @param {boolean} [args.defaultActive]
@@ -657,7 +671,7 @@ function createChartElement({
* @param {Unit} args.unit
* @param {number} args.order
* @param {Accessor<LineData[]>} [args.data]
* @param {Metric} [args.metric]
* @param {MetricAccessor<unknown>} [args.metric]
* @param {Color} [args.color]
* @param {SetDataCallback} [args.setDataCallback]
* @param {number} [args.paneIndex]
@@ -716,7 +730,7 @@ function createChartElement({
* @param {Unit} args.unit
* @param {number} args.order
* @param {Accessor<BaselineData[]>} [args.data]
* @param {Metric} [args.metric]
* @param {MetricAccessor<unknown>} [args.metric]
* @param {SetDataCallback} [args.setDataCallback]
* @param {number} [args.paneIndex]
* @param {boolean} [args.defaultActive]
+42 -11
View File
@@ -1,24 +1,21 @@
/**
* @import * as _ from "./modules/leeoniya-ufuzzy/1.0.19/dist/uFuzzy.d.ts"
*
* @import { Signal, Signals, Accessor } from "./modules/brk-signals/index";
* @import { Signal, Signals, Accessor } from "./signals";
*
* @import { BRK } from "./modules/brk-client/index.js"
* @import { Metric, MetricToIndexes } from "./modules/brk-client/metrics"
* @import { IndexName } from "./modules/brk-client/generated/metrics"
* @import { PoolId, PoolIdToPoolName } from "./modules/brk-client/generated/pools"
* @import { BrkClient, CatalogTree_Computed_Distribution_UtxoCohorts as UtxoCohortTree, CatalogTree_Computed_Distribution_AddressCohorts as AddressCohortTree, CatalogTree_Computed_Distribution_UtxoCohorts_All as AllUtxoPattern, UpTo1dPattern as MaxAgePattern, _10yTo12yPattern as MinAgePattern, _0satsPattern2 as UtxoAmountPattern, _0satsPattern as AddressAmountPattern, Ratio1ySdPattern, Dollars, Price111dSmaPattern as EmaRatioPattern, Index, BlockCountPattern, BitcoinPattern, BlockSizePattern, BlockIntervalPattern, CoinbasePattern, Constant0Pattern, ActivePriceRatioPattern, _0satsPattern, PricePaidPattern2, UnclaimedRewardsPattern as ValuePattern, SentPattern as RewardPattern, Metric } from "./modules/brk-client/index.js"
*
* @import { Resources, MetricResource } from './modules/brk-resources/index.js'
* @import { Resources, MetricResource } from './resources'
*
* @import { Valued, SingleValueData, CandlestickData, Series, ISeries, HistogramData, LineData, BaselineData, LineSeriesPartialOptions, BaselineSeriesPartialOptions, HistogramSeriesPartialOptions, CandlestickSeriesPartialOptions, CreateChartElement, Chart } from "./core/chart/index"
* @import { Valued, SingleValueData, CandlestickData, Series, ISeries, HistogramData, LineData, BaselineData, LineSeriesPartialOptions, BaselineSeriesPartialOptions, HistogramSeriesPartialOptions, CandlestickSeriesPartialOptions, CreateChartElement, Chart } from "./chart/index"
*
* @import { Color, ColorName, Colors } from "./core/colors"
* @import { Color, ColorName, Colors } from "./utils/colors"
*
* @import { WebSockets } from "./core/ws"
* @import { WebSockets } from "./utils/ws"
*
* @import { Option, PartialChartOption, ChartOption, AnyPartialOption, ProcessedOptionAddons, OptionsTree, SimulationOption, AnySeriesBlueprint, SeriesType, AnyFetchedSeriesBlueprint, TableOption, ExplorerOption, UrlOption, PartialOptionsGroup, OptionsGroup, PartialOptionsTree } from "./core/options/partial"
* @import { Option, PartialChartOption, ChartOption, AnyPartialOption, ProcessedOptionAddons, OptionsTree, SimulationOption, AnySeriesBlueprint, SeriesType, AnyFetchedSeriesBlueprint, TableOption, ExplorerOption, UrlOption, PartialOptionsGroup, OptionsGroup, PartialOptionsTree, UtxoCohortObject, AddressCohortObject, CohortObject, UtxoCohortGroupObject, AddressCohortGroupObject, CohortGroupObject, MetricAccessor, FetchedLineSeriesBlueprint, PartialContext, AgeCohortObject, AmountCohortObject, AgeCohortGroupObject, AmountCohortGroupObject } from "./options/partial/index.js"
*
* @import { Unit } from "./core/serde"
* @import { Unit } from "./utils/serde"
*
* @import { ChartableIndexName } from "./panes/chart/index.js";
*/
@@ -28,6 +25,40 @@
/**
* @typedef {typeof import("./lazy")["default"]} Modules
* @typedef {[number, number, number, number]} OHLCTuple
*
* @typedef {InstanceType<typeof BrkClient>["INDEXES"]} Indexes
* @typedef {Indexes[number]} IndexName
* @typedef {InstanceType<typeof BrkClient>["POOL_ID_TO_POOL_NAME"]} PoolIdToPoolName
* @typedef {keyof PoolIdToPoolName} PoolId
*
* Pattern unions by cohort type
* @typedef {AllUtxoPattern | MaxAgePattern | MinAgePattern | UtxoAmountPattern} UtxoCohortPattern
* @typedef {AddressAmountPattern} AddressCohortPattern
* @typedef {UtxoCohortPattern | AddressCohortPattern} CohortPattern
*
* Capability-based pattern groupings (patterns that have specific properties)
* @typedef {AllUtxoPattern | MinAgePattern | UtxoAmountPattern} PatternWithRealizedPrice
* @typedef {AllUtxoPattern} PatternWithFullRealized
* @typedef {AllUtxoPattern | MinAgePattern | UtxoAmountPattern} PatternWithNupl
* @typedef {AllUtxoPattern | MinAgePattern | UtxoAmountPattern} PatternWithPricePaidStats
* @typedef {AllUtxoPattern | MinAgePattern | UtxoAmountPattern} PatternWithActivity
* @typedef {AllUtxoPattern | MaxAgePattern | MinAgePattern} PatternWithPricePercentiles
*
* Cohort objects with specific pattern capabilities
* @typedef {{ name: string, title: string, color: Color, tree: PatternWithRealizedPrice }} CohortWithRealizedPrice
* @typedef {{ name: string, title: string, color: Color, tree: PatternWithFullRealized }} CohortWithFullRealized
* @typedef {{ name: string, title: string, color: Color, tree: PatternWithNupl }} CohortWithNupl
* @typedef {{ name: string, title: string, color: Color, tree: PatternWithPricePaidStats }} CohortWithPricePaidStats
* @typedef {{ name: string, title: string, color: Color, tree: PatternWithActivity }} CohortWithActivity
* @typedef {{ name: string, title: string, color: Color, tree: PatternWithPricePercentiles }} CohortWithPricePercentiles
*
* Tree branch types
* @typedef {InstanceType<typeof BrkClient>["tree"]["computed"]["market"]} Market
* @typedef {Market["movingAverage"]} MarketMovingAverage
* @typedef {Market["dca"]} MarketDca
*
* Generic tree node type for walking
* @typedef {MetricAccessor<unknown> | Record<string, unknown>} TreeNode
*/
// DO NOT CHANGE, Exact format is expected in `brk_bundler`
+5 -5
View File
@@ -1,6 +1,6 @@
const imports = {
async signals() {
return import("./modules/brk-signals/index.js").then((d) => d.default);
return import("./signals.js").then((d) => d.default);
},
async leanQr() {
return import("./modules/lean-qr/2.6.0/index.mjs").then((d) => d);
@@ -13,17 +13,17 @@ const imports = {
async brkClient() {
return import("./modules/brk-client/index.js").then((d) => d);
},
async brkResources() {
return import("./modules/brk-resources/index.js").then((d) => d);
async resources() {
return import("./resources.js").then((d) => d);
},
async chart() {
return window.document.fonts.ready.then(() =>
import("./core/chart/index.js").then((d) => d.default),
import("./chart/index.js").then((d) => d.default),
);
},
async options() {
return import("./core/options/full.js").then((d) => d);
return import("./options/full.js").then((d) => d);
},
};
+11 -11
View File
@@ -1,11 +1,11 @@
import { createColors } from "./core/colors";
import { createWebSockets } from "./core/ws";
import * as formatters from "./core/format";
import { createColors } from "./utils/colors";
import { createWebSockets } from "./utils/ws";
import * as formatters from "./utils/format";
import modules from "./lazy";
import { onFirstIntersection, getElementById, isHidden } from "./core/dom";
import { next } from "./core/timing";
import { replaceHistory } from "./core/url";
import { removeStored, writeToStorage } from "./core/storage";
import { onFirstIntersection, getElementById, isHidden } from "./utils/dom";
import { next } from "./utils/timing";
import { replaceHistory } from "./utils/url";
import { removeStored, writeToStorage } from "./utils/storage";
import {
asideElement,
asideLabelElement,
@@ -22,7 +22,7 @@ import {
simulationElement,
style,
tableElement,
} from "./core/elements";
} from "./utils/elements";
function initFrameSelectors() {
const children = Array.from(frameSelectorsElement.children);
@@ -109,15 +109,15 @@ initFrameSelectors();
Promise.all([
modules.signals(),
modules.brkClient(),
modules.brkResources(),
modules.resources(),
modules.options(),
]).then(([signals, { BrkClient, VERSION }, { createResources }, { initOptions }]) =>
]).then(([signals, { BrkClient }, { createResources }, { initOptions }]) =>
signals.createRoot(() => {
const brk = new BrkClient("/");
const resources = createResources(signals);
const owner = signals.getOwner();
console.log(`VERSION = ${VERSION}`);
console.log(`VERSION = ${brk.VERSION}`);
function initDark() {
const preferredColorSchemeMatchMedia = window.matchMedia(
@@ -1,22 +1,25 @@
import { createPartialOptions } from "./partial";
import { createPartialOptions } from "./partial/index.js";
import {
createButtonElement,
createAnchorElement,
insertElementAtIndex,
} from "../dom";
import { serdeUnit } from "../serde";
import { pushHistory, resetParams } from "../url";
import { readStored, writeToStorage } from "../storage";
import { stringToId } from "../format";
} from "../utils/dom";
import { serdeUnit } from "../utils/serde";
import { pushHistory, resetParams } from "../utils/url";
import { readStored, writeToStorage } from "../utils/storage";
import { stringToId } from "../utils/format";
import { collect, markUsed, logUnused } from "./unused.js";
/**
* @param {Object} args
* @param {Colors} args.colors
* @param {Signals} args.signals
* @param {BRK} args.brk
* @param {BrkClient} args.brk
* @param {Signal<string | null>} args.qrcode
*/
export function initOptions({ colors, signals, brk, qrcode }) {
collect(brk.tree);
const LS_SELECTED_KEY = `selected_path`;
const urlPath_ = window.document.location.pathname
@@ -46,11 +49,9 @@ export function initOptions({ colors, signals, brk, qrcode }) {
*/
function arrayToRecord(arr = []) {
return (arr || []).reduce((record, blueprint) => {
if (!brk.hasMetric(blueprint.metric)) {
// if (localhost && !brk.hasMetric(blueprint.metric)) {
throw Error(`${blueprint.metric} not recognized`);
}
const unit = blueprint.unit ?? serdeUnit.deserialize(blueprint.metric);
markUsed(blueprint.metric);
// Use any index's path - unit is the same regardless of index (e.g., supply is "sats" for both height and dateindex)
const unit = blueprint.unit ?? serdeUnit.deserialize(blueprint.metric.name);
record[unit] ??= [];
record[unit].push(blueprint);
return record;
@@ -347,6 +348,7 @@ export function initOptions({ colors, signals, brk, qrcode }) {
);
}
recursiveProcessPartialTree(partialOptions, parent);
logUnused();
if (!selected()) {
const option =
@@ -0,0 +1,485 @@
/** Chain section builder - typed tree-based patterns */
/**
* Create Chain section
* @param {PartialContext} ctx
* @returns {PartialOptionsGroup}
*/
export function createChainSection(ctx) {
const { colors, brk, s, createPriceLine } = ctx;
const { blocks, transactions, pools, inputs, outputs, market, scripts, supply } = brk.tree.computed;
const { indexed } = brk.tree;
/**
* Create sum/cumulative series from a BlockCountPattern
* @template T
* @param {BlockCountPattern<T>} pattern
* @param {string} name
* @param {Color} [sumColor]
* @param {Color} [cumulativeColor]
* @param {Unit} unit
*/
const fromBlockCount = (pattern, name, unit, sumColor, cumulativeColor) => [
s({ metric: pattern.base, name: `${name} sum`, color: sumColor, unit }),
s({ metric: pattern.cumulative, name: `${name} cumulative`, color: cumulativeColor ?? colors.blue, unit, defaultActive: false }),
];
/**
* Create series from BlockSizePattern (has average, min, max, percentiles)
* @template T
* @param {BlockSizePattern<T>} pattern
* @param {string} name
* @param {Unit} unit
*/
const fromBlockSize = (pattern, name, unit) => [
s({ metric: pattern.average, name: `${name} avg`, unit }),
s({ metric: pattern.sum, name: `${name} sum`, color: colors.blue, unit, defaultActive: false }),
s({ metric: pattern.cumulative, name: `${name} cumulative`, color: colors.indigo, unit, defaultActive: false }),
s({ metric: pattern.min, name: `${name} min`, color: colors.red, unit, defaultActive: false }),
s({ metric: pattern.max, name: `${name} max`, color: colors.green, unit, defaultActive: false }),
s({ metric: pattern.pct10, name: `${name} pct10`, color: colors.rose, unit, defaultActive: false }),
s({ metric: pattern.pct25, name: `${name} pct25`, color: colors.pink, unit, defaultActive: false }),
s({ metric: pattern.median, name: `${name} median`, color: colors.purple, unit, defaultActive: false }),
s({ metric: pattern.pct75, name: `${name} pct75`, color: colors.violet, unit, defaultActive: false }),
s({ metric: pattern.pct90, name: `${name} pct90`, color: colors.fuchsia, unit, defaultActive: false }),
];
/**
* Create series from BlockIntervalPattern (has average, min, max, percentiles)
* @template T
* @param {BlockIntervalPattern<T>} pattern
* @param {string} name
* @param {Unit} unit
*/
const fromBlockInterval = (pattern, name, unit) => [
s({ metric: pattern.average, name: `${name} avg`, unit }),
s({ metric: pattern.min, name: `${name} min`, color: colors.red, unit, defaultActive: false }),
s({ metric: pattern.max, name: `${name} max`, color: colors.green, unit, defaultActive: false }),
s({ metric: pattern.pct10, name: `${name} pct10`, color: colors.rose, unit, defaultActive: false }),
s({ metric: pattern.pct25, name: `${name} pct25`, color: colors.pink, unit, defaultActive: false }),
s({ metric: pattern.median, name: `${name} median`, color: colors.purple, unit, defaultActive: false }),
s({ metric: pattern.pct75, name: `${name} pct75`, color: colors.violet, unit, defaultActive: false }),
s({ metric: pattern.pct90, name: `${name} pct90`, color: colors.fuchsia, unit, defaultActive: false }),
];
/**
* Create series from BitcoinPattern (has base, cumulative)
* @template T
* @param {BitcoinPattern<T>} pattern
* @param {string} name
* @param {Unit} unit
* @param {Color} [sumColor]
* @param {Color} [cumulativeColor]
*/
const fromBitcoin = (pattern, name, unit, sumColor, cumulativeColor) => [
s({ metric: pattern.base, name: `${name}`, color: sumColor, unit }),
s({ metric: pattern.cumulative, name: `${name} cumulative`, color: cumulativeColor ?? colors.blue, unit, defaultActive: false }),
];
/**
* Create series from CoinbasePattern (has sats, bitcoin, dollars as BitcoinPattern)
* BitcoinPattern has .base and .cumulative (no .sum)
* @param {CoinbasePattern} pattern
* @param {string} name
* @param {Color} sumColor
* @param {Color} cumulativeColor
*/
const fromCoinbase = (pattern, name, sumColor, cumulativeColor) => [
s({ metric: pattern.sats.base, name: `${name}`, color: sumColor, unit: "sats" }),
s({ metric: pattern.sats.cumulative, name: `${name} cumulative`, color: cumulativeColor, unit: "sats", defaultActive: false }),
s({ metric: pattern.bitcoin.base, name: `${name}`, color: sumColor, unit: "btc" }),
s({ metric: pattern.bitcoin.cumulative, name: `${name} cumulative`, color: cumulativeColor, unit: "btc", defaultActive: false }),
s({ metric: pattern.dollars.base, name: `${name}`, color: sumColor, unit: "usd" }),
s({ metric: pattern.dollars.cumulative, name: `${name} cumulative`, color: cumulativeColor, unit: "usd", defaultActive: false }),
];
/**
* Create series from ValuePattern (has sats, bitcoin, dollars as BlockCountPattern)
* BlockCountPattern has .base, .sum, and .cumulative
* @param {ValuePattern} pattern
* @param {string} name
* @param {Color} sumColor
* @param {Color} cumulativeColor
*/
const fromValuePattern = (pattern, name, sumColor, cumulativeColor) => [
s({ metric: pattern.sats.base, name: `${name}`, color: sumColor, unit: "sats" }),
s({ metric: pattern.sats.cumulative, name: `${name} cumulative`, color: cumulativeColor, unit: "sats", defaultActive: false }),
s({ metric: pattern.bitcoin.base, name: `${name}`, color: sumColor, unit: "btc" }),
s({ metric: pattern.bitcoin.cumulative, name: `${name} cumulative`, color: cumulativeColor, unit: "btc", defaultActive: false }),
s({ metric: pattern.dollars.base, name: `${name}`, color: sumColor, unit: "usd" }),
s({ metric: pattern.dollars.cumulative, name: `${name} cumulative`, color: cumulativeColor, unit: "usd", defaultActive: false }),
];
/**
* Create series from RewardPattern (has .base as Indexes2<Sats>, plus bitcoin/dollars as BlockCountPattern, sats as SatsPattern)
* Note: SatsPattern only has cumulative and sum, so we use pattern.base for raw sats
* @param {RewardPattern} pattern
* @param {string} name
* @param {Color} sumColor
* @param {Color} cumulativeColor
*/
const fromRewardPattern = (pattern, name, sumColor, cumulativeColor) => [
s({ metric: pattern.base, name: `${name}`, color: sumColor, unit: "sats" }),
s({ metric: pattern.sats.cumulative, name: `${name} cumulative`, color: cumulativeColor, unit: "sats", defaultActive: false }),
s({ metric: pattern.bitcoin.base, name: `${name}`, color: sumColor, unit: "btc" }),
s({ metric: pattern.bitcoin.cumulative, name: `${name} cumulative`, color: cumulativeColor, unit: "btc", defaultActive: false }),
s({ metric: pattern.dollars.base, name: `${name}`, color: sumColor, unit: "usd" }),
s({ metric: pattern.dollars.cumulative, name: `${name} cumulative`, color: cumulativeColor, unit: "usd", defaultActive: false }),
];
// Build pools tree dynamically
const poolEntries = Object.entries(pools.vecs);
const poolsTree = poolEntries.map(([key, pool]) => {
const poolName = brk.POOL_ID_TO_POOL_NAME[/** @type {keyof typeof brk.POOL_ID_TO_POOL_NAME} */ (key.toLowerCase())] || key;
return {
name: poolName,
tree: [
{
name: "Dominance",
title: `Mining Dominance of ${poolName}`,
bottom: [
s({ metric: pool._1dDominance.base, name: "1d", color: colors.rose, unit: "percentage", defaultActive: false }),
s({ metric: pool._1wDominance, name: "1w", color: colors.red, unit: "percentage", defaultActive: false }),
s({ metric: pool._1mDominance, name: "1m", unit: "percentage" }),
s({ metric: pool._1yDominance, name: "1y", color: colors.lime, unit: "percentage", defaultActive: false }),
s({ metric: pool.dominance.base, name: "all time", color: colors.teal, unit: "percentage", defaultActive: false }),
],
},
{
name: "Blocks mined",
title: `Blocks mined by ${poolName}`,
bottom: [
s({ metric: pool.blocksMined.base, name: "Sum", unit: "count" }),
s({ metric: pool.blocksMined.cumulative, name: "Cumulative", color: colors.blue, unit: "count" }),
s({ metric: pool._1wBlocksMined, name: "1w Sum", color: colors.red, unit: "count", defaultActive: false }),
s({ metric: pool._1mBlocksMined, name: "1m Sum", color: colors.pink, unit: "count", defaultActive: false }),
s({ metric: pool._1yBlocksMined, name: "1y Sum", color: colors.purple, unit: "count", defaultActive: false }),
],
},
{
name: "Rewards",
title: `Rewards collected by ${poolName}`,
bottom: [
...fromValuePattern(pool.coinbase, "coinbase", colors.orange, colors.red),
...fromRewardPattern(pool.subsidy, "subsidy", colors.lime, colors.emerald),
...fromRewardPattern(pool.fee, "fee", colors.cyan, colors.indigo),
],
},
{
name: "Days since block",
title: `Days since ${poolName} mined a block`,
bottom: [
s({ metric: pool.daysSinceBlock, name: "Since block", unit: "days" }),
],
},
],
};
});
return {
name: "Chain",
tree: [
// Block
{
name: "Block",
tree: [
{
name: "Count",
title: "Block Count",
bottom: [
...fromBlockCount(blocks.count.blockCount, "Block", "count"),
s({ metric: blocks.count.blockCountTarget, name: "Target", color: colors.gray, unit: "count", options: { lineStyle: 4 } }),
s({ metric: blocks.count._1wBlockCount, name: "1w sum", color: colors.red, unit: "count", defaultActive: false }),
s({ metric: blocks.count._1mBlockCount, name: "1m sum", color: colors.pink, unit: "count", defaultActive: false }),
s({ metric: blocks.count._1yBlockCount, name: "1y sum", color: colors.purple, unit: "count", defaultActive: false }),
],
},
{
name: "Interval",
title: "Block Interval",
bottom: [
s({ metric: blocks.interval.interval, name: "Interval", unit: "secs" }),
...fromBlockInterval(blocks.interval.blockInterval, "Interval", "secs"),
createPriceLine({ unit: "secs", name: "Target", number: 600 }),
],
},
{
name: "Size",
title: "Block Size",
bottom: [
s({ metric: blocks.size.vbytes, name: "vbytes raw", unit: "vb" }),
s({ metric: indexed.block.weight, name: "weight raw", unit: "wu" }),
...fromBlockSize(blocks.size.blockSize, "size", "bytes"),
...fromBlockSize(blocks.weight.blockWeight, "weight", "wu"),
...fromBlockSize(blocks.size.blockVbytes, "vbytes", "vb"),
],
},
],
},
// Transaction
{
name: "Transaction",
tree: [
{
name: "Count",
title: "Transaction Count",
bottom: fromBitcoin(transactions.count.txCount, "Count", "count"),
},
{
name: "Volume",
title: "Transaction Volume",
bottom: [
s({ metric: transactions.volume.sentSum.sats, name: "Sent", unit: "sats" }),
s({ metric: transactions.volume.sentSum.bitcoin.base, name: "Sent", unit: "btc" }),
s({ metric: transactions.volume.sentSum.dollars, name: "Sent", unit: "usd" }),
s({ metric: transactions.volume.annualizedVolume, name: "annualized", color: colors.red, unit: "sats", defaultActive: false }),
s({ metric: transactions.volume.annualizedVolumeBtc, name: "annualized", color: colors.red, unit: "btc", defaultActive: false }),
s({ metric: transactions.volume.annualizedVolumeUsd, name: "annualized", color: colors.lime, unit: "usd", defaultActive: false }),
],
},
{
name: "Size",
title: "Transaction Size",
bottom: [
...fromBlockInterval(transactions.size.txWeight, "weight", "wu"),
...fromBlockInterval(transactions.size.txVsize, "vsize", "vb"),
],
},
{
name: "Versions",
title: "Transaction Versions",
bottom: [
...fromBlockCount(transactions.versions.txV1, "v1", "count", colors.orange, colors.red),
...fromBlockCount(transactions.versions.txV2, "v2", "count", colors.cyan, colors.blue),
...fromBlockCount(transactions.versions.txV3, "v3", "count", colors.lime, colors.green),
],
},
{
name: "Velocity",
title: "Transactions Velocity",
bottom: [
s({ metric: supply.velocity.btc, name: "bitcoin", unit: "ratio" }),
s({ metric: supply.velocity.usd, name: "dollars", color: colors.emerald, unit: "ratio" }),
],
},
{
name: "Speed",
title: "Transactions Per Second",
bottom: [
s({ metric: transactions.volume.txPerSec, name: "Transactions", unit: "/sec" }),
],
},
],
},
// Input
{
name: "Input",
tree: [
{
name: "Count",
title: "Transaction Input Count",
bottom: [
...fromBlockSize(inputs.count.count, "Input", "count"),
],
},
{
name: "Speed",
title: "Inputs Per Second",
bottom: [
s({ metric: transactions.volume.inputsPerSec, name: "Inputs", unit: "/sec" }),
],
},
],
},
// Output
{
name: "Output",
tree: [
{
name: "Count",
title: "Transaction Output Count",
bottom: [
...fromBlockSize(outputs.count.count, "Output", "count"),
],
},
{
name: "Speed",
title: "Outputs Per Second",
bottom: [
s({ metric: transactions.volume.outputsPerSec, name: "Outputs", unit: "/sec" }),
],
},
],
},
// UTXO
{
name: "UTXO",
tree: [
{
name: "Count",
title: "UTXO Count",
bottom: [
s({ metric: outputs.count.utxoCount.base, name: "Count", unit: "count" }),
],
},
],
},
// Coinbase
{
name: "Coinbase",
title: "Coinbase Rewards",
bottom: fromCoinbase(blocks.rewards.coinbase, "Coinbase", colors.orange, colors.red),
},
// Subsidy
{
name: "Subsidy",
title: "Block Subsidy",
bottom: [
...fromCoinbase(blocks.rewards.subsidy, "Subsidy", colors.lime, colors.emerald),
s({ metric: blocks.rewards.subsidyDominance, name: "Dominance", color: colors.purple, unit: "percentage", defaultActive: false }),
],
},
// Fee
{
name: "Fee",
tree: [
{
name: "Total",
title: "Transaction Fees",
bottom: [
s({ metric: transactions.fees.fee.sats.sum, name: "Sum", unit: "sats" }),
s({ metric: transactions.fees.fee.sats.cumulative, name: "Cumulative", color: colors.blue, unit: "sats", defaultActive: false }),
s({ metric: transactions.fees.fee.bitcoin.sum, name: "Sum", unit: "btc" }),
s({ metric: transactions.fees.fee.bitcoin.cumulative, name: "Cumulative", color: colors.blue, unit: "btc", defaultActive: false }),
s({ metric: transactions.fees.fee.dollars.sum, name: "Sum", unit: "usd" }),
s({ metric: transactions.fees.fee.dollars.cumulative, name: "Cumulative", color: colors.blue, unit: "usd", defaultActive: false }),
s({ metric: blocks.rewards.feeDominance, name: "Dominance", color: colors.purple, unit: "percentage", defaultActive: false }),
],
},
{
name: "Rate",
title: "Fee Rate",
bottom: [
s({ metric: transactions.fees.feeRate.base, name: "Rate", unit: "sat/vb" }),
s({ metric: transactions.fees.feeRate.average, name: "Average", color: colors.blue, unit: "sat/vb" }),
s({ metric: transactions.fees.feeRate.median, name: "Median", color: colors.purple, unit: "sat/vb" }),
s({ metric: transactions.fees.feeRate.min, name: "Min", color: colors.red, unit: "sat/vb", defaultActive: false }),
s({ metric: transactions.fees.feeRate.max, name: "Max", color: colors.green, unit: "sat/vb", defaultActive: false }),
s({ metric: transactions.fees.feeRate.pct10, name: "pct10", color: colors.rose, unit: "sat/vb", defaultActive: false }),
s({ metric: transactions.fees.feeRate.pct25, name: "pct25", color: colors.pink, unit: "sat/vb", defaultActive: false }),
s({ metric: transactions.fees.feeRate.pct75, name: "pct75", color: colors.violet, unit: "sat/vb", defaultActive: false }),
s({ metric: transactions.fees.feeRate.pct90, name: "pct90", color: colors.fuchsia, unit: "sat/vb", defaultActive: false }),
],
},
],
},
// Mining
{
name: "Mining",
tree: [
{
name: "Hashrate",
title: "Network Hashrate",
bottom: [
s({ metric: blocks.mining.hashRate, name: "Hashrate", unit: "h/s" }),
s({ metric: blocks.mining.hashRate1wSma, name: "1w SMA", color: colors.red, unit: "h/s", defaultActive: false }),
s({ metric: blocks.mining.hashRate1mSma, name: "1m SMA", color: colors.orange, unit: "h/s", defaultActive: false }),
s({ metric: blocks.mining.hashRate2mSma, name: "2m SMA", color: colors.yellow, unit: "h/s", defaultActive: false }),
s({ metric: blocks.mining.hashRate1ySma, name: "1y SMA", color: colors.lime, unit: "h/s", defaultActive: false }),
],
},
{
name: "Difficulty",
title: "Network Difficulty",
bottom: [
s({ metric: blocks.mining.difficulty, name: "Difficulty", unit: "difficulty" }),
s({ metric: blocks.mining.difficultyAdjustment, name: "Adjustment", color: colors.orange, unit: "percentage", defaultActive: false }),
s({ metric: blocks.mining.difficultyAsHash, name: "As hash", color: colors.default, unit: "h/s", defaultActive: false, options: { lineStyle: 1 } }),
s({ metric: blocks.difficulty.blocksBeforeNextDifficultyAdjustment, name: "Blocks until adj.", color: colors.indigo, unit: "blocks", defaultActive: false }),
s({ metric: blocks.difficulty.daysBeforeNextDifficultyAdjustment, name: "Days until adj.", color: colors.purple, unit: "days", defaultActive: false }),
],
},
{
name: "Hash Price",
title: "Hash Price",
bottom: [
s({ metric: blocks.mining.hashPriceThs, name: "TH/s", color: colors.emerald, unit: "usd/(th/s)/day" }),
s({ metric: blocks.mining.hashPricePhs, name: "PH/s", color: colors.emerald, unit: "usd/(ph/s)/day" }),
s({ metric: blocks.mining.hashPriceRebound, name: "Rebound", color: colors.yellow, unit: "percentage" }),
s({ metric: blocks.mining.hashPriceThsMin, name: "TH/s Min", color: colors.red, unit: "usd/(th/s)/day", options: { lineStyle: 1 } }),
s({ metric: blocks.mining.hashPricePhsMin, name: "PH/s Min", color: colors.red, unit: "usd/(ph/s)/day", options: { lineStyle: 1 } }),
],
},
{
name: "Hash Value",
title: "Hash Value",
bottom: [
s({ metric: blocks.mining.hashValueThs, name: "TH/s", color: colors.orange, unit: "sats/(th/s)/day" }),
s({ metric: blocks.mining.hashValuePhs, name: "PH/s", color: colors.orange, unit: "sats/(ph/s)/day" }),
s({ metric: blocks.mining.hashValueRebound, name: "Rebound", color: colors.yellow, unit: "percentage" }),
s({ metric: blocks.mining.hashValueThsMin, name: "TH/s Min", color: colors.red, unit: "sats/(th/s)/day", options: { lineStyle: 1 } }),
s({ metric: blocks.mining.hashValuePhsMin, name: "PH/s Min", color: colors.red, unit: "sats/(ph/s)/day", options: { lineStyle: 1 } }),
],
},
{
name: "Halving",
title: "Halving Info",
bottom: [
s({ metric: blocks.halving.blocksBeforeNextHalving, name: "Blocks until halving", unit: "blocks" }),
s({ metric: blocks.halving.daysBeforeNextHalving, name: "Days until halving", color: colors.orange, unit: "days" }),
s({ metric: blocks.halving.halvingepoch, name: "Halving epoch", color: colors.purple, unit: "epoch", defaultActive: false }),
],
},
{
name: "Puell Multiple",
title: "Puell Multiple",
bottom: [
s({ metric: market.indicators.puellMultiple, name: "Puell Multiple", unit: "ratio" }),
createPriceLine({ unit: "ratio", number: 1 }),
],
},
],
},
// Pools
{
name: "Pools",
tree: poolsTree,
},
// Unspendable
{
name: "Unspendable",
tree: [
{
name: "OP_RETURN",
tree: [
{
name: "Outputs",
title: "OP_RETURN Outputs",
bottom: fromBitcoin(scripts.count.opreturnCount, "Count", "count"),
},
],
},
],
},
// Inflation
{
name: "Inflation",
title: "Inflation Rate",
bottom: [
s({ metric: supply.inflation.indexes, name: "Rate", unit: "percentage" }),
],
},
],
};
}
@@ -0,0 +1,313 @@
/**
* Address cohort folder builder
* Creates option trees for address-based cohorts (has addrCount)
* Address cohorts use _0satsPattern which has PricePaidPattern (no percentiles)
*/
import {
createSingleSupplySeries,
createGroupedSupplyTotalSeries,
createGroupedSupplyInProfitSeries,
createGroupedSupplyInLossSeries,
createUtxoCountSeries,
createAddressCountSeries,
createRealizedPriceSeries,
createRealizedPriceRatioSeries,
} from "./shared.js";
/**
* Create a cohort folder for address cohorts
* Includes address count section (addrCount exists on AddressCohortObject)
* @param {PartialContext} ctx
* @param {AddressCohortObject | AddressCohortGroupObject} args
* @returns {PartialOptionsGroup}
*/
export function createAddressCohortFolder(ctx, args) {
const list = "list" in args ? args.list : [args];
const useGroupName = "list" in args;
const isSingle = !("list" in args);
const title = args.title ? `${useGroupName ? "by" : "of"} ${args.title}` : "";
return {
name: args.name || "all",
tree: [
// Supply section
isSingle
? {
name: "supply",
title: `Supply ${title}`,
bottom: createSingleSupplySeries(ctx, /** @type {AddressCohortObject} */ (args), title),
}
: {
name: "supply",
tree: [
{
name: "total",
title: `Supply ${title}`,
bottom: createGroupedSupplyTotalSeries(ctx, list),
},
{
name: "in profit",
title: `Supply In Profit ${title}`,
bottom: createGroupedSupplyInProfitSeries(ctx, list),
},
{
name: "in loss",
title: `Supply In Loss ${title}`,
bottom: createGroupedSupplyInLossSeries(ctx, list),
},
],
},
// UTXO count
{
name: "utxo count",
title: `UTXO Count ${title}`,
bottom: createUtxoCountSeries(ctx, list, useGroupName),
},
// Address count (ADDRESS COHORTS ONLY - fully type safe!)
{
name: "address count",
title: `Address Count ${title}`,
bottom: createAddressCountSeries(ctx, list, useGroupName),
},
// Realized section
{
name: "Realized",
tree: [
...(useGroupName
? [
{
name: "Price",
title: `Realized Price ${title}`,
top: createRealizedPriceSeries(ctx, list),
},
{
name: "Ratio",
title: `Realized Price Ratio ${title}`,
bottom: createRealizedPriceRatioSeries(ctx, list),
},
]
: createRealizedPriceOptions(ctx, /** @type {AddressCohortObject} */ (args), title)),
{
name: "capitalization",
title: `Realized Capitalization ${title}`,
bottom: createRealizedCapWithExtras(ctx, list, args, useGroupName, title),
},
...(!useGroupName ? createRealizedPnlSection(ctx, /** @type {AddressCohortObject} */ (args), title) : []),
],
},
// Unrealized section
...createUnrealizedSection(ctx, list, useGroupName, title),
// Price paid section (no percentiles for address cohorts)
...createPricePaidSection(ctx, list, useGroupName, title),
// Activity section
...createActivitySection(ctx, list, useGroupName, title),
],
};
}
/**
* Create realized price options for single cohort
* @param {PartialContext} ctx
* @param {AddressCohortObject} args
* @param {string} title
* @returns {PartialOptionsTree}
*/
function createRealizedPriceOptions(ctx, args, title) {
const { s } = ctx;
const { tree, color } = args;
return [
{
name: "price",
title: `Realized Price ${title}`,
top: [s({ metric: tree.realized.realizedPrice, name: "realized", color })],
},
];
}
/**
* Create realized cap with extras
* @param {PartialContext} ctx
* @param {readonly AddressCohortObject[]} list
* @param {AddressCohortObject | AddressCohortGroupObject} args
* @param {boolean} useGroupName
* @param {string} title
* @returns {AnyFetchedSeriesBlueprint[]}
*/
function createRealizedCapWithExtras(ctx, list, args, useGroupName, title) {
const { colors, s, createPriceLine } = ctx;
const isSingle = !("list" in args);
return list.flatMap(({ color, name, tree }) => [
s({ metric: tree.realized.realizedCap, name: useGroupName ? name : "Capitalization", color }),
...(isSingle
? [
/** @type {AnyFetchedSeriesBlueprint} */ ({
type: "Baseline",
metric: tree.realized.realizedCap30dDelta,
title: "30d change",
defaultActive: false,
}),
createPriceLine({ unit: "usd", defaultActive: false }),
]
: []),
// RealizedPattern (address cohorts) doesn't have realizedCapRelToOwnMarketCap
]);
}
/**
* Create realized PnL section for single cohort
* @param {PartialContext} ctx
* @param {AddressCohortObject} args
* @param {string} title
* @returns {PartialOptionsTree}
*/
function createRealizedPnlSection(ctx, args, title) {
const { colors, s } = ctx;
const { tree } = args;
return [
{
name: "pnl",
title: `Realized Profit And Loss ${title}`,
bottom: [
s({ metric: tree.realized.realizedProfit.base, name: "Profit", color: colors.green }),
s({ metric: tree.realized.realizedLoss.base, name: "Loss", color: colors.red, defaultActive: false }),
// RealizedPattern (address cohorts) doesn't have realizedProfitToLossRatio
s({ metric: tree.realized.totalRealizedPnl.base, name: "Total", color: colors.default, defaultActive: false }),
s({ metric: tree.realized.negRealizedLoss.base, name: "Negative Loss", color: colors.red }),
],
},
];
}
/**
* Create unrealized section
* @param {PartialContext} ctx
* @param {readonly AddressCohortObject[]} list
* @param {boolean} useGroupName
* @param {string} title
* @returns {PartialOptionsTree}
*/
function createUnrealizedSection(ctx, list, useGroupName, title) {
const { colors, s } = ctx;
return [
{
name: "Unrealized",
tree: [
{
name: "nupl",
title: `Net Unrealized Profit/Loss ${title}`,
bottom: list.flatMap(({ color, name, tree }) => [
/** @type {AnyFetchedSeriesBlueprint} */ ({
type: "Baseline",
metric: tree.unrealized.netUnrealizedPnl,
title: useGroupName ? name : "NUPL",
colors: [colors.red, colors.green],
options: { baseValue: { price: 0 } },
}),
]),
},
{
name: "profit",
title: `Unrealized Profit ${title}`,
bottom: list.flatMap(({ color, name, tree }) => [
s({ metric: tree.unrealized.unrealizedProfit, name: useGroupName ? name : "Profit", color }),
]),
},
{
name: "loss",
title: `Unrealized Loss ${title}`,
bottom: list.flatMap(({ color, name, tree }) => [
s({ metric: tree.unrealized.unrealizedLoss, name: useGroupName ? name : "Loss", color }),
]),
},
],
},
];
}
/**
* Create price paid section (no percentiles for address cohorts)
* @param {PartialContext} ctx
* @param {readonly AddressCohortObject[]} list
* @param {boolean} useGroupName
* @param {string} title
* @returns {PartialOptionsTree}
*/
function createPricePaidSection(ctx, list, useGroupName, title) {
const { s } = ctx;
return [
{
name: "Price Paid",
tree: [
{
name: "min",
title: `Min Price Paid ${title}`,
top: list.map(({ color, name, tree }) =>
s({ metric: tree.pricePaid.minPricePaid, name: useGroupName ? name : "Min", color }),
),
},
{
name: "max",
title: `Max Price Paid ${title}`,
top: list.map(({ color, name, tree }) =>
s({ metric: tree.pricePaid.maxPricePaid, name: useGroupName ? name : "Max", color }),
),
},
],
},
];
}
/**
* Create activity section
* @param {PartialContext} ctx
* @param {readonly AddressCohortObject[]} list
* @param {boolean} useGroupName
* @param {string} title
* @returns {PartialOptionsTree}
*/
function createActivitySection(ctx, list, useGroupName, title) {
const { s } = ctx;
return [
{
name: "Activity",
tree: [
{
name: "coinblocks destroyed",
title: `Coinblocks Destroyed ${title}`,
bottom: list.flatMap(({ color, name, tree }) => [
s({
metric: tree.activity.coinblocksDestroyed.base,
name: useGroupName ? name : "Coinblocks",
color,
}),
]),
},
{
name: "coindays destroyed",
title: `Coindays Destroyed ${title}`,
bottom: list.flatMap(({ color, name, tree }) => [
s({
metric: tree.activity.coindaysDestroyed.base,
name: useGroupName ? name : "Coindays",
color,
}),
]),
},
],
},
];
}
@@ -0,0 +1,227 @@
/** Build cohort data arrays from brk.tree */
import {
termColors,
maxAgeColors,
minAgeColors,
ageRangeColors,
epochColors,
geAmountColors,
ltAmountColors,
amountRangeColors,
spendableTypeColors,
} from "../colors/index.js";
/**
* @template {Record<string, any>} T
* @param {T} obj
* @returns {[keyof T & string, T[keyof T & string]][]}
*/
const entries = (obj) => /** @type {[keyof T & string, T[keyof T & string]][]} */ (Object.entries(obj));
/**
* Build all cohort data from brk tree
* @param {Colors} colors
* @param {BrkClient} brk
*/
export function buildCohortData(colors, brk) {
const utxoCohorts = brk.tree.computed.distribution.utxoCohorts;
const addressCohorts = brk.tree.computed.distribution.addressCohorts;
const {
TERM_NAMES,
EPOCH_NAMES,
MAX_AGE_NAMES,
MIN_AGE_NAMES,
AGE_RANGE_NAMES,
GE_AMOUNT_NAMES,
LT_AMOUNT_NAMES,
AMOUNT_RANGE_NAMES,
SPENDABLE_TYPE_NAMES,
} = brk;
// Base cohort representing "all"
/** @type {UtxoCohortObject} */
const cohortAll = {
name: "",
title: "",
color: colors.orange,
tree: utxoCohorts.all,
};
/** @type {UtxoCohortObject} */
const cohortAllForComparison = {
name: "all",
title: "",
color: colors.default,
tree: utxoCohorts.all,
};
// Term cohorts (short/long term holders)
/** @type {readonly UtxoCohortObject[]} */
const terms = entries(utxoCohorts.term).map(([key, tree]) => {
const names = TERM_NAMES[key];
return {
name: names.short,
title: names.long,
color: colors[termColors[key]],
tree,
};
});
// Max age cohorts (up to X time)
/** @type {readonly UtxoCohortObject[]} */
const upToDate = entries(utxoCohorts.maxAge).map(([key, tree]) => {
const names = MAX_AGE_NAMES[key];
return {
name: names.short,
title: names.long,
color: colors[maxAgeColors[key]],
tree,
};
});
// Min age cohorts (from X time)
/** @type {readonly UtxoCohortObject[]} */
const fromDate = entries(utxoCohorts.minAge).map(([key, tree]) => {
const names = MIN_AGE_NAMES[key];
return {
name: names.short,
title: names.long,
color: colors[minAgeColors[key]],
tree,
};
});
// Age range cohorts
/** @type {readonly UtxoCohortObject[]} */
const dateRange = entries(utxoCohorts.ageRange).map(([key, tree]) => {
const names = AGE_RANGE_NAMES[key];
return {
name: names.short,
title: names.long,
color: colors[ageRangeColors[key]],
tree,
};
});
// Epoch cohorts
/** @type {readonly UtxoCohortObject[]} */
const epoch = entries(utxoCohorts.epoch).map(([key, tree]) => {
const names = EPOCH_NAMES[key];
return {
name: names.short,
title: names.long,
color: colors[epochColors[key]],
tree,
};
});
// UTXOs above amount
/** @type {readonly UtxoCohortObject[]} */
const utxosAboveAmount = entries(utxoCohorts.geAmount).map(([key, tree]) => {
const names = GE_AMOUNT_NAMES[key];
return {
name: names.short,
title: names.long,
color: colors[geAmountColors[key]],
tree,
};
});
// Addresses above amount
/** @type {readonly AddressCohortObject[]} */
const addressesAboveAmount = entries(addressCohorts.geAmount).map(
([key, tree]) => {
const names = GE_AMOUNT_NAMES[key];
return {
name: names.short,
title: names.long,
color: colors[geAmountColors[key]],
tree,
};
},
);
// UTXOs under amount
/** @type {readonly UtxoCohortObject[]} */
const utxosUnderAmount = entries(utxoCohorts.ltAmount).map(([key, tree]) => {
const names = LT_AMOUNT_NAMES[key];
return {
name: names.short,
title: names.long,
color: colors[ltAmountColors[key]],
tree,
};
});
// Addresses under amount
/** @type {readonly AddressCohortObject[]} */
const addressesUnderAmount = entries(addressCohorts.ltAmount).map(
([key, tree]) => {
const names = LT_AMOUNT_NAMES[key];
return {
name: names.short,
title: names.long,
color: colors[ltAmountColors[key]],
tree,
};
},
);
// UTXOs amount ranges
/** @type {readonly UtxoCohortObject[]} */
const utxosAmountRanges = entries(utxoCohorts.amountRange).map(
([key, tree]) => {
const names = AMOUNT_RANGE_NAMES[key];
return {
name: names.short,
title: names.long,
color: colors[amountRangeColors[key]],
tree,
};
},
);
// Addresses amount ranges
/** @type {readonly AddressCohortObject[]} */
const addressesAmountRanges = entries(addressCohorts.amountRange).map(
([key, tree]) => {
const names = AMOUNT_RANGE_NAMES[key];
return {
name: names.short,
title: names.long,
color: colors[amountRangeColors[key]],
tree,
};
},
);
// Spendable type cohorts
/** @type {readonly UtxoCohortObject[]} */
const type = entries(utxoCohorts.type).map(([key, tree]) => {
const names = SPENDABLE_TYPE_NAMES[key];
return {
name: names.short,
title: names.long,
color: colors[spendableTypeColors[key]],
tree,
};
});
return {
cohortAll,
cohortAllForComparison,
terms,
upToDate,
fromDate,
dateRange,
epoch,
utxosAboveAmount,
addressesAboveAmount,
utxosUnderAmount,
addressesUnderAmount,
utxosAmountRanges,
addressesAmountRanges,
type,
};
}
@@ -0,0 +1,25 @@
/**
* Cohort module - exports all cohort-related functionality
*/
// Cohort data builder
export { buildCohortData } from "./data.js";
// Cohort folder builders (type-safe!)
export { createUtxoCohortFolder, createAgeCohortFolder, createAmountCohortFolder } from "./utxo.js";
export { createAddressCohortFolder } from "./address.js";
// Shared helpers
export {
createSingleSupplySeries,
createGroupedSupplyTotalSeries,
createGroupedSupplyInProfitSeries,
createGroupedSupplyInLossSeries,
createUtxoCountSeries,
createAddressCountSeries,
createRealizedPriceSeries,
createRealizedPriceRatioSeries,
createRealizedCapSeries,
createPricePaidMinMaxSeries,
createPricePercentilesSeries,
} from "./shared.js";
@@ -0,0 +1,216 @@
/** Shared cohort chart section builders */
/**
* Create supply section for a single cohort
* @param {PartialContext} ctx
* @param {CohortObject} cohort
* @param {string} title
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createSingleSupplySeries(ctx, cohort, title) {
const { colors, s, createPriceLine } = ctx;
const { tree, color, name } = cohort;
return [
s({ metric: tree.supply.supply.sats, name: "Supply", color: colors.default }),
s({ metric: tree.supply.supply.bitcoin, name: "Supply", color: colors.default }),
s({ metric: tree.supply.supply.dollars, name: "Supply", color: colors.default }),
...("supplyRelToCirculatingSupply" in tree.relative
? [s({ metric: tree.relative.supplyRelToCirculatingSupply, name: "Supply", color: colors.default })]
: []),
s({ metric: tree.unrealized.supplyInProfit.sats, name: "In Profit", color: colors.green }),
s({ metric: tree.unrealized.supplyInProfit.bitcoin, name: "In Profit", color: colors.green }),
s({ metric: tree.unrealized.supplyInProfit.dollars, name: "In Profit", color: colors.green }),
s({ metric: tree.unrealized.supplyInLoss.sats, name: "In Loss", color: colors.red }),
s({ metric: tree.unrealized.supplyInLoss.bitcoin, name: "In Loss", color: colors.red }),
s({ metric: tree.unrealized.supplyInLoss.dollars, name: "In Loss", color: colors.red }),
s({ metric: tree.supply.supplyHalf.sats, name: "half", color: colors.gray, options: { lineStyle: 4 } }),
s({ metric: tree.supply.supplyHalf.bitcoin, name: "half", color: colors.gray, options: { lineStyle: 4 } }),
s({ metric: tree.supply.supplyHalf.dollars, name: "half", color: colors.gray, options: { lineStyle: 4 } }),
...("supplyInProfitRelToCirculatingSupply" in tree.relative
? [
s({ metric: tree.relative.supplyInProfitRelToCirculatingSupply, name: "In Profit", color: colors.green }),
s({ metric: tree.relative.supplyInLossRelToCirculatingSupply, name: "In Loss", color: colors.red }),
]
: []),
s({ metric: tree.relative.supplyInProfitRelToOwnSupply, name: "In Profit", color: colors.green }),
s({ metric: tree.relative.supplyInLossRelToOwnSupply, name: "In Loss", color: colors.red }),
createPriceLine({ unit: "%self", number: 100, lineStyle: 0, color: colors.default }),
createPriceLine({ unit: "%self", number: 50 }),
];
}
/**
* Create supply total series for grouped cohorts
* @param {PartialContext} ctx
* @param {readonly CohortObject[]} list
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createGroupedSupplyTotalSeries(ctx, list) {
const { s, constant100 } = ctx;
return list.flatMap(({ color, name, tree }) => [
s({ metric: tree.supply.supply.sats, name, color }),
s({ metric: tree.supply.supply.bitcoin, name, color }),
s({ metric: tree.supply.supply.dollars, name, color }),
"supplyRelToCirculatingSupply" in tree.relative
? s({ metric: tree.relative.supplyRelToCirculatingSupply, name, color })
: s({ unit: "%all", metric: constant100, name, color }),
]);
}
/**
* Create supply in profit series for grouped cohorts
* @param {PartialContext} ctx
* @param {readonly CohortObject[]} list
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createGroupedSupplyInProfitSeries(ctx, list) {
const { s } = ctx;
return list.flatMap(({ color, name, tree }) => [
s({ metric: tree.unrealized.supplyInProfit.sats, name, color }),
s({ metric: tree.unrealized.supplyInProfit.bitcoin, name, color }),
s({ metric: tree.unrealized.supplyInProfit.dollars, name, color }),
...("supplyInProfitRelToCirculatingSupply" in tree.relative
? [s({ metric: tree.relative.supplyInProfitRelToCirculatingSupply, name, color })]
: []),
]);
}
/**
* Create supply in loss series for grouped cohorts
* @param {PartialContext} ctx
* @param {readonly CohortObject[]} list
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createGroupedSupplyInLossSeries(ctx, list) {
const { s } = ctx;
return list.flatMap(({ color, name, tree }) => [
s({ metric: tree.unrealized.supplyInLoss.sats, name, color }),
s({ metric: tree.unrealized.supplyInLoss.bitcoin, name, color }),
s({ metric: tree.unrealized.supplyInLoss.dollars, name, color }),
...("supplyInLossRelToCirculatingSupply" in tree.relative
? [s({ metric: tree.relative.supplyInLossRelToCirculatingSupply, name, color })]
: []),
]);
}
/**
* Create UTXO count series
* @param {PartialContext} ctx
* @param {readonly CohortObject[]} list
* @param {boolean} useGroupName
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createUtxoCountSeries(ctx, list, useGroupName) {
const { s } = ctx;
return list.flatMap(({ color, name, tree }) => [
s({ metric: tree.supply.utxoCount, name: useGroupName ? name : "Count", color }),
]);
}
/**
* Create address count series (for address cohorts only)
* @param {PartialContext} ctx
* @param {readonly AddressCohortObject[]} list
* @param {boolean} useGroupName
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createAddressCountSeries(ctx, list, useGroupName) {
const { s, colors } = ctx;
return list.flatMap(({ color, name, tree }) => [
s({
metric: tree.addrCount,
name: useGroupName ? name : "Count",
color: useGroupName ? color : colors.orange,
}),
]);
}
/**
* Create realized price series for grouped cohorts
* @param {PartialContext} ctx
* @param {readonly CohortObject[]} list
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createRealizedPriceSeries(ctx, list) {
const { s } = ctx;
return list.map(({ color, name, tree }) =>
s({ metric: tree.realized.realizedPrice, name, color }),
);
}
/**
* Create realized price ratio series for grouped cohorts
* @param {PartialContext} ctx
* @param {readonly CohortObject[]} list
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createRealizedPriceRatioSeries(ctx, list) {
const { s, createPriceLine } = ctx;
return [
...list.map(({ color, name, tree }) =>
s({ metric: tree.realized.realizedPriceExtra.ratio, name, color }),
),
createPriceLine({ unit: "ratio", number: 1 }),
];
}
/**
* Create realized capitalization series
* @param {PartialContext} ctx
* @param {readonly CohortObject[]} list
* @param {boolean} useGroupName
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createRealizedCapSeries(ctx, list, useGroupName) {
const { s } = ctx;
return list.flatMap(({ color, name, tree }) => [
s({ metric: tree.realized.realizedCap, name: useGroupName ? name : "Capitalization", color }),
]);
}
/**
* Create price paid min/max series (available on all cohorts)
* @param {PartialContext} ctx
* @param {readonly CohortObject[]} list
* @param {boolean} useGroupName
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createPricePaidMinMaxSeries(ctx, list, useGroupName) {
const { s } = ctx;
return list.flatMap(({ color, name, tree }) => [
s({ metric: tree.pricePaid.minPricePaid, name: useGroupName ? `${name} min` : "Min", color }),
s({ metric: tree.pricePaid.maxPricePaid, name: useGroupName ? `${name} max` : "Max", color }),
]);
}
/**
* Create price percentile series (only for cohorts with PricePaidPattern2)
* @param {PartialContext} ctx
* @param {readonly CohortWithPricePercentiles[]} list
* @param {boolean} useGroupName
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createPricePercentilesSeries(ctx, list, useGroupName) {
const { s, colors } = ctx;
return list.flatMap(({ color, name, tree }) => {
const pp = tree.pricePaid.pricePercentiles;
return [
s({ metric: pp.pct10, name: useGroupName ? `${name} p10` : "p10", color, defaultActive: false }),
s({ metric: pp.pct25, name: useGroupName ? `${name} p25` : "p25", color, defaultActive: false }),
s({ metric: pp.pct50, name: useGroupName ? `${name} p50` : "p50", color }),
s({ metric: pp.pct75, name: useGroupName ? `${name} p75` : "p75", color, defaultActive: false }),
s({ metric: pp.pct90, name: useGroupName ? `${name} p90` : "p90", color, defaultActive: false }),
];
});
}
@@ -0,0 +1,6 @@
/**
* Cohort-related type definitions
* Types are defined in ../types.js, this file exists for documentation
*/
export {};
@@ -0,0 +1,487 @@
/**
* UTXO cohort folder builders
* Creates option trees for UTXO-based cohorts (no addrCount)
*
* Two main builders:
* - createAgeCohortFolder: For term, maxAge, minAge, ageRange, epoch (has price percentiles)
* - createAmountCohortFolder: For geAmount, ltAmount, amountRange, type (no price percentiles)
*/
import {
createSingleSupplySeries,
createGroupedSupplyTotalSeries,
createGroupedSupplyInProfitSeries,
createGroupedSupplyInLossSeries,
createUtxoCountSeries,
createRealizedPriceSeries,
createRealizedPriceRatioSeries,
createRealizedCapSeries,
createPricePaidMinMaxSeries,
createPricePercentilesSeries,
} from "./shared.js";
/**
* Create a cohort folder for age-based UTXO cohorts (term, maxAge, minAge, ageRange, epoch)
* These cohorts have price percentiles via PricePaidPattern2
* @param {PartialContext} ctx
* @param {AgeCohortObject | AgeCohortGroupObject} args
* @returns {PartialOptionsGroup}
*/
export function createAgeCohortFolder(ctx, args) {
const list = "list" in args ? args.list : [args];
const useGroupName = "list" in args;
const isSingle = !("list" in args);
const title = args.title ? `${useGroupName ? "by" : "of"} ${args.title}` : "";
return {
name: args.name || "all",
tree: [
...createSupplySection(ctx, list, args, useGroupName, isSingle, title),
createUtxoCountSection(ctx, list, useGroupName, title),
createRealizedSection(ctx, list, args, useGroupName, isSingle, title),
...createUnrealizedSection(ctx, list, useGroupName, title),
...createPricePaidSectionWithPercentiles(ctx, list, useGroupName, title),
...createActivitySection(ctx, list, useGroupName, title),
],
};
}
/**
* Create a cohort folder for amount-based UTXO cohorts (geAmount, ltAmount, amountRange, type)
* These cohorts have only min/max price paid via PricePaidPattern
* @param {PartialContext} ctx
* @param {AmountCohortObject | AmountCohortGroupObject} args
* @returns {PartialOptionsGroup}
*/
export function createAmountCohortFolder(ctx, args) {
const list = "list" in args ? args.list : [args];
const useGroupName = "list" in args;
const isSingle = !("list" in args);
const title = args.title ? `${useGroupName ? "by" : "of"} ${args.title}` : "";
return {
name: args.name || "all",
tree: [
...createSupplySection(ctx, list, args, useGroupName, isSingle, title),
createUtxoCountSection(ctx, list, useGroupName, title),
createRealizedSection(ctx, list, args, useGroupName, isSingle, title),
...createUnrealizedSection(ctx, list, useGroupName, title),
...createPricePaidSectionBasic(ctx, list, useGroupName, title),
...createActivitySection(ctx, list, useGroupName, title),
],
};
}
// Keep the generic version for backwards compatibility
/**
* Create a cohort folder for UTXO cohorts (generic, uses runtime check for percentiles)
* @deprecated Use createAgeCohortFolder or createAmountCohortFolder for type safety
* @param {PartialContext} ctx
* @param {UtxoCohortObject | UtxoCohortGroupObject} args
* @returns {PartialOptionsGroup}
*/
export function createUtxoCohortFolder(ctx, args) {
const list = "list" in args ? args.list : [args];
const useGroupName = "list" in args;
const isSingle = !("list" in args);
const title = args.title ? `${useGroupName ? "by" : "of"} ${args.title}` : "";
// Runtime check for percentiles
const hasPercentiles = "pricePercentiles" in list[0].tree.pricePaid;
return {
name: args.name || "all",
tree: [
...createSupplySection(ctx, list, args, useGroupName, isSingle, title),
createUtxoCountSection(ctx, list, useGroupName, title),
createRealizedSection(ctx, list, args, useGroupName, isSingle, title),
...createUnrealizedSection(ctx, list, useGroupName, title),
...(hasPercentiles
? createPricePaidSectionWithPercentiles(ctx, /** @type {readonly AgeCohortObject[]} */ (list), useGroupName, title)
: createPricePaidSectionBasic(ctx, list, useGroupName, title)),
...createActivitySection(ctx, list, useGroupName, title),
],
};
}
/**
* Create supply section
* @param {PartialContext} ctx
* @param {readonly UtxoCohortObject[]} list
* @param {UtxoCohortObject | UtxoCohortGroupObject} args
* @param {boolean} useGroupName
* @param {boolean} isSingle
* @param {string} title
* @returns {PartialOptionsTree}
*/
function createSupplySection(ctx, list, args, useGroupName, isSingle, title) {
return [
isSingle
? {
name: "supply",
title: `Supply ${title}`,
bottom: createSingleSupplySeries(ctx, /** @type {UtxoCohortObject} */ (args), title),
}
: {
name: "supply",
tree: [
{
name: "total",
title: `Supply ${title}`,
bottom: createGroupedSupplyTotalSeries(ctx, list),
},
{
name: "in profit",
title: `Supply In Profit ${title}`,
bottom: createGroupedSupplyInProfitSeries(ctx, list),
},
{
name: "in loss",
title: `Supply In Loss ${title}`,
bottom: createGroupedSupplyInLossSeries(ctx, list),
},
],
},
];
}
/**
* Create UTXO count section
* @param {PartialContext} ctx
* @param {readonly UtxoCohortObject[]} list
* @param {boolean} useGroupName
* @param {string} title
* @returns {PartialChartOption}
*/
function createUtxoCountSection(ctx, list, useGroupName, title) {
return {
name: "utxo count",
title: `UTXO Count ${title}`,
bottom: createUtxoCountSeries(ctx, list, useGroupName),
};
}
/**
* Create realized section
* @param {PartialContext} ctx
* @param {readonly UtxoCohortObject[]} list
* @param {UtxoCohortObject | UtxoCohortGroupObject} args
* @param {boolean} useGroupName
* @param {boolean} isSingle
* @param {string} title
* @returns {PartialOptionsGroup}
*/
function createRealizedSection(ctx, list, args, useGroupName, isSingle, title) {
return {
name: "Realized",
tree: [
...(useGroupName
? [
{
name: "Price",
title: `Realized Price ${title}`,
top: createRealizedPriceSeries(ctx, list),
},
{
name: "Ratio",
title: `Realized Price Ratio ${title}`,
bottom: createRealizedPriceRatioSeries(ctx, list),
},
]
: createRealizedPriceOptions(ctx, /** @type {UtxoCohortObject} */ (args), title)),
{
name: "capitalization",
title: `Realized Capitalization ${title}`,
bottom: createRealizedCapWithExtras(ctx, list, args, useGroupName, title),
},
...(!useGroupName ? createRealizedPnlSection(ctx, /** @type {UtxoCohortObject} */ (args), title) : []),
],
};
}
/**
* Create realized price options for single cohort
* @param {PartialContext} ctx
* @param {UtxoCohortObject} args
* @param {string} title
* @returns {PartialOptionsTree}
*/
function createRealizedPriceOptions(ctx, args, title) {
const { s } = ctx;
const { tree, color } = args;
return [
{
name: "price",
title: `Realized Price ${title}`,
top: [s({ metric: tree.realized.realizedPrice, name: "realized", color })],
},
];
}
/**
* Create realized cap with extras
* @param {PartialContext} ctx
* @param {readonly UtxoCohortObject[]} list
* @param {UtxoCohortObject | UtxoCohortGroupObject} args
* @param {boolean} useGroupName
* @param {string} title
* @returns {AnyFetchedSeriesBlueprint[]}
*/
function createRealizedCapWithExtras(ctx, list, args, useGroupName, title) {
const { colors, s, createPriceLine } = ctx;
const isSingle = !("list" in args);
return list.flatMap(({ color, name, tree }) => [
s({
metric: tree.realized.realizedCap,
name: useGroupName ? name : "Capitalization",
color,
}),
...(isSingle
? [
/** @type {AnyFetchedSeriesBlueprint} */ ({
type: "Baseline",
metric: tree.realized.realizedCap30dDelta,
title: "30d change",
defaultActive: false,
}),
createPriceLine({ unit: "usd", defaultActive: false }),
]
: []),
...(isSingle && "realizedCapRelToOwnMarketCap" in tree.realized
? [
/** @type {AnyFetchedSeriesBlueprint} */ ({
type: "Baseline",
metric: tree.realized.realizedCapRelToOwnMarketCap,
title: "ratio",
options: { baseValue: { price: 100 } },
colors: [colors.red, colors.green],
}),
createPriceLine({ unit: "%cmcap", defaultActive: true, number: 100 }),
]
: []),
]);
}
/**
* Create realized PnL section for single cohort
* @param {PartialContext} ctx
* @param {UtxoCohortObject} args
* @param {string} title
* @returns {PartialOptionsTree}
*/
function createRealizedPnlSection(ctx, args, title) {
const { colors, s } = ctx;
const { tree } = args;
return [
{
name: "pnl",
title: `Realized Profit And Loss ${title}`,
bottom: [
s({
metric: tree.realized.realizedProfit.base,
name: "Profit",
color: colors.green,
}),
s({
metric: tree.realized.realizedLoss.base,
name: "Loss",
color: colors.red,
defaultActive: false,
}),
...("realizedProfitToLossRatio" in tree.realized
? [
s({
metric: tree.realized.realizedProfitToLossRatio,
name: "profit / loss",
color: colors.yellow,
}),
]
: []),
s({
metric: tree.realized.totalRealizedPnl.base,
name: "Total",
color: colors.default,
defaultActive: false,
}),
s({
metric: tree.realized.negRealizedLoss.base,
name: "Negative Loss",
color: colors.red,
}),
],
},
];
}
/**
* Create unrealized section
* @param {PartialContext} ctx
* @param {readonly UtxoCohortObject[]} list
* @param {boolean} useGroupName
* @param {string} title
* @returns {PartialOptionsTree}
*/
function createUnrealizedSection(ctx, list, useGroupName, title) {
const { colors, s, createPriceLine } = ctx;
return [
{
name: "Unrealized",
tree: [
{
name: "nupl",
title: `Net Unrealized Profit/Loss ${title}`,
bottom: list.flatMap(({ color, name, tree }) => [
/** @type {AnyFetchedSeriesBlueprint} */ ({
type: "Baseline",
metric: tree.unrealized.netUnrealizedPnl,
title: useGroupName ? name : "NUPL",
colors: [colors.red, colors.green],
options: { baseValue: { price: 0 } },
}),
]),
},
{
name: "profit",
title: `Unrealized Profit ${title}`,
bottom: list.flatMap(({ color, name, tree }) => [
s({
metric: tree.unrealized.unrealizedProfit,
name: useGroupName ? name : "Profit",
color,
}),
]),
},
{
name: "loss",
title: `Unrealized Loss ${title}`,
bottom: list.flatMap(({ color, name, tree }) => [
s({
metric: tree.unrealized.unrealizedLoss,
name: useGroupName ? name : "Loss",
color,
}),
]),
},
],
},
];
}
/**
* Create price paid section for cohorts WITH percentiles (age cohorts)
* @param {PartialContext} ctx
* @param {readonly AgeCohortObject[]} list
* @param {boolean} useGroupName
* @param {string} title
* @returns {PartialOptionsTree}
*/
function createPricePaidSectionWithPercentiles(ctx, list, useGroupName, title) {
const { s } = ctx;
return [
{
name: "Price Paid",
tree: [
{
name: "min",
title: `Min Price Paid ${title}`,
top: list.map(({ color, name, tree }) =>
s({ metric: tree.pricePaid.minPricePaid, name: useGroupName ? name : "Min", color }),
),
},
{
name: "max",
title: `Max Price Paid ${title}`,
top: list.map(({ color, name, tree }) =>
s({ metric: tree.pricePaid.maxPricePaid, name: useGroupName ? name : "Max", color }),
),
},
{
name: "percentiles",
title: `Price Paid Percentiles ${title}`,
top: createPricePercentilesSeries(ctx, list, useGroupName),
},
],
},
];
}
/**
* Create price paid section for cohorts WITHOUT percentiles (amount cohorts)
* @param {PartialContext} ctx
* @param {readonly UtxoCohortObject[]} list
* @param {boolean} useGroupName
* @param {string} title
* @returns {PartialOptionsTree}
*/
function createPricePaidSectionBasic(ctx, list, useGroupName, title) {
const { s } = ctx;
return [
{
name: "Price Paid",
tree: [
{
name: "min",
title: `Min Price Paid ${title}`,
top: list.map(({ color, name, tree }) =>
s({ metric: tree.pricePaid.minPricePaid, name: useGroupName ? name : "Min", color }),
),
},
{
name: "max",
title: `Max Price Paid ${title}`,
top: list.map(({ color, name, tree }) =>
s({ metric: tree.pricePaid.maxPricePaid, name: useGroupName ? name : "Max", color }),
),
},
],
},
];
}
/**
* Create activity section
* @param {PartialContext} ctx
* @param {readonly UtxoCohortObject[]} list
* @param {boolean} useGroupName
* @param {string} title
* @returns {PartialOptionsTree}
*/
function createActivitySection(ctx, list, useGroupName, title) {
const { s } = ctx;
return [
{
name: "Activity",
tree: [
{
name: "coinblocks destroyed",
title: `Coinblocks Destroyed ${title}`,
bottom: list.flatMap(({ color, name, tree }) => [
s({
metric: tree.activity.coinblocksDestroyed.base,
name: useGroupName ? name : "Coinblocks",
color,
}),
]),
},
{
name: "coindays destroyed",
title: `Coindays Destroyed ${title}`,
bottom: list.flatMap(({ color, name, tree }) => [
s({
metric: tree.activity.coindaysDestroyed.base,
name: useGroupName ? name : "Coindays",
color,
}),
]),
},
],
},
];
}
@@ -0,0 +1,306 @@
/** Cointime section builder - typed tree-based patterns */
/**
* Create price with ratio options for cointime prices
* @param {PartialContext} ctx
* @param {Object} args
* @param {string} args.name
* @param {string} args.title
* @param {string} args.legend
* @param {MetricAccessor<any>} args.price
* @param {ActivePriceRatioPattern} args.ratio
* @param {Color} [args.color]
* @returns {PartialOptionsTree}
*/
function createCointimePriceWithRatioOptions(ctx, { name, title, legend, price, ratio, color }) {
const { s, colors, createPriceLine } = ctx;
// Percentile USD mappings
const percentileUsdMap = [
{ name: "pct99", prop: ratio.ratioPct99Usd, color: colors.rose },
{ name: "pct98", prop: ratio.ratioPct98Usd, color: colors.pink },
{ name: "pct95", prop: ratio.ratioPct95Usd, color: colors.fuchsia },
{ name: "pct5", prop: ratio.ratioPct5Usd, color: colors.cyan },
{ name: "pct2", prop: ratio.ratioPct2Usd, color: colors.sky },
{ name: "pct1", prop: ratio.ratioPct1Usd, color: colors.blue },
];
// Percentile ratio mappings
const percentileMap = [
{ name: "pct99", prop: ratio.ratioPct99, color: colors.rose },
{ name: "pct98", prop: ratio.ratioPct98, color: colors.pink },
{ name: "pct95", prop: ratio.ratioPct95, color: colors.fuchsia },
{ name: "pct5", prop: ratio.ratioPct5, color: colors.cyan },
{ name: "pct2", prop: ratio.ratioPct2, color: colors.sky },
{ name: "pct1", prop: ratio.ratioPct1, color: colors.blue },
];
// SD patterns by window
const sdPatterns = [
{ nameAddon: "all", titleAddon: "", sd: ratio.ratioSd },
{ nameAddon: "4y", titleAddon: "4y", sd: ratio.ratio4ySd },
{ nameAddon: "2y", titleAddon: "2y", sd: ratio.ratio2ySd },
{ nameAddon: "1y", titleAddon: "1y", sd: ratio.ratio1ySd },
];
/** @param {Ratio1ySdPattern} sd */
const getSdBands = (sd) => [
{ name: "0σ", prop: sd._0sdUsd, color: colors.lime },
{ name: "+0.5σ", prop: sd.p05sdUsd, color: colors.yellow },
{ name: "+1σ", prop: sd.p1sdUsd, color: colors.amber },
{ name: "+1.5σ", prop: sd.p15sdUsd, color: colors.orange },
{ name: "+2σ", prop: sd.p2sdUsd, color: colors.red },
{ name: "+2.5σ", prop: sd.p25sdUsd, color: colors.rose },
{ name: "+3σ", prop: sd.p3sd, color: colors.pink },
{ name: "0.5σ", prop: sd.m05sdUsd, color: colors.teal },
{ name: "1σ", prop: sd.m1sdUsd, color: colors.cyan },
{ name: "1.5σ", prop: sd.m15sdUsd, color: colors.sky },
{ name: "2σ", prop: sd.m2sdUsd, color: colors.blue },
{ name: "2.5σ", prop: sd.m25sdUsd, color: colors.indigo },
{ name: "3σ", prop: sd.m3sd, color: colors.violet },
];
return [
{
name: "price",
title,
top: [s({ metric: price, name: legend, color, unit: "usd" })],
},
{
name: "Ratio",
title: `${title} Ratio`,
top: [
s({ metric: price, name: legend, color, unit: "usd" }),
...percentileUsdMap.map(({ name: pctName, prop, color: pctColor }) =>
s({
metric: prop,
name: pctName,
color: pctColor,
defaultActive: false,
unit: "usd",
options: { lineStyle: 1 },
}),
),
],
bottom: [
s({ metric: ratio.ratio, name: "ratio", color, unit: "ratio" }),
s({ metric: ratio.ratio1wSma, name: "1w sma", color: colors.lime, unit: "ratio" }),
s({ metric: ratio.ratio1mSma, name: "1m sma", color: colors.teal, unit: "ratio" }),
s({ metric: ratio.ratio1ySd.sma, name: "1y sma", color: colors.sky, unit: "ratio" }),
s({ metric: ratio.ratio2ySd.sma, name: "2y sma", color: colors.indigo, unit: "ratio" }),
s({ metric: ratio.ratio4ySd.sma, name: "4y sma", color: colors.purple, unit: "ratio" }),
s({ metric: ratio.ratioSd.sma, name: "all sma", color: colors.rose, unit: "ratio" }),
...percentileMap.map(({ name: pctName, prop, color: pctColor }) =>
s({
metric: prop,
name: pctName,
color: pctColor,
defaultActive: false,
unit: "ratio",
options: { lineStyle: 1 },
}),
),
createPriceLine({ unit: "ratio", number: 1 }),
],
},
{
name: "ZScores",
tree: sdPatterns.map(({ nameAddon, titleAddon, sd }) => ({
name: nameAddon,
title: `${title} ${titleAddon} Z-Score`,
top: getSdBands(sd).map(({ name: bandName, prop, color: bandColor }) =>
s({ metric: prop, name: bandName, color: bandColor, unit: "usd" }),
),
bottom: [
s({ metric: sd.zscore, name: "zscore", color, unit: "sd" }),
createPriceLine({ unit: "sd", number: 3 }),
createPriceLine({ unit: "sd", number: 2 }),
createPriceLine({ unit: "sd", number: 1 }),
createPriceLine({ unit: "sd", number: 0 }),
createPriceLine({ unit: "sd", number: -1 }),
createPriceLine({ unit: "sd", number: -2 }),
createPriceLine({ unit: "sd", number: -3 }),
],
})),
},
];
}
/**
* Create Cointime section
* @param {PartialContext} ctx
* @returns {PartialOptionsGroup}
*/
export function createCointimeSection(ctx) {
const { colors, brk, s } = ctx;
const { cointime, distribution, supply } = brk.tree.computed;
const { pricing, cap, activity, supply: cointimeSupply, adjusted } = cointime;
const utxoCohorts = distribution.utxoCohorts;
// Cointime prices data
const cointimePrices = [
{
price: pricing.trueMarketMean,
ratio: pricing.trueMarketMeanRatio,
name: "True market mean",
title: "true market mean",
color: colors.blue,
},
{
price: pricing.vaultedPrice,
ratio: pricing.vaultedPriceRatio,
name: "Vaulted",
title: "vaulted price",
color: colors.lime,
},
{
price: pricing.activePrice,
ratio: pricing.activePriceRatio,
name: "Active",
title: "active price",
color: colors.rose,
},
{
price: pricing.cointimePrice,
ratio: pricing.cointimePriceRatio,
name: "cointime",
title: "cointime price",
color: colors.yellow,
},
];
// Cointime capitalizations data
const cointimeCapitalizations = [
{ metric: cap.vaultedCap, name: "vaulted", title: "vaulted Capitalization", color: colors.lime },
{ metric: cap.activeCap, name: "active", title: "active Capitalization", color: colors.rose },
{ metric: cap.cointimeCap, name: "cointime", title: "cointime Capitalization", color: colors.yellow },
{ metric: cap.investorCap, name: "investor", title: "investor Capitalization", color: colors.fuchsia },
{ metric: cap.thermoCap, name: "thermo", title: "thermo Capitalization", color: colors.emerald },
];
return {
name: "Cointime",
tree: [
// Prices
{
name: "Prices",
tree: [
{
name: "Compare",
title: "Compare Cointime Prices",
top: cointimePrices.map(({ price, name, color }) =>
s({ metric: price, name, color, unit: "usd" }),
),
},
...cointimePrices.map(({ price, ratio, name, color, title }) => ({
name,
tree: createCointimePriceWithRatioOptions(ctx, {
price,
ratio,
legend: name,
color,
name,
title,
}),
})),
],
},
// Capitalization
{
name: "Capitalization",
tree: [
{
name: "Compare",
title: "Compare Cointime Capitalizations",
bottom: [
s({ metric: supply.marketCap.height, name: "Market", color: colors.default, unit: "usd" }),
s({ metric: utxoCohorts.all.realized.realizedCap, name: "Realized", color: colors.orange, unit: "usd" }),
...cointimeCapitalizations.map(({ metric, name, color }) =>
s({ metric, name, color, unit: "usd" }),
),
],
},
...cointimeCapitalizations.map(({ metric, name, color, title }) => ({
name,
title,
bottom: [
s({ metric, name, color, unit: "usd" }),
s({ metric: supply.marketCap.height, name: "Market", color: colors.default, unit: "usd" }),
s({ metric: utxoCohorts.all.realized.realizedCap, name: "Realized", color: colors.orange, unit: "usd" }),
],
})),
],
},
// Supply
{
name: "Supply",
title: "Cointime Supply",
bottom: /** @type {const} */ ([
[utxoCohorts.all.supply.supply, "all", colors.orange],
[cointimeSupply.vaultedSupply, "vaulted", colors.lime],
[cointimeSupply.activeSupply, "active", colors.rose],
]).flatMap(([supplyItem, name, color]) => [
s({ metric: supplyItem.sats, name, color, unit: "sats" }),
s({ metric: supplyItem.bitcoin, name, color, unit: "btc" }),
s({ metric: supplyItem.dollars, name, color, unit: "usd" }),
]),
},
// Liveliness & Vaultedness
{
name: "Liveliness & Vaultedness",
title: "Liveliness & Vaultedness",
bottom: [
s({ metric: activity.liveliness, name: "Liveliness", color: colors.rose, unit: "ratio" }),
s({ metric: activity.vaultedness, name: "Vaultedness", color: colors.lime, unit: "ratio" }),
s({ metric: activity.activityToVaultednessRatio, name: "Liveliness / Vaultedness", color: colors.purple, unit: "ratio" }),
],
},
// Coinblocks
{
name: "Coinblocks",
title: "Coinblocks",
bottom: [
// Destroyed comes from the all cohort's activity
s({ metric: utxoCohorts.all.activity.coinblocksDestroyed.base, name: "Destroyed", color: colors.red, unit: "coinblocks" }),
s({ metric: utxoCohorts.all.activity.coinblocksDestroyed.cumulative, name: "Cumulative Destroyed", color: colors.red, defaultActive: false, unit: "coinblocks" }),
// Created and stored from cointime
s({ metric: activity.coinblocksCreated.base, name: "created", color: colors.orange, unit: "coinblocks" }),
s({ metric: activity.coinblocksCreated.cumulative, name: "Cumulative created", color: colors.orange, defaultActive: false, unit: "coinblocks" }),
s({ metric: activity.coinblocksStored.base, name: "stored", color: colors.green, unit: "coinblocks" }),
s({ metric: activity.coinblocksStored.cumulative, name: "Cumulative stored", color: colors.green, defaultActive: false, unit: "coinblocks" }),
],
},
// Adjusted metrics
{
name: "Adjusted",
tree: [
// Inflation
{
name: "inflation",
title: "Cointime-Adjusted inflation rate",
bottom: [
s({ metric: supply.inflation.indexes, name: "base", color: colors.orange, unit: "percentage" }),
s({ metric: adjusted.cointimeAdjInflationRate, name: "adjusted", color: colors.purple, unit: "percentage" }),
],
},
// Velocity
{
name: "Velocity",
title: "Cointime-Adjusted transactions velocity",
bottom: [
s({ metric: supply.velocity.btc, name: "btc", color: colors.orange, unit: "ratio" }),
s({ metric: adjusted.cointimeAdjTxBtcVelocity, name: "adj. btc", color: colors.red, unit: "ratio" }),
s({ metric: supply.velocity.usd, name: "usd", color: colors.emerald, unit: "ratio" }),
s({ metric: adjusted.cointimeAdjTxUsdVelocity, name: "adj. usd", color: colors.lime, unit: "ratio" }),
],
},
],
},
],
};
}
@@ -0,0 +1,152 @@
/** Cohort color mappings */
/** @type {Readonly<Record<string, ColorName>>} */
export const termColors = {
short: "yellow",
long: "fuchsia",
};
/** @type {Readonly<Record<string, ColorName>>} */
export const maxAgeColors = {
_1w: "red",
_1m: "orange",
_2m: "amber",
_3m: "yellow",
_4m: "lime",
_5m: "green",
_6m: "teal",
_1y: "sky",
_2y: "indigo",
_3y: "violet",
_4y: "purple",
_5y: "fuchsia",
_6y: "pink",
_7y: "red",
_8y: "orange",
_10y: "amber",
_12y: "yellow",
_15y: "lime",
};
/** @type {Readonly<Record<string, ColorName>>} */
export const minAgeColors = {
_1d: "red",
_1w: "orange",
_1m: "yellow",
_2m: "lime",
_3m: "green",
_4m: "teal",
_5m: "cyan",
_6m: "blue",
_1y: "indigo",
_2y: "violet",
_3y: "purple",
_4y: "fuchsia",
_5y: "pink",
_6y: "rose",
_7y: "red",
_8y: "orange",
_10y: "yellow",
_12y: "lime",
};
/** @type {Readonly<Record<string, ColorName>>} */
export const ageRangeColors = {
upTo1d: "pink",
_1dTo1w: "red",
_1wTo1m: "orange",
_1mTo2m: "yellow",
_2mTo3m: "yellow",
_3mTo4m: "lime",
_4mTo5m: "lime",
_5mTo6m: "lime",
_6mTo1y: "green",
_1yTo2y: "cyan",
_2yTo3y: "blue",
_3yTo4y: "indigo",
_4yTo5y: "violet",
_5yTo6y: "purple",
_6yTo7y: "purple",
_7yTo8y: "fuchsia",
_8yTo10y: "fuchsia",
_10yTo12y: "pink",
_12yTo15y: "red",
from15y: "orange",
};
/** @type {Readonly<Record<string, ColorName>>} */
export const epochColors = {
_0: "red",
_1: "yellow",
_2: "orange",
_3: "lime",
_4: "green",
};
/** @type {Readonly<Record<string, ColorName>>} */
export const geAmountColors = {
_1sat: "orange",
_10sats: "orange",
_100sats: "yellow",
_1kSats: "lime",
_10kSats: "green",
_100kSats: "cyan",
_1mSats: "blue",
_10mSats: "indigo",
_1btc: "purple",
_10btc: "violet",
_100btc: "fuchsia",
_1kBtc: "pink",
_10kBtc: "red",
};
/** @type {Readonly<Record<string, ColorName>>} */
export const ltAmountColors = {
_10sats: "orange",
_100sats: "yellow",
_1kSats: "lime",
_10kSats: "green",
_100kSats: "cyan",
_1mSats: "blue",
_10mSats: "indigo",
_1btc: "purple",
_10btc: "violet",
_100btc: "fuchsia",
_1kBtc: "pink",
_10kBtc: "red",
_100kBtc: "orange",
};
/** @type {Readonly<Record<string, ColorName>>} */
export const amountRangeColors = {
_0sats: "red",
_1satTo10sats: "orange",
_10satsTo100sats: "yellow",
_100satsTo1kSats: "lime",
_1kSatsTo10kSats: "green",
_10kSatsTo100kSats: "cyan",
_100kSatsTo1mSats: "blue",
_1mSatsTo10mSats: "indigo",
_10mSatsTo1btc: "purple",
_1btcTo10btc: "violet",
_10btcTo100btc: "fuchsia",
_100btcTo1kBtc: "pink",
_1kBtcTo10kBtc: "red",
_10kBtcTo100kBtc: "orange",
_100kBtcOrMore: "yellow",
};
/** @type {Readonly<Record<string, ColorName>>} */
export const spendableTypeColors = {
p2pk65: "red",
p2pk33: "orange",
p2pkh: "yellow",
p2ms: "lime",
p2sh: "green",
p2wpkh: "teal",
p2wsh: "blue",
p2tr: "indigo",
p2a: "purple",
unknown: "violet",
empty: "fuchsia",
};
@@ -0,0 +1,14 @@
// Re-export all color mappings
export {
termColors,
maxAgeColors,
minAgeColors,
ageRangeColors,
epochColors,
geAmountColors,
ltAmountColors,
amountRangeColors,
spendableTypeColors,
} from "./cohorts.js";
export { averageColors, dcaColors } from "./misc.js";
@@ -0,0 +1,42 @@
/** Miscellaneous color mappings for DCA and averages */
/**
* Moving average period colors
* Format: [periodId, days, colorName]
* @type {readonly [string, number, ColorName][]}
*/
export const averageColors = [
["1w", 7, "red"],
["8d", 8, "orange"],
["13d", 13, "amber"],
["21d", 21, "yellow"],
["1m", 30, "lime"],
["34d", 34, "green"],
["55d", 55, "emerald"],
["89d", 89, "teal"],
["144d", 144, "cyan"],
["200d", 200, "sky"],
["1y", 365, "blue"],
["2y", 730, "indigo"],
["200w", 1400, "violet"],
["4y", 1460, "purple"],
];
/**
* DCA class colors by year
* Format: [year, colorName, defaultActive]
* @type {readonly [number, ColorName, boolean][]}
*/
export const dcaColors = [
[2015, "pink", false],
[2016, "red", false],
[2017, "orange", true],
[2018, "yellow", true],
[2019, "green", true],
[2020, "teal", true],
[2021, "sky", true],
[2022, "blue", true],
[2023, "purple", true],
[2024, "fuchsia", true],
[2025, "pink", true],
];
@@ -0,0 +1,141 @@
/** Constant helpers for creating price lines and reference lines */
/**
* Get constant pattern by number dynamically from tree
* Examples: 0 → constant0, 38.2 → constant382, -1 → constantMinus1
* @param {BrkClient["tree"]["computed"]["constants"]} constants
* @param {number} num
* @returns {Constant0Pattern<any>}
*/
export function getConstant(constants, num) {
const key =
num >= 0
? `constant${String(num).replace(".", "")}`
: `constantMinus${Math.abs(num)}`;
const constant = /** @type {Constant0Pattern<any> | undefined} */ (
/** @type {Record<string, Constant0Pattern<any>>} */ (constants)[key]
);
if (!constant) throw new Error(`Unknown constant: ${num} (key: ${key})`);
return constant;
}
/**
* Flatten a Constant0Pattern into a simple MetricAccessor
* Constant0Pattern has { dateindex: { by: {...} }, height: { by: {...} }, ... }
* This flattens it to { by: { dateindex: MetricNode, height: MetricNode, ... } }
* @param {Constant0Pattern<any>} pattern
* @returns {MetricAccessor<any>}
*/
export function flattenConstant(pattern) {
return {
by: {
dateindex: pattern.dateindex.by.dateindex,
decadeindex: pattern.decadeindex.by.decadeindex,
height: pattern.height.by.height,
monthindex: pattern.monthindex.by.monthindex,
quarterindex: pattern.quarterindex.by.quarterindex,
semesterindex: pattern.semesterindex.by.semesterindex,
weekindex: pattern.weekindex.by.weekindex,
yearindex: pattern.yearindex.by.yearindex,
},
indexes() {
return /** @type {Index[]} */ (Object.keys(this.by));
},
};
}
/**
* Create a price line series (horizontal reference line)
* @param {Object} args
* @param {BrkClient["tree"]["computed"]["constants"]} args.constants
* @param {Colors} args.colors
* @param {number} [args.number]
* @param {string} [args.name]
* @param {boolean} [args.defaultActive]
* @param {number} [args.lineStyle]
* @param {Color} [args.color]
* @param {Unit} args.unit
* @returns {FetchedLineSeriesBlueprint}
*/
export function createPriceLine({
constants,
colors,
number = 0,
unit,
defaultActive,
color,
name,
lineStyle,
}) {
return {
metric: flattenConstant(getConstant(constants, number)),
title: name ?? `${number}`,
unit,
defaultActive,
color: color ?? colors.gray,
options: {
lineStyle: lineStyle ?? 4,
lastValueVisible: false,
crosshairMarkerVisible: false,
},
};
}
/**
* Create multiple price lines from an array of numbers
* @param {Object} args
* @param {BrkClient["tree"]["computed"]["constants"]} args.constants
* @param {Colors} args.colors
* @param {number[]} args.numbers
* @param {Unit} args.unit
* @returns {FetchedLineSeriesBlueprint[]}
*/
export function createPriceLines({ constants, colors, numbers, unit }) {
return numbers.map((number) => ({
metric: flattenConstant(getConstant(constants, number)),
title: `${number}`,
unit,
defaultActive: !number,
color: colors.gray,
options: {
lineStyle: 4,
lastValueVisible: false,
crosshairMarkerVisible: false,
},
}));
}
/**
* Create a constant line series
* @param {Object} args
* @param {Colors} args.colors
* @param {Constant0Pattern<any>} args.constant
* @param {string} args.name
* @param {Unit} args.unit
* @param {Color} [args.color]
* @param {number} [args.lineStyle]
* @param {boolean} [args.defaultActive]
* @returns {FetchedLineSeriesBlueprint}
*/
export function line({
colors,
constant,
name,
unit,
color,
lineStyle,
defaultActive,
}) {
return {
metric: flattenConstant(constant),
title: name,
unit,
defaultActive,
color: color ?? colors.gray,
options: {
lineStyle: lineStyle ?? 4,
lastValueVisible: false,
crosshairMarkerVisible: false,
},
};
}
@@ -0,0 +1,44 @@
import { s, fromBlockCount, fromBitcoin, fromBlockSize } from "./series.js";
import {
getConstant,
flattenConstant,
createPriceLine,
createPriceLines,
line,
} from "./constants.js";
/**
* Create a context object with all dependencies for building partial options
* @param {Object} args
* @param {Colors} args.colors
* @param {BrkClient} args.brk
* @returns {PartialContext}
*/
export function createContext({ colors, brk }) {
const constants = brk.tree.computed.constants;
const constant100 = flattenConstant(constants.constant100);
return {
colors,
brk,
constants,
constant100,
// Series helpers
s,
fromBlockCount: (pattern, title, color) =>
fromBlockCount(colors, pattern, title, color),
fromBitcoin: (pattern, title, color) =>
fromBitcoin(colors, pattern, title, color),
fromBlockSize: (pattern, title, color) =>
fromBlockSize(colors, pattern, title, color),
// Constant helpers
getConstant: (num) => getConstant(constants, num),
flattenConstant,
createPriceLine: (args) => createPriceLine({ constants, colors, ...args }),
createPriceLines: (args) =>
createPriceLines({ constants, colors, ...args }),
line: (args) => line({ colors, ...args }),
};
}
@@ -0,0 +1,357 @@
/** Partial options - Main entry point */
import { localhost } from "../../utils/env.js";
import { createContext } from "./context.js";
import {
buildCohortData,
createUtxoCohortFolder,
createAddressCohortFolder,
} from "./cohorts/index.js";
import { createMarketSection } from "./market.js";
import { createChainSection } from "./chain.js";
import { createCointimeSection } from "./cointime.js";
// Re-export types for external consumers
export * from "./types.js";
/**
* Create partial options tree
* @param {Object} args
* @param {Colors} args.colors
* @param {BrkClient} args.brk
* @returns {PartialOptionsTree}
*/
export function createPartialOptions({ colors, brk }) {
// Create context with all helpers
const ctx = createContext({ colors, brk });
// Build cohort data
const {
cohortAll,
cohortAllForComparison,
terms,
upToDate,
fromDate,
dateRange,
epoch,
utxosAboveAmount,
addressesAboveAmount,
utxosUnderAmount,
addressesUnderAmount,
utxosAmountRanges,
addressesAmountRanges,
type,
} = buildCohortData(colors, brk);
// Helper to map UTXO cohorts
const mapUtxoCohorts = (/** @type {any} */ cohort) => createUtxoCohortFolder(ctx, cohort);
// Helper to map Address cohorts
const mapAddressCohorts = (/** @type {any} */ cohort) => createAddressCohortFolder(ctx, cohort);
return [
// Debug explorer (localhost only)
...(localhost
? [
{
kind: /** @type {const} */ ("explorer"),
name: "Explorer",
title: "Debug explorer",
},
]
: []),
// Charts section
{
name: "Charts",
tree: [
// Market section
createMarketSection(ctx),
// Chain section
createChainSection(ctx),
// Cohorts section
{
name: "Cohorts",
tree: [
// All UTXOs
createUtxoCohortFolder(ctx, cohortAll),
// Terms (STH/LTH)
{
name: "terms",
tree: [
createUtxoCohortFolder(ctx, {
name: "Compare",
title: "UTXOs Term",
list: [...terms, cohortAllForComparison],
}),
...terms.map(mapUtxoCohorts),
],
},
// Epochs
{
name: "Epochs",
tree: [
createUtxoCohortFolder(ctx, {
name: "Compare",
title: "Epoch",
list: [...epoch, cohortAllForComparison],
}),
...epoch.map(mapUtxoCohorts),
],
},
// Types
{
name: "types",
tree: [
createUtxoCohortFolder(ctx, {
name: "Compare",
title: "Type",
list: [...type, cohortAllForComparison],
}),
...type.map(mapUtxoCohorts),
],
},
// UTXOs Up to age
{
name: "UTXOs Up to age",
tree: [
createUtxoCohortFolder(ctx, {
name: "Compare",
title: "UTXOs Up To Age",
list: [...upToDate, cohortAllForComparison],
}),
...upToDate.map(mapUtxoCohorts),
],
},
// UTXOs from age
{
name: "UTXOs from age",
tree: [
createUtxoCohortFolder(ctx, {
name: "Compare",
title: "UTXOs from age",
list: [...fromDate, cohortAllForComparison],
}),
...fromDate.map(mapUtxoCohorts),
],
},
// UTXOs age ranges
{
name: "UTXOs age Ranges",
tree: [
createUtxoCohortFolder(ctx, {
name: "Compare",
title: "UTXOs Age Range",
list: [...dateRange, cohortAllForComparison],
}),
...dateRange.map(mapUtxoCohorts),
],
},
// UTXOs under amounts
{
name: "UTXOs under amounts",
tree: [
createUtxoCohortFolder(ctx, {
name: "Compare",
title: "UTXOs under amount",
list: [...utxosUnderAmount, cohortAllForComparison],
}),
...utxosUnderAmount.map(mapUtxoCohorts),
],
},
// UTXOs above amounts
{
name: "UTXOs Above Amounts",
tree: [
createUtxoCohortFolder(ctx, {
name: "Compare",
title: "UTXOs Above Amount",
list: [...utxosAboveAmount, cohortAllForComparison],
}),
...utxosAboveAmount.map(mapUtxoCohorts),
],
},
// UTXOs between amounts
{
name: "UTXOs between amounts",
tree: [
createUtxoCohortFolder(ctx, {
name: "Compare",
title: "UTXOs between amounts",
list: [...utxosAmountRanges, cohortAllForComparison],
}),
...utxosAmountRanges.map(mapUtxoCohorts),
],
},
// Addresses under amount (TYPE SAFE - uses createAddressCohortFolder!)
{
name: "Addresses under amount",
tree: [
createAddressCohortFolder(ctx, {
name: "Compare",
title: "Addresses under Amount",
list: addressesUnderAmount,
}),
...addressesUnderAmount.map(mapAddressCohorts),
],
},
// Addresses above amount (TYPE SAFE - uses createAddressCohortFolder!)
{
name: "Addresses above amount",
tree: [
createAddressCohortFolder(ctx, {
name: "Compare",
title: "Addresses above amount",
list: addressesAboveAmount,
}),
...addressesAboveAmount.map(mapAddressCohorts),
],
},
// Addresses between amounts (TYPE SAFE - uses createAddressCohortFolder!)
{
name: "Addresses between amounts",
tree: [
createAddressCohortFolder(ctx, {
name: "Compare",
title: "Addresses between amounts",
list: addressesAmountRanges,
}),
...addressesAmountRanges.map(mapAddressCohorts),
],
},
],
},
// Cointime section
createCointimeSection(ctx),
],
},
// Table section
{
kind: /** @type {const} */ ("table"),
title: "Table",
name: "Table",
},
// Simulations section
{
name: "Simulations",
tree: [
{
kind: /** @type {const} */ ("simulation"),
name: "Save In Bitcoin",
title: "Save In Bitcoin",
},
],
},
// Tools section
{
name: "Tools",
tree: [
{
name: "Documentation",
tree: [
{
name: "API",
url: () => "/api",
title: "API documentation",
},
{
name: "MCP",
url: () =>
"https://github.com/bitcoinresearchkit/brk/blob/main/crates/brk_mcp/README.md#brk_mcp",
title: "Model Context Protocol documentation",
},
{
name: "Crate",
url: () => "/crate",
title: "View on crates.io",
},
{
name: "Source",
url: () => "/github",
title: "Source code and issues",
},
{
name: "Changelog",
url: () => "/changelog",
title: "Release notes and changelog",
},
],
},
{
name: "Hosting",
tree: [
{
name: "Status",
url: () => "/status",
title: "Service status and uptime",
},
{
name: "Self-host",
url: () => "/install",
title: "Install and run yourself",
},
{
name: "Service",
url: () => "/service",
title: "Hosted service offering",
},
],
},
{
name: "Community",
tree: [
{
name: "Discord",
url: () => "/discord",
title: "Join the Discord server",
},
{
name: "GitHub",
url: () => "/github",
title: "Source code and issues",
},
{
name: "Nostr",
url: () => "/nostr",
title: "Follow on Nostr",
},
],
},
],
},
// Donate
{
name: "Donate",
qrcode: true,
url: () => "bitcoin:bc1q098zsm89m7kgyze338vfejhpdt92ua9p3peuve",
title: "Bitcoin address for donations",
},
// Share
{
name: "Share",
qrcode: true,
url: () => window.location.href,
title: "Share",
},
];
}
@@ -0,0 +1,480 @@
/** Market section builder - typed tree-based patterns */
/**
* Convert period ID to readable name
* @param {string} id
* @param {boolean} [compoundAdjective]
*/
function periodIdToName(id, compoundAdjective) {
const suffix = compoundAdjective || parseInt(id) === 1 ? "" : "s";
return id
.replace("d", ` day${suffix}`)
.replace("w", ` week${suffix}`)
.replace("m", ` month${suffix}`)
.replace("y", ` year${suffix}`);
}
/**
* Create price with ratio options (for moving averages)
* @param {PartialContext} ctx
* @param {Object} args
* @param {string} args.name
* @param {string} args.title
* @param {string} args.legend
* @param {EmaRatioPattern} args.ratio
* @param {Color} [args.color]
* @returns {PartialOptionsTree}
*/
function createPriceWithRatioOptions(ctx, { name, title, legend, ratio, color }) {
const { s, colors, createPriceLine } = ctx;
const priceMetric = ratio.price;
// Percentile USD mappings
const percentileUsdMap = [
{ name: "pct99", prop: ratio.ratioPct99Usd, color: colors.rose },
{ name: "pct98", prop: ratio.ratioPct98Usd, color: colors.pink },
{ name: "pct95", prop: ratio.ratioPct95Usd, color: colors.fuchsia },
{ name: "pct5", prop: ratio.ratioPct5Usd, color: colors.cyan },
{ name: "pct2", prop: ratio.ratioPct2Usd, color: colors.sky },
{ name: "pct1", prop: ratio.ratioPct1Usd, color: colors.blue },
];
// Percentile ratio mappings
const percentileMap = [
{ name: "pct99", prop: ratio.ratioPct99, color: colors.rose },
{ name: "pct98", prop: ratio.ratioPct98, color: colors.pink },
{ name: "pct95", prop: ratio.ratioPct95, color: colors.fuchsia },
{ name: "pct5", prop: ratio.ratioPct5, color: colors.cyan },
{ name: "pct2", prop: ratio.ratioPct2, color: colors.sky },
{ name: "pct1", prop: ratio.ratioPct1, color: colors.blue },
];
// SD patterns by window
const sdPatterns = [
{ nameAddon: "all", titleAddon: "", sd: ratio.ratioSd },
{ nameAddon: "4y", titleAddon: "4y", sd: ratio.ratio4ySd },
{ nameAddon: "2y", titleAddon: "2y", sd: ratio.ratio2ySd },
{ nameAddon: "1y", titleAddon: "1y", sd: ratio.ratio1ySd },
];
/** @param {Ratio1ySdPattern} sd */
const getSdBands = (sd) => [
{ name: "0σ", prop: sd._0sdUsd, color: colors.lime },
{ name: "+0.5σ", prop: sd.p05sdUsd, color: colors.yellow },
{ name: "+1σ", prop: sd.p1sdUsd, color: colors.amber },
{ name: "+1.5σ", prop: sd.p15sdUsd, color: colors.orange },
{ name: "+2σ", prop: sd.p2sdUsd, color: colors.red },
{ name: "+2.5σ", prop: sd.p25sdUsd, color: colors.rose },
{ name: "+3σ", prop: sd.p3sd, color: colors.pink },
{ name: "0.5σ", prop: sd.m05sdUsd, color: colors.teal },
{ name: "1σ", prop: sd.m1sdUsd, color: colors.cyan },
{ name: "1.5σ", prop: sd.m15sdUsd, color: colors.sky },
{ name: "2σ", prop: sd.m2sdUsd, color: colors.blue },
{ name: "2.5σ", prop: sd.m25sdUsd, color: colors.indigo },
{ name: "3σ", prop: sd.m3sd, color: colors.violet },
];
return [
{
name: "price",
title,
top: [s({ metric: priceMetric, name: legend, color, unit: "usd" })],
},
{
name: "Ratio",
title: `${title} Ratio`,
top: [
s({ metric: priceMetric, name: legend, color, unit: "usd" }),
...percentileUsdMap.map(({ name: pctName, prop, color: pctColor }) =>
s({
metric: prop,
name: pctName,
color: pctColor,
defaultActive: false,
unit: "usd",
options: { lineStyle: 1 },
}),
),
],
bottom: [
s({ metric: ratio.ratio, name: "ratio", color, unit: "ratio" }),
s({ metric: ratio.ratio1wSma, name: "1w sma", color: colors.lime, unit: "ratio" }),
s({ metric: ratio.ratio1mSma, name: "1m sma", color: colors.teal, unit: "ratio" }),
s({ metric: ratio.ratio1ySd.sma, name: "1y sma", color: colors.sky, unit: "ratio" }),
s({ metric: ratio.ratio2ySd.sma, name: "2y sma", color: colors.indigo, unit: "ratio" }),
s({ metric: ratio.ratio4ySd.sma, name: "4y sma", color: colors.purple, unit: "ratio" }),
s({ metric: ratio.ratioSd.sma, name: "all sma", color: colors.rose, unit: "ratio" }),
...percentileMap.map(({ name: pctName, prop, color: pctColor }) =>
s({
metric: prop,
name: pctName,
color: pctColor,
defaultActive: false,
unit: "ratio",
options: { lineStyle: 1 },
}),
),
createPriceLine({ unit: "ratio", number: 1 }),
],
},
{
name: "ZScores",
tree: sdPatterns.map(({ nameAddon, titleAddon, sd }) => ({
name: nameAddon,
title: `${title} ${titleAddon} Z-Score`,
top: getSdBands(sd).map(({ name: bandName, prop, color: bandColor }) =>
s({ metric: prop, name: bandName, color: bandColor, unit: "usd" }),
),
bottom: [
s({ metric: sd.zscore, name: "zscore", color, unit: "sd" }),
createPriceLine({ unit: "sd", number: 3 }),
createPriceLine({ unit: "sd", number: 2 }),
createPriceLine({ unit: "sd", number: 1 }),
createPriceLine({ unit: "sd", number: 0 }),
createPriceLine({ unit: "sd", number: -1 }),
createPriceLine({ unit: "sd", number: -2 }),
createPriceLine({ unit: "sd", number: -3 }),
],
})),
},
];
}
/**
* Build averages data array from market patterns
* @param {Colors} colors
* @param {MarketMovingAverage} ma
*/
function buildAverages(colors, ma) {
return /** @type {const} */ ([
["1w", 7, "red", ma.price1wSma, ma.price1wEma],
["8d", 8, "orange", ma.price8dSma, ma.price8dEma],
["13d", 13, "amber", ma.price13dSma, ma.price13dEma],
["21d", 21, "yellow", ma.price21dSma, ma.price21dEma],
["1m", 30, "lime", ma.price1mSma, ma.price1mEma],
["34d", 34, "green", ma.price34dSma, ma.price34dEma],
["55d", 55, "emerald", ma.price55dSma, ma.price55dEma],
["89d", 89, "teal", ma.price89dSma, ma.price89dEma],
["144d", 144, "cyan", ma.price144dSma, ma.price144dEma],
["200d", 200, "sky", ma.price200dSma, ma.price200dEma],
["1y", 365, "blue", ma.price1ySma, ma.price1yEma],
["2y", 730, "indigo", ma.price2ySma, ma.price2yEma],
["200w", 1400, "violet", ma.price200wSma, ma.price200wEma],
["4y", 1460, "purple", ma.price4ySma, ma.price4yEma],
]).map(([id, days, colorKey, sma, ema]) => ({
id,
name: periodIdToName(id, true),
days,
color: colors[colorKey],
sma,
ema,
}));
}
/**
* Build DCA classes data array
* @param {Colors} colors
* @param {MarketDca} dca
*/
function buildDcaClasses(colors, dca) {
return /** @type {const} */ ([
[2015, "pink", false, dca.dcaClass2015AvgPrice, dca.dcaClass2015Returns, dca.dcaClass2015Stack],
[2016, "red", false, dca.dcaClass2016AvgPrice, dca.dcaClass2016Returns, dca.dcaClass2016Stack],
[2017, "orange", true, dca.dcaClass2017AvgPrice, dca.dcaClass2017Returns, dca.dcaClass2017Stack],
[2018, "yellow", true, dca.dcaClass2018AvgPrice, dca.dcaClass2018Returns, dca.dcaClass2018Stack],
[2019, "green", true, dca.dcaClass2019AvgPrice, dca.dcaClass2019Returns, dca.dcaClass2019Stack],
[2020, "teal", true, dca.dcaClass2020AvgPrice, dca.dcaClass2020Returns, dca.dcaClass2020Stack],
[2021, "sky", true, dca.dcaClass2021AvgPrice, dca.dcaClass2021Returns, dca.dcaClass2021Stack],
[2022, "blue", true, dca.dcaClass2022AvgPrice, dca.dcaClass2022Returns, dca.dcaClass2022Stack],
[2023, "purple", true, dca.dcaClass2023AvgPrice, dca.dcaClass2023Returns, dca.dcaClass2023Stack],
[2024, "fuchsia", true, dca.dcaClass2024AvgPrice, dca.dcaClass2024Returns, dca.dcaClass2024Stack],
[2025, "pink", true, dca.dcaClass2025AvgPrice, dca.dcaClass2025Returns, dca.dcaClass2025Stack],
]).map(([year, colorKey, defaultActive, avgPrice, returns, stack]) => ({
year,
color: colors[colorKey],
defaultActive,
avgPrice,
returns,
stack,
}));
}
/**
* Create Market section
* @param {PartialContext} ctx
* @returns {PartialOptionsGroup}
*/
export function createMarketSection(ctx) {
const { colors, brk, s, createPriceLine } = ctx;
const { market, supply } = brk.tree.computed;
const { movingAverage, ath, returns, volatility, range, dca, lookback } = market;
const averages = buildAverages(colors, movingAverage);
const dcaClasses = buildDcaClasses(colors, dca);
return {
name: "Market",
tree: [
// Price (empty chart, shows candlesticks by default)
{
name: "Price",
title: "Bitcoin Price",
},
// Capitalization
{
name: "Capitalization",
title: "Market Capitalization",
bottom: [s({ metric: supply.marketCap.indexes, name: "Capitalization", unit: "usd" })],
},
// All Time High
{
name: "All Time High",
title: "All Time High",
top: [s({ metric: ath.priceAth, name: "ath", unit: "usd" })],
bottom: [
s({ metric: ath.priceDrawdown, name: "Drawdown", color: colors.red, unit: "percentage" }),
s({ metric: ath.daysSincePriceAth, name: "since", unit: "days" }),
s({ metric: ath.maxDaysBetweenPriceAths, name: "Max", color: colors.red, unit: "days" }),
s({ metric: ath.maxYearsBetweenPriceAths, name: "Max", color: colors.red, unit: "years" }),
],
},
// Averages
{
name: "Averages",
tree: [
{ nameAddon: "Simple", metricAddon: /** @type {const} */ ("sma") },
{ nameAddon: "Exponential", metricAddon: /** @type {const} */ ("ema") },
].map(({ nameAddon, metricAddon }) => ({
name: nameAddon,
tree: [
{
name: "Compare",
title: `Market Price ${nameAddon} Moving Averages`,
top: averages.map(({ id, color, sma, ema }) =>
s({
metric: (metricAddon === "sma" ? sma : ema).price,
name: id,
color,
unit: "usd",
}),
),
},
...averages.map(({ name, color, sma, ema }) => ({
name,
tree: createPriceWithRatioOptions(ctx, {
ratio: metricAddon === "sma" ? sma : ema,
name,
title: `${name} Market Price ${nameAddon} Moving Average`,
legend: "average",
color,
}),
})),
],
})),
},
// Performance
{
name: "Performance",
tree: /** @type {const} */ ([
["1d", returns._1dPriceReturns, undefined],
["1w", returns._1wPriceReturns, undefined],
["1m", returns._1mPriceReturns, undefined],
["3m", returns._3mPriceReturns, undefined],
["6m", returns._6mPriceReturns, undefined],
["1y", returns._1yPriceReturns, undefined],
["2y", returns._2yPriceReturns, returns._2yCagr],
["3y", returns._3yPriceReturns, returns._3yCagr],
["4y", returns._4yPriceReturns, returns._4yCagr],
["5y", returns._5yPriceReturns, returns._5yCagr],
["6y", returns._6yPriceReturns, returns._6yCagr],
["8y", returns._8yPriceReturns, returns._8yCagr],
["10y", returns._10yPriceReturns, returns._10yCagr],
]).map(([id, priceReturns, cagr]) => {
const name = periodIdToName(id, true);
return {
name,
title: `${name} Performance`,
bottom: [
/** @type {AnyFetchedSeriesBlueprint} */ ({
metric: priceReturns,
title: "total",
type: "Baseline",
unit: "percentage",
}),
...(cagr
? [
/** @type {AnyFetchedSeriesBlueprint} */ ({
metric: cagr,
title: "cagr",
type: "Baseline",
colors: [colors.lime, colors.pink],
unit: "percentage",
}),
]
: []),
createPriceLine({ unit: "percentage" }),
],
};
}),
},
// Indicators
{
name: "Indicators",
tree: [
// Volatility
{
name: "Volatility",
title: "Bitcoin Price Volatility Index",
bottom: [
s({ metric: volatility.price1wVolatility, name: "1w", color: colors.red, unit: "percentage" }),
s({ metric: volatility.price1mVolatility, name: "1m", color: colors.orange, unit: "percentage" }),
s({ metric: volatility.price1yVolatility, name: "1y", color: colors.lime, unit: "percentage" }),
],
},
// MinMax
{
name: "MinMax",
tree: [
{ id: "1w", title: "1 Week", min: range.price1wMin, max: range.price1wMax },
{ id: "2w", title: "2 Week", min: range.price2wMin, max: range.price2wMax },
{ id: "1m", title: "1 Month", min: range.price1mMin, max: range.price1mMax },
{ id: "1y", title: "1 Year", min: range.price1yMin, max: range.price1yMax },
].map(({ id, title, min, max }) => ({
name: id,
title: `Bitcoin Price ${title} MinMax Bands`,
top: [
s({ metric: min, name: "min", color: colors.red, unit: "usd" }),
s({ metric: max, name: "max", color: colors.green, unit: "usd" }),
],
})),
},
// True range
{
name: "True range",
title: "Bitcoin Price True Range",
bottom: [s({ metric: range.priceTrueRange, name: "value", color: colors.yellow, unit: "usd" })],
},
// Choppiness
{
name: "Choppiness",
title: "Bitcoin Price Choppiness Index",
bottom: [
s({ metric: range.price2wChoppinessIndex, name: "2w", color: colors.red, unit: "index" }),
createPriceLine({ unit: "index", number: 61.8 }),
createPriceLine({ unit: "index", number: 38.2 }),
],
},
// Mayer multiple
{
name: "Mayer multiple",
title: "Mayer multiple",
top: [
s({ metric: movingAverage.price200dSma.price, name: "200d sma", color: colors.yellow, unit: "usd" }),
s({ metric: movingAverage.price200dSmaX24, name: "200d sma x2.4", color: colors.green, unit: "usd" }),
s({ metric: movingAverage.price200dSmaX08, name: "200d sma x0.8", color: colors.red, unit: "usd" }),
],
},
],
},
// Investing
{
name: "Investing",
tree: [
// DCA vs Lump sum
{
name: "DCA vs Lump sum",
tree: [
.../** @type {const} */ ([
["1w", dca._1wDcaAvgPrice, lookback.price1wAgo, dca._1wDcaReturns, returns._1wPriceReturns],
["1m", dca._1mDcaAvgPrice, lookback.price1mAgo, dca._1mDcaReturns, returns._1mPriceReturns],
["3m", dca._3mDcaAvgPrice, lookback.price3mAgo, dca._3mDcaReturns, returns._3mPriceReturns],
["6m", dca._6mDcaAvgPrice, lookback.price6mAgo, dca._6mDcaReturns, returns._6mPriceReturns],
["1y", dca._1yDcaAvgPrice, lookback.price1yAgo, dca._1yDcaReturns, returns._1yPriceReturns],
]).map(([id, dcaAvgPrice, priceAgo, dcaReturns, priceReturns]) => {
const name = periodIdToName(id, true);
return {
name,
tree: [
{
name: "price",
title: `${name} DCA vs Lump Sum (Price)`,
top: [
s({ metric: dcaAvgPrice, name: "DCA avg", color: colors.green, unit: "usd" }),
s({ metric: priceAgo, name: "Lump sum", color: colors.orange, unit: "usd" }),
],
},
{
name: "returns",
title: `${name} DCA vs Lump Sum (Returns)`,
bottom: [
/** @type {AnyFetchedSeriesBlueprint} */ ({
metric: dcaReturns,
title: "DCA",
type: "Baseline",
unit: "percentage",
}),
/** @type {AnyFetchedSeriesBlueprint} */ ({
metric: priceReturns,
title: "Lump sum",
type: "Baseline",
colors: [colors.lime, colors.red],
unit: "percentage",
}),
createPriceLine({ unit: "percentage" }),
],
},
],
};
}),
],
},
// DCA classes
{
name: "DCA classes",
tree: [
{
name: "Average price",
title: "DCA Average Price by Year",
top: dcaClasses.map(({ year, color, defaultActive, avgPrice }) =>
s({ metric: avgPrice, name: `${year}`, color, defaultActive, unit: "usd" }),
),
},
{
name: "Returns",
title: "DCA Returns by Year",
bottom: dcaClasses.map(({ year, color, defaultActive, returns }) =>
/** @type {AnyFetchedSeriesBlueprint} */ ({
metric: returns,
title: `${year}`,
type: "Baseline",
color,
defaultActive,
unit: "percentage",
}),
),
},
{
name: "Stack",
title: "DCA Stack by Year",
bottom: dcaClasses.map(({ year, color, defaultActive, stack }) =>
s({ metric: stack, name: `${year}`, color, defaultActive, unit: "sats" }),
),
},
],
},
],
},
],
};
}
@@ -0,0 +1,181 @@
/** Series helpers for creating chart series blueprints */
/**
* Create a single series from a tree accessor
* @param {Object} args
* @param {MetricAccessor<any>} args.metric - Tree accessor with .by property
* @param {string} args.name - Display name for the series
* @param {Color} [args.color]
* @param {Unit} [args.unit]
* @param {boolean} [args.defaultActive]
* @param {LineSeriesPartialOptions} [args.options]
* @returns {AnyFetchedSeriesBlueprint}
*/
export function s({ metric, name, color, defaultActive, unit, options }) {
return {
metric,
title: name,
color,
unit,
defaultActive,
options,
};
}
/**
* Create series from a BlockCountPattern ({ base, sum, cumulative })
* @param {Colors} colors
* @param {BlockCountPattern<any>} pattern
* @param {string} title
* @param {Color} [color]
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function fromBlockCount(colors, pattern, title, color) {
return [
{ metric: pattern.base, title, color: color ?? colors.default },
{
metric: pattern.sum,
title: `${title} (sum)`,
color: colors.red,
defaultActive: false,
},
{
metric: pattern.cumulative,
title: `${title} (cum.)`,
color: colors.cyan,
defaultActive: false,
},
];
}
/**
* Create series from a BitcoinPattern ({ base, sum, cumulative, average, min, max, median, pct* })
* @param {Colors} colors
* @param {BitcoinPattern<any>} pattern
* @param {string} title
* @param {Color} [color]
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function fromBitcoin(colors, pattern, title, color) {
return [
{ metric: pattern.base, title, color: color ?? colors.default },
{ metric: pattern.average, title: "Average", defaultActive: false },
{
metric: pattern.sum,
title: `${title} (sum)`,
color: colors.red,
defaultActive: false,
},
{
metric: pattern.cumulative,
title: `${title} (cum.)`,
color: colors.cyan,
defaultActive: false,
},
{
metric: pattern.max,
title: "Max",
color: colors.pink,
defaultActive: false,
},
{
metric: pattern.min,
title: "Min",
color: colors.green,
defaultActive: false,
},
{
metric: pattern.median,
title: "Median",
color: colors.amber,
defaultActive: false,
},
{
metric: pattern.pct75,
title: "pct75",
color: colors.red,
defaultActive: false,
},
{
metric: pattern.pct25,
title: "pct25",
color: colors.yellow,
defaultActive: false,
},
{
metric: pattern.pct90,
title: "pct90",
color: colors.rose,
defaultActive: false,
},
{
metric: pattern.pct10,
title: "pct10",
color: colors.lime,
defaultActive: false,
},
];
}
/**
* Create series from a BlockSizePattern ({ sum, cumulative, average, min, max, median, pct* })
* @param {Colors} colors
* @param {BlockSizePattern<any>} pattern
* @param {string} title
* @param {Color} [color]
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function fromBlockSize(colors, pattern, title, color) {
return [
{ metric: pattern.sum, title, color: color ?? colors.default },
{ metric: pattern.average, title: "Average", defaultActive: false },
{
metric: pattern.cumulative,
title: `${title} (cum.)`,
color: colors.cyan,
defaultActive: false,
},
{
metric: pattern.max,
title: "Max",
color: colors.pink,
defaultActive: false,
},
{
metric: pattern.min,
title: "Min",
color: colors.green,
defaultActive: false,
},
{
metric: pattern.median,
title: "Median",
color: colors.amber,
defaultActive: false,
},
{
metric: pattern.pct75,
title: "pct75",
color: colors.red,
defaultActive: false,
},
{
metric: pattern.pct25,
title: "pct25",
color: colors.yellow,
defaultActive: false,
},
{
metric: pattern.pct90,
title: "pct90",
color: colors.rose,
defaultActive: false,
},
{
metric: pattern.pct10,
title: "pct10",
color: colors.lime,
defaultActive: false,
},
];
}
@@ -0,0 +1,185 @@
/**
* @typedef {Object} BaseSeriesBlueprint
* @property {string} title
* @property {boolean} [defaultActive]
*
* @typedef {Object} BaselineSeriesBlueprintSpecific
* @property {"Baseline"} type
* @property {Color} [color]
* @property {[Color, Color]} [colors]
* @property {BaselineSeriesPartialOptions} [options]
* @property {Accessor<BaselineData[]>} [data]
* @typedef {BaseSeriesBlueprint & BaselineSeriesBlueprintSpecific} BaselineSeriesBlueprint
*
* @typedef {Object} CandlestickSeriesBlueprintSpecific
* @property {"Candlestick"} type
* @property {Color} [color]
* @property {CandlestickSeriesPartialOptions} [options]
* @property {Accessor<CandlestickData[]>} [data]
* @typedef {BaseSeriesBlueprint & CandlestickSeriesBlueprintSpecific} CandlestickSeriesBlueprint
*
* @typedef {Object} LineSeriesBlueprintSpecific
* @property {"Line"} [type]
* @property {Color} [color]
* @property {LineSeriesPartialOptions} [options]
* @property {Accessor<LineData[]>} [data]
* @typedef {BaseSeriesBlueprint & LineSeriesBlueprintSpecific} LineSeriesBlueprint
*
* @typedef {Object} HistogramSeriesBlueprintSpecific
* @property {"Histogram"} type
* @property {Color} color
* @property {HistogramSeriesPartialOptions} [options]
* @property {Accessor<HistogramData[]>} [data]
* @typedef {BaseSeriesBlueprint & HistogramSeriesBlueprintSpecific} HistogramSeriesBlueprint
*
* @typedef {BaselineSeriesBlueprint | CandlestickSeriesBlueprint | LineSeriesBlueprint | HistogramSeriesBlueprint} AnySeriesBlueprint
*
* @typedef {AnySeriesBlueprint["type"]} SeriesType
*
* @typedef {{ metric: MetricAccessor<any>, unit?: Unit }} FetchedAnySeriesOptions
*
* @typedef {BaselineSeriesBlueprint & FetchedAnySeriesOptions} FetchedBaselineSeriesBlueprint
* @typedef {CandlestickSeriesBlueprint & FetchedAnySeriesOptions} FetchedCandlestickSeriesBlueprint
* @typedef {LineSeriesBlueprint & FetchedAnySeriesOptions} FetchedLineSeriesBlueprint
* @typedef {HistogramSeriesBlueprint & FetchedAnySeriesOptions} FetchedHistogramSeriesBlueprint
* @typedef {AnySeriesBlueprint & FetchedAnySeriesOptions} AnyFetchedSeriesBlueprint
*
* @typedef {Object} PartialOption
* @property {string} name
*
* @typedef {Object} ProcessedOptionAddons
* @property {string} title
* @property {string[]} path
*
* @typedef {Object} PartialExplorerOptionSpecific
* @property {"explorer"} kind
* @property {string} title
*
* @typedef {PartialOption & PartialExplorerOptionSpecific} PartialExplorerOption
*
* @typedef {Required<PartialExplorerOption> & ProcessedOptionAddons} ExplorerOption
*
* @typedef {Object} PartialChartOptionSpecific
* @property {"chart"} [kind]
* @property {string} title
* @property {AnyFetchedSeriesBlueprint[]} [top]
* @property {AnyFetchedSeriesBlueprint[]} [bottom]
*
* @typedef {PartialOption & PartialChartOptionSpecific} PartialChartOption
*
* @typedef {Object} ProcessedChartOptionAddons
* @property {Record<Unit, AnyFetchedSeriesBlueprint[]>} top
* @property {Record<Unit, AnyFetchedSeriesBlueprint[]>} bottom
*
* @typedef {Required<Omit<PartialChartOption, "top" | "bottom">> & ProcessedChartOptionAddons & ProcessedOptionAddons} ChartOption
*
* @typedef {Object} PartialTableOptionSpecific
* @property {"table"} kind
* @property {string} title
*
* @typedef {PartialOption & PartialTableOptionSpecific} PartialTableOption
*
* @typedef {Required<PartialTableOption> & ProcessedOptionAddons} TableOption
*
* @typedef {Object} PartialSimulationOptionSpecific
* @property {"simulation"} kind
* @property {string} title
*
* @typedef {PartialOption & PartialSimulationOptionSpecific} PartialSimulationOption
*
* @typedef {Required<PartialSimulationOption> & ProcessedOptionAddons} SimulationOption
*
* @typedef {Object} PartialUrlOptionSpecific
* @property {"url"} [kind]
* @property {() => string} url
* @property {string} title
* @property {boolean} [qrcode]
*
* @typedef {PartialOption & PartialUrlOptionSpecific} PartialUrlOption
*
* @typedef {Required<PartialUrlOption> & ProcessedOptionAddons} UrlOption
*
* @typedef {PartialExplorerOption | PartialChartOption | PartialTableOption | PartialSimulationOption | PartialUrlOption} AnyPartialOption
*
* @typedef {ExplorerOption | ChartOption | TableOption | SimulationOption | UrlOption} Option
*
* @typedef {(AnyPartialOption | PartialOptionsGroup)[]} PartialOptionsTree
*
* @typedef {Object} PartialOptionsGroup
* @property {string} name
* @property {PartialOptionsTree} tree
*
* @typedef {Object} OptionsGroup
* @property {string} name
* @property {OptionsTree} tree
*
* @typedef {(Option | OptionsGroup)[]} OptionsTree
*
* @typedef {Object} UtxoCohortObject
* @property {string} name
* @property {string} title
* @property {Color} color
* @property {UtxoCohortPattern} tree
*
* Age cohorts (term, maxAge, minAge, ageRange, epoch) - have price percentiles
* @typedef {Object} AgeCohortObject
* @property {string} name
* @property {string} title
* @property {Color} color
* @property {PatternWithPricePercentiles} tree
*
* Amount cohorts (geAmount, ltAmount, amountRange, type) - no price percentiles
* @typedef {Object} AmountCohortObject
* @property {string} name
* @property {string} title
* @property {Color} color
* @property {UtxoAmountPattern} tree
*
* @typedef {Object} AddressCohortObject
* @property {string} name
* @property {string} title
* @property {Color} color
* @property {AddressCohortPattern} tree
*
* @typedef {UtxoCohortObject | AddressCohortObject} CohortObject
*
* @typedef {Object} UtxoCohortGroupObject
* @property {string} name
* @property {string} title
* @property {readonly UtxoCohortObject[]} list
*
* @typedef {Object} AgeCohortGroupObject
* @property {string} name
* @property {string} title
* @property {readonly AgeCohortObject[]} list
*
* @typedef {Object} AmountCohortGroupObject
* @property {string} name
* @property {string} title
* @property {readonly AmountCohortObject[]} list
*
* @typedef {Object} AddressCohortGroupObject
* @property {string} name
* @property {string} title
* @property {readonly AddressCohortObject[]} list
*
* @typedef {UtxoCohortGroupObject | AddressCohortGroupObject} CohortGroupObject
*
* @typedef {Object} PartialContext
* @property {Colors} colors
* @property {BrkClient} brk
* @property {BrkClient["tree"]["computed"]["constants"]} constants
* @property {(args: { metric: MetricAccessor<any>, name: string, color?: Color, defaultActive?: boolean, unit?: Unit, options?: LineSeriesPartialOptions }) => AnyFetchedSeriesBlueprint} s
* @property {(pattern: BlockCountPattern<any>, title: string, color?: Color) => AnyFetchedSeriesBlueprint[]} fromBlockCount
* @property {(pattern: BitcoinPattern<any>, title: string, color?: Color) => AnyFetchedSeriesBlueprint[]} fromBitcoin
* @property {(pattern: BlockSizePattern<any>, title: string, color?: Color) => AnyFetchedSeriesBlueprint[]} fromBlockSize
* @property {(num: number) => Constant0Pattern<any>} getConstant
* @property {(pattern: Constant0Pattern<any>) => MetricAccessor<any>} flattenConstant
* @property {(args: { number?: number, name?: string, defaultActive?: boolean, lineStyle?: number, color?: Color, unit: Unit }) => FetchedLineSeriesBlueprint} createPriceLine
* @property {(args: { numbers: number[], unit: Unit }) => FetchedLineSeriesBlueprint[]} createPriceLines
* @property {(args: { constant: Constant0Pattern<any>, name: string, unit: Unit, color?: Color, lineStyle?: number, defaultActive?: boolean }) => FetchedLineSeriesBlueprint} line
* @property {MetricAccessor<any>} constant100
*/
// Re-export for type consumers
export {};
@@ -0,0 +1,44 @@
/** Track unused metrics (dev only) */
import { localhost } from "../utils/env.js";
/** @type {Set<MetricAccessor<any>> | null} */
export const unused = localhost ? new Set() : null;
/**
* Walk and collect MetricAccessors
* @param {TreeNode | null | undefined} node
* @param {Set<MetricAccessor<unknown>>} set
*/
function walk(node, set) {
if (node && "by" in node) {
set.add(/** @type {MetricAccessor<unknown>} */ (node));
} else if (node && typeof node === "object") {
for (const value of Object.values(node)) {
walk(/** @type {TreeNode | null | undefined} */ (value), set);
}
}
}
/**
* Collect all MetricAccessors from tree
* @param {TreeNode} tree
*/
export function collect(tree) {
if (unused) walk(tree, unused);
}
/**
* Mark a metric as used
* @param {MetricAccessor<any>} metric
*/
export function markUsed(metric) {
unused?.delete(metric);
}
/** Log unused metrics to console */
export function logUnused() {
if (!unused?.size) return;
const paths = [...unused].map((m) => Object.values(m.by)[0].path);
console.warn("Unused metrics:", paths);
}
+19 -15
View File
@@ -2,11 +2,11 @@ import {
createShadow,
createHorizontalChoiceField,
createHeader,
} from "../../core/dom";
import { chartElement } from "../../core/elements";
import { ios, canShare } from "../../core/env";
import { serdeChartableIndex, serdeOptNumber } from "../../core/serde";
import { throttle } from "../../core/timing";
} from "../../utils/dom";
import { chartElement } from "../../utils/elements";
import { ios, canShare } from "../../utils/env";
import { serdeChartableIndex, serdeOptNumber } from "../../utils/serde";
import { throttle } from "../../utils/timing";
const keyPrefix = "chart";
const ONE_BTC_IN_SATS = 100_000_000;
@@ -26,7 +26,7 @@ const CANDLE = "candle";
* @param {Signals} args.signals
* @param {WebSockets} args.webSockets
* @param {Resources} args.resources
* @param {BRK} args.brk
* @param {BrkClient} args.brk
*/
export function init({
colors,
@@ -78,6 +78,7 @@ export function init({
colors,
id: "charts",
resources,
brk,
index,
timeScaleSetCallback: (unknownTimeScaleCallback) => {
// TODO: Although it mostly works in practice, need to make it more robust, there is no guarantee that this runs in order and wait for `from` and `to` to update when `index` and thus `TIMERANGE_LS_KEY` is updated
@@ -327,7 +328,7 @@ export function init({
case null:
case CANDLE: {
series = chart.addCandlestickSeries({
metric: "price_ohlc",
metric: brk.tree.computed.price.usd.priceOhlc,
name: "Price",
unit: topUnit,
setDataCallback: printLatest,
@@ -337,7 +338,7 @@ export function init({
}
case LINE: {
series = chart.addLineSeries({
metric: "price_close",
metric: brk.tree.computed.price.usd.priceClose,
name: "Price",
unit: topUnit,
color: colors.default,
@@ -357,7 +358,7 @@ export function init({
case null:
case CANDLE: {
series = chart.addCandlestickSeries({
metric: "price_ohlc_in_sats",
metric: brk.tree.computed.price.sats.priceOhlcInSats,
name: "Price",
unit: topUnit,
inverse: true,
@@ -368,7 +369,7 @@ export function init({
}
case LINE: {
series = chart.addLineSeries({
metric: "price_close_in_sats",
metric: brk.tree.computed.price.sats.priceCloseInSats,
name: "Price",
unit: topUnit,
color: colors.default,
@@ -432,7 +433,8 @@ export function init({
blueprints[unit]?.forEach((blueprint, order) => {
order += orderStart;
const indexes = brk.getIndexesFromMetric(blueprint.metric);
// Tree-first: metric is now an accessor with .by property
const indexes = Object.keys(blueprint.metric.by);
if (indexes.includes(index)) {
switch (blueprint.type) {
@@ -503,7 +505,7 @@ export function init({
/**
* @param {Object} args
* @param {Accessor<ChartOption>} args.option
* @param {BRK} args.brk
* @param {BrkClient} args.brk
* @param {Signals} args.signals
*/
function createIndexSelector({ option, brk, signals }) {
@@ -530,9 +532,11 @@ function createIndexSelector({ option, brk, signals }) {
const rawIndexes = new Set(
[Object.values(o.top), Object.values(o.bottom)]
.flat(2)
.filter((blueprint) => !blueprint.metric.startsWith("constant_"))
.map((blueprint) => brk.getIndexesFromMetric(blueprint.metric))
.flat(),
.filter((blueprint) => {
const path = Object.values(blueprint.metric.by)[0]?.path ?? "";
return !path.includes("constant_");
})
.flatMap((blueprint) => blueprint.metric.indexes()),
);
const serializedIndexes = [...rawIndexes].flatMap((index) => {
@@ -1,4 +1,4 @@
import { ios } from "../../core/env";
import { ios } from "../../utils/env";
import { domToBlob } from "../../modules/modern-screenshot/4.6.6/dist/index.mjs";
/**
+3 -3
View File
@@ -1,5 +1,5 @@
import { randomFromArray } from "../core/array";
import { explorerElement } from "../core/elements";
import { randomFromArray } from "../utils/array";
import { explorerElement } from "../utils/elements";
/**
* @param {Object} args
@@ -9,7 +9,7 @@ import { explorerElement } from "../core/elements";
* @param {Signals} args.signals
* @param {WebSockets} args.webSockets
* @param {Resources} args.resources
* @param {BRK} args.brk
* @param {BrkClient} args.brk
*/
export function init({
colors,
+7 -5
View File
@@ -1,21 +1,23 @@
// @ts-nocheck
import {
createDateRange,
dateToDateIndex,
differenceBetweenDates,
} from "../core/date";
} from "../utils/date";
import {
createButtonElement,
createFieldElement,
createHeader,
createSelect,
} from "../core/dom";
import { simulationElement } from "../core/elements";
} from "../utils/dom";
import { simulationElement } from "../utils/elements";
import {
numberToDollars,
numberToPercentage,
numberToUSNumber,
} from "../core/format";
import { serdeDate, serdeOptDate, serdeOptNumber } from "../core/serde";
} from "../utils/format";
import { serdeDate, serdeOptDate, serdeOptNumber } from "../utils/serde";
/**
* @param {Object} args
+9 -7
View File
@@ -1,15 +1,17 @@
import { randomFromArray } from "../core/array";
import { createButtonElement, createHeader, createSelect } from "../core/dom";
import { tableElement } from "../core/elements";
import { serdeMetrics, serdeString, serdeUnit } from "../core/serde";
import { resetParams } from "../core/url";
// @ts-nocheck
import { randomFromArray } from "../utils/array";
import { createButtonElement, createHeader, createSelect } from "../utils/dom";
import { tableElement } from "../utils/elements";
import { serdeMetrics, serdeString, serdeUnit } from "../utils/serde";
import { resetParams } from "../utils/url";
/**
* @param {Object} args
* @param {Signals} args.signals
* @param {Option} args.option
* @param {Resources} args.resources
* @param {BRK} args.brk
* @param {BrkClient} args.brk
*/
export function init({ signals, option, resources, brk }) {
tableElement.innerHTML = "wip, will hopefuly be back soon, sorry !";
@@ -46,7 +48,7 @@ export function init({ signals, option, resources, brk }) {
// * @param {Object} args
// * @param {Option} args.option
// * @param {Signals} args.signals
// * @param {BRK} args.brk
// * @param {BrkClient} args.brk
// * @param {Resources} args.resources
// */
// function createTable({ brk, signals, option, resources }) {
+128
View File
@@ -0,0 +1,128 @@
/**
* @import { Signal, Signals } from "./signals";
* @import { MetricNode } from "./modules/brk-client/index";
*/
/**
* @template T
* @typedef {Object} Resource
* @property {Signal<T | null>} data
* @property {Signal<boolean>} loading
* @property {Signal<Error | null>} error
* @property {(...args: any[]) => Promise<T | null>} fetch
*/
/**
* @template T
* @typedef {Object} RangeState
* @property {Signal<T | null>} response
* @property {Signal<boolean>} loading
*/
/**
* @template T
* @typedef {Object} MetricResource
* @property {string} path
* @property {(from?: number, to?: number) => RangeState<T>} range
* @property {(from?: number, to?: number) => Promise<T[] | null>} fetch
*/
/**
* @typedef {ReturnType<typeof createResources>} Resources
*/
/**
* @param {Signals} signals
*/
export function createResources(signals) {
const owner = signals.getOwner();
/**
* Create a generic reactive resource wrapper for any async fetcher
* @template T
* @template {any[]} Args
* @param {(...args: Args) => Promise<T>} fetcher
* @returns {Resource<T>}
*/
function createResource(fetcher) {
return signals.runWithOwner(owner, () => {
const data = signals.createSignal(/** @type {T | null} */ (null));
const loading = signals.createSignal(false);
const error = signals.createSignal(/** @type {Error | null} */ (null));
return {
data,
loading,
error,
/**
* @param {Args} args
*/
async fetch(...args) {
loading.set(true);
error.set(null);
try {
const result = await fetcher(...args);
data.set(result);
return result;
} catch (e) {
error.set(e instanceof Error ? e : new Error(String(e)));
return null;
} finally {
loading.set(false);
}
},
};
});
}
/**
* Create a reactive resource wrapper for a MetricNode with multi-range support
* @template T
* @param {MetricNode<T>} node
* @returns {MetricResource<T>}
*/
function useMetricNode(node) {
return signals.runWithOwner(owner, () => {
/** @type {Map<string, RangeState<T>>} */
const ranges = new Map();
/**
* Get or create range state
* @param {number} [from=-10000]
* @param {number} [to]
*/
function range(from = -10000, to) {
const key = `${from}-${to ?? ""}`;
if (!ranges.has(key)) {
ranges.set(key, {
response: signals.createSignal(/** @type {T | null} */ (null)),
loading: signals.createSignal(false),
});
}
return /** @type {RangeState<T>} */ (ranges.get(key));
}
return {
path: node.path,
range,
/**
* Fetch data for a range
* @param {number} [from=-10000]
* @param {number} [to]
*/
async fetch(from = -10000, to) {
const r = range(from, to);
r.loading.set(true);
try {
const result = await node.range(from, to, r.response.set);
return result;
} finally {
r.loading.set(false);
}
},
};
});
}
return { createResource, useMetricNode };
}
+218
View File
@@ -0,0 +1,218 @@
/**
* @import { SignalOptions } from "./modules/solidjs-signals/0.6.3/dist/types/core/core"
* @import { getOwner as GetOwner, onCleanup as OnCleanup } from "./modules/solidjs-signals/0.6.3/dist/types/core/owner"
* @import { createSignal as CreateSignal, createEffect as CreateEffect, createMemo as CreateMemo, createRoot as CreateRoot, runWithOwner as RunWithOwner, Setter } from "./modules/solidjs-signals/0.6.3/dist/types/signals";
*/
/**
* @template T
* @typedef {() => T} Accessor
*/
/**
* @template T
* @typedef {Accessor<T> & { set: Setter<T>; reset: VoidFunction }} Signal
*/
import {
createSignal,
createEffect,
getOwner,
createMemo,
createRoot,
runWithOwner,
onCleanup,
} from "./modules/solidjs-signals/0.6.3/dist/prod.js";
let effectCount = 0;
const signals = {
createSolidSignal: /** @type {typeof CreateSignal} */ (createSignal),
createSolidEffect: /** @type {typeof CreateEffect} */ (createEffect),
createEffect: /** @type {typeof CreateEffect} */ (
// @ts-ignore
(compute, effect) => {
let dispose = /** @type {VoidFunction | null} */ (null);
if (getOwner() === null) {
throw Error("No owner");
}
function cleanup() {
if (dispose) {
dispose();
dispose = null;
// console.log("effectCount = ", --effectCount);
}
}
// @ts-ignore
createEffect(compute, (v, oldV) => {
// console.log("effectCount = ", ++effectCount);
cleanup();
signals.createRoot((_dispose) => {
dispose = _dispose;
return effect(v, oldV);
});
signals.onCleanup(cleanup);
});
signals.onCleanup(cleanup);
}
),
createMemo: /** @type {typeof CreateMemo} */ (createMemo),
createRoot: /** @type {typeof CreateRoot} */ (createRoot),
getOwner: /** @type {typeof GetOwner} */ (getOwner),
runWithOwner: /** @type {typeof RunWithOwner} */ (runWithOwner),
onCleanup: /** @type {typeof OnCleanup} */ (onCleanup),
/**
* @template T
* @param {T} initialValue
* @param {SignalOptions<T> & {save?: {keyPrefix: string | Accessor<string>; key: string; serialize: (v: T) => string; deserialize: (v: string) => T; serializeParam?: boolean; saveDefaultValue?: boolean}}} [options]
* @returns {Signal<T>}
*/
createSignal(initialValue, options) {
const [get, set] = this.createSolidSignal(
/** @type {any} */ (initialValue),
options,
);
// @ts-ignore
get.set = set;
// @ts-ignore
get.reset = () => set(initialValue);
if (options?.save) {
const save = options.save;
const paramKey = save.key;
const storageKey = this.createMemo(
() =>
`${
typeof save.keyPrefix === "string"
? save.keyPrefix
: save.keyPrefix()
}-${paramKey}`,
);
/** @type { ((this: Window, ev: PopStateEvent) => any) | undefined} */
let popstateCallback;
let serialized = /** @type {string | null} */ (null);
if (options.save.serializeParam !== false) {
serialized = new URLSearchParams(window.location.search).get(paramKey);
// popstateCallback =
// /** @type {(this: Window, ev: PopStateEvent) => any} */ (
// (_) => {
// serialized = new URLSearchParams(window.location.search).get(
// paramKey,
// );
// set(() =>
// serialized ? save.deserialize(serialized) : initialValue,
// );
// }
// );
// if (!popstateCallback) throw "Unreachable";
// window.addEventListener("popstate", popstateCallback);
// signals.onCleanup(() => {
// if (popstateCallback)
// window.removeEventListener("popstate", popstateCallback);
// });
}
if (serialized === null) {
try {
serialized = localStorage.getItem(storageKey());
} catch (_) {}
}
if (serialized) {
set(() => (serialized ? save.deserialize(serialized) : initialValue));
}
let firstRun1 = true;
this.createEffect(storageKey, (storageKey) => {
if (!firstRun1) {
try {
serialized = localStorage.getItem(storageKey);
set(() =>
serialized ? save.deserialize(serialized) : initialValue,
);
} catch (_) {}
}
firstRun1 = false;
});
let firstRun2 = true;
this.createEffect(get, (value) => {
if (!save) return;
if (!firstRun2) {
try {
if (
value !== undefined &&
value !== null &&
(initialValue === undefined ||
initialValue === null ||
save.saveDefaultValue ||
save.serialize(value) !== save.serialize(initialValue))
) {
localStorage.setItem(storageKey(), save.serialize(value));
} else {
localStorage.removeItem(storageKey());
}
} catch (_) {}
}
if (
value !== undefined &&
value !== null &&
(initialValue === undefined ||
initialValue === null ||
save.saveDefaultValue ||
save.serialize(value) !== save.serialize(initialValue))
) {
writeParam(paramKey, save.serialize(value));
} else {
removeParam(paramKey);
}
firstRun2 = false;
});
}
// @ts-ignore
return get;
},
};
/** @typedef {typeof signals} Signals */
/**
* @param {string} key
* @param {string | undefined} value
*/
function writeParam(key, value) {
const urlParams = new URLSearchParams(window.location.search);
if (value !== undefined) {
urlParams.set(key, String(value));
} else {
urlParams.delete(key);
}
try {
window.history.replaceState(
null,
"",
`${window.location.pathname}?${urlParams.toString()}`,
);
} catch (_) {}
}
/**
* @param {string} key
*/
function removeParam(key) {
writeParam(key, undefined);
}
export default signals;
@@ -1,4 +1,5 @@
const localhost = window.location.hostname === "localhost";
console.log({ localhost });
export const serdeString = {
/**
@@ -249,6 +250,7 @@ export const serdeUnit = {
if (
(!unit || localhost) &&
(v.includes("in_sats") ||
v.endsWith("_sats") ||
(v.endsWith("supply") &&
!(v.endsWith("circulating_supply") || v.endsWith("_own_supply"))) ||
v === "sent" ||
@@ -313,8 +315,10 @@ export const serdeUnit = {
v.endsWith("value_created") ||
v.endsWith("value_destroyed") ||
((v.includes("realized") || v.includes("true_market_mean")) &&
!v.includes("unrealized") &&
!v.includes("ratio") &&
!v.includes("rel_to")) ||
(v.includes("unrealized") && !v.includes("rel_to")) ||
((v.endsWith("sma") || v.includes("sma_x") || v.endsWith("ema")) &&
!v.includes("ratio") &&
!v.includes("sopr") &&
+2
View File
@@ -3,6 +3,8 @@
"allowJs": true,
"checkJs": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"target": "ESNext",
"module": "ESNext",
"outDir": "/tmp/brk",