mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-04-30 01:20:00 -07:00
global: MASSIVE snapshot
This commit is contained in:
@@ -13,12 +13,12 @@ import {
|
||||
createHorizontalChoiceField,
|
||||
createLabeledInput,
|
||||
createSpanName,
|
||||
} from "../utils/dom";
|
||||
import { createOklchToRGBA } from "./oklch";
|
||||
import { throttle } from "../utils/timing";
|
||||
import { serdeBool } from "../utils/serde";
|
||||
import { stringToId } from "../utils/format";
|
||||
import { style } from "../utils/elements";
|
||||
} from "../utils/dom.js";
|
||||
import { createOklchToRGBA } from "./oklch.js";
|
||||
import { throttle } from "../utils/timing.js";
|
||||
import { serdeBool } from "../utils/serde.js";
|
||||
import { stringToId } from "../utils/format.js";
|
||||
import { style } from "../utils/elements.js";
|
||||
|
||||
/**
|
||||
* @typedef {Object} Valued
|
||||
@@ -52,7 +52,12 @@ import { style } from "../utils/elements";
|
||||
* @typedef {_BaselineData<number>} BaselineData
|
||||
* @typedef {_HistogramData<number>} HistogramData
|
||||
*
|
||||
* @typedef {function({ iseries: ISeries; unit: Unit; index: IndexName }): void} SetDataCallback
|
||||
* @typedef {function({ iseries: ISeries; unit: Unit; index: ChartableIndex }): void} SetDataCallback
|
||||
*
|
||||
* @typedef {Object} Legend
|
||||
* @property {HTMLLegendElement} element
|
||||
* @property {function({ series: Series, name: string, order: number, colors: Color[] }): void} addOrReplace
|
||||
* @property {function(number): void} removeFrom
|
||||
*/
|
||||
|
||||
const oklchToRGBA = createOklchToRGBA();
|
||||
@@ -67,7 +72,7 @@ const lineWidth = /** @type {any} */ (1.5);
|
||||
* @param {Colors} args.colors
|
||||
* @param {Resources} args.resources
|
||||
* @param {BrkClient} args.brk
|
||||
* @param {Accessor<IndexName>} args.index
|
||||
* @param {Accessor<ChartableIndex>} args.index
|
||||
* @param {((unknownTimeScaleCallback: VoidFunction) => void)} [args.timeScaleSetCallback]
|
||||
* @param {true} [args.fitContent]
|
||||
* @param {{unit: Unit; blueprints: AnySeriesBlueprint[]}[]} [args.config]
|
||||
@@ -191,10 +196,7 @@ function createChartElement({
|
||||
|
||||
ichart.applyOptions({
|
||||
timeScale: {
|
||||
timeVisible:
|
||||
index === "height" ||
|
||||
index === "difficultyepoch" ||
|
||||
index === "halvingepoch",
|
||||
timeVisible: index === "height",
|
||||
...(!fitContent
|
||||
? {
|
||||
minBarSpacing,
|
||||
@@ -288,7 +290,7 @@ function createChartElement({
|
||||
choices: /** @type {const} */ (["lin", "log"]),
|
||||
id: stringToId(`${id} ${paneIndex} ${unit}`),
|
||||
defaultValue:
|
||||
unit === "usd" && seriesType !== "Baseline" ? "log" : "lin",
|
||||
unit.id === "usd" && seriesType !== "Baseline" ? "log" : "lin",
|
||||
key: `${id}-price-scale-${paneIndex}`,
|
||||
signals,
|
||||
});
|
||||
@@ -379,17 +381,20 @@ function createChartElement({
|
||||
signals.createEffect(index, (index) => {
|
||||
// Get timestamp metric from tree based on index type
|
||||
// timestampFixed has height only, timestamp has date-based indexes
|
||||
/** @type {AnyMetricPattern} */
|
||||
const timeMetric =
|
||||
index === "height"
|
||||
? brk.tree.computed.blocks.time.timestampFixed
|
||||
: brk.tree.computed.blocks.time.timestamp;
|
||||
/** @type {AnyMetricPattern} */
|
||||
const valuesMetric = metric;
|
||||
const timeNode = timeMetric.by[index];
|
||||
const valuesNode = metric.by[index];
|
||||
const valuesNode = valuesMetric.by[index];
|
||||
if (!timeNode || !valuesNode)
|
||||
throw new Error(`Missing node for index: ${index}`);
|
||||
|
||||
const timeResource = resources.useMetricNode(timeNode);
|
||||
const valuesResource = resources.useMetricNode(valuesNode);
|
||||
const timeResource = resources.useMetricEndpoint(timeNode);
|
||||
const valuesResource = resources.useMetricEndpoint(valuesNode);
|
||||
_valuesResource = valuesResource;
|
||||
|
||||
series.url.set(() => `${brk.baseUrl}${valuesResource.path}`);
|
||||
@@ -706,7 +711,7 @@ function createChartElement({
|
||||
data,
|
||||
options,
|
||||
}) {
|
||||
color ||= unit === "usd" ? colors.green : colors.orange;
|
||||
color ||= unit.id === "usd" ? colors.green : colors.orange;
|
||||
|
||||
/** @type {LineISeries} */
|
||||
const iseries = /** @type {any} */ (
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
/**
|
||||
* @import * as _ from "./modules/leeoniya-ufuzzy/1.0.19/dist/uFuzzy.d.ts"
|
||||
*
|
||||
* @import { IChartApi, ISeriesApi as _ISeriesApi, SeriesDefinition, SingleValueData as _SingleValueData, CandlestickData as _CandlestickData, BaselineData as _BaselineData, HistogramData as _HistogramData, SeriesType as LCSeriesType, IPaneApi, LineSeriesPartialOptions as _LineSeriesPartialOptions, HistogramSeriesPartialOptions as _HistogramSeriesPartialOptions, BaselineSeriesPartialOptions as _BaselineSeriesPartialOptions, CandlestickSeriesPartialOptions as _CandlestickSeriesPartialOptions, WhitespaceData, DeepPartial, ChartOptions, Time, LineData as _LineData, createChart as CreateChart, LineStyle } 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 as LCSeriesType, IPaneApi, LineSeriesPartialOptions as _LineSeriesPartialOptions, HistogramSeriesPartialOptions as _HistogramSeriesPartialOptions, BaselineSeriesPartialOptions as _BaselineSeriesPartialOptions, CandlestickSeriesPartialOptions as _CandlestickSeriesPartialOptions, WhitespaceData, DeepPartial, ChartOptions, Time, LineData as _LineData, createChart as CreateChart, LineStyle } from './modules/lightweight-charts/5.0.9/dist/typings.js'
|
||||
*
|
||||
* @import { Signal, Signals, Accessor } from "./signals";
|
||||
* @import { Signal, Signals, Accessor } from "./signals.js";
|
||||
*
|
||||
* @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, ActivePriceRatioPattern, _0satsPattern, UnclaimedRewardsPattern as ValuePattern, SentPattern as RewardPattern, Metric, MetricPattern, AnyMetricPattern } from "./modules/brk-client/index.js"
|
||||
* @import { BrkClient, CatalogTree_Computed_Distribution_UtxoCohorts as UtxoCohortTree, CatalogTree_Computed_Distribution_AddressCohorts as AddressCohortTree, CatalogTree_Computed_Distribution_UtxoCohorts_All as AllUtxoPattern, _10yTo12yPattern as MinAgePattern, _0satsPattern2 as UtxoAmountPattern, _0satsPattern as AddressAmountPattern, Ratio1ySdPattern, Dollars, Price111dSmaPattern as EmaRatioPattern, Index, BlockCountPattern, BitcoinPattern, BlockSizePattern, BlockIntervalPattern, CoinbasePattern, ActivePriceRatioPattern, _0satsPattern, UnclaimedRewardsPattern as ValuePattern, SentPattern as RewardPattern, Metric, MetricPattern, AnyMetricPattern, MetricEndpoint, MetricData, AnyMetricEndpoint, AnyMetricData, DollarsPattern, CountPattern2 } from "./modules/brk-client/index.js"
|
||||
*
|
||||
* @import { Resources, MetricResource } from './resources'
|
||||
* @import { Resources, MetricResource } from './resources.js'
|
||||
*
|
||||
* @import { Valued, SingleValueData, CandlestickData, Series, ISeries, HistogramData, LineData, BaselineData, LineSeriesPartialOptions, BaselineSeriesPartialOptions, HistogramSeriesPartialOptions, CandlestickSeriesPartialOptions, CreateChartElement, Chart } from "./chart/index"
|
||||
* @import { Valued, SingleValueData, CandlestickData, Series, ISeries, HistogramData, LineData, BaselineData, LineSeriesPartialOptions, BaselineSeriesPartialOptions, HistogramSeriesPartialOptions, CandlestickSeriesPartialOptions, CreateChartElement, Chart, Legend } from "./chart/index.js"
|
||||
*
|
||||
* @import { Color, ColorName, Colors } from "./utils/colors"
|
||||
* @import { Color, ColorName, Colors } from "./utils/colors.js"
|
||||
*
|
||||
* @import { WebSockets } from "./utils/ws"
|
||||
* @import { WebSockets } from "./utils/ws.js"
|
||||
*
|
||||
* @import { Option, PartialChartOption, ChartOption, AnyPartialOption, ProcessedOptionAddons, OptionsTree, SimulationOption, AnySeriesBlueprint, SeriesType, AnyFetchedSeriesBlueprint, TableOption, ExplorerOption, UrlOption, PartialOptionsGroup, OptionsGroup, PartialOptionsTree, UtxoCohortObject, AddressCohortObject, CohortObject, UtxoCohortGroupObject, AddressCohortGroupObject, CohortGroupObject, FetchedLineSeriesBlueprint, FetchedHistogramSeriesBlueprint, PartialContext, AgeCohortObject, AmountCohortObject, AgeCohortGroupObject, AmountCohortGroupObject } from "./options/partial/index.js"
|
||||
* @import { Option, PartialChartOption, ChartOption, AnyPartialOption, ProcessedOptionAddons, OptionsTree, SimulationOption, AnySeriesBlueprint, SeriesType, AnyFetchedSeriesBlueprint, TableOption, ExplorerOption, UrlOption, PartialOptionsGroup, OptionsGroup, PartialOptionsTree, UtxoCohortObject, AddressCohortObject, CohortObject, UtxoCohortGroupObject, AddressCohortGroupObject, CohortGroupObject, FetchedLineSeriesBlueprint, FetchedHistogramSeriesBlueprint, PartialContext, AgeCohortObject, AmountCohortObject, AgeCohortGroupObject, AmountCohortGroupObject } from "./options/partial.js"
|
||||
*
|
||||
* @import { Unit } from "./utils/serde"
|
||||
* @import { UnitObject as Unit } from "./utils/units.js"
|
||||
*
|
||||
* @import { ChartableIndexName } from "./panes/chart/index.js";
|
||||
*/
|
||||
@@ -34,7 +34,7 @@
|
||||
* @typedef {keyof PoolIdToPoolName} PoolId
|
||||
*
|
||||
* Pattern unions by cohort type
|
||||
* @typedef {AllUtxoPattern | MaxAgePattern | MinAgePattern | UtxoAmountPattern} UtxoCohortPattern
|
||||
* @typedef {AllUtxoPattern | MinAgePattern | UtxoAmountPattern} UtxoCohortPattern
|
||||
* @typedef {AddressAmountPattern} AddressCohortPattern
|
||||
* @typedef {UtxoCohortPattern | AddressCohortPattern} CohortPattern
|
||||
*
|
||||
@@ -44,7 +44,7 @@
|
||||
* @typedef {AllUtxoPattern | MinAgePattern | UtxoAmountPattern} PatternWithNupl
|
||||
* @typedef {AllUtxoPattern | MinAgePattern | UtxoAmountPattern} PatternWithCostBasis
|
||||
* @typedef {AllUtxoPattern | MinAgePattern | UtxoAmountPattern} PatternWithActivity
|
||||
* @typedef {AllUtxoPattern | MaxAgePattern | MinAgePattern} PatternWithCostBasisPercentiles
|
||||
* @typedef {AllUtxoPattern | MinAgePattern} PatternWithCostBasisPercentiles
|
||||
*
|
||||
* Cohort objects with specific pattern capabilities
|
||||
* @typedef {{ name: string, title: string, color: Color, tree: PatternWithRealizedPrice }} CohortWithRealizedPrice
|
||||
@@ -61,6 +61,9 @@
|
||||
*
|
||||
* Generic tree node type for walking
|
||||
* @typedef {AnyMetricPattern | Record<string, unknown>} TreeNode
|
||||
*
|
||||
* Chartable index IDs (subset of IndexName that can be charted)
|
||||
* @typedef {"height" | "dateindex" | "weekindex" | "monthindex" | "quarterindex" | "semesterindex" | "yearindex" | "decadeindex"} ChartableIndex
|
||||
*/
|
||||
|
||||
// DO NOT CHANGE, Exact format is expected in `brk_bundler`
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
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 "./utils/dom";
|
||||
import { next } from "./utils/timing";
|
||||
import { replaceHistory } from "./utils/url";
|
||||
import { removeStored, writeToStorage } from "./utils/storage";
|
||||
import { createColors } from "./utils/colors.js";
|
||||
import { createWebSockets } from "./utils/ws.js";
|
||||
import * as formatters from "./utils/format.js";
|
||||
import modules from "./lazy.js";
|
||||
import { onFirstIntersection, getElementById, isHidden } from "./utils/dom.js";
|
||||
import { next } from "./utils/timing.js";
|
||||
import { replaceHistory } from "./utils/url.js";
|
||||
import { removeStored, writeToStorage } from "./utils/storage.js";
|
||||
import {
|
||||
asideElement,
|
||||
asideLabelElement,
|
||||
@@ -164,7 +164,7 @@ Promise.all([
|
||||
qrcode,
|
||||
});
|
||||
|
||||
window.addEventListener("popstate", (event) => {
|
||||
window.addEventListener("popstate", (_event) => {
|
||||
const path = window.document.location.pathname
|
||||
.split("/")
|
||||
.filter((v) => v);
|
||||
@@ -375,7 +375,6 @@ Promise.all([
|
||||
await getFirstChild();
|
||||
if (!ul) throw Error("Unreachable");
|
||||
|
||||
let i = 0;
|
||||
while (path.length > 1) {
|
||||
const name = path.shift();
|
||||
if (!name) throw "Unreachable";
|
||||
@@ -440,7 +439,7 @@ Promise.all([
|
||||
/** @type {{ option: Option, title: string }[]} */
|
||||
let list = [];
|
||||
|
||||
let [indexes, info, order] = searchResult || [null, null, null];
|
||||
let [indexes, _info, order] = searchResult || [null, null, null];
|
||||
|
||||
const minIndex = pageIndex * RESULTS_PER_PAGE;
|
||||
|
||||
@@ -577,7 +576,6 @@ Promise.all([
|
||||
|
||||
const element = options.createOptionElement({
|
||||
option,
|
||||
frame: "search",
|
||||
name: title,
|
||||
qrcode,
|
||||
});
|
||||
|
||||
1242
websites/bitview/scripts/options/chain.js
Normal file
1242
websites/bitview/scripts/options/chain.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,7 @@
|
||||
* Address cohorts use _0satsPattern which has CostBasisPattern (no percentiles)
|
||||
*/
|
||||
|
||||
import { Unit } from "../../utils/units.js";
|
||||
import {
|
||||
createSingleSupplySeries,
|
||||
createGroupedSupplyTotalSeries,
|
||||
@@ -37,7 +38,10 @@ export function createAddressCohortFolder(ctx, args) {
|
||||
? {
|
||||
name: "supply",
|
||||
title: `Supply ${title}`,
|
||||
bottom: createSingleSupplySeries(ctx, /** @type {AddressCohortObject} */ (args), title),
|
||||
bottom: createSingleSupplySeries(
|
||||
ctx,
|
||||
/** @type {AddressCohortObject} */ (args),
|
||||
),
|
||||
}
|
||||
: {
|
||||
name: "supply",
|
||||
@@ -91,13 +95,23 @@ export function createAddressCohortFolder(ctx, args) {
|
||||
bottom: createRealizedPriceRatioSeries(ctx, list),
|
||||
},
|
||||
]
|
||||
: createRealizedPriceOptions(ctx, /** @type {AddressCohortObject} */ (args), title)),
|
||||
: createRealizedPriceOptions(
|
||||
ctx,
|
||||
/** @type {AddressCohortObject} */ (args),
|
||||
title,
|
||||
)),
|
||||
{
|
||||
name: "capitalization",
|
||||
title: `Realized Capitalization ${title}`,
|
||||
bottom: createRealizedCapWithExtras(ctx, list, args, useGroupName, title),
|
||||
bottom: createRealizedCapWithExtras(ctx, list, args, useGroupName),
|
||||
},
|
||||
...(!useGroupName ? createRealizedPnlSection(ctx, /** @type {AddressCohortObject} */ (args), title) : []),
|
||||
...(!useGroupName
|
||||
? createRealizedPnlSection(
|
||||
ctx,
|
||||
/** @type {AddressCohortObject} */ (args),
|
||||
title,
|
||||
)
|
||||
: []),
|
||||
],
|
||||
},
|
||||
|
||||
@@ -128,7 +142,14 @@ function createRealizedPriceOptions(ctx, args, title) {
|
||||
{
|
||||
name: "price",
|
||||
title: `Realized Price ${title}`,
|
||||
top: [s({ metric: tree.realized.realizedPrice, name: "realized", color })],
|
||||
top: [
|
||||
s({
|
||||
metric: tree.realized.realizedPrice,
|
||||
name: "Realized",
|
||||
color,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -139,24 +160,29 @@ function createRealizedPriceOptions(ctx, args, title) {
|
||||
* @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;
|
||||
function createRealizedCapWithExtras(ctx, list, args, useGroupName) {
|
||||
const { s, createPriceLine } = ctx;
|
||||
const isSingle = !("list" in args);
|
||||
|
||||
return list.flatMap(({ color, name, tree }) => [
|
||||
s({ metric: tree.realized.realizedCap, name: useGroupName ? name : "Capitalization", color }),
|
||||
s({
|
||||
metric: tree.realized.realizedCap,
|
||||
name: useGroupName ? name : "Capitalization",
|
||||
color,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
...(isSingle
|
||||
? [
|
||||
/** @type {AnyFetchedSeriesBlueprint} */ ({
|
||||
type: "Baseline",
|
||||
metric: tree.realized.realizedCap30dDelta,
|
||||
title: "30d change",
|
||||
title: "30d Change",
|
||||
unit: Unit.usd,
|
||||
defaultActive: false,
|
||||
}),
|
||||
createPriceLine({ unit: "usd", defaultActive: false }),
|
||||
createPriceLine({ unit: Unit.usd, defaultActive: false }),
|
||||
]
|
||||
: []),
|
||||
// RealizedPattern (address cohorts) doesn't have realizedCapRelToOwnMarketCap
|
||||
@@ -172,18 +198,50 @@ function createRealizedCapWithExtras(ctx, list, args, useGroupName, title) {
|
||||
*/
|
||||
function createRealizedPnlSection(ctx, args, title) {
|
||||
const { colors, s } = ctx;
|
||||
const { tree } = args;
|
||||
const { mergeMetricPatterns } = ctx.brk;
|
||||
const { realized } = args.tree;
|
||||
|
||||
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 }),
|
||||
s({
|
||||
metric: mergeMetricPatterns(
|
||||
realized.realizedProfit.base,
|
||||
realized.realizedProfit.sum,
|
||||
),
|
||||
name: "Profit",
|
||||
color: colors.green,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
s({
|
||||
metric: mergeMetricPatterns(
|
||||
realized.realizedLoss.base,
|
||||
realized.realizedLoss.sum,
|
||||
),
|
||||
name: "Loss",
|
||||
color: colors.red,
|
||||
unit: Unit.usd,
|
||||
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 }),
|
||||
s({
|
||||
metric: realized.totalRealizedPnl,
|
||||
name: "Total",
|
||||
color: colors.default,
|
||||
defaultActive: false,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
s({
|
||||
metric: mergeMetricPatterns(
|
||||
realized.negRealizedLoss.base,
|
||||
realized.negRealizedLoss.sum,
|
||||
),
|
||||
name: "Negative Loss",
|
||||
color: colors.red,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -212,7 +270,9 @@ function createUnrealizedSection(ctx, list, useGroupName, title) {
|
||||
type: "Baseline",
|
||||
metric: tree.unrealized.netUnrealizedPnl,
|
||||
title: useGroupName ? name : "NUPL",
|
||||
colors: [colors.red, colors.green],
|
||||
color: useGroupName ? color : undefined,
|
||||
colors: useGroupName ? undefined : [colors.red, colors.green],
|
||||
unit: Unit.ratio,
|
||||
options: { baseValue: { price: 0 } },
|
||||
}),
|
||||
]),
|
||||
@@ -221,14 +281,24 @@ function createUnrealizedSection(ctx, list, useGroupName, title) {
|
||||
name: "profit",
|
||||
title: `Unrealized Profit ${title}`,
|
||||
bottom: list.flatMap(({ color, name, tree }) => [
|
||||
s({ metric: tree.unrealized.unrealizedProfit, name: useGroupName ? name : "Profit", color }),
|
||||
s({
|
||||
metric: tree.unrealized.unrealizedProfit,
|
||||
name: useGroupName ? name : "Profit",
|
||||
color,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
]),
|
||||
},
|
||||
{
|
||||
name: "loss",
|
||||
title: `Unrealized Loss ${title}`,
|
||||
bottom: list.flatMap(({ color, name, tree }) => [
|
||||
s({ metric: tree.unrealized.unrealizedLoss, name: useGroupName ? name : "Loss", color }),
|
||||
s({
|
||||
metric: tree.unrealized.unrealizedLoss,
|
||||
name: useGroupName ? name : "Loss",
|
||||
color,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
]),
|
||||
},
|
||||
],
|
||||
@@ -255,14 +325,24 @@ function createCostBasisSection(ctx, list, useGroupName, title) {
|
||||
name: "min",
|
||||
title: `Min Cost Basis ${title}`,
|
||||
top: list.map(({ color, name, tree }) =>
|
||||
s({ metric: tree.costBasis.minCostBasis, name: useGroupName ? name : "Min", color }),
|
||||
s({
|
||||
metric: tree.costBasis.minCostBasis,
|
||||
name: useGroupName ? name : "Min",
|
||||
color,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "max",
|
||||
title: `Max Cost Basis ${title}`,
|
||||
top: list.map(({ color, name, tree }) =>
|
||||
s({ metric: tree.costBasis.maxCostBasis, name: useGroupName ? name : "Max", color }),
|
||||
s({
|
||||
metric: tree.costBasis.maxCostBasis,
|
||||
name: useGroupName ? name : "Max",
|
||||
color,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
),
|
||||
},
|
||||
],
|
||||
@@ -279,7 +359,8 @@ function createCostBasisSection(ctx, list, useGroupName, title) {
|
||||
* @returns {PartialOptionsTree}
|
||||
*/
|
||||
function createActivitySection(ctx, list, useGroupName, title) {
|
||||
const { s } = ctx;
|
||||
const { s, brk } = ctx;
|
||||
const { mergeMetricPatterns } = brk;
|
||||
|
||||
return [
|
||||
{
|
||||
@@ -290,9 +371,13 @@ function createActivitySection(ctx, list, useGroupName, title) {
|
||||
title: `Coinblocks Destroyed ${title}`,
|
||||
bottom: list.flatMap(({ color, name, tree }) => [
|
||||
s({
|
||||
metric: tree.activity.coinblocksDestroyed.base,
|
||||
metric: mergeMetricPatterns(
|
||||
tree.activity.coinblocksDestroyed.base,
|
||||
tree.activity.coinblocksDestroyed.sum,
|
||||
),
|
||||
name: useGroupName ? name : "Coinblocks",
|
||||
color,
|
||||
unit: Unit.coinblocks,
|
||||
}),
|
||||
]),
|
||||
},
|
||||
@@ -301,9 +386,13 @@ function createActivitySection(ctx, list, useGroupName, title) {
|
||||
title: `Coindays Destroyed ${title}`,
|
||||
bottom: list.flatMap(({ color, name, tree }) => [
|
||||
s({
|
||||
metric: tree.activity.coindaysDestroyed.base,
|
||||
metric: mergeMetricPatterns(
|
||||
tree.activity.coindaysDestroyed.base,
|
||||
tree.activity.coindaysDestroyed.sum,
|
||||
),
|
||||
name: useGroupName ? name : "Coindays",
|
||||
color,
|
||||
unit: Unit.coindays,
|
||||
}),
|
||||
]),
|
||||
},
|
||||
@@ -1,42 +1,43 @@
|
||||
/** Shared cohort chart section builders */
|
||||
|
||||
import { Unit } from "../../utils/units.js";
|
||||
|
||||
/**
|
||||
* Create supply section for a single cohort
|
||||
* @param {PartialContext} ctx
|
||||
* @param {CohortObject} cohort
|
||||
* @param {string} title
|
||||
* @returns {AnyFetchedSeriesBlueprint[]}
|
||||
*/
|
||||
export function createSingleSupplySeries(ctx, cohort, title) {
|
||||
export function createSingleSupplySeries(ctx, cohort) {
|
||||
const { colors, s, createPriceLine } = ctx;
|
||||
const { tree, color, name } = cohort;
|
||||
const { tree } = 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 }),
|
||||
s({ metric: tree.supply.supply.sats, name: "Supply", color: colors.default, unit: Unit.sats }),
|
||||
s({ metric: tree.supply.supply.bitcoin, name: "Supply", color: colors.default, unit: Unit.btc }),
|
||||
s({ metric: tree.supply.supply.dollars, name: "Supply", color: colors.default, unit: Unit.usd }),
|
||||
...("supplyRelToCirculatingSupply" in tree.relative
|
||||
? [s({ metric: tree.relative.supplyRelToCirculatingSupply, name: "Supply", color: colors.default })]
|
||||
? [s({ metric: tree.relative.supplyRelToCirculatingSupply, name: "Supply", color: colors.default, unit: Unit.pctSupply })]
|
||||
: []),
|
||||
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 } }),
|
||||
s({ metric: tree.unrealized.supplyInProfit.sats, name: "In Profit", color: colors.green, unit: Unit.sats }),
|
||||
s({ metric: tree.unrealized.supplyInProfit.bitcoin, name: "In Profit", color: colors.green, unit: Unit.btc }),
|
||||
s({ metric: tree.unrealized.supplyInProfit.dollars, name: "In Profit", color: colors.green, unit: Unit.usd }),
|
||||
s({ metric: tree.unrealized.supplyInLoss.sats, name: "In Loss", color: colors.red, unit: Unit.sats }),
|
||||
s({ metric: tree.unrealized.supplyInLoss.bitcoin, name: "In Loss", color: colors.red, unit: Unit.btc }),
|
||||
s({ metric: tree.unrealized.supplyInLoss.dollars, name: "In Loss", color: colors.red, unit: Unit.usd }),
|
||||
s({ metric: tree.supply.supplyHalf.sats, name: "half", color: colors.gray, unit: Unit.sats, options: { lineStyle: 4 } }),
|
||||
s({ metric: tree.supply.supplyHalf.bitcoin, name: "half", color: colors.gray, unit: Unit.btc, options: { lineStyle: 4 } }),
|
||||
s({ metric: tree.supply.supplyHalf.dollars, name: "half", color: colors.gray, unit: Unit.usd, 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.supplyInProfitRelToCirculatingSupply, name: "In Profit", color: colors.green, unit: Unit.pctSupply }),
|
||||
s({ metric: tree.relative.supplyInLossRelToCirculatingSupply, name: "In Loss", color: colors.red, unit: Unit.pctSupply }),
|
||||
]
|
||||
: []),
|
||||
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 }),
|
||||
s({ metric: tree.relative.supplyInProfitRelToOwnSupply, name: "In Profit", color: colors.green, unit: Unit.pctOwn }),
|
||||
s({ metric: tree.relative.supplyInLossRelToOwnSupply, name: "In Loss", color: colors.red, unit: Unit.pctOwn }),
|
||||
createPriceLine({ unit: Unit.pctOwn, number: 100, lineStyle: 0, color: colors.default }),
|
||||
createPriceLine({ unit: Unit.pctOwn, number: 50 }),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -51,12 +52,12 @@ export function createGroupedSupplyTotalSeries(ctx, list) {
|
||||
const constant100 = brk.tree.computed.constants.constant100;
|
||||
|
||||
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 }),
|
||||
s({ metric: tree.supply.supply.sats, name, color, unit: Unit.sats }),
|
||||
s({ metric: tree.supply.supply.bitcoin, name, color, unit: Unit.btc }),
|
||||
s({ metric: tree.supply.supply.dollars, name, color, unit: Unit.usd }),
|
||||
"supplyRelToCirculatingSupply" in tree.relative
|
||||
? s({ metric: tree.relative.supplyRelToCirculatingSupply, name, color })
|
||||
: s({ unit: "%all", metric: constant100, name, color }),
|
||||
? s({ metric: tree.relative.supplyRelToCirculatingSupply, name, color, unit: Unit.pctSupply })
|
||||
: s({ metric: constant100, name, color, unit: Unit.pctSupply }),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -70,11 +71,11 @@ 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 }),
|
||||
s({ metric: tree.unrealized.supplyInProfit.sats, name, color, unit: Unit.sats }),
|
||||
s({ metric: tree.unrealized.supplyInProfit.bitcoin, name, color, unit: Unit.btc }),
|
||||
s({ metric: tree.unrealized.supplyInProfit.dollars, name, color, unit: Unit.usd }),
|
||||
...("supplyInProfitRelToCirculatingSupply" in tree.relative
|
||||
? [s({ metric: tree.relative.supplyInProfitRelToCirculatingSupply, name, color })]
|
||||
? [s({ metric: tree.relative.supplyInProfitRelToCirculatingSupply, name, color, unit: Unit.pctSupply })]
|
||||
: []),
|
||||
]);
|
||||
}
|
||||
@@ -89,11 +90,11 @@ 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 }),
|
||||
s({ metric: tree.unrealized.supplyInLoss.sats, name, color, unit: Unit.sats }),
|
||||
s({ metric: tree.unrealized.supplyInLoss.bitcoin, name, color, unit: Unit.btc }),
|
||||
s({ metric: tree.unrealized.supplyInLoss.dollars, name, color, unit: Unit.usd }),
|
||||
...("supplyInLossRelToCirculatingSupply" in tree.relative
|
||||
? [s({ metric: tree.relative.supplyInLossRelToCirculatingSupply, name, color })]
|
||||
? [s({ metric: tree.relative.supplyInLossRelToCirculatingSupply, name, color, unit: Unit.pctSupply })]
|
||||
: []),
|
||||
]);
|
||||
}
|
||||
@@ -109,7 +110,7 @@ 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 }),
|
||||
s({ metric: tree.supply.utxoCount, name: useGroupName ? name : "Count", color, unit: Unit.count }),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -128,6 +129,7 @@ export function createAddressCountSeries(ctx, list, useGroupName) {
|
||||
metric: tree.addrCount,
|
||||
name: useGroupName ? name : "Count",
|
||||
color: useGroupName ? color : colors.orange,
|
||||
unit: Unit.count,
|
||||
}),
|
||||
]);
|
||||
}
|
||||
@@ -142,7 +144,7 @@ export function createRealizedPriceSeries(ctx, list) {
|
||||
const { s } = ctx;
|
||||
|
||||
return list.map(({ color, name, tree }) =>
|
||||
s({ metric: tree.realized.realizedPrice, name, color }),
|
||||
s({ metric: tree.realized.realizedPrice, name, color, unit: Unit.usd }),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -157,9 +159,9 @@ export function createRealizedPriceRatioSeries(ctx, list) {
|
||||
|
||||
return [
|
||||
...list.map(({ color, name, tree }) =>
|
||||
s({ metric: tree.realized.realizedPriceExtra.ratio, name, color }),
|
||||
s({ metric: tree.realized.realizedPriceExtra.ratio, name, color, unit: Unit.ratio }),
|
||||
),
|
||||
createPriceLine({ unit: "ratio", number: 1 }),
|
||||
createPriceLine({ unit: Unit.ratio, number: 1 }),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -174,7 +176,7 @@ 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 }),
|
||||
s({ metric: tree.realized.realizedCap, name: useGroupName ? name : "Capitalization", color, unit: Unit.usd }),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -189,8 +191,8 @@ export function createCostBasisMinMaxSeries(ctx, list, useGroupName) {
|
||||
const { s } = ctx;
|
||||
|
||||
return list.flatMap(({ color, name, tree }) => [
|
||||
s({ metric: tree.costBasis.minCostBasis, name: useGroupName ? `${name} min` : "Min", color }),
|
||||
s({ metric: tree.costBasis.maxCostBasis, name: useGroupName ? `${name} max` : "Max", color }),
|
||||
s({ metric: tree.costBasis.minCostBasis, name: useGroupName ? `${name} min` : "Min", color, unit: Unit.usd }),
|
||||
s({ metric: tree.costBasis.maxCostBasis, name: useGroupName ? `${name} max` : "Max", color, unit: Unit.usd }),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -202,16 +204,16 @@ export function createCostBasisMinMaxSeries(ctx, list, useGroupName) {
|
||||
* @returns {AnyFetchedSeriesBlueprint[]}
|
||||
*/
|
||||
export function createCostBasisPercentilesSeries(ctx, list, useGroupName) {
|
||||
const { s, colors } = ctx;
|
||||
const { s } = ctx;
|
||||
|
||||
return list.flatMap(({ color, name, tree }) => {
|
||||
const percentiles = tree.costBasis.percentiles;
|
||||
return [
|
||||
s({ metric: percentiles.costBasisPct10, name: useGroupName ? `${name} p10` : "p10", color, defaultActive: false }),
|
||||
s({ metric: percentiles.costBasisPct25, name: useGroupName ? `${name} p25` : "p25", color, defaultActive: false }),
|
||||
s({ metric: percentiles.costBasisPct50, name: useGroupName ? `${name} p50` : "p50", color }),
|
||||
s({ metric: percentiles.costBasisPct75, name: useGroupName ? `${name} p75` : "p75", color, defaultActive: false }),
|
||||
s({ metric: percentiles.costBasisPct90, name: useGroupName ? `${name} p90` : "p90", color, defaultActive: false }),
|
||||
s({ metric: percentiles.costBasisPct10, name: useGroupName ? `${name} p10` : "p10", color, unit: Unit.usd, defaultActive: false }),
|
||||
s({ metric: percentiles.costBasisPct25, name: useGroupName ? `${name} p25` : "p25", color, unit: Unit.usd, defaultActive: false }),
|
||||
s({ metric: percentiles.costBasisPct50, name: useGroupName ? `${name} p50` : "p50", color, unit: Unit.usd }),
|
||||
s({ metric: percentiles.costBasisPct75, name: useGroupName ? `${name} p75` : "p75", color, unit: Unit.usd, defaultActive: false }),
|
||||
s({ metric: percentiles.costBasisPct90, name: useGroupName ? `${name} p90` : "p90", color, unit: Unit.usd, defaultActive: false }),
|
||||
];
|
||||
});
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
createRealizedPriceRatioSeries,
|
||||
createCostBasisPercentilesSeries,
|
||||
} from "./shared.js";
|
||||
import { Unit } from "../../utils/units.js";
|
||||
|
||||
/**
|
||||
* Create a cohort folder for age-based UTXO cohorts (term, maxAge, minAge, ageRange, epoch)
|
||||
@@ -28,15 +29,14 @@ import {
|
||||
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),
|
||||
...createSupplySection(ctx, list, args, useGroupName, title),
|
||||
createUtxoCountSection(ctx, list, useGroupName, title),
|
||||
createRealizedSection(ctx, list, args, useGroupName, isSingle, title),
|
||||
createRealizedSection(ctx, list, args, useGroupName, title),
|
||||
...createUnrealizedSection(ctx, list, useGroupName, title),
|
||||
...createCostBasisSectionWithPercentiles(ctx, list, useGroupName, title),
|
||||
...createActivitySection(ctx, list, useGroupName, title),
|
||||
@@ -54,15 +54,14 @@ export function createAgeCohortFolder(ctx, args) {
|
||||
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),
|
||||
...createSupplySection(ctx, list, args, useGroupName, title),
|
||||
createUtxoCountSection(ctx, list, useGroupName, title),
|
||||
createRealizedSection(ctx, list, args, useGroupName, isSingle, title),
|
||||
createRealizedSection(ctx, list, args, useGroupName, title),
|
||||
...createUnrealizedSection(ctx, list, useGroupName, title),
|
||||
...createCostBasisSectionBasic(ctx, list, useGroupName, title),
|
||||
...createActivitySection(ctx, list, useGroupName, title),
|
||||
@@ -81,7 +80,6 @@ export function createAmountCohortFolder(ctx, args) {
|
||||
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
|
||||
@@ -90,9 +88,9 @@ export function createUtxoCohortFolder(ctx, args) {
|
||||
return {
|
||||
name: args.name || "all",
|
||||
tree: [
|
||||
...createSupplySection(ctx, list, args, useGroupName, isSingle, title),
|
||||
...createSupplySection(ctx, list, args, useGroupName, title),
|
||||
createUtxoCountSection(ctx, list, useGroupName, title),
|
||||
createRealizedSection(ctx, list, args, useGroupName, isSingle, title),
|
||||
createRealizedSection(ctx, list, args, useGroupName, title),
|
||||
...createUnrealizedSection(ctx, list, useGroupName, title),
|
||||
...(hasPercentiles
|
||||
? createCostBasisSectionWithPercentiles(
|
||||
@@ -113,11 +111,11 @@ export function createUtxoCohortFolder(ctx, args) {
|
||||
* @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) {
|
||||
function createSupplySection(ctx, list, args, useGroupName, title) {
|
||||
const isSingle = !useGroupName;
|
||||
return [
|
||||
isSingle
|
||||
? {
|
||||
@@ -126,7 +124,6 @@ function createSupplySection(ctx, list, args, useGroupName, isSingle, title) {
|
||||
bottom: createSingleSupplySeries(
|
||||
ctx,
|
||||
/** @type {UtxoCohortObject} */ (args),
|
||||
title,
|
||||
),
|
||||
}
|
||||
: {
|
||||
@@ -174,11 +171,10 @@ function createUtxoCountSection(ctx, list, useGroupName, title) {
|
||||
* @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) {
|
||||
function createRealizedSection(ctx, list, args, useGroupName, title) {
|
||||
return {
|
||||
name: "Realized",
|
||||
tree: [
|
||||
@@ -203,13 +199,7 @@ function createRealizedSection(ctx, list, args, useGroupName, isSingle, title) {
|
||||
{
|
||||
name: "capitalization",
|
||||
title: `Realized Capitalization ${title}`,
|
||||
bottom: createRealizedCapWithExtras(
|
||||
ctx,
|
||||
list,
|
||||
args,
|
||||
useGroupName,
|
||||
title,
|
||||
),
|
||||
bottom: createRealizedCapWithExtras(ctx, list, args, useGroupName),
|
||||
},
|
||||
...(!useGroupName
|
||||
? createRealizedPnlSection(
|
||||
@@ -238,7 +228,7 @@ function createRealizedPriceOptions(ctx, args, title) {
|
||||
name: "price",
|
||||
title: `Realized Price ${title}`,
|
||||
top: [
|
||||
s({ metric: tree.realized.realizedPrice, name: "realized", color }),
|
||||
s({ metric: tree.realized.realizedPrice, name: "realized", color, unit: Unit.usd }),
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -250,10 +240,9 @@ function createRealizedPriceOptions(ctx, args, title) {
|
||||
* @param {readonly UtxoCohortObject[]} list
|
||||
* @param {UtxoCohortObject | UtxoCohortGroupObject} args
|
||||
* @param {boolean} useGroupName
|
||||
* @param {string} title
|
||||
* @returns {AnyFetchedSeriesBlueprint[]}
|
||||
*/
|
||||
function createRealizedCapWithExtras(ctx, list, args, useGroupName, title) {
|
||||
function createRealizedCapWithExtras(ctx, list, args, useGroupName) {
|
||||
const { colors, s, createPriceLine } = ctx;
|
||||
const isSingle = !("list" in args);
|
||||
|
||||
@@ -262,6 +251,7 @@ function createRealizedCapWithExtras(ctx, list, args, useGroupName, title) {
|
||||
metric: tree.realized.realizedCap,
|
||||
name: useGroupName ? name : "Capitalization",
|
||||
color,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
...(isSingle
|
||||
? [
|
||||
@@ -269,9 +259,10 @@ function createRealizedCapWithExtras(ctx, list, args, useGroupName, title) {
|
||||
type: "Baseline",
|
||||
metric: tree.realized.realizedCap30dDelta,
|
||||
title: "30d change",
|
||||
unit: Unit.usd,
|
||||
defaultActive: false,
|
||||
}),
|
||||
createPriceLine({ unit: "usd", defaultActive: false }),
|
||||
createPriceLine({ unit: Unit.usd, defaultActive: false }),
|
||||
]
|
||||
: []),
|
||||
...(isSingle && "realizedCapRelToOwnMarketCap" in tree.realized
|
||||
@@ -280,10 +271,11 @@ function createRealizedCapWithExtras(ctx, list, args, useGroupName, title) {
|
||||
type: "Baseline",
|
||||
metric: tree.realized.realizedCapRelToOwnMarketCap,
|
||||
title: "ratio",
|
||||
unit: Unit.pctOwnMcap,
|
||||
options: { baseValue: { price: 100 } },
|
||||
colors: [colors.red, colors.green],
|
||||
}),
|
||||
createPriceLine({ unit: "%cmcap", defaultActive: true, number: 100 }),
|
||||
createPriceLine({ unit: Unit.pctOwnMcap, defaultActive: true, number: 100 }),
|
||||
]
|
||||
: []),
|
||||
]);
|
||||
@@ -297,7 +289,8 @@ function createRealizedCapWithExtras(ctx, list, args, useGroupName, title) {
|
||||
* @returns {PartialOptionsTree}
|
||||
*/
|
||||
function createRealizedPnlSection(ctx, args, title) {
|
||||
const { colors, s } = ctx;
|
||||
const { colors, s, brk } = ctx;
|
||||
const { mergeMetricPatterns } = brk;
|
||||
const { tree } = args;
|
||||
|
||||
return [
|
||||
@@ -306,35 +299,49 @@ function createRealizedPnlSection(ctx, args, title) {
|
||||
title: `Realized Profit And Loss ${title}`,
|
||||
bottom: [
|
||||
s({
|
||||
metric: tree.realized.realizedProfit.base,
|
||||
metric: mergeMetricPatterns(
|
||||
tree.realized.realizedProfit.base,
|
||||
tree.realized.realizedProfit.sum,
|
||||
),
|
||||
name: "Profit",
|
||||
color: colors.green,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
s({
|
||||
metric: tree.realized.realizedLoss.base,
|
||||
metric: mergeMetricPatterns(
|
||||
tree.realized.realizedLoss.base,
|
||||
tree.realized.realizedLoss.sum,
|
||||
),
|
||||
name: "Loss",
|
||||
color: colors.red,
|
||||
defaultActive: false,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
...("realizedProfitToLossRatio" in tree.realized
|
||||
? [
|
||||
s({
|
||||
metric: tree.realized.realizedProfitToLossRatio,
|
||||
name: "profit / loss",
|
||||
name: "Profit / Loss",
|
||||
color: colors.yellow,
|
||||
unit: Unit.ratio,
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
s({
|
||||
metric: tree.realized.totalRealizedPnl.base,
|
||||
metric: tree.realized.totalRealizedPnl,
|
||||
name: "Total",
|
||||
color: colors.default,
|
||||
defaultActive: false,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
s({
|
||||
metric: tree.realized.negRealizedLoss.base,
|
||||
metric: mergeMetricPatterns(
|
||||
tree.realized.negRealizedLoss.base,
|
||||
tree.realized.negRealizedLoss.sum,
|
||||
),
|
||||
name: "Negative Loss",
|
||||
color: colors.red,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
],
|
||||
},
|
||||
@@ -350,7 +357,7 @@ function createRealizedPnlSection(ctx, args, title) {
|
||||
* @returns {PartialOptionsTree}
|
||||
*/
|
||||
function createUnrealizedSection(ctx, list, useGroupName, title) {
|
||||
const { colors, s, createPriceLine } = ctx;
|
||||
const { colors, s } = ctx;
|
||||
|
||||
return [
|
||||
{
|
||||
@@ -359,12 +366,13 @@ function createUnrealizedSection(ctx, list, useGroupName, title) {
|
||||
{
|
||||
name: "nupl",
|
||||
title: `Net Unrealized Profit/Loss ${title}`,
|
||||
bottom: list.flatMap(({ color, name, tree }) => [
|
||||
bottom: list.flatMap(({ name, tree }) => [
|
||||
/** @type {AnyFetchedSeriesBlueprint} */ ({
|
||||
type: "Baseline",
|
||||
metric: tree.unrealized.netUnrealizedPnl,
|
||||
title: useGroupName ? name : "NUPL",
|
||||
colors: [colors.red, colors.green],
|
||||
unit: Unit.ratio,
|
||||
options: { baseValue: { price: 0 } },
|
||||
}),
|
||||
]),
|
||||
@@ -377,6 +385,7 @@ function createUnrealizedSection(ctx, list, useGroupName, title) {
|
||||
metric: tree.unrealized.unrealizedProfit,
|
||||
name: useGroupName ? name : "Profit",
|
||||
color,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
]),
|
||||
},
|
||||
@@ -388,6 +397,7 @@ function createUnrealizedSection(ctx, list, useGroupName, title) {
|
||||
metric: tree.unrealized.unrealizedLoss,
|
||||
name: useGroupName ? name : "Loss",
|
||||
color,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
]),
|
||||
},
|
||||
@@ -419,6 +429,7 @@ function createCostBasisSectionWithPercentiles(ctx, list, useGroupName, title) {
|
||||
metric: tree.costBasis.minCostBasis,
|
||||
name: useGroupName ? name : "Min",
|
||||
color,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
),
|
||||
},
|
||||
@@ -430,6 +441,7 @@ function createCostBasisSectionWithPercentiles(ctx, list, useGroupName, title) {
|
||||
metric: tree.costBasis.maxCostBasis,
|
||||
name: useGroupName ? name : "Max",
|
||||
color,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
),
|
||||
},
|
||||
@@ -466,6 +478,7 @@ function createCostBasisSectionBasic(ctx, list, useGroupName, title) {
|
||||
metric: tree.costBasis.minCostBasis,
|
||||
name: useGroupName ? name : "Min",
|
||||
color,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
),
|
||||
},
|
||||
@@ -477,6 +490,7 @@ function createCostBasisSectionBasic(ctx, list, useGroupName, title) {
|
||||
metric: tree.costBasis.maxCostBasis,
|
||||
name: useGroupName ? name : "Max",
|
||||
color,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
),
|
||||
},
|
||||
@@ -494,7 +508,8 @@ function createCostBasisSectionBasic(ctx, list, useGroupName, title) {
|
||||
* @returns {PartialOptionsTree}
|
||||
*/
|
||||
function createActivitySection(ctx, list, useGroupName, title) {
|
||||
const { s } = ctx;
|
||||
const { s, brk } = ctx;
|
||||
const { mergeMetricPatterns } = brk;
|
||||
|
||||
return [
|
||||
{
|
||||
@@ -505,9 +520,13 @@ function createActivitySection(ctx, list, useGroupName, title) {
|
||||
title: `Coinblocks Destroyed ${title}`,
|
||||
bottom: list.flatMap(({ color, name, tree }) => [
|
||||
s({
|
||||
metric: tree.activity.coinblocksDestroyed.base,
|
||||
metric: mergeMetricPatterns(
|
||||
tree.activity.coinblocksDestroyed.base,
|
||||
tree.activity.coinblocksDestroyed.sum,
|
||||
),
|
||||
name: useGroupName ? name : "Coinblocks",
|
||||
color,
|
||||
unit: Unit.coinblocks,
|
||||
}),
|
||||
]),
|
||||
},
|
||||
@@ -516,9 +535,13 @@ function createActivitySection(ctx, list, useGroupName, title) {
|
||||
title: `Coindays Destroyed ${title}`,
|
||||
bottom: list.flatMap(({ color, name, tree }) => [
|
||||
s({
|
||||
metric: tree.activity.coindaysDestroyed.base,
|
||||
metric: mergeMetricPatterns(
|
||||
tree.activity.coindaysDestroyed.base,
|
||||
tree.activity.coindaysDestroyed.sum,
|
||||
),
|
||||
name: useGroupName ? name : "Coindays",
|
||||
color,
|
||||
unit: Unit.coindays,
|
||||
}),
|
||||
]),
|
||||
},
|
||||
502
websites/bitview/scripts/options/cointime.js
Normal file
502
websites/bitview/scripts/options/cointime.js
Normal file
@@ -0,0 +1,502 @@
|
||||
/** Cointime section builder - typed tree-based patterns */
|
||||
|
||||
import { Unit } from "../utils/units.js";
|
||||
|
||||
/**
|
||||
* Create price with ratio options for cointime prices
|
||||
* @param {PartialContext} ctx
|
||||
* @param {Object} args
|
||||
* @param {string} args.title
|
||||
* @param {string} args.legend
|
||||
* @param {AnyMetricPattern} args.price
|
||||
* @param {ActivePriceRatioPattern} args.ratio
|
||||
* @param {Color} [args.color]
|
||||
* @returns {PartialOptionsTree}
|
||||
*/
|
||||
function createCointimePriceWithRatioOptions(
|
||||
ctx,
|
||||
{ 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: Unit.usd })],
|
||||
},
|
||||
{
|
||||
name: "Ratio",
|
||||
title: `${title} Ratio`,
|
||||
top: [
|
||||
s({ metric: price, name: legend, color, unit: Unit.usd }),
|
||||
...percentileUsdMap.map(({ name: pctName, prop, color: pctColor }) =>
|
||||
s({
|
||||
metric: prop,
|
||||
name: pctName,
|
||||
color: pctColor,
|
||||
defaultActive: false,
|
||||
unit: Unit.usd,
|
||||
options: { lineStyle: 1 },
|
||||
}),
|
||||
),
|
||||
],
|
||||
bottom: [
|
||||
s({ metric: ratio.ratio, name: "Ratio", color, unit: Unit.ratio }),
|
||||
s({
|
||||
metric: ratio.ratio1wSma,
|
||||
name: "1w SMA",
|
||||
color: colors.lime,
|
||||
unit: Unit.ratio,
|
||||
}),
|
||||
s({
|
||||
metric: ratio.ratio1mSma,
|
||||
name: "1m SMA",
|
||||
color: colors.teal,
|
||||
unit: Unit.ratio,
|
||||
}),
|
||||
s({
|
||||
metric: ratio.ratio1ySd.sma,
|
||||
name: "1y SMA",
|
||||
color: colors.sky,
|
||||
unit: Unit.ratio,
|
||||
}),
|
||||
s({
|
||||
metric: ratio.ratio2ySd.sma,
|
||||
name: "2y SMA",
|
||||
color: colors.indigo,
|
||||
unit: Unit.ratio,
|
||||
}),
|
||||
s({
|
||||
metric: ratio.ratio4ySd.sma,
|
||||
name: "4y SMA",
|
||||
color: colors.purple,
|
||||
unit: Unit.ratio,
|
||||
}),
|
||||
s({
|
||||
metric: ratio.ratioSd.sma,
|
||||
name: "All SMA",
|
||||
color: colors.rose,
|
||||
unit: Unit.ratio,
|
||||
}),
|
||||
...percentileMap.map(({ name: pctName, prop, color: pctColor }) =>
|
||||
s({
|
||||
metric: prop,
|
||||
name: pctName,
|
||||
color: pctColor,
|
||||
defaultActive: false,
|
||||
unit: Unit.ratio,
|
||||
options: { lineStyle: 1 },
|
||||
}),
|
||||
),
|
||||
createPriceLine({ unit: 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: Unit.usd }),
|
||||
),
|
||||
bottom: [
|
||||
s({ metric: sd.zscore, name: "Z-Score", color, unit: Unit.sd }),
|
||||
createPriceLine({ unit: Unit.sd, number: 3 }),
|
||||
createPriceLine({ unit: Unit.sd, number: 2 }),
|
||||
createPriceLine({ unit: Unit.sd, number: 1 }),
|
||||
createPriceLine({ unit: Unit.sd, number: 0 }),
|
||||
createPriceLine({ unit: Unit.sd, number: -1 }),
|
||||
createPriceLine({ unit: Unit.sd, number: -2 }),
|
||||
createPriceLine({ unit: Unit.sd, number: -3 }),
|
||||
],
|
||||
})),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create Cointime section
|
||||
* @param {PartialContext} ctx
|
||||
* @returns {PartialOptionsGroup}
|
||||
*/
|
||||
export function createCointimeSection(ctx) {
|
||||
const { colors, brk, s } = ctx;
|
||||
const { mergeMetricPatterns } = brk;
|
||||
const { cointime, distribution, supply } = brk.tree.computed;
|
||||
const { pricing, cap, activity, supply: cointimeSupply, adjusted } = cointime;
|
||||
const { all } = 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: Unit.usd }),
|
||||
),
|
||||
},
|
||||
...cointimePrices.map(({ price, ratio, name, color, title }) => ({
|
||||
name,
|
||||
tree: createCointimePriceWithRatioOptions(ctx, {
|
||||
price,
|
||||
ratio,
|
||||
legend: name,
|
||||
color,
|
||||
title,
|
||||
}),
|
||||
})),
|
||||
],
|
||||
},
|
||||
|
||||
// Capitalization
|
||||
{
|
||||
name: "Capitalization",
|
||||
tree: [
|
||||
{
|
||||
name: "Compare",
|
||||
title: "Compare Cointime Capitalizations",
|
||||
bottom: [
|
||||
s({
|
||||
metric: supply.marketCap.height,
|
||||
name: "Market",
|
||||
color: colors.default,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
s({
|
||||
metric: all.realized.realizedCap,
|
||||
name: "Realized",
|
||||
color: colors.orange,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
...cointimeCapitalizations.map(({ metric, name, color }) =>
|
||||
s({ metric, name, color, unit: Unit.usd }),
|
||||
),
|
||||
],
|
||||
},
|
||||
...cointimeCapitalizations.map(({ metric, name, color, title }) => ({
|
||||
name,
|
||||
title,
|
||||
bottom: [
|
||||
s({ metric, name, color, unit: Unit.usd }),
|
||||
s({
|
||||
metric: supply.marketCap.height,
|
||||
name: "Market",
|
||||
color: colors.default,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
s({
|
||||
metric: all.realized.realizedCap,
|
||||
name: "Realized",
|
||||
color: colors.orange,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
],
|
||||
})),
|
||||
],
|
||||
},
|
||||
|
||||
// Supply
|
||||
{
|
||||
name: "Supply",
|
||||
title: "Cointime Supply",
|
||||
bottom: [
|
||||
// All supply (different pattern structure)
|
||||
s({
|
||||
metric: all.supply.supply.sats,
|
||||
name: "All",
|
||||
color: colors.orange,
|
||||
unit: Unit.sats,
|
||||
}),
|
||||
s({
|
||||
metric: all.supply.supply.bitcoin,
|
||||
name: "All",
|
||||
color: colors.orange,
|
||||
unit: Unit.btc,
|
||||
}),
|
||||
s({
|
||||
metric: all.supply.supply.dollars,
|
||||
name: "All",
|
||||
color: colors.orange,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
// Cointime supplies (ActiveSupplyPattern)
|
||||
.../** @type {const} */ ([
|
||||
[cointimeSupply.vaultedSupply, "Vaulted", colors.lime],
|
||||
[cointimeSupply.activeSupply, "Active", colors.rose],
|
||||
]).flatMap(([supplyItem, name, color]) => [
|
||||
s({ metric: supplyItem.sats, name, color, unit: Unit.sats }),
|
||||
s({ metric: supplyItem.bitcoin, name, color, unit: Unit.btc }),
|
||||
s({ metric: supplyItem.dollars, name, color, unit: Unit.usd }),
|
||||
]),
|
||||
],
|
||||
},
|
||||
|
||||
// Liveliness & Vaultedness
|
||||
{
|
||||
name: "Liveliness & Vaultedness",
|
||||
title: "Liveliness & Vaultedness",
|
||||
bottom: [
|
||||
s({
|
||||
metric: activity.liveliness,
|
||||
name: "Liveliness",
|
||||
color: colors.rose,
|
||||
unit: Unit.ratio,
|
||||
}),
|
||||
s({
|
||||
metric: activity.vaultedness,
|
||||
name: "Vaultedness",
|
||||
color: colors.lime,
|
||||
unit: Unit.ratio,
|
||||
}),
|
||||
s({
|
||||
metric: activity.activityToVaultednessRatio,
|
||||
name: "Liveliness / Vaultedness",
|
||||
color: colors.purple,
|
||||
unit: Unit.ratio,
|
||||
}),
|
||||
],
|
||||
},
|
||||
|
||||
// Coinblocks
|
||||
{
|
||||
name: "Coinblocks",
|
||||
title: "Coinblocks",
|
||||
bottom: [
|
||||
// Destroyed comes from the all cohort's activity
|
||||
s({
|
||||
metric: mergeMetricPatterns(
|
||||
all.activity.coinblocksDestroyed.base,
|
||||
all.activity.coinblocksDestroyed.sum,
|
||||
),
|
||||
name: "Destroyed",
|
||||
color: colors.red,
|
||||
unit: Unit.coinblocks,
|
||||
}),
|
||||
s({
|
||||
metric: all.activity.coinblocksDestroyed.cumulative,
|
||||
name: "Cumulative Destroyed",
|
||||
color: colors.red,
|
||||
defaultActive: false,
|
||||
unit: Unit.coinblocks,
|
||||
}),
|
||||
// Created and stored from cointime
|
||||
s({
|
||||
metric: mergeMetricPatterns(
|
||||
activity.coinblocksCreated.base,
|
||||
activity.coinblocksCreated.sum,
|
||||
),
|
||||
name: "Created",
|
||||
color: colors.orange,
|
||||
unit: Unit.coinblocks,
|
||||
}),
|
||||
s({
|
||||
metric: activity.coinblocksCreated.cumulative,
|
||||
name: "Cumulative Created",
|
||||
color: colors.orange,
|
||||
defaultActive: false,
|
||||
unit: Unit.coinblocks,
|
||||
}),
|
||||
s({
|
||||
metric: mergeMetricPatterns(
|
||||
activity.coinblocksStored.base,
|
||||
activity.coinblocksStored.sum,
|
||||
),
|
||||
name: "Stored",
|
||||
color: colors.green,
|
||||
unit: Unit.coinblocks,
|
||||
}),
|
||||
s({
|
||||
metric: activity.coinblocksStored.cumulative,
|
||||
name: "Cumulative Stored",
|
||||
color: colors.green,
|
||||
defaultActive: false,
|
||||
unit: Unit.coinblocks,
|
||||
}),
|
||||
],
|
||||
},
|
||||
|
||||
// Adjusted metrics
|
||||
{
|
||||
name: "Adjusted",
|
||||
tree: [
|
||||
// Inflation
|
||||
{
|
||||
name: "Inflation",
|
||||
title: "Cointime-Adjusted Inflation Rate",
|
||||
bottom: [
|
||||
s({
|
||||
metric: mergeMetricPatterns(
|
||||
supply.inflation.indexes.dateindex,
|
||||
supply.inflation.indexes.rest,
|
||||
),
|
||||
name: "Base",
|
||||
color: colors.orange,
|
||||
unit: Unit.percentage,
|
||||
}),
|
||||
s({
|
||||
metric: adjusted.cointimeAdjInflationRate,
|
||||
name: "Adjusted",
|
||||
color: colors.purple,
|
||||
unit: Unit.percentage,
|
||||
}),
|
||||
],
|
||||
},
|
||||
// Velocity
|
||||
{
|
||||
name: "Velocity",
|
||||
title: "Cointime-Adjusted Transactions Velocity",
|
||||
bottom: [
|
||||
s({
|
||||
metric: mergeMetricPatterns(
|
||||
supply.velocity.btc.dateindex,
|
||||
supply.velocity.btc.rest,
|
||||
),
|
||||
name: "BTC",
|
||||
color: colors.orange,
|
||||
unit: Unit.ratio,
|
||||
}),
|
||||
s({
|
||||
metric: adjusted.cointimeAdjTxBtcVelocity,
|
||||
name: "Adj. BTC",
|
||||
color: colors.red,
|
||||
unit: Unit.ratio,
|
||||
}),
|
||||
s({
|
||||
metric: mergeMetricPatterns(
|
||||
supply.velocity.usd.dateindex,
|
||||
supply.velocity.usd.rest,
|
||||
),
|
||||
name: "USD",
|
||||
color: colors.emerald,
|
||||
unit: Unit.ratio,
|
||||
}),
|
||||
s({
|
||||
metric: adjusted.cointimeAdjTxUsdVelocity,
|
||||
name: "Adj. USD",
|
||||
color: colors.lime,
|
||||
unit: Unit.ratio,
|
||||
}),
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -1,13 +1,12 @@
|
||||
import { createPartialOptions } from "./partial/index.js";
|
||||
import { createPartialOptions } from "./partial.js";
|
||||
import {
|
||||
createButtonElement,
|
||||
createAnchorElement,
|
||||
insertElementAtIndex,
|
||||
} 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";
|
||||
} from "../utils/dom.js";
|
||||
import { pushHistory, resetParams } from "../utils/url.js";
|
||||
import { readStored, writeToStorage } from "../utils/storage.js";
|
||||
import { stringToId } from "../utils/format.js";
|
||||
import { collect, markUsed, logUnused } from "./unused.js";
|
||||
|
||||
/**
|
||||
@@ -47,21 +46,28 @@ export function initOptions({ colors, signals, brk, qrcode }) {
|
||||
/**
|
||||
* @param {AnyFetchedSeriesBlueprint[]} [arr]
|
||||
*/
|
||||
function arrayToRecord(arr = []) {
|
||||
return [...(arr || [])].reduce((record, blueprint) => {
|
||||
function arrayToMap(arr = []) {
|
||||
/** @type {Map<Unit, AnyFetchedSeriesBlueprint[]>} */
|
||||
const map = new Map();
|
||||
for (const blueprint of arr || []) {
|
||||
if (!blueprint.metric) {
|
||||
throw new Error(
|
||||
`Blueprint missing metric: ${JSON.stringify(blueprint)}`,
|
||||
);
|
||||
}
|
||||
if (!blueprint.unit) {
|
||||
throw new Error(
|
||||
`Blueprint missing unit: ${blueprint.title}`,
|
||||
);
|
||||
}
|
||||
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;
|
||||
}, /** @type {Record<Unit, AnyFetchedSeriesBlueprint[]>} */ ({}));
|
||||
const unit = blueprint.unit;
|
||||
if (!map.has(unit)) {
|
||||
map.set(unit, []);
|
||||
}
|
||||
map.get(unit)?.push(blueprint);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -77,11 +83,10 @@ export function initOptions({ colors, signals, brk, qrcode }) {
|
||||
/**
|
||||
* @param {Object} args
|
||||
* @param {Option} args.option
|
||||
* @param {string} args.frame
|
||||
* @param {Signal<string | null>} args.qrcode
|
||||
* @param {string} [args.name]
|
||||
*/
|
||||
function createOptionElement({ option, frame, name, qrcode }) {
|
||||
function createOptionElement({ option, name, qrcode }) {
|
||||
const title = option.title;
|
||||
if (option.kind === "url") {
|
||||
const href = option.url();
|
||||
@@ -299,8 +304,8 @@ export function initOptions({ colors, signals, brk, qrcode }) {
|
||||
name,
|
||||
title,
|
||||
path,
|
||||
top: arrayToRecord(anyPartial.top),
|
||||
bottom: arrayToRecord(anyPartial.bottom),
|
||||
top: arrayToMap(anyPartial.top),
|
||||
bottom: arrayToMap(anyPartial.bottom),
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -338,7 +343,6 @@ export function initOptions({ colors, signals, brk, qrcode }) {
|
||||
|
||||
const element = createOptionElement({
|
||||
option,
|
||||
frame: "nav",
|
||||
qrcode,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/** Moving averages section */
|
||||
|
||||
import { Unit } from "../../utils/units.js";
|
||||
import { periodIdToName } from "./utils.js";
|
||||
|
||||
/**
|
||||
@@ -93,29 +94,29 @@ export function createPriceWithRatioOptions(ctx, { title, legend, ratio, color }
|
||||
{
|
||||
name: "price",
|
||||
title,
|
||||
top: [s({ metric: priceMetric, name: legend, color, unit: "usd" })],
|
||||
top: [s({ metric: priceMetric, name: legend, color, unit: Unit.usd })],
|
||||
},
|
||||
{
|
||||
name: "Ratio",
|
||||
title: `${title} Ratio`,
|
||||
top: [
|
||||
s({ metric: priceMetric, name: legend, color, unit: "usd" }),
|
||||
s({ metric: priceMetric, name: legend, color, unit: Unit.usd }),
|
||||
...percentileUsdMap.map(({ name: pctName, prop, color: pctColor }) =>
|
||||
s({ metric: prop, name: pctName, color: pctColor, defaultActive: false, unit: "usd", options: { lineStyle: 1 } }),
|
||||
s({ metric: prop, name: pctName, color: pctColor, defaultActive: false, unit: 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" }),
|
||||
s({ metric: ratio.ratio, name: "Ratio", color, unit: Unit.ratio }),
|
||||
s({ metric: ratio.ratio1wSma, name: "1w SMA", color: colors.lime, unit: Unit.ratio }),
|
||||
s({ metric: ratio.ratio1mSma, name: "1m SMA", color: colors.teal, unit: Unit.ratio }),
|
||||
s({ metric: ratio.ratio1ySd.sma, name: "1y SMA", color: colors.sky, unit: Unit.ratio }),
|
||||
s({ metric: ratio.ratio2ySd.sma, name: "2y SMA", color: colors.indigo, unit: Unit.ratio }),
|
||||
s({ metric: ratio.ratio4ySd.sma, name: "4y SMA", color: colors.purple, unit: Unit.ratio }),
|
||||
s({ metric: ratio.ratioSd.sma, name: "All SMA", color: colors.rose, unit: Unit.ratio }),
|
||||
...percentileMap.map(({ name: pctName, prop, color: pctColor }) =>
|
||||
s({ metric: prop, name: pctName, color: pctColor, defaultActive: false, unit: "ratio", options: { lineStyle: 1 } }),
|
||||
s({ metric: prop, name: pctName, color: pctColor, defaultActive: false, unit: Unit.ratio, options: { lineStyle: 1 } }),
|
||||
),
|
||||
createPriceLine({ unit: "ratio", number: 1 }),
|
||||
createPriceLine({ unit: Unit.ratio, number: 1 }),
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -124,17 +125,17 @@ export function createPriceWithRatioOptions(ctx, { title, legend, ratio, color }
|
||||
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" }),
|
||||
s({ metric: prop, name: bandName, color: bandColor, unit: 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 }),
|
||||
s({ metric: sd.zscore, name: "Z-Score", color, unit: Unit.sd }),
|
||||
createPriceLine({ unit: Unit.sd, number: 3 }),
|
||||
createPriceLine({ unit: Unit.sd, number: 2 }),
|
||||
createPriceLine({ unit: Unit.sd, number: 1 }),
|
||||
createPriceLine({ unit: Unit.sd, number: 0 }),
|
||||
createPriceLine({ unit: Unit.sd, number: -1 }),
|
||||
createPriceLine({ unit: Unit.sd, number: -2 }),
|
||||
createPriceLine({ unit: Unit.sd, number: -3 }),
|
||||
],
|
||||
})),
|
||||
},
|
||||
@@ -161,7 +162,7 @@ export function createAveragesSection(ctx, averages) {
|
||||
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" }),
|
||||
s({ metric: (metricAddon === "sma" ? sma : ema).price, name: id, color, unit: Unit.usd }),
|
||||
),
|
||||
},
|
||||
...averages.map(({ name, color, sma, ema }) => ({
|
||||
106
websites/bitview/scripts/options/market/index.js
Normal file
106
websites/bitview/scripts/options/market/index.js
Normal file
@@ -0,0 +1,106 @@
|
||||
/** Market section - Main entry point */
|
||||
|
||||
import { Unit } from "../../utils/units.js";
|
||||
import { buildAverages, createAveragesSection } from "./averages.js";
|
||||
import { createPerformanceSection } from "./performance.js";
|
||||
import { createIndicatorsSection } from "./indicators/index.js";
|
||||
import { createInvestingSection } from "./investing.js";
|
||||
|
||||
/**
|
||||
* Create Market section
|
||||
* @param {PartialContext} ctx
|
||||
* @returns {PartialOptionsGroup}
|
||||
*/
|
||||
export function createMarketSection(ctx) {
|
||||
const { colors, brk, s } = ctx;
|
||||
const { market, supply } = brk.tree.computed;
|
||||
const {
|
||||
movingAverage,
|
||||
ath,
|
||||
returns,
|
||||
volatility,
|
||||
range,
|
||||
dca,
|
||||
lookback,
|
||||
indicators,
|
||||
} = market;
|
||||
|
||||
const averages = buildAverages(colors, movingAverage);
|
||||
|
||||
return {
|
||||
name: "Market",
|
||||
tree: [
|
||||
// Price
|
||||
{
|
||||
name: "Price",
|
||||
title: "Bitcoin Price",
|
||||
},
|
||||
|
||||
// Capitalization
|
||||
{
|
||||
name: "Capitalization",
|
||||
title: "Market Capitalization",
|
||||
bottom: [
|
||||
s({
|
||||
metric: brk.mergeMetricPatterns(
|
||||
supply.marketCap.height,
|
||||
supply.marketCap.indexes,
|
||||
),
|
||||
name: "Capitalization",
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
],
|
||||
},
|
||||
|
||||
// All Time High
|
||||
{
|
||||
name: "All Time High",
|
||||
title: "All Time High",
|
||||
top: [s({ metric: ath.priceAth, name: "ATH", unit: Unit.usd })],
|
||||
bottom: [
|
||||
s({
|
||||
metric: ath.priceDrawdown,
|
||||
name: "Drawdown",
|
||||
color: colors.red,
|
||||
unit: Unit.percentage,
|
||||
}),
|
||||
s({ metric: ath.daysSincePriceAth, name: "Since", unit: Unit.days }),
|
||||
s({
|
||||
metric: ath.yearsSincePriceAth,
|
||||
name: "Since",
|
||||
unit: Unit.years,
|
||||
}),
|
||||
s({
|
||||
metric: ath.maxDaysBetweenPriceAths,
|
||||
name: "Max",
|
||||
color: colors.red,
|
||||
unit: Unit.days,
|
||||
}),
|
||||
s({
|
||||
metric: ath.maxYearsBetweenPriceAths,
|
||||
name: "Max",
|
||||
color: colors.red,
|
||||
unit: Unit.years,
|
||||
}),
|
||||
],
|
||||
},
|
||||
|
||||
// Averages
|
||||
createAveragesSection(ctx, averages),
|
||||
|
||||
// Performance
|
||||
createPerformanceSection(ctx, returns),
|
||||
|
||||
// Indicators
|
||||
createIndicatorsSection(ctx, {
|
||||
volatility,
|
||||
range,
|
||||
movingAverage,
|
||||
indicators,
|
||||
}),
|
||||
|
||||
// Investing
|
||||
createInvestingSection(ctx, { dca, lookback, returns }),
|
||||
],
|
||||
};
|
||||
}
|
||||
85
websites/bitview/scripts/options/market/indicators/bands.js
Normal file
85
websites/bitview/scripts/options/market/indicators/bands.js
Normal file
@@ -0,0 +1,85 @@
|
||||
/** Bands indicators (MinMax, Mayer Multiple) */
|
||||
|
||||
import { Unit } from "../../../utils/units.js";
|
||||
|
||||
/**
|
||||
* Create Bands section
|
||||
* @param {PartialContext} ctx
|
||||
* @param {Object} args
|
||||
* @param {Market["range"]} args.range
|
||||
* @param {Market["movingAverage"]} args.movingAverage
|
||||
*/
|
||||
export function createBandsSection(ctx, { range, movingAverage }) {
|
||||
const { s, colors } = ctx;
|
||||
|
||||
return {
|
||||
name: "Bands",
|
||||
tree: [
|
||||
{
|
||||
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: Unit.usd }),
|
||||
s({
|
||||
metric: max,
|
||||
name: "Max",
|
||||
color: colors.green,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
],
|
||||
})),
|
||||
},
|
||||
{
|
||||
name: "Mayer Multiple",
|
||||
title: "Mayer Multiple",
|
||||
top: [
|
||||
s({
|
||||
metric: movingAverage.price200dSma.price,
|
||||
name: "200d SMA",
|
||||
color: colors.yellow,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
s({
|
||||
metric: movingAverage.price200dSmaX24,
|
||||
name: "200d SMA x2.4",
|
||||
color: colors.green,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
s({
|
||||
metric: movingAverage.price200dSmaX08,
|
||||
name: "200d SMA x0.8",
|
||||
color: colors.red,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
/** Momentum indicators (RSI, StochRSI, Stochastic, MACD) */
|
||||
|
||||
import { Unit } from "../../../utils/units.js";
|
||||
|
||||
/**
|
||||
* Create Momentum section
|
||||
* @param {PartialContext} ctx
|
||||
@@ -19,25 +21,25 @@ export function createMomentumSection(ctx, indicators) {
|
||||
metric: indicators.rsi14d,
|
||||
name: "RSI",
|
||||
color: colors.indigo,
|
||||
unit: "index",
|
||||
unit: Unit.index,
|
||||
}),
|
||||
s({
|
||||
metric: indicators.rsi14dMin,
|
||||
name: "Min",
|
||||
color: colors.red,
|
||||
defaultActive: false,
|
||||
unit: "index",
|
||||
unit: Unit.index,
|
||||
}),
|
||||
s({
|
||||
metric: indicators.rsi14dMax,
|
||||
name: "Max",
|
||||
color: colors.green,
|
||||
defaultActive: false,
|
||||
unit: "index",
|
||||
unit: Unit.index,
|
||||
}),
|
||||
createPriceLine({ unit: "index", number: 70 }),
|
||||
createPriceLine({ unit: "index", number: 50, defaultActive: false }),
|
||||
createPriceLine({ unit: "index", number: 30 }),
|
||||
createPriceLine({ unit: Unit.index, number: 70 }),
|
||||
createPriceLine({ unit: Unit.index, number: 50, defaultActive: false }),
|
||||
createPriceLine({ unit: Unit.index, number: 30 }),
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -48,32 +50,32 @@ export function createMomentumSection(ctx, indicators) {
|
||||
// metric: indicators.stochRsi,
|
||||
// name: "Stoch RSI",
|
||||
// color: colors.purple,
|
||||
// unit: "index",
|
||||
// unit: Unit.index,
|
||||
// }),
|
||||
s({
|
||||
metric: indicators.stochRsiK,
|
||||
name: "K",
|
||||
color: colors.blue,
|
||||
unit: "index",
|
||||
unit: Unit.index,
|
||||
}),
|
||||
s({
|
||||
metric: indicators.stochRsiD,
|
||||
name: "D",
|
||||
color: colors.orange,
|
||||
unit: "index",
|
||||
unit: Unit.index,
|
||||
}),
|
||||
createPriceLine({ unit: "index", number: 80 }),
|
||||
createPriceLine({ unit: "index", number: 20 }),
|
||||
createPriceLine({ unit: Unit.index, number: 80 }),
|
||||
createPriceLine({ unit: Unit.index, number: 20 }),
|
||||
],
|
||||
},
|
||||
// {
|
||||
// name: "Stochastic",
|
||||
// title: "Stochastic Oscillator",
|
||||
// bottom: [
|
||||
// s({ metric: indicators.stochK, name: "K", color: colors.blue, unit: "index" }),
|
||||
// s({ metric: indicators.stochD, name: "D", color: colors.orange, unit: "index" }),
|
||||
// createPriceLine({ unit: "index", number: 80 }),
|
||||
// createPriceLine({ unit: "index", number: 20 }),
|
||||
// s({ metric: indicators.stochK, name: "K", color: colors.blue, unit: Unit.index }),
|
||||
// s({ metric: indicators.stochD, name: "D", color: colors.orange, unit: Unit.index }),
|
||||
// createPriceLine({ unit: Unit.index, number: 80 }),
|
||||
// createPriceLine({ unit: Unit.index, number: 20 }),
|
||||
// ],
|
||||
// },
|
||||
{
|
||||
@@ -84,21 +86,21 @@ export function createMomentumSection(ctx, indicators) {
|
||||
metric: indicators.macdLine,
|
||||
name: "MACD",
|
||||
color: colors.blue,
|
||||
unit: "usd",
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
s({
|
||||
metric: indicators.macdSignal,
|
||||
name: "Signal",
|
||||
color: colors.orange,
|
||||
unit: "usd",
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
/** @type {FetchedHistogramSeriesBlueprint} */ ({
|
||||
metric: indicators.macdHistogram,
|
||||
title: "Histogram",
|
||||
type: "Histogram",
|
||||
unit: "usd",
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
createPriceLine({ unit: "usd" }),
|
||||
createPriceLine({ unit: Unit.usd }),
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -1,5 +1,7 @@
|
||||
/** On-chain indicators (Pi Cycle, Puell, NVT, Gini) */
|
||||
|
||||
import { Unit } from "../../../utils/units.js";
|
||||
|
||||
/**
|
||||
* Create On-chain section
|
||||
* @param {PartialContext} ctx
|
||||
@@ -21,13 +23,13 @@ export function createOnchainSection(ctx, { indicators, movingAverage }) {
|
||||
metric: movingAverage.price111dSma.price,
|
||||
name: "111d SMA",
|
||||
color: colors.green,
|
||||
unit: "usd",
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
s({
|
||||
metric: movingAverage.price350dSmaX2,
|
||||
name: "350d SMA x2",
|
||||
color: colors.red,
|
||||
unit: "usd",
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
],
|
||||
bottom: [
|
||||
@@ -35,9 +37,9 @@ export function createOnchainSection(ctx, { indicators, movingAverage }) {
|
||||
metric: indicators.piCycle,
|
||||
name: "Pi Cycle",
|
||||
color: colors.purple,
|
||||
unit: "ratio",
|
||||
unit: Unit.ratio,
|
||||
}),
|
||||
createPriceLine({ unit: "ratio", number: 1 }),
|
||||
createPriceLine({ unit: Unit.ratio, number: 1 }),
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -48,7 +50,7 @@ export function createOnchainSection(ctx, { indicators, movingAverage }) {
|
||||
metric: indicators.puellMultiple,
|
||||
name: "Puell",
|
||||
color: colors.green,
|
||||
unit: "ratio",
|
||||
unit: Unit.ratio,
|
||||
}),
|
||||
],
|
||||
},
|
||||
@@ -60,7 +62,7 @@ export function createOnchainSection(ctx, { indicators, movingAverage }) {
|
||||
metric: indicators.nvt,
|
||||
name: "NVT",
|
||||
color: colors.orange,
|
||||
unit: "ratio",
|
||||
unit: Unit.ratio,
|
||||
}),
|
||||
],
|
||||
},
|
||||
@@ -72,7 +74,7 @@ export function createOnchainSection(ctx, { indicators, movingAverage }) {
|
||||
metric: indicators.gini,
|
||||
name: "Gini",
|
||||
color: colors.red,
|
||||
unit: "ratio",
|
||||
unit: Unit.ratio,
|
||||
}),
|
||||
],
|
||||
},
|
||||
@@ -1,5 +1,7 @@
|
||||
/** Volatility indicators (Index, True Range, Choppiness, Sharpe, Sortino) */
|
||||
|
||||
import { Unit } from "../../../utils/units.js";
|
||||
|
||||
/**
|
||||
* Create Volatility section
|
||||
* @param {PartialContext} ctx
|
||||
@@ -17,43 +19,43 @@ export function createVolatilitySection(ctx, { volatility, range }) {
|
||||
name: "Index",
|
||||
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" }),
|
||||
s({ metric: volatility.price1wVolatility, name: "1w", color: colors.red, unit: Unit.percentage }),
|
||||
s({ metric: volatility.price1mVolatility, name: "1m", color: colors.orange, unit: Unit.percentage }),
|
||||
s({ metric: volatility.price1yVolatility, name: "1y", color: colors.lime, unit: Unit.percentage }),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "True Range",
|
||||
title: "Bitcoin Price True Range",
|
||||
bottom: [s({ metric: range.priceTrueRange, name: "value", color: colors.yellow, unit: "usd" })],
|
||||
bottom: [s({ metric: range.priceTrueRange, name: "Value", color: colors.yellow, unit: Unit.usd })],
|
||||
},
|
||||
{
|
||||
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 }),
|
||||
s({ metric: range.price2wChoppinessIndex, name: "2w", color: colors.red, unit: Unit.index }),
|
||||
createPriceLine({ unit: Unit.index, number: 61.8 }),
|
||||
createPriceLine({ unit: Unit.index, number: 38.2 }),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Sharpe Ratio",
|
||||
title: "Sharpe Ratio",
|
||||
bottom: [
|
||||
s({ metric: volatility.sharpe1w, name: "1w", color: colors.red, unit: "ratio" }),
|
||||
s({ metric: volatility.sharpe1m, name: "1m", color: colors.orange, unit: "ratio" }),
|
||||
s({ metric: volatility.sharpe1y, name: "1y", color: colors.lime, unit: "ratio" }),
|
||||
createPriceLine({ unit: "ratio" }),
|
||||
s({ metric: volatility.sharpe1w, name: "1w", color: colors.red, unit: Unit.ratio }),
|
||||
s({ metric: volatility.sharpe1m, name: "1m", color: colors.orange, unit: Unit.ratio }),
|
||||
s({ metric: volatility.sharpe1y, name: "1y", color: colors.lime, unit: Unit.ratio }),
|
||||
createPriceLine({ unit: Unit.ratio }),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Sortino Ratio",
|
||||
title: "Sortino Ratio",
|
||||
bottom: [
|
||||
s({ metric: volatility.sortino1w, name: "1w", color: colors.red, unit: "ratio" }),
|
||||
s({ metric: volatility.sortino1m, name: "1m", color: colors.orange, unit: "ratio" }),
|
||||
s({ metric: volatility.sortino1y, name: "1y", color: colors.lime, unit: "ratio" }),
|
||||
createPriceLine({ unit: "ratio" }),
|
||||
s({ metric: volatility.sortino1w, name: "1w", color: colors.red, unit: Unit.ratio }),
|
||||
s({ metric: volatility.sortino1m, name: "1m", color: colors.orange, unit: Unit.ratio }),
|
||||
s({ metric: volatility.sortino1y, name: "1y", color: colors.lime, unit: Unit.ratio }),
|
||||
createPriceLine({ unit: Unit.ratio }),
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -1,5 +1,6 @@
|
||||
/** Investing section (DCA) */
|
||||
|
||||
import { Unit } from "../../utils/units.js";
|
||||
import { periodIdToName } from "./utils.js";
|
||||
|
||||
/**
|
||||
@@ -24,7 +25,7 @@ export function buildDcaClasses(colors, dca) {
|
||||
year,
|
||||
color: colors[colorKey],
|
||||
defaultActive,
|
||||
costBasis: dca.classAvgPrice[`_${year}`],
|
||||
costBasis: dca.classAveragePrice[`_${year}`],
|
||||
returns: dca.classReturns[`_${year}`],
|
||||
stack: dca.classStack[`_${year}`],
|
||||
}));
|
||||
@@ -65,7 +66,7 @@ export function createInvestingSection(ctx, { dca, lookback, returns }) {
|
||||
const name = periodIdToName(id, true);
|
||||
const priceAgo = lookback.priceAgo[key];
|
||||
const priceReturns = returns.priceReturns[key];
|
||||
const dcaCostBasis = dca.periodAvgPrice[key];
|
||||
const dcaCostBasis = dca.periodAveragePrice[key];
|
||||
const dcaReturns = dca.periodReturns[key];
|
||||
const dcaStack = dca.periodStack[key];
|
||||
const lumpSumStack = dca.periodLumpSumStack[key];
|
||||
@@ -76,8 +77,18 @@ export function createInvestingSection(ctx, { dca, lookback, returns }) {
|
||||
name: "Cost basis",
|
||||
title: `${name} DCA vs Lump Sum (Cost Basis)`,
|
||||
top: [
|
||||
s({ metric: dcaCostBasis, name: "DCA", color: colors.green, unit: "usd" }),
|
||||
s({ metric: priceAgo, name: "Lump sum", color: colors.orange, unit: "usd" }),
|
||||
s({
|
||||
metric: dcaCostBasis,
|
||||
name: "DCA",
|
||||
color: colors.green,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
s({
|
||||
metric: priceAgo,
|
||||
name: "Lump sum",
|
||||
color: colors.orange,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -88,28 +99,58 @@ export function createInvestingSection(ctx, { dca, lookback, returns }) {
|
||||
metric: dcaReturns,
|
||||
title: "DCA",
|
||||
type: "Baseline",
|
||||
unit: "percentage",
|
||||
unit: Unit.percentage,
|
||||
}),
|
||||
/** @type {AnyFetchedSeriesBlueprint} */ ({
|
||||
metric: priceReturns,
|
||||
title: "Lump sum",
|
||||
type: "Baseline",
|
||||
colors: [colors.lime, colors.red],
|
||||
unit: "percentage",
|
||||
unit: Unit.percentage,
|
||||
}),
|
||||
createPriceLine({ unit: "percentage" }),
|
||||
createPriceLine({ unit: Unit.percentage }),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Stack",
|
||||
title: `${name} DCA vs Lump Sum Stack ($100/day)`,
|
||||
bottom: [
|
||||
s({ metric: dcaStack.sats, name: "DCA", color: colors.green, unit: "sats" }),
|
||||
s({ metric: dcaStack.bitcoin, name: "DCA", color: colors.green, unit: "btc" }),
|
||||
s({ metric: dcaStack.dollars, name: "DCA", color: colors.green, unit: "usd" }),
|
||||
s({ metric: lumpSumStack.sats, name: "Lump sum", color: colors.orange, unit: "sats" }),
|
||||
s({ metric: lumpSumStack.bitcoin, name: "Lump sum", color: colors.orange, unit: "btc" }),
|
||||
s({ metric: lumpSumStack.dollars, name: "Lump sum", color: colors.orange, unit: "usd" }),
|
||||
s({
|
||||
metric: dcaStack.sats,
|
||||
name: "DCA",
|
||||
color: colors.green,
|
||||
unit: Unit.sats,
|
||||
}),
|
||||
s({
|
||||
metric: dcaStack.bitcoin,
|
||||
name: "DCA",
|
||||
color: colors.green,
|
||||
unit: Unit.btc,
|
||||
}),
|
||||
s({
|
||||
metric: dcaStack.dollars,
|
||||
name: "DCA",
|
||||
color: colors.green,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
s({
|
||||
metric: lumpSumStack.sats,
|
||||
name: "Lump sum",
|
||||
color: colors.orange,
|
||||
unit: Unit.sats,
|
||||
}),
|
||||
s({
|
||||
metric: lumpSumStack.bitcoin,
|
||||
name: "Lump sum",
|
||||
color: colors.orange,
|
||||
unit: Unit.btc,
|
||||
}),
|
||||
s({
|
||||
metric: lumpSumStack.dollars,
|
||||
name: "Lump sum",
|
||||
color: colors.orange,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -128,32 +169,60 @@ export function createInvestingSection(ctx, { dca, lookback, returns }) {
|
||||
{
|
||||
name: "Cost basis",
|
||||
title: "DCA Cost Basis by Year",
|
||||
top: dcaClasses.map(({ year, color, defaultActive, costBasis }) =>
|
||||
s({ metric: costBasis, name: `${year}`, color, defaultActive, unit: "usd" }),
|
||||
top: dcaClasses.map(
|
||||
({ year, color, defaultActive, costBasis }) =>
|
||||
s({
|
||||
metric: costBasis,
|
||||
name: `${year}`,
|
||||
color,
|
||||
defaultActive,
|
||||
unit: 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",
|
||||
}),
|
||||
bottom: dcaClasses.map(
|
||||
({ year, color, defaultActive, returns }) =>
|
||||
/** @type {AnyFetchedSeriesBlueprint} */ ({
|
||||
metric: returns,
|
||||
title: `${year}`,
|
||||
type: "Baseline",
|
||||
color,
|
||||
defaultActive,
|
||||
unit: Unit.percentage,
|
||||
}),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "Stack",
|
||||
title: "DCA Stack by Year ($100/day)",
|
||||
bottom: dcaClasses.flatMap(({ year, color, defaultActive, stack }) => [
|
||||
s({ metric: stack.sats, name: `${year}`, color, defaultActive, unit: "sats" }),
|
||||
s({ metric: stack.bitcoin, name: `${year}`, color, defaultActive, unit: "btc" }),
|
||||
s({ metric: stack.dollars, name: `${year}`, color, defaultActive, unit: "usd" }),
|
||||
]),
|
||||
bottom: dcaClasses.flatMap(
|
||||
({ year, color, defaultActive, stack }) => [
|
||||
s({
|
||||
metric: stack.sats,
|
||||
name: `${year}`,
|
||||
color,
|
||||
defaultActive,
|
||||
unit: Unit.sats,
|
||||
}),
|
||||
s({
|
||||
metric: stack.bitcoin,
|
||||
name: `${year}`,
|
||||
color,
|
||||
defaultActive,
|
||||
unit: Unit.btc,
|
||||
}),
|
||||
s({
|
||||
metric: stack.dollars,
|
||||
name: `${year}`,
|
||||
color,
|
||||
defaultActive,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
],
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -164,7 +233,14 @@ export function createInvestingSection(ctx, { dca, lookback, returns }) {
|
||||
{
|
||||
name: "Cost basis",
|
||||
title: `DCA Class ${year} Cost Basis`,
|
||||
top: [s({ metric: costBasis, name: "Cost basis", color, unit: "usd" })],
|
||||
top: [
|
||||
s({
|
||||
metric: costBasis,
|
||||
name: "Cost basis",
|
||||
color,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Returns",
|
||||
@@ -175,7 +251,7 @@ export function createInvestingSection(ctx, { dca, lookback, returns }) {
|
||||
title: "Returns",
|
||||
type: "Baseline",
|
||||
color,
|
||||
unit: "percentage",
|
||||
unit: Unit.percentage,
|
||||
}),
|
||||
],
|
||||
},
|
||||
@@ -183,9 +259,24 @@ export function createInvestingSection(ctx, { dca, lookback, returns }) {
|
||||
name: "Stack",
|
||||
title: `DCA Class ${year} Stack ($100/day)`,
|
||||
bottom: [
|
||||
s({ metric: stack.sats, name: "Stack", color, unit: "sats" }),
|
||||
s({ metric: stack.bitcoin, name: "Stack", color, unit: "btc" }),
|
||||
s({ metric: stack.dollars, name: "Stack", color, unit: "usd" }),
|
||||
s({
|
||||
metric: stack.sats,
|
||||
name: "Stack",
|
||||
color,
|
||||
unit: Unit.sats,
|
||||
}),
|
||||
s({
|
||||
metric: stack.bitcoin,
|
||||
name: "Stack",
|
||||
color,
|
||||
unit: Unit.btc,
|
||||
}),
|
||||
s({
|
||||
metric: stack.dollars,
|
||||
name: "Stack",
|
||||
color,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -1,5 +1,6 @@
|
||||
/** Performance section */
|
||||
|
||||
import { Unit } from "../../utils/units.js";
|
||||
import { periodIdToName } from "./utils.js";
|
||||
|
||||
/**
|
||||
@@ -36,22 +37,22 @@ export function createPerformanceSection(ctx, returns) {
|
||||
bottom: [
|
||||
/** @type {AnyFetchedSeriesBlueprint} */ ({
|
||||
metric: priceReturns,
|
||||
title: "total",
|
||||
title: "Total",
|
||||
type: "Baseline",
|
||||
unit: "percentage",
|
||||
unit: Unit.percentage,
|
||||
}),
|
||||
...(cagr
|
||||
? [
|
||||
/** @type {AnyFetchedSeriesBlueprint} */ ({
|
||||
metric: cagr,
|
||||
title: "cagr",
|
||||
title: "CAGR",
|
||||
type: "Baseline",
|
||||
colors: [colors.lime, colors.pink],
|
||||
unit: "percentage",
|
||||
unit: Unit.percentage,
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
createPriceLine({ unit: "percentage" }),
|
||||
createPriceLine({ unit: Unit.percentage }),
|
||||
],
|
||||
};
|
||||
}),
|
||||
@@ -1,6 +1,6 @@
|
||||
/** Partial options - Main entry point */
|
||||
|
||||
import { localhost } from "../../utils/env.js";
|
||||
import { localhost } from "../utils/env.js";
|
||||
import { createContext } from "./context.js";
|
||||
import {
|
||||
buildCohortData,
|
||||
@@ -1,485 +0,0 @@
|
||||
/** 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" }),
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -1,306 +0,0 @@
|
||||
/** 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 {AnyMetricPattern} 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" }),
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
/** Market section - Main entry point */
|
||||
|
||||
import { buildAverages, createAveragesSection } from "./averages.js";
|
||||
import { createPerformanceSection } from "./performance.js";
|
||||
import { createIndicatorsSection } from "./indicators/index.js";
|
||||
import { createInvestingSection } from "./investing.js";
|
||||
|
||||
/**
|
||||
* Create Market section
|
||||
* @param {PartialContext} ctx
|
||||
* @returns {PartialOptionsGroup}
|
||||
*/
|
||||
export function createMarketSection(ctx) {
|
||||
const { colors, brk, s } = ctx;
|
||||
const { market, supply } = brk.tree.computed;
|
||||
const { movingAverage, ath, returns, volatility, range, dca, lookback, indicators } = market;
|
||||
|
||||
const averages = buildAverages(colors, movingAverage);
|
||||
|
||||
return {
|
||||
name: "Market",
|
||||
tree: [
|
||||
// Price
|
||||
{
|
||||
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
|
||||
createAveragesSection(ctx, averages),
|
||||
|
||||
// Performance
|
||||
createPerformanceSection(ctx, returns),
|
||||
|
||||
// Indicators
|
||||
createIndicatorsSection(ctx, { volatility, range, movingAverage, indicators }),
|
||||
|
||||
// Investing
|
||||
createInvestingSection(ctx, { dca, lookback, returns }),
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
/** Bands indicators (MinMax, Mayer Multiple) */
|
||||
|
||||
/**
|
||||
* Create Bands section
|
||||
* @param {PartialContext} ctx
|
||||
* @param {Object} args
|
||||
* @param {Market["range"]} args.range
|
||||
* @param {Market["movingAverage"]} args.movingAverage
|
||||
*/
|
||||
export function createBandsSection(ctx, { range, movingAverage }) {
|
||||
const { s, colors } = ctx;
|
||||
|
||||
return {
|
||||
name: "Bands",
|
||||
tree: [
|
||||
{
|
||||
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" }),
|
||||
],
|
||||
})),
|
||||
},
|
||||
{
|
||||
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" }),
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -49,9 +49,9 @@ export function fromBlockCount(colors, pattern, title, color) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create series from a BitcoinPattern ({ base, sum, cumulative, average, min, max, median, pct* })
|
||||
* Create series from a DollarsPattern ({ base, sum, cumulative, average, min, max, percentiles.* })
|
||||
* @param {Colors} colors
|
||||
* @param {BitcoinPattern<any>} pattern
|
||||
* @param {DollarsPattern<any>} pattern
|
||||
* @param {string} title
|
||||
* @param {Color} [color]
|
||||
* @returns {AnyFetchedSeriesBlueprint[]}
|
||||
@@ -85,31 +85,31 @@ export function fromBitcoin(colors, pattern, title, color) {
|
||||
defaultActive: false,
|
||||
},
|
||||
{
|
||||
metric: pattern.median,
|
||||
metric: pattern.percentiles.median,
|
||||
title: "Median",
|
||||
color: colors.amber,
|
||||
defaultActive: false,
|
||||
},
|
||||
{
|
||||
metric: pattern.pct75,
|
||||
metric: pattern.percentiles.pct75,
|
||||
title: "pct75",
|
||||
color: colors.red,
|
||||
defaultActive: false,
|
||||
},
|
||||
{
|
||||
metric: pattern.pct25,
|
||||
metric: pattern.percentiles.pct25,
|
||||
title: "pct25",
|
||||
color: colors.yellow,
|
||||
defaultActive: false,
|
||||
},
|
||||
{
|
||||
metric: pattern.pct90,
|
||||
metric: pattern.percentiles.pct90,
|
||||
title: "pct90",
|
||||
color: colors.rose,
|
||||
defaultActive: false,
|
||||
},
|
||||
{
|
||||
metric: pattern.pct10,
|
||||
metric: pattern.percentiles.pct10,
|
||||
title: "pct10",
|
||||
color: colors.lime,
|
||||
defaultActive: false,
|
||||
@@ -118,7 +118,7 @@ export function fromBitcoin(colors, pattern, title, color) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create series from a BlockSizePattern ({ sum, cumulative, average, min, max, median, pct* })
|
||||
* Create series from a BlockSizePattern ({ sum, cumulative, avg, min, max, distribution.percentiles.* })
|
||||
* @param {Colors} colors
|
||||
* @param {BlockSizePattern<any>} pattern
|
||||
* @param {string} title
|
||||
@@ -148,31 +148,31 @@ export function fromBlockSize(colors, pattern, title, color) {
|
||||
defaultActive: false,
|
||||
},
|
||||
{
|
||||
metric: pattern.median,
|
||||
metric: pattern.distribution.percentiles.median,
|
||||
title: "Median",
|
||||
color: colors.amber,
|
||||
defaultActive: false,
|
||||
},
|
||||
{
|
||||
metric: pattern.pct75,
|
||||
metric: pattern.distribution.percentiles.pct75,
|
||||
title: "pct75",
|
||||
color: colors.red,
|
||||
defaultActive: false,
|
||||
},
|
||||
{
|
||||
metric: pattern.pct25,
|
||||
metric: pattern.distribution.percentiles.pct25,
|
||||
title: "pct25",
|
||||
color: colors.yellow,
|
||||
defaultActive: false,
|
||||
},
|
||||
{
|
||||
metric: pattern.pct90,
|
||||
metric: pattern.distribution.percentiles.pct90,
|
||||
title: "pct90",
|
||||
color: colors.rose,
|
||||
defaultActive: false,
|
||||
},
|
||||
{
|
||||
metric: pattern.pct10,
|
||||
metric: pattern.distribution.percentiles.pct10,
|
||||
title: "pct10",
|
||||
color: colors.lime,
|
||||
defaultActive: false,
|
||||
@@ -68,8 +68,8 @@
|
||||
* @typedef {PartialOption & PartialChartOptionSpecific} PartialChartOption
|
||||
*
|
||||
* @typedef {Object} ProcessedChartOptionAddons
|
||||
* @property {Record<Unit, AnyFetchedSeriesBlueprint[]>} top
|
||||
* @property {Record<Unit, AnyFetchedSeriesBlueprint[]>} bottom
|
||||
* @property {Map<Unit, AnyFetchedSeriesBlueprint[]>} top
|
||||
* @property {Map<Unit, AnyFetchedSeriesBlueprint[]>} bottom
|
||||
*
|
||||
* @typedef {Required<Omit<PartialChartOption, "top" | "bottom">> & ProcessedChartOptionAddons & ProcessedOptionAddons} ChartOption
|
||||
*
|
||||
@@ -168,9 +168,9 @@
|
||||
* @typedef {Object} PartialContext
|
||||
* @property {Colors} colors
|
||||
* @property {BrkClient} brk
|
||||
* @property {(args: { metric: AnyMetricPattern, name: string, color?: Color, defaultActive?: boolean, unit?: Unit, options?: LineSeriesPartialOptions }) => AnyFetchedSeriesBlueprint} s
|
||||
* @property {(args: { metric: AnyMetricPattern, name: string, unit: Unit, color?: Color, defaultActive?: boolean, 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: DollarsPattern<any>, title: string, color?: Color) => AnyFetchedSeriesBlueprint[]} fromBitcoin
|
||||
* @property {(pattern: BlockSizePattern<any>, title: string, color?: Color) => AnyFetchedSeriesBlueprint[]} fromBlockSize
|
||||
* @property {(args: { number?: number, name?: string, defaultActive?: boolean, lineStyle?: LineStyle, color?: Color, unit: Unit }) => FetchedLineSeriesBlueprint} createPriceLine
|
||||
* @property {(args: { numbers: number[], unit: Unit }) => FetchedLineSeriesBlueprint[]} createPriceLines
|
||||
@@ -2,11 +2,12 @@ import {
|
||||
createShadow,
|
||||
createHorizontalChoiceField,
|
||||
createHeader,
|
||||
} 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";
|
||||
} from "../../utils/dom.js";
|
||||
import { chartElement } from "../../utils/elements.js";
|
||||
import { ios, canShare } from "../../utils/env.js";
|
||||
import { serdeChartableIndex, serdeOptNumber } from "../../utils/serde.js";
|
||||
import { throttle } from "../../utils/timing.js";
|
||||
import { Unit } from "../../utils/units.js";
|
||||
|
||||
const keyPrefix = "chart";
|
||||
const ONE_BTC_IN_SATS = 100_000_000;
|
||||
@@ -45,7 +46,6 @@ export function init({
|
||||
|
||||
const { index, fieldset } = createIndexSelector({
|
||||
option,
|
||||
brk,
|
||||
signals,
|
||||
});
|
||||
|
||||
@@ -169,13 +169,12 @@ export function init({
|
||||
|
||||
const { field: topUnitField, selected: topUnit } =
|
||||
createHorizontalChoiceField({
|
||||
defaultValue: "usd",
|
||||
defaultValue: Unit.usd,
|
||||
keyPrefix,
|
||||
key: "unit-0",
|
||||
choices: /** @type {const} */ ([
|
||||
/** @satisfies {Unit} */ ("usd"),
|
||||
/** @satisfies {Unit} */ ("sats"),
|
||||
]),
|
||||
choices: [Unit.usd, Unit.sats],
|
||||
toKey: (u) => u.id,
|
||||
toLabel: (u) => u.name,
|
||||
signals,
|
||||
sorted: true,
|
||||
});
|
||||
@@ -205,7 +204,7 @@ export function init({
|
||||
|
||||
const latest = { ..._latest };
|
||||
|
||||
if (unit === "sats") {
|
||||
if (unit === Unit.sats) {
|
||||
latest.open = Math.floor(ONE_BTC_IN_SATS / latest.open);
|
||||
latest.high = Math.floor(ONE_BTC_IN_SATS / latest.high);
|
||||
latest.low = Math.floor(ONE_BTC_IN_SATS / latest.low);
|
||||
@@ -277,26 +276,30 @@ export function init({
|
||||
signals.createEffect(option, (option) => {
|
||||
headingElement.innerHTML = option.title;
|
||||
|
||||
const bottomUnits = /** @type {readonly Unit[]} */ (
|
||||
Object.keys(option.bottom)
|
||||
);
|
||||
const { field: bottomUnitField, selected: bottomUnit } =
|
||||
createHorizontalChoiceField({
|
||||
defaultValue: bottomUnits.at(0) || "",
|
||||
const bottomUnits = Array.from(option.bottom.keys());
|
||||
|
||||
/** @type {{ field: HTMLDivElement, selected: Accessor<Unit> } | undefined} */
|
||||
let bottomUnitSelector;
|
||||
|
||||
if (bottomUnits.length) {
|
||||
bottomUnitSelector = createHorizontalChoiceField({
|
||||
defaultValue: bottomUnits[0],
|
||||
keyPrefix,
|
||||
key: "unit-1",
|
||||
choices: bottomUnits,
|
||||
toKey: (u) => u.id,
|
||||
toLabel: (u) => u.name,
|
||||
signals,
|
||||
sorted: true,
|
||||
});
|
||||
|
||||
if (bottomUnits.length) {
|
||||
const field = bottomUnitSelector.field;
|
||||
chart.addFieldsetIfNeeded({
|
||||
id: "charts-unit-1",
|
||||
paneIndex: 1,
|
||||
position: "nw",
|
||||
createChild() {
|
||||
return bottomUnitField;
|
||||
return field;
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -323,7 +326,7 @@ export function init({
|
||||
console.log({ topUnit, topSeriesType });
|
||||
|
||||
switch (topUnit) {
|
||||
case "usd": {
|
||||
case Unit.usd: {
|
||||
switch (topSeriesType) {
|
||||
case null:
|
||||
case CANDLE: {
|
||||
@@ -353,7 +356,7 @@ export function init({
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "sats": {
|
||||
case Unit.sats: {
|
||||
switch (topSeriesType) {
|
||||
case null:
|
||||
case CANDLE: {
|
||||
@@ -404,100 +407,117 @@ export function init({
|
||||
},
|
||||
);
|
||||
|
||||
[
|
||||
{
|
||||
blueprints: option.top,
|
||||
paneIndex: 0,
|
||||
unit: topUnit,
|
||||
seriesList: seriesListTop,
|
||||
orderStart: 1,
|
||||
legend: chart.legendTop,
|
||||
},
|
||||
{
|
||||
/**
|
||||
* @param {Object} args
|
||||
* @param {Map<Unit, AnyFetchedSeriesBlueprint[]>} args.blueprints
|
||||
* @param {number} args.paneIndex
|
||||
* @param {Accessor<Unit>} args.unit
|
||||
* @param {Series[]} args.seriesList
|
||||
* @param {number} args.orderStart
|
||||
* @param {Legend} args.legend
|
||||
*/
|
||||
function processPane({
|
||||
blueprints,
|
||||
paneIndex,
|
||||
unit,
|
||||
seriesList,
|
||||
orderStart,
|
||||
legend,
|
||||
}) {
|
||||
signals.createEffect(unit, (unit) => {
|
||||
legend.removeFrom(orderStart);
|
||||
|
||||
seriesList.splice(orderStart).forEach((series) => {
|
||||
series.remove();
|
||||
});
|
||||
|
||||
blueprints.get(unit)?.forEach((blueprint, order) => {
|
||||
order += orderStart;
|
||||
|
||||
const options = blueprint.options;
|
||||
|
||||
// Tree-first: metric is now an accessor with .by property
|
||||
const indexes = Object.keys(blueprint.metric.by);
|
||||
|
||||
if (indexes.includes(index)) {
|
||||
switch (blueprint.type) {
|
||||
case "Baseline": {
|
||||
seriesList.push(
|
||||
chart.addBaselineSeries({
|
||||
metric: blueprint.metric,
|
||||
name: blueprint.title,
|
||||
unit,
|
||||
defaultActive: blueprint.defaultActive,
|
||||
paneIndex,
|
||||
options: {
|
||||
...options,
|
||||
topLineColor:
|
||||
blueprint.color?.() ?? blueprint.colors?.[0](),
|
||||
bottomLineColor:
|
||||
blueprint.color?.() ?? blueprint.colors?.[1](),
|
||||
},
|
||||
order,
|
||||
}),
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "Histogram": {
|
||||
seriesList.push(
|
||||
chart.addHistogramSeries({
|
||||
metric: blueprint.metric,
|
||||
name: blueprint.title,
|
||||
unit,
|
||||
color: blueprint.color,
|
||||
defaultActive: blueprint.defaultActive,
|
||||
paneIndex,
|
||||
options,
|
||||
order,
|
||||
}),
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "Candlestick": {
|
||||
throw Error("TODO");
|
||||
}
|
||||
case "Line":
|
||||
case undefined:
|
||||
seriesList.push(
|
||||
chart.addLineSeries({
|
||||
metric: blueprint.metric,
|
||||
color: blueprint.color,
|
||||
name: blueprint.title,
|
||||
unit,
|
||||
defaultActive: blueprint.defaultActive,
|
||||
paneIndex,
|
||||
options,
|
||||
order,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
processPane({
|
||||
blueprints: option.top,
|
||||
paneIndex: 0,
|
||||
unit: topUnit,
|
||||
seriesList: seriesListTop,
|
||||
orderStart: 1,
|
||||
legend: chart.legendTop,
|
||||
});
|
||||
|
||||
if (bottomUnitSelector) {
|
||||
processPane({
|
||||
blueprints: option.bottom,
|
||||
paneIndex: 1,
|
||||
unit: bottomUnit,
|
||||
unit: bottomUnitSelector.selected,
|
||||
seriesList: seriesListBottom,
|
||||
orderStart: 0,
|
||||
legend: chart.legendBottom,
|
||||
},
|
||||
].forEach(
|
||||
({ blueprints, paneIndex, unit, seriesList, orderStart, legend }) => {
|
||||
signals.createEffect(unit, (unit) => {
|
||||
legend.removeFrom(orderStart);
|
||||
|
||||
seriesList.splice(orderStart).forEach((series) => {
|
||||
series.remove();
|
||||
});
|
||||
|
||||
blueprints[unit]?.forEach((blueprint, order) => {
|
||||
order += orderStart;
|
||||
|
||||
const options = blueprint.options;
|
||||
|
||||
// Tree-first: metric is now an accessor with .by property
|
||||
const indexes = Object.keys(blueprint.metric.by);
|
||||
|
||||
if (indexes.includes(index)) {
|
||||
switch (blueprint.type) {
|
||||
case "Baseline": {
|
||||
seriesList.push(
|
||||
chart.addBaselineSeries({
|
||||
metric: blueprint.metric,
|
||||
name: blueprint.title,
|
||||
unit,
|
||||
defaultActive: blueprint.defaultActive,
|
||||
paneIndex,
|
||||
options: {
|
||||
...options,
|
||||
topLineColor:
|
||||
blueprint.color?.() ?? blueprint.colors?.[0](),
|
||||
bottomLineColor:
|
||||
blueprint.color?.() ?? blueprint.colors?.[1](),
|
||||
},
|
||||
order,
|
||||
}),
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "Histogram": {
|
||||
seriesList.push(
|
||||
chart.addHistogramSeries({
|
||||
metric: blueprint.metric,
|
||||
name: blueprint.title,
|
||||
unit,
|
||||
color: blueprint.color,
|
||||
defaultActive: blueprint.defaultActive,
|
||||
paneIndex,
|
||||
options,
|
||||
order,
|
||||
}),
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "Candlestick": {
|
||||
throw Error("TODO");
|
||||
}
|
||||
case "Line":
|
||||
case undefined:
|
||||
seriesList.push(
|
||||
chart.addLineSeries({
|
||||
metric: blueprint.metric,
|
||||
color: blueprint.color,
|
||||
name: blueprint.title,
|
||||
unit,
|
||||
defaultActive: blueprint.defaultActive,
|
||||
paneIndex,
|
||||
options,
|
||||
order,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
firstRun = false;
|
||||
});
|
||||
@@ -507,10 +527,9 @@ export function init({
|
||||
/**
|
||||
* @param {Object} args
|
||||
* @param {Accessor<ChartOption>} args.option
|
||||
* @param {BrkClient} args.brk
|
||||
* @param {Signals} args.signals
|
||||
*/
|
||||
function createIndexSelector({ option, brk, signals }) {
|
||||
function createIndexSelector({ option, signals }) {
|
||||
const choices_ = /** @satisfies {ChartableIndexName[]} */ ([
|
||||
"timestamp",
|
||||
"date",
|
||||
@@ -526,11 +545,11 @@ function createIndexSelector({ option, brk, signals }) {
|
||||
const choices = signals.createMemo(() => {
|
||||
const o = option();
|
||||
|
||||
if (!Object.keys(o.top).length && !Object.keys(o.bottom).length) {
|
||||
if (!o.top.size && !o.bottom.size) {
|
||||
return [...choices_];
|
||||
}
|
||||
const rawIndexes = new Set(
|
||||
[Object.values(o.top), Object.values(o.bottom)]
|
||||
[Array.from(o.top.values()), Array.from(o.bottom.values())]
|
||||
.flat(2)
|
||||
.filter((blueprint) => {
|
||||
const path = Object.values(blueprint.metric.by)[0]?.path ?? "";
|
||||
@@ -549,8 +568,10 @@ function createIndexSelector({ option, brk, signals }) {
|
||||
);
|
||||
});
|
||||
|
||||
/** @type {ChartableIndexName} */
|
||||
const defaultIndex = "date";
|
||||
const { field, selected } = createHorizontalChoiceField({
|
||||
defaultValue: "date",
|
||||
defaultValue: defaultIndex,
|
||||
keyPrefix,
|
||||
key: "index",
|
||||
choices,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ios } from "../../utils/env";
|
||||
import { ios } from "../../utils/env.js";
|
||||
import { domToBlob } from "../../modules/modern-screenshot/4.6.6/dist/index.mjs";
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { randomFromArray } from "../utils/array";
|
||||
import { explorerElement } from "../utils/elements";
|
||||
import { randomFromArray } from "../utils/array.js";
|
||||
import { explorerElement } from "../utils/elements.js";
|
||||
|
||||
/**
|
||||
* @param {Object} args
|
||||
@@ -12,13 +12,13 @@ import { explorerElement } from "../utils/elements";
|
||||
* @param {BrkClient} args.brk
|
||||
*/
|
||||
export function init({
|
||||
colors,
|
||||
createChartElement,
|
||||
option,
|
||||
signals,
|
||||
webSockets,
|
||||
resources,
|
||||
brk,
|
||||
colors: _colors,
|
||||
createChartElement: _createChartElement,
|
||||
option: _option,
|
||||
signals: _signals,
|
||||
webSockets: _webSockets,
|
||||
resources: _resources,
|
||||
brk: _brk,
|
||||
}) {
|
||||
const chain = window.document.createElement("div");
|
||||
chain.id = "chain";
|
||||
@@ -39,7 +39,7 @@ export function init({
|
||||
];
|
||||
|
||||
for (let i = 0; i <= 10; i++) {
|
||||
const { name, color } = randomFromArray(miners);
|
||||
const { name, color: _color } = randomFromArray(miners);
|
||||
const { cubeElement, leftFaceElement, rightFaceElement, topFaceElement } =
|
||||
createCube();
|
||||
|
||||
|
||||
@@ -4,20 +4,20 @@ import {
|
||||
createDateRange,
|
||||
dateToDateIndex,
|
||||
differenceBetweenDates,
|
||||
} from "../utils/date";
|
||||
} from "../utils/date.js";
|
||||
import {
|
||||
createButtonElement,
|
||||
createFieldElement,
|
||||
createHeader,
|
||||
createSelect,
|
||||
} from "../utils/dom";
|
||||
import { simulationElement } from "../utils/elements";
|
||||
} from "../utils/dom.js";
|
||||
import { simulationElement } from "../utils/elements.js";
|
||||
import {
|
||||
numberToDollars,
|
||||
numberToPercentage,
|
||||
numberToUSNumber,
|
||||
} from "../utils/format";
|
||||
import { serdeDate, serdeOptDate, serdeOptNumber } from "../utils/serde";
|
||||
} from "../utils/format.js";
|
||||
import { serdeDate, serdeOptDate, serdeOptNumber } from "../utils/serde.js";
|
||||
|
||||
/**
|
||||
* @param {Object} args
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// @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";
|
||||
import { randomFromArray } from "../utils/array.js";
|
||||
import { createButtonElement, createHeader, createSelect } from "../utils/dom.js";
|
||||
import { tableElement } from "../utils/elements.js";
|
||||
import { serdeMetrics, serdeString } from "../utils/serde.js";
|
||||
import { resetParams } from "../utils/url.js";
|
||||
|
||||
/**
|
||||
* @param {Object} args
|
||||
|
||||
@@ -10,17 +10,19 @@
|
||||
/**
|
||||
* @template T
|
||||
* @typedef {Object} RangeState
|
||||
* @property {Signal<T | null>} response
|
||||
* @property {Signal<MetricData<T> | null>} response
|
||||
* @property {Signal<boolean>} loading
|
||||
*/
|
||||
/** @typedef {RangeState<unknown>} AnyRangeState */
|
||||
|
||||
/**
|
||||
* @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
|
||||
* @property {(from?: number, to?: number) => Promise<MetricData<T> | null>} fetch
|
||||
*/
|
||||
/** @typedef {MetricResource<unknown>} AnyMetricResource */
|
||||
|
||||
/**
|
||||
* @typedef {ReturnType<typeof createResources>} Resources
|
||||
@@ -57,7 +59,7 @@ export function createResources(signals) {
|
||||
error.set(null);
|
||||
try {
|
||||
const result = await fetcher(...args);
|
||||
data.set(result);
|
||||
data.set(() => result);
|
||||
return result;
|
||||
} catch (e) {
|
||||
error.set(e instanceof Error ? e : new Error(String(e)));
|
||||
@@ -71,12 +73,12 @@ export function createResources(signals) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a reactive resource wrapper for a MetricNode with multi-range support
|
||||
* Create a reactive resource wrapper for a MetricEndpoint with multi-range support
|
||||
* @template T
|
||||
* @param {MetricNode<T>} node
|
||||
* @param {MetricEndpoint<T>} endpoint
|
||||
* @returns {MetricResource<T>}
|
||||
*/
|
||||
function useMetricNode(node) {
|
||||
function useMetricEndpoint(endpoint) {
|
||||
return signals.runWithOwner(owner, () => {
|
||||
/** @type {Map<string, RangeState<T>>} */
|
||||
const ranges = new Map();
|
||||
@@ -85,20 +87,24 @@ export function createResources(signals) {
|
||||
* Get or create range state
|
||||
* @param {number} [from=-10000]
|
||||
* @param {number} [to]
|
||||
* @returns {RangeState<T>}
|
||||
*/
|
||||
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));
|
||||
const existing = ranges.get(key);
|
||||
if (existing) return existing;
|
||||
|
||||
/** @type {RangeState<T>} */
|
||||
const state = {
|
||||
response: signals.createSignal(/** @type {MetricData<T> | null} */ (null)),
|
||||
loading: signals.createSignal(false),
|
||||
};
|
||||
ranges.set(key, state);
|
||||
return state;
|
||||
}
|
||||
|
||||
return {
|
||||
path: node.path,
|
||||
path: endpoint.path,
|
||||
range,
|
||||
/**
|
||||
* Fetch data for a range
|
||||
@@ -109,7 +115,7 @@ export function createResources(signals) {
|
||||
const r = range(from, to);
|
||||
r.loading.set(true);
|
||||
try {
|
||||
const result = await node.range(from, to, r.response.set);
|
||||
const result = await endpoint.range(from, to, r.response.set);
|
||||
return result;
|
||||
} finally {
|
||||
r.loading.set(false);
|
||||
@@ -119,5 +125,5 @@ export function createResources(signals) {
|
||||
});
|
||||
}
|
||||
|
||||
return { createResource, useMetricNode };
|
||||
return { createResource, useMetricEndpoint };
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* @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";
|
||||
* @import { SignalOptions } from "./modules/solidjs-signals/0.6.3/dist/types/core/core.js"
|
||||
* @import { getOwner as GetOwner, onCleanup as OnCleanup } from "./modules/solidjs-signals/0.6.3/dist/types/core/owner.js"
|
||||
* @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.js";
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
onCleanup,
|
||||
} from "./modules/solidjs-signals/0.6.3/dist/prod.js";
|
||||
|
||||
let effectCount = 0;
|
||||
// let effectCount = 0;
|
||||
|
||||
const signals = {
|
||||
createSolidSignal: /** @type {typeof CreateSignal} */ (createSignal),
|
||||
@@ -95,8 +95,8 @@ const signals = {
|
||||
}-${paramKey}`,
|
||||
);
|
||||
|
||||
/** @type { ((this: Window, ev: PopStateEvent) => any) | undefined} */
|
||||
let popstateCallback;
|
||||
// /** @type { ((this: Window, ev: PopStateEvent) => any) | undefined} */
|
||||
// let popstateCallback;
|
||||
|
||||
let serialized = /** @type {string | null} */ (null);
|
||||
if (options.save.serializeParam !== false) {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { serdeString } from "./serde";
|
||||
|
||||
/**
|
||||
* @param {string} id
|
||||
* @returns {HTMLElement}
|
||||
@@ -215,15 +213,17 @@ export function importStyle(href) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @template {Readonly<string[]>} T
|
||||
* @template T
|
||||
* @param {Object} args
|
||||
* @param {T[number]} args.defaultValue
|
||||
* @param {T} args.defaultValue
|
||||
* @param {string} [args.id]
|
||||
* @param {T | Accessor<T>} args.choices
|
||||
* @param {readonly T[] | Accessor<readonly T[]>} args.choices
|
||||
* @param {string} [args.keyPrefix]
|
||||
* @param {string} args.key
|
||||
* @param {boolean} [args.sorted]
|
||||
* @param {Signals} args.signals
|
||||
* @param {(choice: T) => string} [args.toKey] - Extract string key for storage (defaults to identity for strings)
|
||||
* @param {(choice: T) => string} [args.toLabel] - Extract display label (defaults to identity for strings)
|
||||
*/
|
||||
export function createHorizontalChoiceField({
|
||||
id,
|
||||
@@ -233,9 +233,13 @@ export function createHorizontalChoiceField({
|
||||
key,
|
||||
signals,
|
||||
sorted,
|
||||
toKey = /** @type {(choice: T) => string} */ ((/** @type {any} */ c) => c),
|
||||
toLabel = /** @type {(choice: T) => string} */ ((/** @type {any} */ c) => c),
|
||||
}) {
|
||||
const defaultKey = toKey(defaultValue);
|
||||
|
||||
const choices = signals.createMemo(() => {
|
||||
/** @type {T} */
|
||||
/** @type {readonly T[]} */
|
||||
let c;
|
||||
if (typeof unsortedChoices === "function") {
|
||||
c = unsortedChoices();
|
||||
@@ -244,16 +248,28 @@ export function createHorizontalChoiceField({
|
||||
}
|
||||
|
||||
return sorted
|
||||
? /** @type {T} */ (
|
||||
/** @type {any} */ (c.toSorted((a, b) => a.localeCompare(b)))
|
||||
? /** @type {readonly T[]} */ (
|
||||
/** @type {any} */ (
|
||||
c.toSorted((a, b) => toLabel(a).localeCompare(toLabel(b)))
|
||||
)
|
||||
)
|
||||
: c;
|
||||
});
|
||||
|
||||
/** @type {Signal<T[number]>} */
|
||||
/**
|
||||
* @param {string} storedKey
|
||||
* @returns {T}
|
||||
*/
|
||||
function fromKey(storedKey) {
|
||||
const found = choices().find((c) => toKey(c) === storedKey);
|
||||
return found ?? defaultValue;
|
||||
}
|
||||
|
||||
/** @type {Signal<T>} */
|
||||
const selected = signals.createSignal(defaultValue, {
|
||||
save: {
|
||||
...serdeString,
|
||||
serialize: (v) => toKey(v),
|
||||
deserialize: (s) => fromKey(s),
|
||||
keyPrefix: keyPrefix ?? "",
|
||||
key,
|
||||
saveDefaultValue: true,
|
||||
@@ -268,8 +284,10 @@ export function createHorizontalChoiceField({
|
||||
|
||||
signals.createEffect(choices, (choices) => {
|
||||
const s = selected();
|
||||
if (!choices.includes(s)) {
|
||||
if (choices.includes(defaultValue)) {
|
||||
const sKey = toKey(s);
|
||||
const keys = choices.map(toKey);
|
||||
if (!keys.includes(sKey)) {
|
||||
if (keys.includes(defaultKey)) {
|
||||
selected.set(() => defaultValue);
|
||||
} else if (choices.length) {
|
||||
selected.set(() => choices[0]);
|
||||
@@ -279,17 +297,18 @@ export function createHorizontalChoiceField({
|
||||
div.innerHTML = "";
|
||||
|
||||
choices.forEach((choice) => {
|
||||
const inputValue = choice;
|
||||
const choiceKey = toKey(choice);
|
||||
const choiceLabel = toLabel(choice);
|
||||
const { label } = createLabeledInput({
|
||||
inputId: `${id ?? key}-${choice.toLowerCase()}`,
|
||||
inputId: `${id ?? key}-${choiceKey.toLowerCase()}`,
|
||||
inputName: id ?? key,
|
||||
inputValue,
|
||||
inputChecked: inputValue === selected(),
|
||||
// title: choice,
|
||||
inputValue: choiceKey,
|
||||
inputChecked: choiceKey === sKey,
|
||||
// title: choiceLabel,
|
||||
type: "radio",
|
||||
});
|
||||
|
||||
const text = window.document.createTextNode(choice);
|
||||
const text = window.document.createTextNode(choiceLabel);
|
||||
label.append(text);
|
||||
div.append(label);
|
||||
});
|
||||
@@ -298,7 +317,7 @@ export function createHorizontalChoiceField({
|
||||
field.addEventListener("change", (event) => {
|
||||
// @ts-ignore
|
||||
const value = event.target.value;
|
||||
selected.set(value);
|
||||
selected.set(() => fromKey(value));
|
||||
});
|
||||
|
||||
return { field, selected };
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getElementById } from "./dom";
|
||||
import { getElementById } from "./dom.js";
|
||||
|
||||
export const style = getComputedStyle(window.document.documentElement);
|
||||
|
||||
|
||||
@@ -141,7 +141,7 @@ export const serdeChartableIndex = {
|
||||
},
|
||||
/**
|
||||
* @param {ChartableIndexName} v
|
||||
* @returns {IndexName}
|
||||
* @returns {ChartableIndex}
|
||||
*/
|
||||
deserialize(v) {
|
||||
switch (v) {
|
||||
@@ -223,309 +223,3 @@ export const serdeChartableIndex = {
|
||||
* "years" |
|
||||
* "" } Unit
|
||||
*/
|
||||
|
||||
export const serdeUnit = {
|
||||
/**
|
||||
* @param {string} v
|
||||
*/
|
||||
deserialize(v) {
|
||||
/** @type {Unit | undefined} */
|
||||
let unit;
|
||||
|
||||
/**
|
||||
* @param {Unit} u
|
||||
*/
|
||||
function setUnit(u) {
|
||||
if (unit)
|
||||
throw Error(
|
||||
`Can't assign "${u}" to unit, "${unit}" is already assigned to "${v}"`,
|
||||
);
|
||||
unit = u;
|
||||
}
|
||||
|
||||
if (
|
||||
(!unit || localhost) &&
|
||||
(v.includes("in_sats") ||
|
||||
v.endsWith("_sats") ||
|
||||
(v.endsWith("supply") &&
|
||||
!(v.endsWith("circulating_supply") || v.endsWith("_own_supply"))) ||
|
||||
v === "sent" ||
|
||||
v === "annualized_volume" ||
|
||||
v.endsWith("supply_half") ||
|
||||
v.endsWith("supply_in_profit") ||
|
||||
v.endsWith("supply_in_loss") ||
|
||||
v.endsWith("stack") ||
|
||||
(v.endsWith("value") && !v.includes("realized")) ||
|
||||
((v.includes("coinbase") ||
|
||||
v.includes("fee") ||
|
||||
v.includes("subsidy") ||
|
||||
v.includes("rewards")) &&
|
||||
!(
|
||||
v.startsWith("is_") ||
|
||||
v.includes("_btc") ||
|
||||
v.includes("_usd") ||
|
||||
v.includes("fee_rate") ||
|
||||
v.endsWith("dominance")
|
||||
)))
|
||||
) {
|
||||
setUnit("sats");
|
||||
}
|
||||
if (
|
||||
(!unit || localhost) &&
|
||||
!v.endsWith("velocity") &&
|
||||
((v.includes("_btc") &&
|
||||
!(v.includes("0k_btc") || v.includes("1k_btc"))) ||
|
||||
v.endsWith("_btc"))
|
||||
) {
|
||||
setUnit("btc");
|
||||
}
|
||||
if ((!unit || localhost) && v === "chain") {
|
||||
setUnit("block");
|
||||
}
|
||||
if ((!unit || localhost) && v.startsWith("blocks_before")) {
|
||||
setUnit("blocks");
|
||||
}
|
||||
if (
|
||||
(!unit || localhost) &&
|
||||
(v === "emptyaddressdata" || v === "loadedaddressdata")
|
||||
) {
|
||||
setUnit("address data");
|
||||
}
|
||||
if (
|
||||
(!unit || localhost) &&
|
||||
(v === "price_high" ||
|
||||
v === "price_ohlc" ||
|
||||
v === "price_low" ||
|
||||
v === "price_close" ||
|
||||
v === "price_open" ||
|
||||
v === "price_ath" ||
|
||||
v === "market_cap" ||
|
||||
v.startsWith("price_true_range") ||
|
||||
(v.includes("_usd") && !v.endsWith("velocity")) ||
|
||||
v.includes("cointime_value") ||
|
||||
v.endsWith("_ago") ||
|
||||
v.includes("cost_basis") ||
|
||||
v.endsWith("_price") ||
|
||||
(v.startsWith("price") && (v.endsWith("min") || v.endsWith("max"))) ||
|
||||
(v.endsWith("_cap") && !v.includes("rel_to")) ||
|
||||
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") &&
|
||||
!v.includes("hash_rate")) ||
|
||||
v === "ath")
|
||||
) {
|
||||
setUnit("usd");
|
||||
}
|
||||
if ((!unit || localhost) && v.endsWith("cents")) {
|
||||
setUnit("cents");
|
||||
}
|
||||
if (
|
||||
((!unit || localhost) &&
|
||||
(v.endsWith("ratio") ||
|
||||
(v.includes("ratio") &&
|
||||
(v.endsWith("sma") || v.endsWith("ema") || v.endsWith("zscore"))) ||
|
||||
v.includes("sopr") ||
|
||||
v.endsWith("_5sd") ||
|
||||
v.endsWith("1sd") ||
|
||||
v.endsWith("2sd") ||
|
||||
v.endsWith("3sd") ||
|
||||
v.endsWith("pct1") ||
|
||||
v.endsWith("pct2") ||
|
||||
v.endsWith("pct5") ||
|
||||
v.endsWith("pct95") ||
|
||||
v.endsWith("pct98") ||
|
||||
v.endsWith("pct99"))) ||
|
||||
v.includes("liveliness") ||
|
||||
v.includes("vaultedness") ||
|
||||
v == "puell_multiple" ||
|
||||
v.endsWith("velocity")
|
||||
) {
|
||||
setUnit("ratio");
|
||||
}
|
||||
if (
|
||||
(!unit || localhost) &&
|
||||
(v === "price_drawdown" ||
|
||||
v === "difficulty_adjustment" ||
|
||||
v.endsWith("inflation_rate") ||
|
||||
v.endsWith("_oscillator") ||
|
||||
v.endsWith("_dominance") ||
|
||||
v.endsWith("_returns") ||
|
||||
v.endsWith("_rebound") ||
|
||||
v.endsWith("_volatility") ||
|
||||
v.endsWith("_cagr"))
|
||||
) {
|
||||
setUnit("percentage");
|
||||
}
|
||||
if (
|
||||
(!unit || localhost) &&
|
||||
(v.endsWith("count") ||
|
||||
v.includes("_count_") ||
|
||||
v.startsWith("block_count") ||
|
||||
v.includes("blocks_mined") ||
|
||||
(v.includes("tx_v") && !v.includes("vsize")))
|
||||
) {
|
||||
setUnit("count");
|
||||
}
|
||||
if (
|
||||
(!unit || localhost) &&
|
||||
(v.startsWith("hash_rate") || v.endsWith("as_hash"))
|
||||
) {
|
||||
setUnit("h/s");
|
||||
}
|
||||
if ((!unit || localhost) && v === "pool") {
|
||||
setUnit("id");
|
||||
}
|
||||
if ((!unit || localhost) && v.includes("fee_rate")) {
|
||||
setUnit("sat/vb");
|
||||
}
|
||||
if ((!unit || localhost) && v.startsWith("is_")) {
|
||||
setUnit("bool");
|
||||
}
|
||||
if ((!unit || localhost) && v.endsWith("type")) {
|
||||
setUnit("type");
|
||||
}
|
||||
if (
|
||||
(!unit || localhost) &&
|
||||
(v === "interval" || v.startsWith("block_interval"))
|
||||
) {
|
||||
setUnit("secs");
|
||||
}
|
||||
if ((!unit || localhost) && v.endsWith("_per_sec")) {
|
||||
setUnit("/sec");
|
||||
}
|
||||
if ((!unit || localhost) && v.endsWith("locktime")) {
|
||||
setUnit("locktime");
|
||||
}
|
||||
|
||||
if ((!unit || localhost) && v.endsWith("version")) {
|
||||
setUnit("version");
|
||||
}
|
||||
if (
|
||||
(!unit || localhost) &&
|
||||
(v === "txid" ||
|
||||
(v.endsWith("bytes") && !v.endsWith("vbytes")) ||
|
||||
v.endsWith("base_size") ||
|
||||
v.endsWith("total_size") ||
|
||||
v.includes("block_size"))
|
||||
) {
|
||||
setUnit("bytes");
|
||||
}
|
||||
if ((!unit || localhost) && v.endsWith("_sd")) {
|
||||
setUnit("sd");
|
||||
}
|
||||
if ((!unit || localhost) && (v.includes("vsize") || v.includes("vbytes"))) {
|
||||
setUnit("vb");
|
||||
}
|
||||
if ((!unit || localhost) && v.includes("weight")) {
|
||||
setUnit("wu");
|
||||
}
|
||||
if ((!unit || localhost) && v.endsWith("index")) {
|
||||
setUnit("index");
|
||||
}
|
||||
if ((!unit || localhost) && (v === "date" || v === "date_fixed")) {
|
||||
setUnit("date");
|
||||
}
|
||||
if (
|
||||
(!unit || localhost) &&
|
||||
(v === "timestamp" || v === "timestamp_fixed")
|
||||
) {
|
||||
setUnit("timestamp");
|
||||
}
|
||||
if ((!unit || localhost) && v.includes("coinblocks")) {
|
||||
setUnit("coinblocks");
|
||||
}
|
||||
if ((!unit || localhost) && v.includes("coindays")) {
|
||||
setUnit("coindays");
|
||||
}
|
||||
if ((!unit || localhost) && v.includes("satblocks")) {
|
||||
setUnit("satblocks");
|
||||
}
|
||||
if ((!unit || localhost) && v.includes("satdays")) {
|
||||
setUnit("satdays");
|
||||
}
|
||||
if ((!unit || localhost) && v.endsWith("height")) {
|
||||
setUnit("height");
|
||||
}
|
||||
if ((!unit || localhost) && v.endsWith("rel_to_market_cap")) {
|
||||
setUnit("%mcap");
|
||||
}
|
||||
if ((!unit || localhost) && v.endsWith("rel_to_own_market_cap")) {
|
||||
setUnit("%cmcap");
|
||||
}
|
||||
if ((!unit || localhost) && v.endsWith("rel_to_own_total_unrealized_pnl")) {
|
||||
setUnit("%cp+l");
|
||||
}
|
||||
if ((!unit || localhost) && v.endsWith("rel_to_realized_cap")) {
|
||||
setUnit("%rcap");
|
||||
}
|
||||
if ((!unit || localhost) && v.endsWith("rel_to_circulating_supply")) {
|
||||
setUnit("%all");
|
||||
}
|
||||
if (
|
||||
(!unit || localhost) &&
|
||||
(v.includes("rel_to_realized_profit") ||
|
||||
v.includes("rel_to_realized_loss"))
|
||||
) {
|
||||
setUnit("%pnl");
|
||||
}
|
||||
if ((!unit || localhost) && v.endsWith("rel_to_own_supply")) {
|
||||
setUnit("%self");
|
||||
}
|
||||
if ((!unit || localhost) && v.endsWith("epoch")) {
|
||||
setUnit("epoch");
|
||||
}
|
||||
if ((!unit || localhost) && v === "difficulty") {
|
||||
setUnit("difficulty");
|
||||
}
|
||||
if ((!unit || localhost) && v === "blockhash") {
|
||||
setUnit("hash");
|
||||
}
|
||||
if ((!unit || localhost) && v.startsWith("hash_price_phs")) {
|
||||
setUnit("usd/(ph/s)/day");
|
||||
}
|
||||
if ((!unit || localhost) && v.startsWith("hash_price_ths")) {
|
||||
setUnit("usd/(th/s)/day");
|
||||
}
|
||||
if ((!unit || localhost) && v.startsWith("hash_value_phs")) {
|
||||
setUnit("sats/(ph/s)/day");
|
||||
}
|
||||
if ((!unit || localhost) && v.startsWith("hash_value_ths")) {
|
||||
setUnit("sats/(th/s)/day");
|
||||
}
|
||||
|
||||
if (
|
||||
(!unit || localhost) &&
|
||||
(v.includes("days_between") ||
|
||||
v.includes("days_since") ||
|
||||
v.startsWith("days_before"))
|
||||
) {
|
||||
setUnit("days");
|
||||
}
|
||||
if ((!unit || localhost) && v.includes("years_between")) {
|
||||
setUnit("years");
|
||||
}
|
||||
if ((!unit || localhost) && v == "len") {
|
||||
setUnit("len");
|
||||
}
|
||||
if ((!unit || localhost) && v == "position") {
|
||||
setUnit("position");
|
||||
}
|
||||
if ((!unit || localhost) && v.startsWith("constant")) {
|
||||
setUnit("constant");
|
||||
}
|
||||
|
||||
if (!unit) {
|
||||
console.log();
|
||||
throw Error(`Unit not set for "${v}"`);
|
||||
}
|
||||
|
||||
return /** @type {Unit} */ (unit);
|
||||
},
|
||||
};
|
||||
|
||||
62
websites/bitview/scripts/utils/units.js
Normal file
62
websites/bitview/scripts/utils/units.js
Normal file
@@ -0,0 +1,62 @@
|
||||
/** Unit definitions for chart series */
|
||||
|
||||
/**
|
||||
* Unit enum with id (for serialization) and name (for display)
|
||||
*/
|
||||
export const Unit = /** @type {const} */ ({
|
||||
// Value units
|
||||
sats: { id: "sats", name: "Satoshis" },
|
||||
btc: { id: "btc", name: "Bitcoin" },
|
||||
usd: { id: "usd", name: "US Dollars" },
|
||||
|
||||
// Ratios & percentages
|
||||
percentage: { id: "percentage", name: "Percentage" },
|
||||
ratio: { id: "ratio", name: "Ratio" },
|
||||
index: { id: "index", name: "Index" },
|
||||
sd: { id: "sd", name: "Std Dev" },
|
||||
|
||||
// Relative percentages
|
||||
pctSupply: { id: "pct-supply", name: "% of Supply" },
|
||||
pctOwn: { id: "pct-own", name: "% of Own Supply" },
|
||||
pctMcap: { id: "pct-mcap", name: "% of Market Cap" },
|
||||
pctRcap: { id: "pct-rcap", name: "% of Realized Cap" },
|
||||
pctOwnMcap: { id: "pct-own-mcap", name: "% of Own Market Cap" },
|
||||
|
||||
// Time
|
||||
days: { id: "days", name: "Days" },
|
||||
years: { id: "years", name: "Years" },
|
||||
secs: { id: "secs", name: "Seconds" },
|
||||
|
||||
// Counts
|
||||
count: { id: "count", name: "Count" },
|
||||
blocks: { id: "blocks", name: "Blocks" },
|
||||
|
||||
// Size
|
||||
bytes: { id: "bytes", name: "Bytes" },
|
||||
vb: { id: "vb", name: "Virtual Bytes" },
|
||||
wu: { id: "wu", name: "Weight Units" },
|
||||
|
||||
// Mining
|
||||
hashRate: { id: "hashrate", name: "Hash Rate" },
|
||||
difficulty: { id: "difficulty", name: "Difficulty" },
|
||||
epoch: { id: "epoch", name: "Epoch" },
|
||||
|
||||
// Fees
|
||||
feeRate: { id: "feerate", name: "Sats/vByte" },
|
||||
|
||||
// Rates
|
||||
perSec: { id: "per-sec", name: "Per Second" },
|
||||
|
||||
// Cointime
|
||||
coinblocks: { id: "coinblocks", name: "Coinblocks" },
|
||||
coindays: { id: "coindays", name: "Coindays" },
|
||||
|
||||
// Hash price/value
|
||||
usdPerThsPerDay: { id: "usd-ths-day", name: "USD/TH/s/Day" },
|
||||
usdPerPhsPerDay: { id: "usd-phs-day", name: "USD/PH/s/Day" },
|
||||
satsPerThsPerDay: { id: "sats-ths-day", name: "Sats/TH/s/Day" },
|
||||
satsPerPhsPerDay: { id: "sats-phs-day", name: "Sats/PH/s/Day" },
|
||||
});
|
||||
|
||||
/** @typedef {keyof typeof Unit} UnitKey */
|
||||
/** @typedef {typeof Unit[UnitKey]} UnitObject */
|
||||
Reference in New Issue
Block a user