mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-06-07 21:51:56 -07:00
global: MASSIVE snapshot
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
# Types
|
||||
|
||||
To check types run:
|
||||
|
||||
```sh
|
||||
npx --package typescript tsc --noEmit --pretty false | grep -v "modules/"
|
||||
```
|
||||
+38
-24
@@ -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]
|
||||
@@ -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`
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
+14
-12
@@ -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 =
|
||||
+1461
-1380
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }) {
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -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") &&
|
||||
@@ -3,6 +3,8 @@
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"outDir": "/tmp/brk",
|
||||
|
||||
Reference in New Issue
Block a user