mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-07-28 03:08:10 -07:00
global: MASSIVE snapshot
This commit is contained in:
@@ -0,0 +1,485 @@
|
||||
/** Chain section builder - typed tree-based patterns */
|
||||
|
||||
/**
|
||||
* Create Chain section
|
||||
* @param {PartialContext} ctx
|
||||
* @returns {PartialOptionsGroup}
|
||||
*/
|
||||
export function createChainSection(ctx) {
|
||||
const { colors, brk, s, createPriceLine } = ctx;
|
||||
const { blocks, transactions, pools, inputs, outputs, market, scripts, supply } = brk.tree.computed;
|
||||
const { indexed } = brk.tree;
|
||||
|
||||
/**
|
||||
* Create sum/cumulative series from a BlockCountPattern
|
||||
* @template T
|
||||
* @param {BlockCountPattern<T>} pattern
|
||||
* @param {string} name
|
||||
* @param {Color} [sumColor]
|
||||
* @param {Color} [cumulativeColor]
|
||||
* @param {Unit} unit
|
||||
*/
|
||||
const fromBlockCount = (pattern, name, unit, sumColor, cumulativeColor) => [
|
||||
s({ metric: pattern.base, name: `${name} sum`, color: sumColor, unit }),
|
||||
s({ metric: pattern.cumulative, name: `${name} cumulative`, color: cumulativeColor ?? colors.blue, unit, defaultActive: false }),
|
||||
];
|
||||
|
||||
/**
|
||||
* Create series from BlockSizePattern (has average, min, max, percentiles)
|
||||
* @template T
|
||||
* @param {BlockSizePattern<T>} pattern
|
||||
* @param {string} name
|
||||
* @param {Unit} unit
|
||||
*/
|
||||
const fromBlockSize = (pattern, name, unit) => [
|
||||
s({ metric: pattern.average, name: `${name} avg`, unit }),
|
||||
s({ metric: pattern.sum, name: `${name} sum`, color: colors.blue, unit, defaultActive: false }),
|
||||
s({ metric: pattern.cumulative, name: `${name} cumulative`, color: colors.indigo, unit, defaultActive: false }),
|
||||
s({ metric: pattern.min, name: `${name} min`, color: colors.red, unit, defaultActive: false }),
|
||||
s({ metric: pattern.max, name: `${name} max`, color: colors.green, unit, defaultActive: false }),
|
||||
s({ metric: pattern.pct10, name: `${name} pct10`, color: colors.rose, unit, defaultActive: false }),
|
||||
s({ metric: pattern.pct25, name: `${name} pct25`, color: colors.pink, unit, defaultActive: false }),
|
||||
s({ metric: pattern.median, name: `${name} median`, color: colors.purple, unit, defaultActive: false }),
|
||||
s({ metric: pattern.pct75, name: `${name} pct75`, color: colors.violet, unit, defaultActive: false }),
|
||||
s({ metric: pattern.pct90, name: `${name} pct90`, color: colors.fuchsia, unit, defaultActive: false }),
|
||||
];
|
||||
|
||||
/**
|
||||
* Create series from BlockIntervalPattern (has average, min, max, percentiles)
|
||||
* @template T
|
||||
* @param {BlockIntervalPattern<T>} pattern
|
||||
* @param {string} name
|
||||
* @param {Unit} unit
|
||||
*/
|
||||
const fromBlockInterval = (pattern, name, unit) => [
|
||||
s({ metric: pattern.average, name: `${name} avg`, unit }),
|
||||
s({ metric: pattern.min, name: `${name} min`, color: colors.red, unit, defaultActive: false }),
|
||||
s({ metric: pattern.max, name: `${name} max`, color: colors.green, unit, defaultActive: false }),
|
||||
s({ metric: pattern.pct10, name: `${name} pct10`, color: colors.rose, unit, defaultActive: false }),
|
||||
s({ metric: pattern.pct25, name: `${name} pct25`, color: colors.pink, unit, defaultActive: false }),
|
||||
s({ metric: pattern.median, name: `${name} median`, color: colors.purple, unit, defaultActive: false }),
|
||||
s({ metric: pattern.pct75, name: `${name} pct75`, color: colors.violet, unit, defaultActive: false }),
|
||||
s({ metric: pattern.pct90, name: `${name} pct90`, color: colors.fuchsia, unit, defaultActive: false }),
|
||||
];
|
||||
|
||||
/**
|
||||
* Create series from BitcoinPattern (has base, cumulative)
|
||||
* @template T
|
||||
* @param {BitcoinPattern<T>} pattern
|
||||
* @param {string} name
|
||||
* @param {Unit} unit
|
||||
* @param {Color} [sumColor]
|
||||
* @param {Color} [cumulativeColor]
|
||||
*/
|
||||
const fromBitcoin = (pattern, name, unit, sumColor, cumulativeColor) => [
|
||||
s({ metric: pattern.base, name: `${name}`, color: sumColor, unit }),
|
||||
s({ metric: pattern.cumulative, name: `${name} cumulative`, color: cumulativeColor ?? colors.blue, unit, defaultActive: false }),
|
||||
];
|
||||
|
||||
/**
|
||||
* Create series from CoinbasePattern (has sats, bitcoin, dollars as BitcoinPattern)
|
||||
* BitcoinPattern has .base and .cumulative (no .sum)
|
||||
* @param {CoinbasePattern} pattern
|
||||
* @param {string} name
|
||||
* @param {Color} sumColor
|
||||
* @param {Color} cumulativeColor
|
||||
*/
|
||||
const fromCoinbase = (pattern, name, sumColor, cumulativeColor) => [
|
||||
s({ metric: pattern.sats.base, name: `${name}`, color: sumColor, unit: "sats" }),
|
||||
s({ metric: pattern.sats.cumulative, name: `${name} cumulative`, color: cumulativeColor, unit: "sats", defaultActive: false }),
|
||||
s({ metric: pattern.bitcoin.base, name: `${name}`, color: sumColor, unit: "btc" }),
|
||||
s({ metric: pattern.bitcoin.cumulative, name: `${name} cumulative`, color: cumulativeColor, unit: "btc", defaultActive: false }),
|
||||
s({ metric: pattern.dollars.base, name: `${name}`, color: sumColor, unit: "usd" }),
|
||||
s({ metric: pattern.dollars.cumulative, name: `${name} cumulative`, color: cumulativeColor, unit: "usd", defaultActive: false }),
|
||||
];
|
||||
|
||||
/**
|
||||
* Create series from ValuePattern (has sats, bitcoin, dollars as BlockCountPattern)
|
||||
* BlockCountPattern has .base, .sum, and .cumulative
|
||||
* @param {ValuePattern} pattern
|
||||
* @param {string} name
|
||||
* @param {Color} sumColor
|
||||
* @param {Color} cumulativeColor
|
||||
*/
|
||||
const fromValuePattern = (pattern, name, sumColor, cumulativeColor) => [
|
||||
s({ metric: pattern.sats.base, name: `${name}`, color: sumColor, unit: "sats" }),
|
||||
s({ metric: pattern.sats.cumulative, name: `${name} cumulative`, color: cumulativeColor, unit: "sats", defaultActive: false }),
|
||||
s({ metric: pattern.bitcoin.base, name: `${name}`, color: sumColor, unit: "btc" }),
|
||||
s({ metric: pattern.bitcoin.cumulative, name: `${name} cumulative`, color: cumulativeColor, unit: "btc", defaultActive: false }),
|
||||
s({ metric: pattern.dollars.base, name: `${name}`, color: sumColor, unit: "usd" }),
|
||||
s({ metric: pattern.dollars.cumulative, name: `${name} cumulative`, color: cumulativeColor, unit: "usd", defaultActive: false }),
|
||||
];
|
||||
|
||||
/**
|
||||
* Create series from RewardPattern (has .base as Indexes2<Sats>, plus bitcoin/dollars as BlockCountPattern, sats as SatsPattern)
|
||||
* Note: SatsPattern only has cumulative and sum, so we use pattern.base for raw sats
|
||||
* @param {RewardPattern} pattern
|
||||
* @param {string} name
|
||||
* @param {Color} sumColor
|
||||
* @param {Color} cumulativeColor
|
||||
*/
|
||||
const fromRewardPattern = (pattern, name, sumColor, cumulativeColor) => [
|
||||
s({ metric: pattern.base, name: `${name}`, color: sumColor, unit: "sats" }),
|
||||
s({ metric: pattern.sats.cumulative, name: `${name} cumulative`, color: cumulativeColor, unit: "sats", defaultActive: false }),
|
||||
s({ metric: pattern.bitcoin.base, name: `${name}`, color: sumColor, unit: "btc" }),
|
||||
s({ metric: pattern.bitcoin.cumulative, name: `${name} cumulative`, color: cumulativeColor, unit: "btc", defaultActive: false }),
|
||||
s({ metric: pattern.dollars.base, name: `${name}`, color: sumColor, unit: "usd" }),
|
||||
s({ metric: pattern.dollars.cumulative, name: `${name} cumulative`, color: cumulativeColor, unit: "usd", defaultActive: false }),
|
||||
];
|
||||
|
||||
// Build pools tree dynamically
|
||||
const poolEntries = Object.entries(pools.vecs);
|
||||
const poolsTree = poolEntries.map(([key, pool]) => {
|
||||
const poolName = brk.POOL_ID_TO_POOL_NAME[/** @type {keyof typeof brk.POOL_ID_TO_POOL_NAME} */ (key.toLowerCase())] || key;
|
||||
return {
|
||||
name: poolName,
|
||||
tree: [
|
||||
{
|
||||
name: "Dominance",
|
||||
title: `Mining Dominance of ${poolName}`,
|
||||
bottom: [
|
||||
s({ metric: pool._1dDominance.base, name: "1d", color: colors.rose, unit: "percentage", defaultActive: false }),
|
||||
s({ metric: pool._1wDominance, name: "1w", color: colors.red, unit: "percentage", defaultActive: false }),
|
||||
s({ metric: pool._1mDominance, name: "1m", unit: "percentage" }),
|
||||
s({ metric: pool._1yDominance, name: "1y", color: colors.lime, unit: "percentage", defaultActive: false }),
|
||||
s({ metric: pool.dominance.base, name: "all time", color: colors.teal, unit: "percentage", defaultActive: false }),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Blocks mined",
|
||||
title: `Blocks mined by ${poolName}`,
|
||||
bottom: [
|
||||
s({ metric: pool.blocksMined.base, name: "Sum", unit: "count" }),
|
||||
s({ metric: pool.blocksMined.cumulative, name: "Cumulative", color: colors.blue, unit: "count" }),
|
||||
s({ metric: pool._1wBlocksMined, name: "1w Sum", color: colors.red, unit: "count", defaultActive: false }),
|
||||
s({ metric: pool._1mBlocksMined, name: "1m Sum", color: colors.pink, unit: "count", defaultActive: false }),
|
||||
s({ metric: pool._1yBlocksMined, name: "1y Sum", color: colors.purple, unit: "count", defaultActive: false }),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Rewards",
|
||||
title: `Rewards collected by ${poolName}`,
|
||||
bottom: [
|
||||
...fromValuePattern(pool.coinbase, "coinbase", colors.orange, colors.red),
|
||||
...fromRewardPattern(pool.subsidy, "subsidy", colors.lime, colors.emerald),
|
||||
...fromRewardPattern(pool.fee, "fee", colors.cyan, colors.indigo),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Days since block",
|
||||
title: `Days since ${poolName} mined a block`,
|
||||
bottom: [
|
||||
s({ metric: pool.daysSinceBlock, name: "Since block", unit: "days" }),
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
name: "Chain",
|
||||
tree: [
|
||||
// Block
|
||||
{
|
||||
name: "Block",
|
||||
tree: [
|
||||
{
|
||||
name: "Count",
|
||||
title: "Block Count",
|
||||
bottom: [
|
||||
...fromBlockCount(blocks.count.blockCount, "Block", "count"),
|
||||
s({ metric: blocks.count.blockCountTarget, name: "Target", color: colors.gray, unit: "count", options: { lineStyle: 4 } }),
|
||||
s({ metric: blocks.count._1wBlockCount, name: "1w sum", color: colors.red, unit: "count", defaultActive: false }),
|
||||
s({ metric: blocks.count._1mBlockCount, name: "1m sum", color: colors.pink, unit: "count", defaultActive: false }),
|
||||
s({ metric: blocks.count._1yBlockCount, name: "1y sum", color: colors.purple, unit: "count", defaultActive: false }),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Interval",
|
||||
title: "Block Interval",
|
||||
bottom: [
|
||||
s({ metric: blocks.interval.interval, name: "Interval", unit: "secs" }),
|
||||
...fromBlockInterval(blocks.interval.blockInterval, "Interval", "secs"),
|
||||
createPriceLine({ unit: "secs", name: "Target", number: 600 }),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Size",
|
||||
title: "Block Size",
|
||||
bottom: [
|
||||
s({ metric: blocks.size.vbytes, name: "vbytes raw", unit: "vb" }),
|
||||
s({ metric: indexed.block.weight, name: "weight raw", unit: "wu" }),
|
||||
...fromBlockSize(blocks.size.blockSize, "size", "bytes"),
|
||||
...fromBlockSize(blocks.weight.blockWeight, "weight", "wu"),
|
||||
...fromBlockSize(blocks.size.blockVbytes, "vbytes", "vb"),
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// Transaction
|
||||
{
|
||||
name: "Transaction",
|
||||
tree: [
|
||||
{
|
||||
name: "Count",
|
||||
title: "Transaction Count",
|
||||
bottom: fromBitcoin(transactions.count.txCount, "Count", "count"),
|
||||
},
|
||||
{
|
||||
name: "Volume",
|
||||
title: "Transaction Volume",
|
||||
bottom: [
|
||||
s({ metric: transactions.volume.sentSum.sats, name: "Sent", unit: "sats" }),
|
||||
s({ metric: transactions.volume.sentSum.bitcoin.base, name: "Sent", unit: "btc" }),
|
||||
s({ metric: transactions.volume.sentSum.dollars, name: "Sent", unit: "usd" }),
|
||||
s({ metric: transactions.volume.annualizedVolume, name: "annualized", color: colors.red, unit: "sats", defaultActive: false }),
|
||||
s({ metric: transactions.volume.annualizedVolumeBtc, name: "annualized", color: colors.red, unit: "btc", defaultActive: false }),
|
||||
s({ metric: transactions.volume.annualizedVolumeUsd, name: "annualized", color: colors.lime, unit: "usd", defaultActive: false }),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Size",
|
||||
title: "Transaction Size",
|
||||
bottom: [
|
||||
...fromBlockInterval(transactions.size.txWeight, "weight", "wu"),
|
||||
...fromBlockInterval(transactions.size.txVsize, "vsize", "vb"),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Versions",
|
||||
title: "Transaction Versions",
|
||||
bottom: [
|
||||
...fromBlockCount(transactions.versions.txV1, "v1", "count", colors.orange, colors.red),
|
||||
...fromBlockCount(transactions.versions.txV2, "v2", "count", colors.cyan, colors.blue),
|
||||
...fromBlockCount(transactions.versions.txV3, "v3", "count", colors.lime, colors.green),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Velocity",
|
||||
title: "Transactions Velocity",
|
||||
bottom: [
|
||||
s({ metric: supply.velocity.btc, name: "bitcoin", unit: "ratio" }),
|
||||
s({ metric: supply.velocity.usd, name: "dollars", color: colors.emerald, unit: "ratio" }),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Speed",
|
||||
title: "Transactions Per Second",
|
||||
bottom: [
|
||||
s({ metric: transactions.volume.txPerSec, name: "Transactions", unit: "/sec" }),
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// Input
|
||||
{
|
||||
name: "Input",
|
||||
tree: [
|
||||
{
|
||||
name: "Count",
|
||||
title: "Transaction Input Count",
|
||||
bottom: [
|
||||
...fromBlockSize(inputs.count.count, "Input", "count"),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Speed",
|
||||
title: "Inputs Per Second",
|
||||
bottom: [
|
||||
s({ metric: transactions.volume.inputsPerSec, name: "Inputs", unit: "/sec" }),
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// Output
|
||||
{
|
||||
name: "Output",
|
||||
tree: [
|
||||
{
|
||||
name: "Count",
|
||||
title: "Transaction Output Count",
|
||||
bottom: [
|
||||
...fromBlockSize(outputs.count.count, "Output", "count"),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Speed",
|
||||
title: "Outputs Per Second",
|
||||
bottom: [
|
||||
s({ metric: transactions.volume.outputsPerSec, name: "Outputs", unit: "/sec" }),
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// UTXO
|
||||
{
|
||||
name: "UTXO",
|
||||
tree: [
|
||||
{
|
||||
name: "Count",
|
||||
title: "UTXO Count",
|
||||
bottom: [
|
||||
s({ metric: outputs.count.utxoCount.base, name: "Count", unit: "count" }),
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// Coinbase
|
||||
{
|
||||
name: "Coinbase",
|
||||
title: "Coinbase Rewards",
|
||||
bottom: fromCoinbase(blocks.rewards.coinbase, "Coinbase", colors.orange, colors.red),
|
||||
},
|
||||
|
||||
// Subsidy
|
||||
{
|
||||
name: "Subsidy",
|
||||
title: "Block Subsidy",
|
||||
bottom: [
|
||||
...fromCoinbase(blocks.rewards.subsidy, "Subsidy", colors.lime, colors.emerald),
|
||||
s({ metric: blocks.rewards.subsidyDominance, name: "Dominance", color: colors.purple, unit: "percentage", defaultActive: false }),
|
||||
],
|
||||
},
|
||||
|
||||
// Fee
|
||||
{
|
||||
name: "Fee",
|
||||
tree: [
|
||||
{
|
||||
name: "Total",
|
||||
title: "Transaction Fees",
|
||||
bottom: [
|
||||
s({ metric: transactions.fees.fee.sats.sum, name: "Sum", unit: "sats" }),
|
||||
s({ metric: transactions.fees.fee.sats.cumulative, name: "Cumulative", color: colors.blue, unit: "sats", defaultActive: false }),
|
||||
s({ metric: transactions.fees.fee.bitcoin.sum, name: "Sum", unit: "btc" }),
|
||||
s({ metric: transactions.fees.fee.bitcoin.cumulative, name: "Cumulative", color: colors.blue, unit: "btc", defaultActive: false }),
|
||||
s({ metric: transactions.fees.fee.dollars.sum, name: "Sum", unit: "usd" }),
|
||||
s({ metric: transactions.fees.fee.dollars.cumulative, name: "Cumulative", color: colors.blue, unit: "usd", defaultActive: false }),
|
||||
s({ metric: blocks.rewards.feeDominance, name: "Dominance", color: colors.purple, unit: "percentage", defaultActive: false }),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Rate",
|
||||
title: "Fee Rate",
|
||||
bottom: [
|
||||
s({ metric: transactions.fees.feeRate.base, name: "Rate", unit: "sat/vb" }),
|
||||
s({ metric: transactions.fees.feeRate.average, name: "Average", color: colors.blue, unit: "sat/vb" }),
|
||||
s({ metric: transactions.fees.feeRate.median, name: "Median", color: colors.purple, unit: "sat/vb" }),
|
||||
s({ metric: transactions.fees.feeRate.min, name: "Min", color: colors.red, unit: "sat/vb", defaultActive: false }),
|
||||
s({ metric: transactions.fees.feeRate.max, name: "Max", color: colors.green, unit: "sat/vb", defaultActive: false }),
|
||||
s({ metric: transactions.fees.feeRate.pct10, name: "pct10", color: colors.rose, unit: "sat/vb", defaultActive: false }),
|
||||
s({ metric: transactions.fees.feeRate.pct25, name: "pct25", color: colors.pink, unit: "sat/vb", defaultActive: false }),
|
||||
s({ metric: transactions.fees.feeRate.pct75, name: "pct75", color: colors.violet, unit: "sat/vb", defaultActive: false }),
|
||||
s({ metric: transactions.fees.feeRate.pct90, name: "pct90", color: colors.fuchsia, unit: "sat/vb", defaultActive: false }),
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// Mining
|
||||
{
|
||||
name: "Mining",
|
||||
tree: [
|
||||
{
|
||||
name: "Hashrate",
|
||||
title: "Network Hashrate",
|
||||
bottom: [
|
||||
s({ metric: blocks.mining.hashRate, name: "Hashrate", unit: "h/s" }),
|
||||
s({ metric: blocks.mining.hashRate1wSma, name: "1w SMA", color: colors.red, unit: "h/s", defaultActive: false }),
|
||||
s({ metric: blocks.mining.hashRate1mSma, name: "1m SMA", color: colors.orange, unit: "h/s", defaultActive: false }),
|
||||
s({ metric: blocks.mining.hashRate2mSma, name: "2m SMA", color: colors.yellow, unit: "h/s", defaultActive: false }),
|
||||
s({ metric: blocks.mining.hashRate1ySma, name: "1y SMA", color: colors.lime, unit: "h/s", defaultActive: false }),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Difficulty",
|
||||
title: "Network Difficulty",
|
||||
bottom: [
|
||||
s({ metric: blocks.mining.difficulty, name: "Difficulty", unit: "difficulty" }),
|
||||
s({ metric: blocks.mining.difficultyAdjustment, name: "Adjustment", color: colors.orange, unit: "percentage", defaultActive: false }),
|
||||
s({ metric: blocks.mining.difficultyAsHash, name: "As hash", color: colors.default, unit: "h/s", defaultActive: false, options: { lineStyle: 1 } }),
|
||||
s({ metric: blocks.difficulty.blocksBeforeNextDifficultyAdjustment, name: "Blocks until adj.", color: colors.indigo, unit: "blocks", defaultActive: false }),
|
||||
s({ metric: blocks.difficulty.daysBeforeNextDifficultyAdjustment, name: "Days until adj.", color: colors.purple, unit: "days", defaultActive: false }),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Hash Price",
|
||||
title: "Hash Price",
|
||||
bottom: [
|
||||
s({ metric: blocks.mining.hashPriceThs, name: "TH/s", color: colors.emerald, unit: "usd/(th/s)/day" }),
|
||||
s({ metric: blocks.mining.hashPricePhs, name: "PH/s", color: colors.emerald, unit: "usd/(ph/s)/day" }),
|
||||
s({ metric: blocks.mining.hashPriceRebound, name: "Rebound", color: colors.yellow, unit: "percentage" }),
|
||||
s({ metric: blocks.mining.hashPriceThsMin, name: "TH/s Min", color: colors.red, unit: "usd/(th/s)/day", options: { lineStyle: 1 } }),
|
||||
s({ metric: blocks.mining.hashPricePhsMin, name: "PH/s Min", color: colors.red, unit: "usd/(ph/s)/day", options: { lineStyle: 1 } }),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Hash Value",
|
||||
title: "Hash Value",
|
||||
bottom: [
|
||||
s({ metric: blocks.mining.hashValueThs, name: "TH/s", color: colors.orange, unit: "sats/(th/s)/day" }),
|
||||
s({ metric: blocks.mining.hashValuePhs, name: "PH/s", color: colors.orange, unit: "sats/(ph/s)/day" }),
|
||||
s({ metric: blocks.mining.hashValueRebound, name: "Rebound", color: colors.yellow, unit: "percentage" }),
|
||||
s({ metric: blocks.mining.hashValueThsMin, name: "TH/s Min", color: colors.red, unit: "sats/(th/s)/day", options: { lineStyle: 1 } }),
|
||||
s({ metric: blocks.mining.hashValuePhsMin, name: "PH/s Min", color: colors.red, unit: "sats/(ph/s)/day", options: { lineStyle: 1 } }),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Halving",
|
||||
title: "Halving Info",
|
||||
bottom: [
|
||||
s({ metric: blocks.halving.blocksBeforeNextHalving, name: "Blocks until halving", unit: "blocks" }),
|
||||
s({ metric: blocks.halving.daysBeforeNextHalving, name: "Days until halving", color: colors.orange, unit: "days" }),
|
||||
s({ metric: blocks.halving.halvingepoch, name: "Halving epoch", color: colors.purple, unit: "epoch", defaultActive: false }),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Puell Multiple",
|
||||
title: "Puell Multiple",
|
||||
bottom: [
|
||||
s({ metric: market.indicators.puellMultiple, name: "Puell Multiple", unit: "ratio" }),
|
||||
createPriceLine({ unit: "ratio", number: 1 }),
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// Pools
|
||||
{
|
||||
name: "Pools",
|
||||
tree: poolsTree,
|
||||
},
|
||||
|
||||
// Unspendable
|
||||
{
|
||||
name: "Unspendable",
|
||||
tree: [
|
||||
{
|
||||
name: "OP_RETURN",
|
||||
tree: [
|
||||
{
|
||||
name: "Outputs",
|
||||
title: "OP_RETURN Outputs",
|
||||
bottom: fromBitcoin(scripts.count.opreturnCount, "Count", "count"),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// Inflation
|
||||
{
|
||||
name: "Inflation",
|
||||
title: "Inflation Rate",
|
||||
bottom: [
|
||||
s({ metric: supply.inflation.indexes, name: "Rate", unit: "percentage" }),
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,313 @@
|
||||
/**
|
||||
* Address cohort folder builder
|
||||
* Creates option trees for address-based cohorts (has addrCount)
|
||||
* Address cohorts use _0satsPattern which has PricePaidPattern (no percentiles)
|
||||
*/
|
||||
|
||||
import {
|
||||
createSingleSupplySeries,
|
||||
createGroupedSupplyTotalSeries,
|
||||
createGroupedSupplyInProfitSeries,
|
||||
createGroupedSupplyInLossSeries,
|
||||
createUtxoCountSeries,
|
||||
createAddressCountSeries,
|
||||
createRealizedPriceSeries,
|
||||
createRealizedPriceRatioSeries,
|
||||
} from "./shared.js";
|
||||
|
||||
/**
|
||||
* Create a cohort folder for address cohorts
|
||||
* Includes address count section (addrCount exists on AddressCohortObject)
|
||||
* @param {PartialContext} ctx
|
||||
* @param {AddressCohortObject | AddressCohortGroupObject} args
|
||||
* @returns {PartialOptionsGroup}
|
||||
*/
|
||||
export function createAddressCohortFolder(ctx, args) {
|
||||
const list = "list" in args ? args.list : [args];
|
||||
const useGroupName = "list" in args;
|
||||
const isSingle = !("list" in args);
|
||||
|
||||
const title = args.title ? `${useGroupName ? "by" : "of"} ${args.title}` : "";
|
||||
|
||||
return {
|
||||
name: args.name || "all",
|
||||
tree: [
|
||||
// Supply section
|
||||
isSingle
|
||||
? {
|
||||
name: "supply",
|
||||
title: `Supply ${title}`,
|
||||
bottom: createSingleSupplySeries(ctx, /** @type {AddressCohortObject} */ (args), title),
|
||||
}
|
||||
: {
|
||||
name: "supply",
|
||||
tree: [
|
||||
{
|
||||
name: "total",
|
||||
title: `Supply ${title}`,
|
||||
bottom: createGroupedSupplyTotalSeries(ctx, list),
|
||||
},
|
||||
{
|
||||
name: "in profit",
|
||||
title: `Supply In Profit ${title}`,
|
||||
bottom: createGroupedSupplyInProfitSeries(ctx, list),
|
||||
},
|
||||
{
|
||||
name: "in loss",
|
||||
title: `Supply In Loss ${title}`,
|
||||
bottom: createGroupedSupplyInLossSeries(ctx, list),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// UTXO count
|
||||
{
|
||||
name: "utxo count",
|
||||
title: `UTXO Count ${title}`,
|
||||
bottom: createUtxoCountSeries(ctx, list, useGroupName),
|
||||
},
|
||||
|
||||
// Address count (ADDRESS COHORTS ONLY - fully type safe!)
|
||||
{
|
||||
name: "address count",
|
||||
title: `Address Count ${title}`,
|
||||
bottom: createAddressCountSeries(ctx, list, useGroupName),
|
||||
},
|
||||
|
||||
// Realized section
|
||||
{
|
||||
name: "Realized",
|
||||
tree: [
|
||||
...(useGroupName
|
||||
? [
|
||||
{
|
||||
name: "Price",
|
||||
title: `Realized Price ${title}`,
|
||||
top: createRealizedPriceSeries(ctx, list),
|
||||
},
|
||||
{
|
||||
name: "Ratio",
|
||||
title: `Realized Price Ratio ${title}`,
|
||||
bottom: createRealizedPriceRatioSeries(ctx, list),
|
||||
},
|
||||
]
|
||||
: createRealizedPriceOptions(ctx, /** @type {AddressCohortObject} */ (args), title)),
|
||||
{
|
||||
name: "capitalization",
|
||||
title: `Realized Capitalization ${title}`,
|
||||
bottom: createRealizedCapWithExtras(ctx, list, args, useGroupName, title),
|
||||
},
|
||||
...(!useGroupName ? createRealizedPnlSection(ctx, /** @type {AddressCohortObject} */ (args), title) : []),
|
||||
],
|
||||
},
|
||||
|
||||
// Unrealized section
|
||||
...createUnrealizedSection(ctx, list, useGroupName, title),
|
||||
|
||||
// Price paid section (no percentiles for address cohorts)
|
||||
...createPricePaidSection(ctx, list, useGroupName, title),
|
||||
|
||||
// Activity section
|
||||
...createActivitySection(ctx, list, useGroupName, title),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create realized price options for single cohort
|
||||
* @param {PartialContext} ctx
|
||||
* @param {AddressCohortObject} args
|
||||
* @param {string} title
|
||||
* @returns {PartialOptionsTree}
|
||||
*/
|
||||
function createRealizedPriceOptions(ctx, args, title) {
|
||||
const { s } = ctx;
|
||||
const { tree, color } = args;
|
||||
|
||||
return [
|
||||
{
|
||||
name: "price",
|
||||
title: `Realized Price ${title}`,
|
||||
top: [s({ metric: tree.realized.realizedPrice, name: "realized", color })],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create realized cap with extras
|
||||
* @param {PartialContext} ctx
|
||||
* @param {readonly AddressCohortObject[]} list
|
||||
* @param {AddressCohortObject | AddressCohortGroupObject} args
|
||||
* @param {boolean} useGroupName
|
||||
* @param {string} title
|
||||
* @returns {AnyFetchedSeriesBlueprint[]}
|
||||
*/
|
||||
function createRealizedCapWithExtras(ctx, list, args, useGroupName, title) {
|
||||
const { colors, s, createPriceLine } = ctx;
|
||||
const isSingle = !("list" in args);
|
||||
|
||||
return list.flatMap(({ color, name, tree }) => [
|
||||
s({ metric: tree.realized.realizedCap, name: useGroupName ? name : "Capitalization", color }),
|
||||
...(isSingle
|
||||
? [
|
||||
/** @type {AnyFetchedSeriesBlueprint} */ ({
|
||||
type: "Baseline",
|
||||
metric: tree.realized.realizedCap30dDelta,
|
||||
title: "30d change",
|
||||
defaultActive: false,
|
||||
}),
|
||||
createPriceLine({ unit: "usd", defaultActive: false }),
|
||||
]
|
||||
: []),
|
||||
// RealizedPattern (address cohorts) doesn't have realizedCapRelToOwnMarketCap
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create realized PnL section for single cohort
|
||||
* @param {PartialContext} ctx
|
||||
* @param {AddressCohortObject} args
|
||||
* @param {string} title
|
||||
* @returns {PartialOptionsTree}
|
||||
*/
|
||||
function createRealizedPnlSection(ctx, args, title) {
|
||||
const { colors, s } = ctx;
|
||||
const { tree } = args;
|
||||
|
||||
return [
|
||||
{
|
||||
name: "pnl",
|
||||
title: `Realized Profit And Loss ${title}`,
|
||||
bottom: [
|
||||
s({ metric: tree.realized.realizedProfit.base, name: "Profit", color: colors.green }),
|
||||
s({ metric: tree.realized.realizedLoss.base, name: "Loss", color: colors.red, defaultActive: false }),
|
||||
// RealizedPattern (address cohorts) doesn't have realizedProfitToLossRatio
|
||||
s({ metric: tree.realized.totalRealizedPnl.base, name: "Total", color: colors.default, defaultActive: false }),
|
||||
s({ metric: tree.realized.negRealizedLoss.base, name: "Negative Loss", color: colors.red }),
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create unrealized section
|
||||
* @param {PartialContext} ctx
|
||||
* @param {readonly AddressCohortObject[]} list
|
||||
* @param {boolean} useGroupName
|
||||
* @param {string} title
|
||||
* @returns {PartialOptionsTree}
|
||||
*/
|
||||
function createUnrealizedSection(ctx, list, useGroupName, title) {
|
||||
const { colors, s } = ctx;
|
||||
|
||||
return [
|
||||
{
|
||||
name: "Unrealized",
|
||||
tree: [
|
||||
{
|
||||
name: "nupl",
|
||||
title: `Net Unrealized Profit/Loss ${title}`,
|
||||
bottom: list.flatMap(({ color, name, tree }) => [
|
||||
/** @type {AnyFetchedSeriesBlueprint} */ ({
|
||||
type: "Baseline",
|
||||
metric: tree.unrealized.netUnrealizedPnl,
|
||||
title: useGroupName ? name : "NUPL",
|
||||
colors: [colors.red, colors.green],
|
||||
options: { baseValue: { price: 0 } },
|
||||
}),
|
||||
]),
|
||||
},
|
||||
{
|
||||
name: "profit",
|
||||
title: `Unrealized Profit ${title}`,
|
||||
bottom: list.flatMap(({ color, name, tree }) => [
|
||||
s({ metric: tree.unrealized.unrealizedProfit, name: useGroupName ? name : "Profit", color }),
|
||||
]),
|
||||
},
|
||||
{
|
||||
name: "loss",
|
||||
title: `Unrealized Loss ${title}`,
|
||||
bottom: list.flatMap(({ color, name, tree }) => [
|
||||
s({ metric: tree.unrealized.unrealizedLoss, name: useGroupName ? name : "Loss", color }),
|
||||
]),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create price paid section (no percentiles for address cohorts)
|
||||
* @param {PartialContext} ctx
|
||||
* @param {readonly AddressCohortObject[]} list
|
||||
* @param {boolean} useGroupName
|
||||
* @param {string} title
|
||||
* @returns {PartialOptionsTree}
|
||||
*/
|
||||
function createPricePaidSection(ctx, list, useGroupName, title) {
|
||||
const { s } = ctx;
|
||||
|
||||
return [
|
||||
{
|
||||
name: "Price Paid",
|
||||
tree: [
|
||||
{
|
||||
name: "min",
|
||||
title: `Min Price Paid ${title}`,
|
||||
top: list.map(({ color, name, tree }) =>
|
||||
s({ metric: tree.pricePaid.minPricePaid, name: useGroupName ? name : "Min", color }),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "max",
|
||||
title: `Max Price Paid ${title}`,
|
||||
top: list.map(({ color, name, tree }) =>
|
||||
s({ metric: tree.pricePaid.maxPricePaid, name: useGroupName ? name : "Max", color }),
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create activity section
|
||||
* @param {PartialContext} ctx
|
||||
* @param {readonly AddressCohortObject[]} list
|
||||
* @param {boolean} useGroupName
|
||||
* @param {string} title
|
||||
* @returns {PartialOptionsTree}
|
||||
*/
|
||||
function createActivitySection(ctx, list, useGroupName, title) {
|
||||
const { s } = ctx;
|
||||
|
||||
return [
|
||||
{
|
||||
name: "Activity",
|
||||
tree: [
|
||||
{
|
||||
name: "coinblocks destroyed",
|
||||
title: `Coinblocks Destroyed ${title}`,
|
||||
bottom: list.flatMap(({ color, name, tree }) => [
|
||||
s({
|
||||
metric: tree.activity.coinblocksDestroyed.base,
|
||||
name: useGroupName ? name : "Coinblocks",
|
||||
color,
|
||||
}),
|
||||
]),
|
||||
},
|
||||
{
|
||||
name: "coindays destroyed",
|
||||
title: `Coindays Destroyed ${title}`,
|
||||
bottom: list.flatMap(({ color, name, tree }) => [
|
||||
s({
|
||||
metric: tree.activity.coindaysDestroyed.base,
|
||||
name: useGroupName ? name : "Coindays",
|
||||
color,
|
||||
}),
|
||||
]),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
/** Build cohort data arrays from brk.tree */
|
||||
|
||||
import {
|
||||
termColors,
|
||||
maxAgeColors,
|
||||
minAgeColors,
|
||||
ageRangeColors,
|
||||
epochColors,
|
||||
geAmountColors,
|
||||
ltAmountColors,
|
||||
amountRangeColors,
|
||||
spendableTypeColors,
|
||||
} from "../colors/index.js";
|
||||
|
||||
/**
|
||||
* @template {Record<string, any>} T
|
||||
* @param {T} obj
|
||||
* @returns {[keyof T & string, T[keyof T & string]][]}
|
||||
*/
|
||||
const entries = (obj) => /** @type {[keyof T & string, T[keyof T & string]][]} */ (Object.entries(obj));
|
||||
|
||||
/**
|
||||
* Build all cohort data from brk tree
|
||||
* @param {Colors} colors
|
||||
* @param {BrkClient} brk
|
||||
*/
|
||||
export function buildCohortData(colors, brk) {
|
||||
const utxoCohorts = brk.tree.computed.distribution.utxoCohorts;
|
||||
const addressCohorts = brk.tree.computed.distribution.addressCohorts;
|
||||
const {
|
||||
TERM_NAMES,
|
||||
EPOCH_NAMES,
|
||||
MAX_AGE_NAMES,
|
||||
MIN_AGE_NAMES,
|
||||
AGE_RANGE_NAMES,
|
||||
GE_AMOUNT_NAMES,
|
||||
LT_AMOUNT_NAMES,
|
||||
AMOUNT_RANGE_NAMES,
|
||||
SPENDABLE_TYPE_NAMES,
|
||||
} = brk;
|
||||
|
||||
// Base cohort representing "all"
|
||||
/** @type {UtxoCohortObject} */
|
||||
const cohortAll = {
|
||||
name: "",
|
||||
title: "",
|
||||
color: colors.orange,
|
||||
tree: utxoCohorts.all,
|
||||
};
|
||||
|
||||
/** @type {UtxoCohortObject} */
|
||||
const cohortAllForComparison = {
|
||||
name: "all",
|
||||
title: "",
|
||||
color: colors.default,
|
||||
tree: utxoCohorts.all,
|
||||
};
|
||||
|
||||
// Term cohorts (short/long term holders)
|
||||
/** @type {readonly UtxoCohortObject[]} */
|
||||
const terms = entries(utxoCohorts.term).map(([key, tree]) => {
|
||||
const names = TERM_NAMES[key];
|
||||
return {
|
||||
name: names.short,
|
||||
title: names.long,
|
||||
color: colors[termColors[key]],
|
||||
tree,
|
||||
};
|
||||
});
|
||||
|
||||
// Max age cohorts (up to X time)
|
||||
/** @type {readonly UtxoCohortObject[]} */
|
||||
const upToDate = entries(utxoCohorts.maxAge).map(([key, tree]) => {
|
||||
const names = MAX_AGE_NAMES[key];
|
||||
return {
|
||||
name: names.short,
|
||||
title: names.long,
|
||||
color: colors[maxAgeColors[key]],
|
||||
tree,
|
||||
};
|
||||
});
|
||||
|
||||
// Min age cohorts (from X time)
|
||||
/** @type {readonly UtxoCohortObject[]} */
|
||||
const fromDate = entries(utxoCohorts.minAge).map(([key, tree]) => {
|
||||
const names = MIN_AGE_NAMES[key];
|
||||
return {
|
||||
name: names.short,
|
||||
title: names.long,
|
||||
color: colors[minAgeColors[key]],
|
||||
tree,
|
||||
};
|
||||
});
|
||||
|
||||
// Age range cohorts
|
||||
/** @type {readonly UtxoCohortObject[]} */
|
||||
const dateRange = entries(utxoCohorts.ageRange).map(([key, tree]) => {
|
||||
const names = AGE_RANGE_NAMES[key];
|
||||
return {
|
||||
name: names.short,
|
||||
title: names.long,
|
||||
color: colors[ageRangeColors[key]],
|
||||
tree,
|
||||
};
|
||||
});
|
||||
|
||||
// Epoch cohorts
|
||||
/** @type {readonly UtxoCohortObject[]} */
|
||||
const epoch = entries(utxoCohorts.epoch).map(([key, tree]) => {
|
||||
const names = EPOCH_NAMES[key];
|
||||
return {
|
||||
name: names.short,
|
||||
title: names.long,
|
||||
color: colors[epochColors[key]],
|
||||
tree,
|
||||
};
|
||||
});
|
||||
|
||||
// UTXOs above amount
|
||||
/** @type {readonly UtxoCohortObject[]} */
|
||||
const utxosAboveAmount = entries(utxoCohorts.geAmount).map(([key, tree]) => {
|
||||
const names = GE_AMOUNT_NAMES[key];
|
||||
return {
|
||||
name: names.short,
|
||||
title: names.long,
|
||||
color: colors[geAmountColors[key]],
|
||||
tree,
|
||||
};
|
||||
});
|
||||
|
||||
// Addresses above amount
|
||||
/** @type {readonly AddressCohortObject[]} */
|
||||
const addressesAboveAmount = entries(addressCohorts.geAmount).map(
|
||||
([key, tree]) => {
|
||||
const names = GE_AMOUNT_NAMES[key];
|
||||
return {
|
||||
name: names.short,
|
||||
title: names.long,
|
||||
color: colors[geAmountColors[key]],
|
||||
tree,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
// UTXOs under amount
|
||||
/** @type {readonly UtxoCohortObject[]} */
|
||||
const utxosUnderAmount = entries(utxoCohorts.ltAmount).map(([key, tree]) => {
|
||||
const names = LT_AMOUNT_NAMES[key];
|
||||
return {
|
||||
name: names.short,
|
||||
title: names.long,
|
||||
color: colors[ltAmountColors[key]],
|
||||
tree,
|
||||
};
|
||||
});
|
||||
|
||||
// Addresses under amount
|
||||
/** @type {readonly AddressCohortObject[]} */
|
||||
const addressesUnderAmount = entries(addressCohorts.ltAmount).map(
|
||||
([key, tree]) => {
|
||||
const names = LT_AMOUNT_NAMES[key];
|
||||
return {
|
||||
name: names.short,
|
||||
title: names.long,
|
||||
color: colors[ltAmountColors[key]],
|
||||
tree,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
// UTXOs amount ranges
|
||||
/** @type {readonly UtxoCohortObject[]} */
|
||||
const utxosAmountRanges = entries(utxoCohorts.amountRange).map(
|
||||
([key, tree]) => {
|
||||
const names = AMOUNT_RANGE_NAMES[key];
|
||||
return {
|
||||
name: names.short,
|
||||
title: names.long,
|
||||
color: colors[amountRangeColors[key]],
|
||||
tree,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
// Addresses amount ranges
|
||||
/** @type {readonly AddressCohortObject[]} */
|
||||
const addressesAmountRanges = entries(addressCohorts.amountRange).map(
|
||||
([key, tree]) => {
|
||||
const names = AMOUNT_RANGE_NAMES[key];
|
||||
return {
|
||||
name: names.short,
|
||||
title: names.long,
|
||||
color: colors[amountRangeColors[key]],
|
||||
tree,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
// Spendable type cohorts
|
||||
/** @type {readonly UtxoCohortObject[]} */
|
||||
const type = entries(utxoCohorts.type).map(([key, tree]) => {
|
||||
const names = SPENDABLE_TYPE_NAMES[key];
|
||||
return {
|
||||
name: names.short,
|
||||
title: names.long,
|
||||
color: colors[spendableTypeColors[key]],
|
||||
tree,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
cohortAll,
|
||||
cohortAllForComparison,
|
||||
terms,
|
||||
upToDate,
|
||||
fromDate,
|
||||
dateRange,
|
||||
epoch,
|
||||
utxosAboveAmount,
|
||||
addressesAboveAmount,
|
||||
utxosUnderAmount,
|
||||
addressesUnderAmount,
|
||||
utxosAmountRanges,
|
||||
addressesAmountRanges,
|
||||
type,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Cohort module - exports all cohort-related functionality
|
||||
*/
|
||||
|
||||
// Cohort data builder
|
||||
export { buildCohortData } from "./data.js";
|
||||
|
||||
// Cohort folder builders (type-safe!)
|
||||
export { createUtxoCohortFolder, createAgeCohortFolder, createAmountCohortFolder } from "./utxo.js";
|
||||
export { createAddressCohortFolder } from "./address.js";
|
||||
|
||||
// Shared helpers
|
||||
export {
|
||||
createSingleSupplySeries,
|
||||
createGroupedSupplyTotalSeries,
|
||||
createGroupedSupplyInProfitSeries,
|
||||
createGroupedSupplyInLossSeries,
|
||||
createUtxoCountSeries,
|
||||
createAddressCountSeries,
|
||||
createRealizedPriceSeries,
|
||||
createRealizedPriceRatioSeries,
|
||||
createRealizedCapSeries,
|
||||
createPricePaidMinMaxSeries,
|
||||
createPricePercentilesSeries,
|
||||
} from "./shared.js";
|
||||
@@ -0,0 +1,216 @@
|
||||
/** Shared cohort chart section builders */
|
||||
|
||||
/**
|
||||
* Create supply section for a single cohort
|
||||
* @param {PartialContext} ctx
|
||||
* @param {CohortObject} cohort
|
||||
* @param {string} title
|
||||
* @returns {AnyFetchedSeriesBlueprint[]}
|
||||
*/
|
||||
export function createSingleSupplySeries(ctx, cohort, title) {
|
||||
const { colors, s, createPriceLine } = ctx;
|
||||
const { tree, color, name } = cohort;
|
||||
|
||||
return [
|
||||
s({ metric: tree.supply.supply.sats, name: "Supply", color: colors.default }),
|
||||
s({ metric: tree.supply.supply.bitcoin, name: "Supply", color: colors.default }),
|
||||
s({ metric: tree.supply.supply.dollars, name: "Supply", color: colors.default }),
|
||||
...("supplyRelToCirculatingSupply" in tree.relative
|
||||
? [s({ metric: tree.relative.supplyRelToCirculatingSupply, name: "Supply", color: colors.default })]
|
||||
: []),
|
||||
s({ metric: tree.unrealized.supplyInProfit.sats, name: "In Profit", color: colors.green }),
|
||||
s({ metric: tree.unrealized.supplyInProfit.bitcoin, name: "In Profit", color: colors.green }),
|
||||
s({ metric: tree.unrealized.supplyInProfit.dollars, name: "In Profit", color: colors.green }),
|
||||
s({ metric: tree.unrealized.supplyInLoss.sats, name: "In Loss", color: colors.red }),
|
||||
s({ metric: tree.unrealized.supplyInLoss.bitcoin, name: "In Loss", color: colors.red }),
|
||||
s({ metric: tree.unrealized.supplyInLoss.dollars, name: "In Loss", color: colors.red }),
|
||||
s({ metric: tree.supply.supplyHalf.sats, name: "half", color: colors.gray, options: { lineStyle: 4 } }),
|
||||
s({ metric: tree.supply.supplyHalf.bitcoin, name: "half", color: colors.gray, options: { lineStyle: 4 } }),
|
||||
s({ metric: tree.supply.supplyHalf.dollars, name: "half", color: colors.gray, options: { lineStyle: 4 } }),
|
||||
...("supplyInProfitRelToCirculatingSupply" in tree.relative
|
||||
? [
|
||||
s({ metric: tree.relative.supplyInProfitRelToCirculatingSupply, name: "In Profit", color: colors.green }),
|
||||
s({ metric: tree.relative.supplyInLossRelToCirculatingSupply, name: "In Loss", color: colors.red }),
|
||||
]
|
||||
: []),
|
||||
s({ metric: tree.relative.supplyInProfitRelToOwnSupply, name: "In Profit", color: colors.green }),
|
||||
s({ metric: tree.relative.supplyInLossRelToOwnSupply, name: "In Loss", color: colors.red }),
|
||||
createPriceLine({ unit: "%self", number: 100, lineStyle: 0, color: colors.default }),
|
||||
createPriceLine({ unit: "%self", number: 50 }),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create supply total series for grouped cohorts
|
||||
* @param {PartialContext} ctx
|
||||
* @param {readonly CohortObject[]} list
|
||||
* @returns {AnyFetchedSeriesBlueprint[]}
|
||||
*/
|
||||
export function createGroupedSupplyTotalSeries(ctx, list) {
|
||||
const { s, constant100 } = ctx;
|
||||
|
||||
return list.flatMap(({ color, name, tree }) => [
|
||||
s({ metric: tree.supply.supply.sats, name, color }),
|
||||
s({ metric: tree.supply.supply.bitcoin, name, color }),
|
||||
s({ metric: tree.supply.supply.dollars, name, color }),
|
||||
"supplyRelToCirculatingSupply" in tree.relative
|
||||
? s({ metric: tree.relative.supplyRelToCirculatingSupply, name, color })
|
||||
: s({ unit: "%all", metric: constant100, name, color }),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create supply in profit series for grouped cohorts
|
||||
* @param {PartialContext} ctx
|
||||
* @param {readonly CohortObject[]} list
|
||||
* @returns {AnyFetchedSeriesBlueprint[]}
|
||||
*/
|
||||
export function createGroupedSupplyInProfitSeries(ctx, list) {
|
||||
const { s } = ctx;
|
||||
|
||||
return list.flatMap(({ color, name, tree }) => [
|
||||
s({ metric: tree.unrealized.supplyInProfit.sats, name, color }),
|
||||
s({ metric: tree.unrealized.supplyInProfit.bitcoin, name, color }),
|
||||
s({ metric: tree.unrealized.supplyInProfit.dollars, name, color }),
|
||||
...("supplyInProfitRelToCirculatingSupply" in tree.relative
|
||||
? [s({ metric: tree.relative.supplyInProfitRelToCirculatingSupply, name, color })]
|
||||
: []),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create supply in loss series for grouped cohorts
|
||||
* @param {PartialContext} ctx
|
||||
* @param {readonly CohortObject[]} list
|
||||
* @returns {AnyFetchedSeriesBlueprint[]}
|
||||
*/
|
||||
export function createGroupedSupplyInLossSeries(ctx, list) {
|
||||
const { s } = ctx;
|
||||
|
||||
return list.flatMap(({ color, name, tree }) => [
|
||||
s({ metric: tree.unrealized.supplyInLoss.sats, name, color }),
|
||||
s({ metric: tree.unrealized.supplyInLoss.bitcoin, name, color }),
|
||||
s({ metric: tree.unrealized.supplyInLoss.dollars, name, color }),
|
||||
...("supplyInLossRelToCirculatingSupply" in tree.relative
|
||||
? [s({ metric: tree.relative.supplyInLossRelToCirculatingSupply, name, color })]
|
||||
: []),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create UTXO count series
|
||||
* @param {PartialContext} ctx
|
||||
* @param {readonly CohortObject[]} list
|
||||
* @param {boolean} useGroupName
|
||||
* @returns {AnyFetchedSeriesBlueprint[]}
|
||||
*/
|
||||
export function createUtxoCountSeries(ctx, list, useGroupName) {
|
||||
const { s } = ctx;
|
||||
|
||||
return list.flatMap(({ color, name, tree }) => [
|
||||
s({ metric: tree.supply.utxoCount, name: useGroupName ? name : "Count", color }),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create address count series (for address cohorts only)
|
||||
* @param {PartialContext} ctx
|
||||
* @param {readonly AddressCohortObject[]} list
|
||||
* @param {boolean} useGroupName
|
||||
* @returns {AnyFetchedSeriesBlueprint[]}
|
||||
*/
|
||||
export function createAddressCountSeries(ctx, list, useGroupName) {
|
||||
const { s, colors } = ctx;
|
||||
|
||||
return list.flatMap(({ color, name, tree }) => [
|
||||
s({
|
||||
metric: tree.addrCount,
|
||||
name: useGroupName ? name : "Count",
|
||||
color: useGroupName ? color : colors.orange,
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create realized price series for grouped cohorts
|
||||
* @param {PartialContext} ctx
|
||||
* @param {readonly CohortObject[]} list
|
||||
* @returns {AnyFetchedSeriesBlueprint[]}
|
||||
*/
|
||||
export function createRealizedPriceSeries(ctx, list) {
|
||||
const { s } = ctx;
|
||||
|
||||
return list.map(({ color, name, tree }) =>
|
||||
s({ metric: tree.realized.realizedPrice, name, color }),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create realized price ratio series for grouped cohorts
|
||||
* @param {PartialContext} ctx
|
||||
* @param {readonly CohortObject[]} list
|
||||
* @returns {AnyFetchedSeriesBlueprint[]}
|
||||
*/
|
||||
export function createRealizedPriceRatioSeries(ctx, list) {
|
||||
const { s, createPriceLine } = ctx;
|
||||
|
||||
return [
|
||||
...list.map(({ color, name, tree }) =>
|
||||
s({ metric: tree.realized.realizedPriceExtra.ratio, name, color }),
|
||||
),
|
||||
createPriceLine({ unit: "ratio", number: 1 }),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create realized capitalization series
|
||||
* @param {PartialContext} ctx
|
||||
* @param {readonly CohortObject[]} list
|
||||
* @param {boolean} useGroupName
|
||||
* @returns {AnyFetchedSeriesBlueprint[]}
|
||||
*/
|
||||
export function createRealizedCapSeries(ctx, list, useGroupName) {
|
||||
const { s } = ctx;
|
||||
|
||||
return list.flatMap(({ color, name, tree }) => [
|
||||
s({ metric: tree.realized.realizedCap, name: useGroupName ? name : "Capitalization", color }),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create price paid min/max series (available on all cohorts)
|
||||
* @param {PartialContext} ctx
|
||||
* @param {readonly CohortObject[]} list
|
||||
* @param {boolean} useGroupName
|
||||
* @returns {AnyFetchedSeriesBlueprint[]}
|
||||
*/
|
||||
export function createPricePaidMinMaxSeries(ctx, list, useGroupName) {
|
||||
const { s } = ctx;
|
||||
|
||||
return list.flatMap(({ color, name, tree }) => [
|
||||
s({ metric: tree.pricePaid.minPricePaid, name: useGroupName ? `${name} min` : "Min", color }),
|
||||
s({ metric: tree.pricePaid.maxPricePaid, name: useGroupName ? `${name} max` : "Max", color }),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create price percentile series (only for cohorts with PricePaidPattern2)
|
||||
* @param {PartialContext} ctx
|
||||
* @param {readonly CohortWithPricePercentiles[]} list
|
||||
* @param {boolean} useGroupName
|
||||
* @returns {AnyFetchedSeriesBlueprint[]}
|
||||
*/
|
||||
export function createPricePercentilesSeries(ctx, list, useGroupName) {
|
||||
const { s, colors } = ctx;
|
||||
|
||||
return list.flatMap(({ color, name, tree }) => {
|
||||
const pp = tree.pricePaid.pricePercentiles;
|
||||
return [
|
||||
s({ metric: pp.pct10, name: useGroupName ? `${name} p10` : "p10", color, defaultActive: false }),
|
||||
s({ metric: pp.pct25, name: useGroupName ? `${name} p25` : "p25", color, defaultActive: false }),
|
||||
s({ metric: pp.pct50, name: useGroupName ? `${name} p50` : "p50", color }),
|
||||
s({ metric: pp.pct75, name: useGroupName ? `${name} p75` : "p75", color, defaultActive: false }),
|
||||
s({ metric: pp.pct90, name: useGroupName ? `${name} p90` : "p90", color, defaultActive: false }),
|
||||
];
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Cohort-related type definitions
|
||||
* Types are defined in ../types.js, this file exists for documentation
|
||||
*/
|
||||
|
||||
export {};
|
||||
@@ -0,0 +1,487 @@
|
||||
/**
|
||||
* UTXO cohort folder builders
|
||||
* Creates option trees for UTXO-based cohorts (no addrCount)
|
||||
*
|
||||
* Two main builders:
|
||||
* - createAgeCohortFolder: For term, maxAge, minAge, ageRange, epoch (has price percentiles)
|
||||
* - createAmountCohortFolder: For geAmount, ltAmount, amountRange, type (no price percentiles)
|
||||
*/
|
||||
|
||||
import {
|
||||
createSingleSupplySeries,
|
||||
createGroupedSupplyTotalSeries,
|
||||
createGroupedSupplyInProfitSeries,
|
||||
createGroupedSupplyInLossSeries,
|
||||
createUtxoCountSeries,
|
||||
createRealizedPriceSeries,
|
||||
createRealizedPriceRatioSeries,
|
||||
createRealizedCapSeries,
|
||||
createPricePaidMinMaxSeries,
|
||||
createPricePercentilesSeries,
|
||||
} from "./shared.js";
|
||||
|
||||
/**
|
||||
* Create a cohort folder for age-based UTXO cohorts (term, maxAge, minAge, ageRange, epoch)
|
||||
* These cohorts have price percentiles via PricePaidPattern2
|
||||
* @param {PartialContext} ctx
|
||||
* @param {AgeCohortObject | AgeCohortGroupObject} args
|
||||
* @returns {PartialOptionsGroup}
|
||||
*/
|
||||
export function createAgeCohortFolder(ctx, args) {
|
||||
const list = "list" in args ? args.list : [args];
|
||||
const useGroupName = "list" in args;
|
||||
const isSingle = !("list" in args);
|
||||
const title = args.title ? `${useGroupName ? "by" : "of"} ${args.title}` : "";
|
||||
|
||||
return {
|
||||
name: args.name || "all",
|
||||
tree: [
|
||||
...createSupplySection(ctx, list, args, useGroupName, isSingle, title),
|
||||
createUtxoCountSection(ctx, list, useGroupName, title),
|
||||
createRealizedSection(ctx, list, args, useGroupName, isSingle, title),
|
||||
...createUnrealizedSection(ctx, list, useGroupName, title),
|
||||
...createPricePaidSectionWithPercentiles(ctx, list, useGroupName, title),
|
||||
...createActivitySection(ctx, list, useGroupName, title),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a cohort folder for amount-based UTXO cohorts (geAmount, ltAmount, amountRange, type)
|
||||
* These cohorts have only min/max price paid via PricePaidPattern
|
||||
* @param {PartialContext} ctx
|
||||
* @param {AmountCohortObject | AmountCohortGroupObject} args
|
||||
* @returns {PartialOptionsGroup}
|
||||
*/
|
||||
export function createAmountCohortFolder(ctx, args) {
|
||||
const list = "list" in args ? args.list : [args];
|
||||
const useGroupName = "list" in args;
|
||||
const isSingle = !("list" in args);
|
||||
const title = args.title ? `${useGroupName ? "by" : "of"} ${args.title}` : "";
|
||||
|
||||
return {
|
||||
name: args.name || "all",
|
||||
tree: [
|
||||
...createSupplySection(ctx, list, args, useGroupName, isSingle, title),
|
||||
createUtxoCountSection(ctx, list, useGroupName, title),
|
||||
createRealizedSection(ctx, list, args, useGroupName, isSingle, title),
|
||||
...createUnrealizedSection(ctx, list, useGroupName, title),
|
||||
...createPricePaidSectionBasic(ctx, list, useGroupName, title),
|
||||
...createActivitySection(ctx, list, useGroupName, title),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// Keep the generic version for backwards compatibility
|
||||
/**
|
||||
* Create a cohort folder for UTXO cohorts (generic, uses runtime check for percentiles)
|
||||
* @deprecated Use createAgeCohortFolder or createAmountCohortFolder for type safety
|
||||
* @param {PartialContext} ctx
|
||||
* @param {UtxoCohortObject | UtxoCohortGroupObject} args
|
||||
* @returns {PartialOptionsGroup}
|
||||
*/
|
||||
export function createUtxoCohortFolder(ctx, args) {
|
||||
const list = "list" in args ? args.list : [args];
|
||||
const useGroupName = "list" in args;
|
||||
const isSingle = !("list" in args);
|
||||
const title = args.title ? `${useGroupName ? "by" : "of"} ${args.title}` : "";
|
||||
|
||||
// Runtime check for percentiles
|
||||
const hasPercentiles = "pricePercentiles" in list[0].tree.pricePaid;
|
||||
|
||||
return {
|
||||
name: args.name || "all",
|
||||
tree: [
|
||||
...createSupplySection(ctx, list, args, useGroupName, isSingle, title),
|
||||
createUtxoCountSection(ctx, list, useGroupName, title),
|
||||
createRealizedSection(ctx, list, args, useGroupName, isSingle, title),
|
||||
...createUnrealizedSection(ctx, list, useGroupName, title),
|
||||
...(hasPercentiles
|
||||
? createPricePaidSectionWithPercentiles(ctx, /** @type {readonly AgeCohortObject[]} */ (list), useGroupName, title)
|
||||
: createPricePaidSectionBasic(ctx, list, useGroupName, title)),
|
||||
...createActivitySection(ctx, list, useGroupName, title),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create supply section
|
||||
* @param {PartialContext} ctx
|
||||
* @param {readonly UtxoCohortObject[]} list
|
||||
* @param {UtxoCohortObject | UtxoCohortGroupObject} args
|
||||
* @param {boolean} useGroupName
|
||||
* @param {boolean} isSingle
|
||||
* @param {string} title
|
||||
* @returns {PartialOptionsTree}
|
||||
*/
|
||||
function createSupplySection(ctx, list, args, useGroupName, isSingle, title) {
|
||||
return [
|
||||
isSingle
|
||||
? {
|
||||
name: "supply",
|
||||
title: `Supply ${title}`,
|
||||
bottom: createSingleSupplySeries(ctx, /** @type {UtxoCohortObject} */ (args), title),
|
||||
}
|
||||
: {
|
||||
name: "supply",
|
||||
tree: [
|
||||
{
|
||||
name: "total",
|
||||
title: `Supply ${title}`,
|
||||
bottom: createGroupedSupplyTotalSeries(ctx, list),
|
||||
},
|
||||
{
|
||||
name: "in profit",
|
||||
title: `Supply In Profit ${title}`,
|
||||
bottom: createGroupedSupplyInProfitSeries(ctx, list),
|
||||
},
|
||||
{
|
||||
name: "in loss",
|
||||
title: `Supply In Loss ${title}`,
|
||||
bottom: createGroupedSupplyInLossSeries(ctx, list),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create UTXO count section
|
||||
* @param {PartialContext} ctx
|
||||
* @param {readonly UtxoCohortObject[]} list
|
||||
* @param {boolean} useGroupName
|
||||
* @param {string} title
|
||||
* @returns {PartialChartOption}
|
||||
*/
|
||||
function createUtxoCountSection(ctx, list, useGroupName, title) {
|
||||
return {
|
||||
name: "utxo count",
|
||||
title: `UTXO Count ${title}`,
|
||||
bottom: createUtxoCountSeries(ctx, list, useGroupName),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create realized section
|
||||
* @param {PartialContext} ctx
|
||||
* @param {readonly UtxoCohortObject[]} list
|
||||
* @param {UtxoCohortObject | UtxoCohortGroupObject} args
|
||||
* @param {boolean} useGroupName
|
||||
* @param {boolean} isSingle
|
||||
* @param {string} title
|
||||
* @returns {PartialOptionsGroup}
|
||||
*/
|
||||
function createRealizedSection(ctx, list, args, useGroupName, isSingle, title) {
|
||||
return {
|
||||
name: "Realized",
|
||||
tree: [
|
||||
...(useGroupName
|
||||
? [
|
||||
{
|
||||
name: "Price",
|
||||
title: `Realized Price ${title}`,
|
||||
top: createRealizedPriceSeries(ctx, list),
|
||||
},
|
||||
{
|
||||
name: "Ratio",
|
||||
title: `Realized Price Ratio ${title}`,
|
||||
bottom: createRealizedPriceRatioSeries(ctx, list),
|
||||
},
|
||||
]
|
||||
: createRealizedPriceOptions(ctx, /** @type {UtxoCohortObject} */ (args), title)),
|
||||
{
|
||||
name: "capitalization",
|
||||
title: `Realized Capitalization ${title}`,
|
||||
bottom: createRealizedCapWithExtras(ctx, list, args, useGroupName, title),
|
||||
},
|
||||
...(!useGroupName ? createRealizedPnlSection(ctx, /** @type {UtxoCohortObject} */ (args), title) : []),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create realized price options for single cohort
|
||||
* @param {PartialContext} ctx
|
||||
* @param {UtxoCohortObject} args
|
||||
* @param {string} title
|
||||
* @returns {PartialOptionsTree}
|
||||
*/
|
||||
function createRealizedPriceOptions(ctx, args, title) {
|
||||
const { s } = ctx;
|
||||
const { tree, color } = args;
|
||||
|
||||
return [
|
||||
{
|
||||
name: "price",
|
||||
title: `Realized Price ${title}`,
|
||||
top: [s({ metric: tree.realized.realizedPrice, name: "realized", color })],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create realized cap with extras
|
||||
* @param {PartialContext} ctx
|
||||
* @param {readonly UtxoCohortObject[]} list
|
||||
* @param {UtxoCohortObject | UtxoCohortGroupObject} args
|
||||
* @param {boolean} useGroupName
|
||||
* @param {string} title
|
||||
* @returns {AnyFetchedSeriesBlueprint[]}
|
||||
*/
|
||||
function createRealizedCapWithExtras(ctx, list, args, useGroupName, title) {
|
||||
const { colors, s, createPriceLine } = ctx;
|
||||
const isSingle = !("list" in args);
|
||||
|
||||
return list.flatMap(({ color, name, tree }) => [
|
||||
s({
|
||||
metric: tree.realized.realizedCap,
|
||||
name: useGroupName ? name : "Capitalization",
|
||||
color,
|
||||
}),
|
||||
...(isSingle
|
||||
? [
|
||||
/** @type {AnyFetchedSeriesBlueprint} */ ({
|
||||
type: "Baseline",
|
||||
metric: tree.realized.realizedCap30dDelta,
|
||||
title: "30d change",
|
||||
defaultActive: false,
|
||||
}),
|
||||
createPriceLine({ unit: "usd", defaultActive: false }),
|
||||
]
|
||||
: []),
|
||||
...(isSingle && "realizedCapRelToOwnMarketCap" in tree.realized
|
||||
? [
|
||||
/** @type {AnyFetchedSeriesBlueprint} */ ({
|
||||
type: "Baseline",
|
||||
metric: tree.realized.realizedCapRelToOwnMarketCap,
|
||||
title: "ratio",
|
||||
options: { baseValue: { price: 100 } },
|
||||
colors: [colors.red, colors.green],
|
||||
}),
|
||||
createPriceLine({ unit: "%cmcap", defaultActive: true, number: 100 }),
|
||||
]
|
||||
: []),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create realized PnL section for single cohort
|
||||
* @param {PartialContext} ctx
|
||||
* @param {UtxoCohortObject} args
|
||||
* @param {string} title
|
||||
* @returns {PartialOptionsTree}
|
||||
*/
|
||||
function createRealizedPnlSection(ctx, args, title) {
|
||||
const { colors, s } = ctx;
|
||||
const { tree } = args;
|
||||
|
||||
return [
|
||||
{
|
||||
name: "pnl",
|
||||
title: `Realized Profit And Loss ${title}`,
|
||||
bottom: [
|
||||
s({
|
||||
metric: tree.realized.realizedProfit.base,
|
||||
name: "Profit",
|
||||
color: colors.green,
|
||||
}),
|
||||
s({
|
||||
metric: tree.realized.realizedLoss.base,
|
||||
name: "Loss",
|
||||
color: colors.red,
|
||||
defaultActive: false,
|
||||
}),
|
||||
...("realizedProfitToLossRatio" in tree.realized
|
||||
? [
|
||||
s({
|
||||
metric: tree.realized.realizedProfitToLossRatio,
|
||||
name: "profit / loss",
|
||||
color: colors.yellow,
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
s({
|
||||
metric: tree.realized.totalRealizedPnl.base,
|
||||
name: "Total",
|
||||
color: colors.default,
|
||||
defaultActive: false,
|
||||
}),
|
||||
s({
|
||||
metric: tree.realized.negRealizedLoss.base,
|
||||
name: "Negative Loss",
|
||||
color: colors.red,
|
||||
}),
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create unrealized section
|
||||
* @param {PartialContext} ctx
|
||||
* @param {readonly UtxoCohortObject[]} list
|
||||
* @param {boolean} useGroupName
|
||||
* @param {string} title
|
||||
* @returns {PartialOptionsTree}
|
||||
*/
|
||||
function createUnrealizedSection(ctx, list, useGroupName, title) {
|
||||
const { colors, s, createPriceLine } = ctx;
|
||||
|
||||
return [
|
||||
{
|
||||
name: "Unrealized",
|
||||
tree: [
|
||||
{
|
||||
name: "nupl",
|
||||
title: `Net Unrealized Profit/Loss ${title}`,
|
||||
bottom: list.flatMap(({ color, name, tree }) => [
|
||||
/** @type {AnyFetchedSeriesBlueprint} */ ({
|
||||
type: "Baseline",
|
||||
metric: tree.unrealized.netUnrealizedPnl,
|
||||
title: useGroupName ? name : "NUPL",
|
||||
colors: [colors.red, colors.green],
|
||||
options: { baseValue: { price: 0 } },
|
||||
}),
|
||||
]),
|
||||
},
|
||||
{
|
||||
name: "profit",
|
||||
title: `Unrealized Profit ${title}`,
|
||||
bottom: list.flatMap(({ color, name, tree }) => [
|
||||
s({
|
||||
metric: tree.unrealized.unrealizedProfit,
|
||||
name: useGroupName ? name : "Profit",
|
||||
color,
|
||||
}),
|
||||
]),
|
||||
},
|
||||
{
|
||||
name: "loss",
|
||||
title: `Unrealized Loss ${title}`,
|
||||
bottom: list.flatMap(({ color, name, tree }) => [
|
||||
s({
|
||||
metric: tree.unrealized.unrealizedLoss,
|
||||
name: useGroupName ? name : "Loss",
|
||||
color,
|
||||
}),
|
||||
]),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create price paid section for cohorts WITH percentiles (age cohorts)
|
||||
* @param {PartialContext} ctx
|
||||
* @param {readonly AgeCohortObject[]} list
|
||||
* @param {boolean} useGroupName
|
||||
* @param {string} title
|
||||
* @returns {PartialOptionsTree}
|
||||
*/
|
||||
function createPricePaidSectionWithPercentiles(ctx, list, useGroupName, title) {
|
||||
const { s } = ctx;
|
||||
|
||||
return [
|
||||
{
|
||||
name: "Price Paid",
|
||||
tree: [
|
||||
{
|
||||
name: "min",
|
||||
title: `Min Price Paid ${title}`,
|
||||
top: list.map(({ color, name, tree }) =>
|
||||
s({ metric: tree.pricePaid.minPricePaid, name: useGroupName ? name : "Min", color }),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "max",
|
||||
title: `Max Price Paid ${title}`,
|
||||
top: list.map(({ color, name, tree }) =>
|
||||
s({ metric: tree.pricePaid.maxPricePaid, name: useGroupName ? name : "Max", color }),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "percentiles",
|
||||
title: `Price Paid Percentiles ${title}`,
|
||||
top: createPricePercentilesSeries(ctx, list, useGroupName),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create price paid section for cohorts WITHOUT percentiles (amount cohorts)
|
||||
* @param {PartialContext} ctx
|
||||
* @param {readonly UtxoCohortObject[]} list
|
||||
* @param {boolean} useGroupName
|
||||
* @param {string} title
|
||||
* @returns {PartialOptionsTree}
|
||||
*/
|
||||
function createPricePaidSectionBasic(ctx, list, useGroupName, title) {
|
||||
const { s } = ctx;
|
||||
|
||||
return [
|
||||
{
|
||||
name: "Price Paid",
|
||||
tree: [
|
||||
{
|
||||
name: "min",
|
||||
title: `Min Price Paid ${title}`,
|
||||
top: list.map(({ color, name, tree }) =>
|
||||
s({ metric: tree.pricePaid.minPricePaid, name: useGroupName ? name : "Min", color }),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "max",
|
||||
title: `Max Price Paid ${title}`,
|
||||
top: list.map(({ color, name, tree }) =>
|
||||
s({ metric: tree.pricePaid.maxPricePaid, name: useGroupName ? name : "Max", color }),
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create activity section
|
||||
* @param {PartialContext} ctx
|
||||
* @param {readonly UtxoCohortObject[]} list
|
||||
* @param {boolean} useGroupName
|
||||
* @param {string} title
|
||||
* @returns {PartialOptionsTree}
|
||||
*/
|
||||
function createActivitySection(ctx, list, useGroupName, title) {
|
||||
const { s } = ctx;
|
||||
|
||||
return [
|
||||
{
|
||||
name: "Activity",
|
||||
tree: [
|
||||
{
|
||||
name: "coinblocks destroyed",
|
||||
title: `Coinblocks Destroyed ${title}`,
|
||||
bottom: list.flatMap(({ color, name, tree }) => [
|
||||
s({
|
||||
metric: tree.activity.coinblocksDestroyed.base,
|
||||
name: useGroupName ? name : "Coinblocks",
|
||||
color,
|
||||
}),
|
||||
]),
|
||||
},
|
||||
{
|
||||
name: "coindays destroyed",
|
||||
title: `Coindays Destroyed ${title}`,
|
||||
bottom: list.flatMap(({ color, name, tree }) => [
|
||||
s({
|
||||
metric: tree.activity.coindaysDestroyed.base,
|
||||
name: useGroupName ? name : "Coindays",
|
||||
color,
|
||||
}),
|
||||
]),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,306 @@
|
||||
/** Cointime section builder - typed tree-based patterns */
|
||||
|
||||
/**
|
||||
* Create price with ratio options for cointime prices
|
||||
* @param {PartialContext} ctx
|
||||
* @param {Object} args
|
||||
* @param {string} args.name
|
||||
* @param {string} args.title
|
||||
* @param {string} args.legend
|
||||
* @param {MetricAccessor<any>} args.price
|
||||
* @param {ActivePriceRatioPattern} args.ratio
|
||||
* @param {Color} [args.color]
|
||||
* @returns {PartialOptionsTree}
|
||||
*/
|
||||
function createCointimePriceWithRatioOptions(ctx, { name, title, legend, price, ratio, color }) {
|
||||
const { s, colors, createPriceLine } = ctx;
|
||||
|
||||
// Percentile USD mappings
|
||||
const percentileUsdMap = [
|
||||
{ name: "pct99", prop: ratio.ratioPct99Usd, color: colors.rose },
|
||||
{ name: "pct98", prop: ratio.ratioPct98Usd, color: colors.pink },
|
||||
{ name: "pct95", prop: ratio.ratioPct95Usd, color: colors.fuchsia },
|
||||
{ name: "pct5", prop: ratio.ratioPct5Usd, color: colors.cyan },
|
||||
{ name: "pct2", prop: ratio.ratioPct2Usd, color: colors.sky },
|
||||
{ name: "pct1", prop: ratio.ratioPct1Usd, color: colors.blue },
|
||||
];
|
||||
|
||||
// Percentile ratio mappings
|
||||
const percentileMap = [
|
||||
{ name: "pct99", prop: ratio.ratioPct99, color: colors.rose },
|
||||
{ name: "pct98", prop: ratio.ratioPct98, color: colors.pink },
|
||||
{ name: "pct95", prop: ratio.ratioPct95, color: colors.fuchsia },
|
||||
{ name: "pct5", prop: ratio.ratioPct5, color: colors.cyan },
|
||||
{ name: "pct2", prop: ratio.ratioPct2, color: colors.sky },
|
||||
{ name: "pct1", prop: ratio.ratioPct1, color: colors.blue },
|
||||
];
|
||||
|
||||
// SD patterns by window
|
||||
const sdPatterns = [
|
||||
{ nameAddon: "all", titleAddon: "", sd: ratio.ratioSd },
|
||||
{ nameAddon: "4y", titleAddon: "4y", sd: ratio.ratio4ySd },
|
||||
{ nameAddon: "2y", titleAddon: "2y", sd: ratio.ratio2ySd },
|
||||
{ nameAddon: "1y", titleAddon: "1y", sd: ratio.ratio1ySd },
|
||||
];
|
||||
|
||||
/** @param {Ratio1ySdPattern} sd */
|
||||
const getSdBands = (sd) => [
|
||||
{ name: "0σ", prop: sd._0sdUsd, color: colors.lime },
|
||||
{ name: "+0.5σ", prop: sd.p05sdUsd, color: colors.yellow },
|
||||
{ name: "+1σ", prop: sd.p1sdUsd, color: colors.amber },
|
||||
{ name: "+1.5σ", prop: sd.p15sdUsd, color: colors.orange },
|
||||
{ name: "+2σ", prop: sd.p2sdUsd, color: colors.red },
|
||||
{ name: "+2.5σ", prop: sd.p25sdUsd, color: colors.rose },
|
||||
{ name: "+3σ", prop: sd.p3sd, color: colors.pink },
|
||||
{ name: "−0.5σ", prop: sd.m05sdUsd, color: colors.teal },
|
||||
{ name: "−1σ", prop: sd.m1sdUsd, color: colors.cyan },
|
||||
{ name: "−1.5σ", prop: sd.m15sdUsd, color: colors.sky },
|
||||
{ name: "−2σ", prop: sd.m2sdUsd, color: colors.blue },
|
||||
{ name: "−2.5σ", prop: sd.m25sdUsd, color: colors.indigo },
|
||||
{ name: "−3σ", prop: sd.m3sd, color: colors.violet },
|
||||
];
|
||||
|
||||
return [
|
||||
{
|
||||
name: "price",
|
||||
title,
|
||||
top: [s({ metric: price, name: legend, color, unit: "usd" })],
|
||||
},
|
||||
{
|
||||
name: "Ratio",
|
||||
title: `${title} Ratio`,
|
||||
top: [
|
||||
s({ metric: price, name: legend, color, unit: "usd" }),
|
||||
...percentileUsdMap.map(({ name: pctName, prop, color: pctColor }) =>
|
||||
s({
|
||||
metric: prop,
|
||||
name: pctName,
|
||||
color: pctColor,
|
||||
defaultActive: false,
|
||||
unit: "usd",
|
||||
options: { lineStyle: 1 },
|
||||
}),
|
||||
),
|
||||
],
|
||||
bottom: [
|
||||
s({ metric: ratio.ratio, name: "ratio", color, unit: "ratio" }),
|
||||
s({ metric: ratio.ratio1wSma, name: "1w sma", color: colors.lime, unit: "ratio" }),
|
||||
s({ metric: ratio.ratio1mSma, name: "1m sma", color: colors.teal, unit: "ratio" }),
|
||||
s({ metric: ratio.ratio1ySd.sma, name: "1y sma", color: colors.sky, unit: "ratio" }),
|
||||
s({ metric: ratio.ratio2ySd.sma, name: "2y sma", color: colors.indigo, unit: "ratio" }),
|
||||
s({ metric: ratio.ratio4ySd.sma, name: "4y sma", color: colors.purple, unit: "ratio" }),
|
||||
s({ metric: ratio.ratioSd.sma, name: "all sma", color: colors.rose, unit: "ratio" }),
|
||||
...percentileMap.map(({ name: pctName, prop, color: pctColor }) =>
|
||||
s({
|
||||
metric: prop,
|
||||
name: pctName,
|
||||
color: pctColor,
|
||||
defaultActive: false,
|
||||
unit: "ratio",
|
||||
options: { lineStyle: 1 },
|
||||
}),
|
||||
),
|
||||
createPriceLine({ unit: "ratio", number: 1 }),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "ZScores",
|
||||
tree: sdPatterns.map(({ nameAddon, titleAddon, sd }) => ({
|
||||
name: nameAddon,
|
||||
title: `${title} ${titleAddon} Z-Score`,
|
||||
top: getSdBands(sd).map(({ name: bandName, prop, color: bandColor }) =>
|
||||
s({ metric: prop, name: bandName, color: bandColor, unit: "usd" }),
|
||||
),
|
||||
bottom: [
|
||||
s({ metric: sd.zscore, name: "zscore", color, unit: "sd" }),
|
||||
createPriceLine({ unit: "sd", number: 3 }),
|
||||
createPriceLine({ unit: "sd", number: 2 }),
|
||||
createPriceLine({ unit: "sd", number: 1 }),
|
||||
createPriceLine({ unit: "sd", number: 0 }),
|
||||
createPriceLine({ unit: "sd", number: -1 }),
|
||||
createPriceLine({ unit: "sd", number: -2 }),
|
||||
createPriceLine({ unit: "sd", number: -3 }),
|
||||
],
|
||||
})),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create Cointime section
|
||||
* @param {PartialContext} ctx
|
||||
* @returns {PartialOptionsGroup}
|
||||
*/
|
||||
export function createCointimeSection(ctx) {
|
||||
const { colors, brk, s } = ctx;
|
||||
const { cointime, distribution, supply } = brk.tree.computed;
|
||||
const { pricing, cap, activity, supply: cointimeSupply, adjusted } = cointime;
|
||||
const utxoCohorts = distribution.utxoCohorts;
|
||||
|
||||
// Cointime prices data
|
||||
const cointimePrices = [
|
||||
{
|
||||
price: pricing.trueMarketMean,
|
||||
ratio: pricing.trueMarketMeanRatio,
|
||||
name: "True market mean",
|
||||
title: "true market mean",
|
||||
color: colors.blue,
|
||||
},
|
||||
{
|
||||
price: pricing.vaultedPrice,
|
||||
ratio: pricing.vaultedPriceRatio,
|
||||
name: "Vaulted",
|
||||
title: "vaulted price",
|
||||
color: colors.lime,
|
||||
},
|
||||
{
|
||||
price: pricing.activePrice,
|
||||
ratio: pricing.activePriceRatio,
|
||||
name: "Active",
|
||||
title: "active price",
|
||||
color: colors.rose,
|
||||
},
|
||||
{
|
||||
price: pricing.cointimePrice,
|
||||
ratio: pricing.cointimePriceRatio,
|
||||
name: "cointime",
|
||||
title: "cointime price",
|
||||
color: colors.yellow,
|
||||
},
|
||||
];
|
||||
|
||||
// Cointime capitalizations data
|
||||
const cointimeCapitalizations = [
|
||||
{ metric: cap.vaultedCap, name: "vaulted", title: "vaulted Capitalization", color: colors.lime },
|
||||
{ metric: cap.activeCap, name: "active", title: "active Capitalization", color: colors.rose },
|
||||
{ metric: cap.cointimeCap, name: "cointime", title: "cointime Capitalization", color: colors.yellow },
|
||||
{ metric: cap.investorCap, name: "investor", title: "investor Capitalization", color: colors.fuchsia },
|
||||
{ metric: cap.thermoCap, name: "thermo", title: "thermo Capitalization", color: colors.emerald },
|
||||
];
|
||||
|
||||
return {
|
||||
name: "Cointime",
|
||||
tree: [
|
||||
// Prices
|
||||
{
|
||||
name: "Prices",
|
||||
tree: [
|
||||
{
|
||||
name: "Compare",
|
||||
title: "Compare Cointime Prices",
|
||||
top: cointimePrices.map(({ price, name, color }) =>
|
||||
s({ metric: price, name, color, unit: "usd" }),
|
||||
),
|
||||
},
|
||||
...cointimePrices.map(({ price, ratio, name, color, title }) => ({
|
||||
name,
|
||||
tree: createCointimePriceWithRatioOptions(ctx, {
|
||||
price,
|
||||
ratio,
|
||||
legend: name,
|
||||
color,
|
||||
name,
|
||||
title,
|
||||
}),
|
||||
})),
|
||||
],
|
||||
},
|
||||
|
||||
// Capitalization
|
||||
{
|
||||
name: "Capitalization",
|
||||
tree: [
|
||||
{
|
||||
name: "Compare",
|
||||
title: "Compare Cointime Capitalizations",
|
||||
bottom: [
|
||||
s({ metric: supply.marketCap.height, name: "Market", color: colors.default, unit: "usd" }),
|
||||
s({ metric: utxoCohorts.all.realized.realizedCap, name: "Realized", color: colors.orange, unit: "usd" }),
|
||||
...cointimeCapitalizations.map(({ metric, name, color }) =>
|
||||
s({ metric, name, color, unit: "usd" }),
|
||||
),
|
||||
],
|
||||
},
|
||||
...cointimeCapitalizations.map(({ metric, name, color, title }) => ({
|
||||
name,
|
||||
title,
|
||||
bottom: [
|
||||
s({ metric, name, color, unit: "usd" }),
|
||||
s({ metric: supply.marketCap.height, name: "Market", color: colors.default, unit: "usd" }),
|
||||
s({ metric: utxoCohorts.all.realized.realizedCap, name: "Realized", color: colors.orange, unit: "usd" }),
|
||||
],
|
||||
})),
|
||||
],
|
||||
},
|
||||
|
||||
// Supply
|
||||
{
|
||||
name: "Supply",
|
||||
title: "Cointime Supply",
|
||||
bottom: /** @type {const} */ ([
|
||||
[utxoCohorts.all.supply.supply, "all", colors.orange],
|
||||
[cointimeSupply.vaultedSupply, "vaulted", colors.lime],
|
||||
[cointimeSupply.activeSupply, "active", colors.rose],
|
||||
]).flatMap(([supplyItem, name, color]) => [
|
||||
s({ metric: supplyItem.sats, name, color, unit: "sats" }),
|
||||
s({ metric: supplyItem.bitcoin, name, color, unit: "btc" }),
|
||||
s({ metric: supplyItem.dollars, name, color, unit: "usd" }),
|
||||
]),
|
||||
},
|
||||
|
||||
// Liveliness & Vaultedness
|
||||
{
|
||||
name: "Liveliness & Vaultedness",
|
||||
title: "Liveliness & Vaultedness",
|
||||
bottom: [
|
||||
s({ metric: activity.liveliness, name: "Liveliness", color: colors.rose, unit: "ratio" }),
|
||||
s({ metric: activity.vaultedness, name: "Vaultedness", color: colors.lime, unit: "ratio" }),
|
||||
s({ metric: activity.activityToVaultednessRatio, name: "Liveliness / Vaultedness", color: colors.purple, unit: "ratio" }),
|
||||
],
|
||||
},
|
||||
|
||||
// Coinblocks
|
||||
{
|
||||
name: "Coinblocks",
|
||||
title: "Coinblocks",
|
||||
bottom: [
|
||||
// Destroyed comes from the all cohort's activity
|
||||
s({ metric: utxoCohorts.all.activity.coinblocksDestroyed.base, name: "Destroyed", color: colors.red, unit: "coinblocks" }),
|
||||
s({ metric: utxoCohorts.all.activity.coinblocksDestroyed.cumulative, name: "Cumulative Destroyed", color: colors.red, defaultActive: false, unit: "coinblocks" }),
|
||||
// Created and stored from cointime
|
||||
s({ metric: activity.coinblocksCreated.base, name: "created", color: colors.orange, unit: "coinblocks" }),
|
||||
s({ metric: activity.coinblocksCreated.cumulative, name: "Cumulative created", color: colors.orange, defaultActive: false, unit: "coinblocks" }),
|
||||
s({ metric: activity.coinblocksStored.base, name: "stored", color: colors.green, unit: "coinblocks" }),
|
||||
s({ metric: activity.coinblocksStored.cumulative, name: "Cumulative stored", color: colors.green, defaultActive: false, unit: "coinblocks" }),
|
||||
],
|
||||
},
|
||||
|
||||
// Adjusted metrics
|
||||
{
|
||||
name: "Adjusted",
|
||||
tree: [
|
||||
// Inflation
|
||||
{
|
||||
name: "inflation",
|
||||
title: "Cointime-Adjusted inflation rate",
|
||||
bottom: [
|
||||
s({ metric: supply.inflation.indexes, name: "base", color: colors.orange, unit: "percentage" }),
|
||||
s({ metric: adjusted.cointimeAdjInflationRate, name: "adjusted", color: colors.purple, unit: "percentage" }),
|
||||
],
|
||||
},
|
||||
// Velocity
|
||||
{
|
||||
name: "Velocity",
|
||||
title: "Cointime-Adjusted transactions velocity",
|
||||
bottom: [
|
||||
s({ metric: supply.velocity.btc, name: "btc", color: colors.orange, unit: "ratio" }),
|
||||
s({ metric: adjusted.cointimeAdjTxBtcVelocity, name: "adj. btc", color: colors.red, unit: "ratio" }),
|
||||
s({ metric: supply.velocity.usd, name: "usd", color: colors.emerald, unit: "ratio" }),
|
||||
s({ metric: adjusted.cointimeAdjTxUsdVelocity, name: "adj. usd", color: colors.lime, unit: "ratio" }),
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
/** Cohort color mappings */
|
||||
|
||||
/** @type {Readonly<Record<string, ColorName>>} */
|
||||
export const termColors = {
|
||||
short: "yellow",
|
||||
long: "fuchsia",
|
||||
};
|
||||
|
||||
/** @type {Readonly<Record<string, ColorName>>} */
|
||||
export const maxAgeColors = {
|
||||
_1w: "red",
|
||||
_1m: "orange",
|
||||
_2m: "amber",
|
||||
_3m: "yellow",
|
||||
_4m: "lime",
|
||||
_5m: "green",
|
||||
_6m: "teal",
|
||||
_1y: "sky",
|
||||
_2y: "indigo",
|
||||
_3y: "violet",
|
||||
_4y: "purple",
|
||||
_5y: "fuchsia",
|
||||
_6y: "pink",
|
||||
_7y: "red",
|
||||
_8y: "orange",
|
||||
_10y: "amber",
|
||||
_12y: "yellow",
|
||||
_15y: "lime",
|
||||
};
|
||||
|
||||
/** @type {Readonly<Record<string, ColorName>>} */
|
||||
export const minAgeColors = {
|
||||
_1d: "red",
|
||||
_1w: "orange",
|
||||
_1m: "yellow",
|
||||
_2m: "lime",
|
||||
_3m: "green",
|
||||
_4m: "teal",
|
||||
_5m: "cyan",
|
||||
_6m: "blue",
|
||||
_1y: "indigo",
|
||||
_2y: "violet",
|
||||
_3y: "purple",
|
||||
_4y: "fuchsia",
|
||||
_5y: "pink",
|
||||
_6y: "rose",
|
||||
_7y: "red",
|
||||
_8y: "orange",
|
||||
_10y: "yellow",
|
||||
_12y: "lime",
|
||||
};
|
||||
|
||||
/** @type {Readonly<Record<string, ColorName>>} */
|
||||
export const ageRangeColors = {
|
||||
upTo1d: "pink",
|
||||
_1dTo1w: "red",
|
||||
_1wTo1m: "orange",
|
||||
_1mTo2m: "yellow",
|
||||
_2mTo3m: "yellow",
|
||||
_3mTo4m: "lime",
|
||||
_4mTo5m: "lime",
|
||||
_5mTo6m: "lime",
|
||||
_6mTo1y: "green",
|
||||
_1yTo2y: "cyan",
|
||||
_2yTo3y: "blue",
|
||||
_3yTo4y: "indigo",
|
||||
_4yTo5y: "violet",
|
||||
_5yTo6y: "purple",
|
||||
_6yTo7y: "purple",
|
||||
_7yTo8y: "fuchsia",
|
||||
_8yTo10y: "fuchsia",
|
||||
_10yTo12y: "pink",
|
||||
_12yTo15y: "red",
|
||||
from15y: "orange",
|
||||
};
|
||||
|
||||
/** @type {Readonly<Record<string, ColorName>>} */
|
||||
export const epochColors = {
|
||||
_0: "red",
|
||||
_1: "yellow",
|
||||
_2: "orange",
|
||||
_3: "lime",
|
||||
_4: "green",
|
||||
};
|
||||
|
||||
/** @type {Readonly<Record<string, ColorName>>} */
|
||||
export const geAmountColors = {
|
||||
_1sat: "orange",
|
||||
_10sats: "orange",
|
||||
_100sats: "yellow",
|
||||
_1kSats: "lime",
|
||||
_10kSats: "green",
|
||||
_100kSats: "cyan",
|
||||
_1mSats: "blue",
|
||||
_10mSats: "indigo",
|
||||
_1btc: "purple",
|
||||
_10btc: "violet",
|
||||
_100btc: "fuchsia",
|
||||
_1kBtc: "pink",
|
||||
_10kBtc: "red",
|
||||
};
|
||||
|
||||
/** @type {Readonly<Record<string, ColorName>>} */
|
||||
export const ltAmountColors = {
|
||||
_10sats: "orange",
|
||||
_100sats: "yellow",
|
||||
_1kSats: "lime",
|
||||
_10kSats: "green",
|
||||
_100kSats: "cyan",
|
||||
_1mSats: "blue",
|
||||
_10mSats: "indigo",
|
||||
_1btc: "purple",
|
||||
_10btc: "violet",
|
||||
_100btc: "fuchsia",
|
||||
_1kBtc: "pink",
|
||||
_10kBtc: "red",
|
||||
_100kBtc: "orange",
|
||||
};
|
||||
|
||||
/** @type {Readonly<Record<string, ColorName>>} */
|
||||
export const amountRangeColors = {
|
||||
_0sats: "red",
|
||||
_1satTo10sats: "orange",
|
||||
_10satsTo100sats: "yellow",
|
||||
_100satsTo1kSats: "lime",
|
||||
_1kSatsTo10kSats: "green",
|
||||
_10kSatsTo100kSats: "cyan",
|
||||
_100kSatsTo1mSats: "blue",
|
||||
_1mSatsTo10mSats: "indigo",
|
||||
_10mSatsTo1btc: "purple",
|
||||
_1btcTo10btc: "violet",
|
||||
_10btcTo100btc: "fuchsia",
|
||||
_100btcTo1kBtc: "pink",
|
||||
_1kBtcTo10kBtc: "red",
|
||||
_10kBtcTo100kBtc: "orange",
|
||||
_100kBtcOrMore: "yellow",
|
||||
};
|
||||
|
||||
/** @type {Readonly<Record<string, ColorName>>} */
|
||||
export const spendableTypeColors = {
|
||||
p2pk65: "red",
|
||||
p2pk33: "orange",
|
||||
p2pkh: "yellow",
|
||||
p2ms: "lime",
|
||||
p2sh: "green",
|
||||
p2wpkh: "teal",
|
||||
p2wsh: "blue",
|
||||
p2tr: "indigo",
|
||||
p2a: "purple",
|
||||
unknown: "violet",
|
||||
empty: "fuchsia",
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
// Re-export all color mappings
|
||||
export {
|
||||
termColors,
|
||||
maxAgeColors,
|
||||
minAgeColors,
|
||||
ageRangeColors,
|
||||
epochColors,
|
||||
geAmountColors,
|
||||
ltAmountColors,
|
||||
amountRangeColors,
|
||||
spendableTypeColors,
|
||||
} from "./cohorts.js";
|
||||
|
||||
export { averageColors, dcaColors } from "./misc.js";
|
||||
@@ -0,0 +1,42 @@
|
||||
/** Miscellaneous color mappings for DCA and averages */
|
||||
|
||||
/**
|
||||
* Moving average period colors
|
||||
* Format: [periodId, days, colorName]
|
||||
* @type {readonly [string, number, ColorName][]}
|
||||
*/
|
||||
export const averageColors = [
|
||||
["1w", 7, "red"],
|
||||
["8d", 8, "orange"],
|
||||
["13d", 13, "amber"],
|
||||
["21d", 21, "yellow"],
|
||||
["1m", 30, "lime"],
|
||||
["34d", 34, "green"],
|
||||
["55d", 55, "emerald"],
|
||||
["89d", 89, "teal"],
|
||||
["144d", 144, "cyan"],
|
||||
["200d", 200, "sky"],
|
||||
["1y", 365, "blue"],
|
||||
["2y", 730, "indigo"],
|
||||
["200w", 1400, "violet"],
|
||||
["4y", 1460, "purple"],
|
||||
];
|
||||
|
||||
/**
|
||||
* DCA class colors by year
|
||||
* Format: [year, colorName, defaultActive]
|
||||
* @type {readonly [number, ColorName, boolean][]}
|
||||
*/
|
||||
export const dcaColors = [
|
||||
[2015, "pink", false],
|
||||
[2016, "red", false],
|
||||
[2017, "orange", true],
|
||||
[2018, "yellow", true],
|
||||
[2019, "green", true],
|
||||
[2020, "teal", true],
|
||||
[2021, "sky", true],
|
||||
[2022, "blue", true],
|
||||
[2023, "purple", true],
|
||||
[2024, "fuchsia", true],
|
||||
[2025, "pink", true],
|
||||
];
|
||||
@@ -0,0 +1,141 @@
|
||||
/** Constant helpers for creating price lines and reference lines */
|
||||
|
||||
/**
|
||||
* Get constant pattern by number dynamically from tree
|
||||
* Examples: 0 → constant0, 38.2 → constant382, -1 → constantMinus1
|
||||
* @param {BrkClient["tree"]["computed"]["constants"]} constants
|
||||
* @param {number} num
|
||||
* @returns {Constant0Pattern<any>}
|
||||
*/
|
||||
export function getConstant(constants, num) {
|
||||
const key =
|
||||
num >= 0
|
||||
? `constant${String(num).replace(".", "")}`
|
||||
: `constantMinus${Math.abs(num)}`;
|
||||
const constant = /** @type {Constant0Pattern<any> | undefined} */ (
|
||||
/** @type {Record<string, Constant0Pattern<any>>} */ (constants)[key]
|
||||
);
|
||||
if (!constant) throw new Error(`Unknown constant: ${num} (key: ${key})`);
|
||||
return constant;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flatten a Constant0Pattern into a simple MetricAccessor
|
||||
* Constant0Pattern has { dateindex: { by: {...} }, height: { by: {...} }, ... }
|
||||
* This flattens it to { by: { dateindex: MetricNode, height: MetricNode, ... } }
|
||||
* @param {Constant0Pattern<any>} pattern
|
||||
* @returns {MetricAccessor<any>}
|
||||
*/
|
||||
export function flattenConstant(pattern) {
|
||||
return {
|
||||
by: {
|
||||
dateindex: pattern.dateindex.by.dateindex,
|
||||
decadeindex: pattern.decadeindex.by.decadeindex,
|
||||
height: pattern.height.by.height,
|
||||
monthindex: pattern.monthindex.by.monthindex,
|
||||
quarterindex: pattern.quarterindex.by.quarterindex,
|
||||
semesterindex: pattern.semesterindex.by.semesterindex,
|
||||
weekindex: pattern.weekindex.by.weekindex,
|
||||
yearindex: pattern.yearindex.by.yearindex,
|
||||
},
|
||||
indexes() {
|
||||
return /** @type {Index[]} */ (Object.keys(this.by));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a price line series (horizontal reference line)
|
||||
* @param {Object} args
|
||||
* @param {BrkClient["tree"]["computed"]["constants"]} args.constants
|
||||
* @param {Colors} args.colors
|
||||
* @param {number} [args.number]
|
||||
* @param {string} [args.name]
|
||||
* @param {boolean} [args.defaultActive]
|
||||
* @param {number} [args.lineStyle]
|
||||
* @param {Color} [args.color]
|
||||
* @param {Unit} args.unit
|
||||
* @returns {FetchedLineSeriesBlueprint}
|
||||
*/
|
||||
export function createPriceLine({
|
||||
constants,
|
||||
colors,
|
||||
number = 0,
|
||||
unit,
|
||||
defaultActive,
|
||||
color,
|
||||
name,
|
||||
lineStyle,
|
||||
}) {
|
||||
return {
|
||||
metric: flattenConstant(getConstant(constants, number)),
|
||||
title: name ?? `${number}`,
|
||||
unit,
|
||||
defaultActive,
|
||||
color: color ?? colors.gray,
|
||||
options: {
|
||||
lineStyle: lineStyle ?? 4,
|
||||
lastValueVisible: false,
|
||||
crosshairMarkerVisible: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create multiple price lines from an array of numbers
|
||||
* @param {Object} args
|
||||
* @param {BrkClient["tree"]["computed"]["constants"]} args.constants
|
||||
* @param {Colors} args.colors
|
||||
* @param {number[]} args.numbers
|
||||
* @param {Unit} args.unit
|
||||
* @returns {FetchedLineSeriesBlueprint[]}
|
||||
*/
|
||||
export function createPriceLines({ constants, colors, numbers, unit }) {
|
||||
return numbers.map((number) => ({
|
||||
metric: flattenConstant(getConstant(constants, number)),
|
||||
title: `${number}`,
|
||||
unit,
|
||||
defaultActive: !number,
|
||||
color: colors.gray,
|
||||
options: {
|
||||
lineStyle: 4,
|
||||
lastValueVisible: false,
|
||||
crosshairMarkerVisible: false,
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a constant line series
|
||||
* @param {Object} args
|
||||
* @param {Colors} args.colors
|
||||
* @param {Constant0Pattern<any>} args.constant
|
||||
* @param {string} args.name
|
||||
* @param {Unit} args.unit
|
||||
* @param {Color} [args.color]
|
||||
* @param {number} [args.lineStyle]
|
||||
* @param {boolean} [args.defaultActive]
|
||||
* @returns {FetchedLineSeriesBlueprint}
|
||||
*/
|
||||
export function line({
|
||||
colors,
|
||||
constant,
|
||||
name,
|
||||
unit,
|
||||
color,
|
||||
lineStyle,
|
||||
defaultActive,
|
||||
}) {
|
||||
return {
|
||||
metric: flattenConstant(constant),
|
||||
title: name,
|
||||
unit,
|
||||
defaultActive,
|
||||
color: color ?? colors.gray,
|
||||
options: {
|
||||
lineStyle: lineStyle ?? 4,
|
||||
lastValueVisible: false,
|
||||
crosshairMarkerVisible: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { s, fromBlockCount, fromBitcoin, fromBlockSize } from "./series.js";
|
||||
import {
|
||||
getConstant,
|
||||
flattenConstant,
|
||||
createPriceLine,
|
||||
createPriceLines,
|
||||
line,
|
||||
} from "./constants.js";
|
||||
|
||||
/**
|
||||
* Create a context object with all dependencies for building partial options
|
||||
* @param {Object} args
|
||||
* @param {Colors} args.colors
|
||||
* @param {BrkClient} args.brk
|
||||
* @returns {PartialContext}
|
||||
*/
|
||||
export function createContext({ colors, brk }) {
|
||||
const constants = brk.tree.computed.constants;
|
||||
const constant100 = flattenConstant(constants.constant100);
|
||||
|
||||
return {
|
||||
colors,
|
||||
brk,
|
||||
constants,
|
||||
constant100,
|
||||
|
||||
// Series helpers
|
||||
s,
|
||||
fromBlockCount: (pattern, title, color) =>
|
||||
fromBlockCount(colors, pattern, title, color),
|
||||
fromBitcoin: (pattern, title, color) =>
|
||||
fromBitcoin(colors, pattern, title, color),
|
||||
fromBlockSize: (pattern, title, color) =>
|
||||
fromBlockSize(colors, pattern, title, color),
|
||||
|
||||
// Constant helpers
|
||||
getConstant: (num) => getConstant(constants, num),
|
||||
flattenConstant,
|
||||
createPriceLine: (args) => createPriceLine({ constants, colors, ...args }),
|
||||
createPriceLines: (args) =>
|
||||
createPriceLines({ constants, colors, ...args }),
|
||||
line: (args) => line({ colors, ...args }),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,357 @@
|
||||
/** Partial options - Main entry point */
|
||||
|
||||
import { localhost } from "../../utils/env.js";
|
||||
import { createContext } from "./context.js";
|
||||
import {
|
||||
buildCohortData,
|
||||
createUtxoCohortFolder,
|
||||
createAddressCohortFolder,
|
||||
} from "./cohorts/index.js";
|
||||
import { createMarketSection } from "./market.js";
|
||||
import { createChainSection } from "./chain.js";
|
||||
import { createCointimeSection } from "./cointime.js";
|
||||
|
||||
// Re-export types for external consumers
|
||||
export * from "./types.js";
|
||||
|
||||
/**
|
||||
* Create partial options tree
|
||||
* @param {Object} args
|
||||
* @param {Colors} args.colors
|
||||
* @param {BrkClient} args.brk
|
||||
* @returns {PartialOptionsTree}
|
||||
*/
|
||||
export function createPartialOptions({ colors, brk }) {
|
||||
// Create context with all helpers
|
||||
const ctx = createContext({ colors, brk });
|
||||
|
||||
// Build cohort data
|
||||
const {
|
||||
cohortAll,
|
||||
cohortAllForComparison,
|
||||
terms,
|
||||
upToDate,
|
||||
fromDate,
|
||||
dateRange,
|
||||
epoch,
|
||||
utxosAboveAmount,
|
||||
addressesAboveAmount,
|
||||
utxosUnderAmount,
|
||||
addressesUnderAmount,
|
||||
utxosAmountRanges,
|
||||
addressesAmountRanges,
|
||||
type,
|
||||
} = buildCohortData(colors, brk);
|
||||
|
||||
// Helper to map UTXO cohorts
|
||||
const mapUtxoCohorts = (/** @type {any} */ cohort) => createUtxoCohortFolder(ctx, cohort);
|
||||
|
||||
// Helper to map Address cohorts
|
||||
const mapAddressCohorts = (/** @type {any} */ cohort) => createAddressCohortFolder(ctx, cohort);
|
||||
|
||||
return [
|
||||
// Debug explorer (localhost only)
|
||||
...(localhost
|
||||
? [
|
||||
{
|
||||
kind: /** @type {const} */ ("explorer"),
|
||||
name: "Explorer",
|
||||
title: "Debug explorer",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
|
||||
// Charts section
|
||||
{
|
||||
name: "Charts",
|
||||
tree: [
|
||||
// Market section
|
||||
createMarketSection(ctx),
|
||||
|
||||
// Chain section
|
||||
createChainSection(ctx),
|
||||
|
||||
// Cohorts section
|
||||
{
|
||||
name: "Cohorts",
|
||||
tree: [
|
||||
// All UTXOs
|
||||
createUtxoCohortFolder(ctx, cohortAll),
|
||||
|
||||
// Terms (STH/LTH)
|
||||
{
|
||||
name: "terms",
|
||||
tree: [
|
||||
createUtxoCohortFolder(ctx, {
|
||||
name: "Compare",
|
||||
title: "UTXOs Term",
|
||||
list: [...terms, cohortAllForComparison],
|
||||
}),
|
||||
...terms.map(mapUtxoCohorts),
|
||||
],
|
||||
},
|
||||
|
||||
// Epochs
|
||||
{
|
||||
name: "Epochs",
|
||||
tree: [
|
||||
createUtxoCohortFolder(ctx, {
|
||||
name: "Compare",
|
||||
title: "Epoch",
|
||||
list: [...epoch, cohortAllForComparison],
|
||||
}),
|
||||
...epoch.map(mapUtxoCohorts),
|
||||
],
|
||||
},
|
||||
|
||||
// Types
|
||||
{
|
||||
name: "types",
|
||||
tree: [
|
||||
createUtxoCohortFolder(ctx, {
|
||||
name: "Compare",
|
||||
title: "Type",
|
||||
list: [...type, cohortAllForComparison],
|
||||
}),
|
||||
...type.map(mapUtxoCohorts),
|
||||
],
|
||||
},
|
||||
|
||||
// UTXOs Up to age
|
||||
{
|
||||
name: "UTXOs Up to age",
|
||||
tree: [
|
||||
createUtxoCohortFolder(ctx, {
|
||||
name: "Compare",
|
||||
title: "UTXOs Up To Age",
|
||||
list: [...upToDate, cohortAllForComparison],
|
||||
}),
|
||||
...upToDate.map(mapUtxoCohorts),
|
||||
],
|
||||
},
|
||||
|
||||
// UTXOs from age
|
||||
{
|
||||
name: "UTXOs from age",
|
||||
tree: [
|
||||
createUtxoCohortFolder(ctx, {
|
||||
name: "Compare",
|
||||
title: "UTXOs from age",
|
||||
list: [...fromDate, cohortAllForComparison],
|
||||
}),
|
||||
...fromDate.map(mapUtxoCohorts),
|
||||
],
|
||||
},
|
||||
|
||||
// UTXOs age ranges
|
||||
{
|
||||
name: "UTXOs age Ranges",
|
||||
tree: [
|
||||
createUtxoCohortFolder(ctx, {
|
||||
name: "Compare",
|
||||
title: "UTXOs Age Range",
|
||||
list: [...dateRange, cohortAllForComparison],
|
||||
}),
|
||||
...dateRange.map(mapUtxoCohorts),
|
||||
],
|
||||
},
|
||||
|
||||
// UTXOs under amounts
|
||||
{
|
||||
name: "UTXOs under amounts",
|
||||
tree: [
|
||||
createUtxoCohortFolder(ctx, {
|
||||
name: "Compare",
|
||||
title: "UTXOs under amount",
|
||||
list: [...utxosUnderAmount, cohortAllForComparison],
|
||||
}),
|
||||
...utxosUnderAmount.map(mapUtxoCohorts),
|
||||
],
|
||||
},
|
||||
|
||||
// UTXOs above amounts
|
||||
{
|
||||
name: "UTXOs Above Amounts",
|
||||
tree: [
|
||||
createUtxoCohortFolder(ctx, {
|
||||
name: "Compare",
|
||||
title: "UTXOs Above Amount",
|
||||
list: [...utxosAboveAmount, cohortAllForComparison],
|
||||
}),
|
||||
...utxosAboveAmount.map(mapUtxoCohorts),
|
||||
],
|
||||
},
|
||||
|
||||
// UTXOs between amounts
|
||||
{
|
||||
name: "UTXOs between amounts",
|
||||
tree: [
|
||||
createUtxoCohortFolder(ctx, {
|
||||
name: "Compare",
|
||||
title: "UTXOs between amounts",
|
||||
list: [...utxosAmountRanges, cohortAllForComparison],
|
||||
}),
|
||||
...utxosAmountRanges.map(mapUtxoCohorts),
|
||||
],
|
||||
},
|
||||
|
||||
// Addresses under amount (TYPE SAFE - uses createAddressCohortFolder!)
|
||||
{
|
||||
name: "Addresses under amount",
|
||||
tree: [
|
||||
createAddressCohortFolder(ctx, {
|
||||
name: "Compare",
|
||||
title: "Addresses under Amount",
|
||||
list: addressesUnderAmount,
|
||||
}),
|
||||
...addressesUnderAmount.map(mapAddressCohorts),
|
||||
],
|
||||
},
|
||||
|
||||
// Addresses above amount (TYPE SAFE - uses createAddressCohortFolder!)
|
||||
{
|
||||
name: "Addresses above amount",
|
||||
tree: [
|
||||
createAddressCohortFolder(ctx, {
|
||||
name: "Compare",
|
||||
title: "Addresses above amount",
|
||||
list: addressesAboveAmount,
|
||||
}),
|
||||
...addressesAboveAmount.map(mapAddressCohorts),
|
||||
],
|
||||
},
|
||||
|
||||
// Addresses between amounts (TYPE SAFE - uses createAddressCohortFolder!)
|
||||
{
|
||||
name: "Addresses between amounts",
|
||||
tree: [
|
||||
createAddressCohortFolder(ctx, {
|
||||
name: "Compare",
|
||||
title: "Addresses between amounts",
|
||||
list: addressesAmountRanges,
|
||||
}),
|
||||
...addressesAmountRanges.map(mapAddressCohorts),
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// Cointime section
|
||||
createCointimeSection(ctx),
|
||||
],
|
||||
},
|
||||
|
||||
// Table section
|
||||
{
|
||||
kind: /** @type {const} */ ("table"),
|
||||
title: "Table",
|
||||
name: "Table",
|
||||
},
|
||||
|
||||
// Simulations section
|
||||
{
|
||||
name: "Simulations",
|
||||
tree: [
|
||||
{
|
||||
kind: /** @type {const} */ ("simulation"),
|
||||
name: "Save In Bitcoin",
|
||||
title: "Save In Bitcoin",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// Tools section
|
||||
{
|
||||
name: "Tools",
|
||||
tree: [
|
||||
{
|
||||
name: "Documentation",
|
||||
tree: [
|
||||
{
|
||||
name: "API",
|
||||
url: () => "/api",
|
||||
title: "API documentation",
|
||||
},
|
||||
{
|
||||
name: "MCP",
|
||||
url: () =>
|
||||
"https://github.com/bitcoinresearchkit/brk/blob/main/crates/brk_mcp/README.md#brk_mcp",
|
||||
title: "Model Context Protocol documentation",
|
||||
},
|
||||
{
|
||||
name: "Crate",
|
||||
url: () => "/crate",
|
||||
title: "View on crates.io",
|
||||
},
|
||||
{
|
||||
name: "Source",
|
||||
url: () => "/github",
|
||||
title: "Source code and issues",
|
||||
},
|
||||
{
|
||||
name: "Changelog",
|
||||
url: () => "/changelog",
|
||||
title: "Release notes and changelog",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Hosting",
|
||||
tree: [
|
||||
{
|
||||
name: "Status",
|
||||
url: () => "/status",
|
||||
title: "Service status and uptime",
|
||||
},
|
||||
{
|
||||
name: "Self-host",
|
||||
url: () => "/install",
|
||||
title: "Install and run yourself",
|
||||
},
|
||||
{
|
||||
name: "Service",
|
||||
url: () => "/service",
|
||||
title: "Hosted service offering",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Community",
|
||||
tree: [
|
||||
{
|
||||
name: "Discord",
|
||||
url: () => "/discord",
|
||||
title: "Join the Discord server",
|
||||
},
|
||||
{
|
||||
name: "GitHub",
|
||||
url: () => "/github",
|
||||
title: "Source code and issues",
|
||||
},
|
||||
{
|
||||
name: "Nostr",
|
||||
url: () => "/nostr",
|
||||
title: "Follow on Nostr",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// Donate
|
||||
{
|
||||
name: "Donate",
|
||||
qrcode: true,
|
||||
url: () => "bitcoin:bc1q098zsm89m7kgyze338vfejhpdt92ua9p3peuve",
|
||||
title: "Bitcoin address for donations",
|
||||
},
|
||||
|
||||
// Share
|
||||
{
|
||||
name: "Share",
|
||||
qrcode: true,
|
||||
url: () => window.location.href,
|
||||
title: "Share",
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,480 @@
|
||||
/** Market section builder - typed tree-based patterns */
|
||||
|
||||
/**
|
||||
* Convert period ID to readable name
|
||||
* @param {string} id
|
||||
* @param {boolean} [compoundAdjective]
|
||||
*/
|
||||
function periodIdToName(id, compoundAdjective) {
|
||||
const suffix = compoundAdjective || parseInt(id) === 1 ? "" : "s";
|
||||
return id
|
||||
.replace("d", ` day${suffix}`)
|
||||
.replace("w", ` week${suffix}`)
|
||||
.replace("m", ` month${suffix}`)
|
||||
.replace("y", ` year${suffix}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create price with ratio options (for moving averages)
|
||||
* @param {PartialContext} ctx
|
||||
* @param {Object} args
|
||||
* @param {string} args.name
|
||||
* @param {string} args.title
|
||||
* @param {string} args.legend
|
||||
* @param {EmaRatioPattern} args.ratio
|
||||
* @param {Color} [args.color]
|
||||
* @returns {PartialOptionsTree}
|
||||
*/
|
||||
function createPriceWithRatioOptions(ctx, { name, title, legend, ratio, color }) {
|
||||
const { s, colors, createPriceLine } = ctx;
|
||||
const priceMetric = ratio.price;
|
||||
|
||||
// Percentile USD mappings
|
||||
const percentileUsdMap = [
|
||||
{ name: "pct99", prop: ratio.ratioPct99Usd, color: colors.rose },
|
||||
{ name: "pct98", prop: ratio.ratioPct98Usd, color: colors.pink },
|
||||
{ name: "pct95", prop: ratio.ratioPct95Usd, color: colors.fuchsia },
|
||||
{ name: "pct5", prop: ratio.ratioPct5Usd, color: colors.cyan },
|
||||
{ name: "pct2", prop: ratio.ratioPct2Usd, color: colors.sky },
|
||||
{ name: "pct1", prop: ratio.ratioPct1Usd, color: colors.blue },
|
||||
];
|
||||
|
||||
// Percentile ratio mappings
|
||||
const percentileMap = [
|
||||
{ name: "pct99", prop: ratio.ratioPct99, color: colors.rose },
|
||||
{ name: "pct98", prop: ratio.ratioPct98, color: colors.pink },
|
||||
{ name: "pct95", prop: ratio.ratioPct95, color: colors.fuchsia },
|
||||
{ name: "pct5", prop: ratio.ratioPct5, color: colors.cyan },
|
||||
{ name: "pct2", prop: ratio.ratioPct2, color: colors.sky },
|
||||
{ name: "pct1", prop: ratio.ratioPct1, color: colors.blue },
|
||||
];
|
||||
|
||||
// SD patterns by window
|
||||
const sdPatterns = [
|
||||
{ nameAddon: "all", titleAddon: "", sd: ratio.ratioSd },
|
||||
{ nameAddon: "4y", titleAddon: "4y", sd: ratio.ratio4ySd },
|
||||
{ nameAddon: "2y", titleAddon: "2y", sd: ratio.ratio2ySd },
|
||||
{ nameAddon: "1y", titleAddon: "1y", sd: ratio.ratio1ySd },
|
||||
];
|
||||
|
||||
/** @param {Ratio1ySdPattern} sd */
|
||||
const getSdBands = (sd) => [
|
||||
{ name: "0σ", prop: sd._0sdUsd, color: colors.lime },
|
||||
{ name: "+0.5σ", prop: sd.p05sdUsd, color: colors.yellow },
|
||||
{ name: "+1σ", prop: sd.p1sdUsd, color: colors.amber },
|
||||
{ name: "+1.5σ", prop: sd.p15sdUsd, color: colors.orange },
|
||||
{ name: "+2σ", prop: sd.p2sdUsd, color: colors.red },
|
||||
{ name: "+2.5σ", prop: sd.p25sdUsd, color: colors.rose },
|
||||
{ name: "+3σ", prop: sd.p3sd, color: colors.pink },
|
||||
{ name: "−0.5σ", prop: sd.m05sdUsd, color: colors.teal },
|
||||
{ name: "−1σ", prop: sd.m1sdUsd, color: colors.cyan },
|
||||
{ name: "−1.5σ", prop: sd.m15sdUsd, color: colors.sky },
|
||||
{ name: "−2σ", prop: sd.m2sdUsd, color: colors.blue },
|
||||
{ name: "−2.5σ", prop: sd.m25sdUsd, color: colors.indigo },
|
||||
{ name: "−3σ", prop: sd.m3sd, color: colors.violet },
|
||||
];
|
||||
|
||||
return [
|
||||
{
|
||||
name: "price",
|
||||
title,
|
||||
top: [s({ metric: priceMetric, name: legend, color, unit: "usd" })],
|
||||
},
|
||||
{
|
||||
name: "Ratio",
|
||||
title: `${title} Ratio`,
|
||||
top: [
|
||||
s({ metric: priceMetric, name: legend, color, unit: "usd" }),
|
||||
...percentileUsdMap.map(({ name: pctName, prop, color: pctColor }) =>
|
||||
s({
|
||||
metric: prop,
|
||||
name: pctName,
|
||||
color: pctColor,
|
||||
defaultActive: false,
|
||||
unit: "usd",
|
||||
options: { lineStyle: 1 },
|
||||
}),
|
||||
),
|
||||
],
|
||||
bottom: [
|
||||
s({ metric: ratio.ratio, name: "ratio", color, unit: "ratio" }),
|
||||
s({ metric: ratio.ratio1wSma, name: "1w sma", color: colors.lime, unit: "ratio" }),
|
||||
s({ metric: ratio.ratio1mSma, name: "1m sma", color: colors.teal, unit: "ratio" }),
|
||||
s({ metric: ratio.ratio1ySd.sma, name: "1y sma", color: colors.sky, unit: "ratio" }),
|
||||
s({ metric: ratio.ratio2ySd.sma, name: "2y sma", color: colors.indigo, unit: "ratio" }),
|
||||
s({ metric: ratio.ratio4ySd.sma, name: "4y sma", color: colors.purple, unit: "ratio" }),
|
||||
s({ metric: ratio.ratioSd.sma, name: "all sma", color: colors.rose, unit: "ratio" }),
|
||||
...percentileMap.map(({ name: pctName, prop, color: pctColor }) =>
|
||||
s({
|
||||
metric: prop,
|
||||
name: pctName,
|
||||
color: pctColor,
|
||||
defaultActive: false,
|
||||
unit: "ratio",
|
||||
options: { lineStyle: 1 },
|
||||
}),
|
||||
),
|
||||
createPriceLine({ unit: "ratio", number: 1 }),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "ZScores",
|
||||
tree: sdPatterns.map(({ nameAddon, titleAddon, sd }) => ({
|
||||
name: nameAddon,
|
||||
title: `${title} ${titleAddon} Z-Score`,
|
||||
top: getSdBands(sd).map(({ name: bandName, prop, color: bandColor }) =>
|
||||
s({ metric: prop, name: bandName, color: bandColor, unit: "usd" }),
|
||||
),
|
||||
bottom: [
|
||||
s({ metric: sd.zscore, name: "zscore", color, unit: "sd" }),
|
||||
createPriceLine({ unit: "sd", number: 3 }),
|
||||
createPriceLine({ unit: "sd", number: 2 }),
|
||||
createPriceLine({ unit: "sd", number: 1 }),
|
||||
createPriceLine({ unit: "sd", number: 0 }),
|
||||
createPriceLine({ unit: "sd", number: -1 }),
|
||||
createPriceLine({ unit: "sd", number: -2 }),
|
||||
createPriceLine({ unit: "sd", number: -3 }),
|
||||
],
|
||||
})),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Build averages data array from market patterns
|
||||
* @param {Colors} colors
|
||||
* @param {MarketMovingAverage} ma
|
||||
*/
|
||||
function buildAverages(colors, ma) {
|
||||
return /** @type {const} */ ([
|
||||
["1w", 7, "red", ma.price1wSma, ma.price1wEma],
|
||||
["8d", 8, "orange", ma.price8dSma, ma.price8dEma],
|
||||
["13d", 13, "amber", ma.price13dSma, ma.price13dEma],
|
||||
["21d", 21, "yellow", ma.price21dSma, ma.price21dEma],
|
||||
["1m", 30, "lime", ma.price1mSma, ma.price1mEma],
|
||||
["34d", 34, "green", ma.price34dSma, ma.price34dEma],
|
||||
["55d", 55, "emerald", ma.price55dSma, ma.price55dEma],
|
||||
["89d", 89, "teal", ma.price89dSma, ma.price89dEma],
|
||||
["144d", 144, "cyan", ma.price144dSma, ma.price144dEma],
|
||||
["200d", 200, "sky", ma.price200dSma, ma.price200dEma],
|
||||
["1y", 365, "blue", ma.price1ySma, ma.price1yEma],
|
||||
["2y", 730, "indigo", ma.price2ySma, ma.price2yEma],
|
||||
["200w", 1400, "violet", ma.price200wSma, ma.price200wEma],
|
||||
["4y", 1460, "purple", ma.price4ySma, ma.price4yEma],
|
||||
]).map(([id, days, colorKey, sma, ema]) => ({
|
||||
id,
|
||||
name: periodIdToName(id, true),
|
||||
days,
|
||||
color: colors[colorKey],
|
||||
sma,
|
||||
ema,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Build DCA classes data array
|
||||
* @param {Colors} colors
|
||||
* @param {MarketDca} dca
|
||||
*/
|
||||
function buildDcaClasses(colors, dca) {
|
||||
return /** @type {const} */ ([
|
||||
[2015, "pink", false, dca.dcaClass2015AvgPrice, dca.dcaClass2015Returns, dca.dcaClass2015Stack],
|
||||
[2016, "red", false, dca.dcaClass2016AvgPrice, dca.dcaClass2016Returns, dca.dcaClass2016Stack],
|
||||
[2017, "orange", true, dca.dcaClass2017AvgPrice, dca.dcaClass2017Returns, dca.dcaClass2017Stack],
|
||||
[2018, "yellow", true, dca.dcaClass2018AvgPrice, dca.dcaClass2018Returns, dca.dcaClass2018Stack],
|
||||
[2019, "green", true, dca.dcaClass2019AvgPrice, dca.dcaClass2019Returns, dca.dcaClass2019Stack],
|
||||
[2020, "teal", true, dca.dcaClass2020AvgPrice, dca.dcaClass2020Returns, dca.dcaClass2020Stack],
|
||||
[2021, "sky", true, dca.dcaClass2021AvgPrice, dca.dcaClass2021Returns, dca.dcaClass2021Stack],
|
||||
[2022, "blue", true, dca.dcaClass2022AvgPrice, dca.dcaClass2022Returns, dca.dcaClass2022Stack],
|
||||
[2023, "purple", true, dca.dcaClass2023AvgPrice, dca.dcaClass2023Returns, dca.dcaClass2023Stack],
|
||||
[2024, "fuchsia", true, dca.dcaClass2024AvgPrice, dca.dcaClass2024Returns, dca.dcaClass2024Stack],
|
||||
[2025, "pink", true, dca.dcaClass2025AvgPrice, dca.dcaClass2025Returns, dca.dcaClass2025Stack],
|
||||
]).map(([year, colorKey, defaultActive, avgPrice, returns, stack]) => ({
|
||||
year,
|
||||
color: colors[colorKey],
|
||||
defaultActive,
|
||||
avgPrice,
|
||||
returns,
|
||||
stack,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create Market section
|
||||
* @param {PartialContext} ctx
|
||||
* @returns {PartialOptionsGroup}
|
||||
*/
|
||||
export function createMarketSection(ctx) {
|
||||
const { colors, brk, s, createPriceLine } = ctx;
|
||||
const { market, supply } = brk.tree.computed;
|
||||
const { movingAverage, ath, returns, volatility, range, dca, lookback } = market;
|
||||
|
||||
const averages = buildAverages(colors, movingAverage);
|
||||
const dcaClasses = buildDcaClasses(colors, dca);
|
||||
|
||||
return {
|
||||
name: "Market",
|
||||
tree: [
|
||||
// Price (empty chart, shows candlesticks by default)
|
||||
{
|
||||
name: "Price",
|
||||
title: "Bitcoin Price",
|
||||
},
|
||||
|
||||
// Capitalization
|
||||
{
|
||||
name: "Capitalization",
|
||||
title: "Market Capitalization",
|
||||
bottom: [s({ metric: supply.marketCap.indexes, name: "Capitalization", unit: "usd" })],
|
||||
},
|
||||
|
||||
// All Time High
|
||||
{
|
||||
name: "All Time High",
|
||||
title: "All Time High",
|
||||
top: [s({ metric: ath.priceAth, name: "ath", unit: "usd" })],
|
||||
bottom: [
|
||||
s({ metric: ath.priceDrawdown, name: "Drawdown", color: colors.red, unit: "percentage" }),
|
||||
s({ metric: ath.daysSincePriceAth, name: "since", unit: "days" }),
|
||||
s({ metric: ath.maxDaysBetweenPriceAths, name: "Max", color: colors.red, unit: "days" }),
|
||||
s({ metric: ath.maxYearsBetweenPriceAths, name: "Max", color: colors.red, unit: "years" }),
|
||||
],
|
||||
},
|
||||
|
||||
// Averages
|
||||
{
|
||||
name: "Averages",
|
||||
tree: [
|
||||
{ nameAddon: "Simple", metricAddon: /** @type {const} */ ("sma") },
|
||||
{ nameAddon: "Exponential", metricAddon: /** @type {const} */ ("ema") },
|
||||
].map(({ nameAddon, metricAddon }) => ({
|
||||
name: nameAddon,
|
||||
tree: [
|
||||
{
|
||||
name: "Compare",
|
||||
title: `Market Price ${nameAddon} Moving Averages`,
|
||||
top: averages.map(({ id, color, sma, ema }) =>
|
||||
s({
|
||||
metric: (metricAddon === "sma" ? sma : ema).price,
|
||||
name: id,
|
||||
color,
|
||||
unit: "usd",
|
||||
}),
|
||||
),
|
||||
},
|
||||
...averages.map(({ name, color, sma, ema }) => ({
|
||||
name,
|
||||
tree: createPriceWithRatioOptions(ctx, {
|
||||
ratio: metricAddon === "sma" ? sma : ema,
|
||||
name,
|
||||
title: `${name} Market Price ${nameAddon} Moving Average`,
|
||||
legend: "average",
|
||||
color,
|
||||
}),
|
||||
})),
|
||||
],
|
||||
})),
|
||||
},
|
||||
|
||||
// Performance
|
||||
{
|
||||
name: "Performance",
|
||||
tree: /** @type {const} */ ([
|
||||
["1d", returns._1dPriceReturns, undefined],
|
||||
["1w", returns._1wPriceReturns, undefined],
|
||||
["1m", returns._1mPriceReturns, undefined],
|
||||
["3m", returns._3mPriceReturns, undefined],
|
||||
["6m", returns._6mPriceReturns, undefined],
|
||||
["1y", returns._1yPriceReturns, undefined],
|
||||
["2y", returns._2yPriceReturns, returns._2yCagr],
|
||||
["3y", returns._3yPriceReturns, returns._3yCagr],
|
||||
["4y", returns._4yPriceReturns, returns._4yCagr],
|
||||
["5y", returns._5yPriceReturns, returns._5yCagr],
|
||||
["6y", returns._6yPriceReturns, returns._6yCagr],
|
||||
["8y", returns._8yPriceReturns, returns._8yCagr],
|
||||
["10y", returns._10yPriceReturns, returns._10yCagr],
|
||||
]).map(([id, priceReturns, cagr]) => {
|
||||
const name = periodIdToName(id, true);
|
||||
return {
|
||||
name,
|
||||
title: `${name} Performance`,
|
||||
bottom: [
|
||||
/** @type {AnyFetchedSeriesBlueprint} */ ({
|
||||
metric: priceReturns,
|
||||
title: "total",
|
||||
type: "Baseline",
|
||||
unit: "percentage",
|
||||
}),
|
||||
...(cagr
|
||||
? [
|
||||
/** @type {AnyFetchedSeriesBlueprint} */ ({
|
||||
metric: cagr,
|
||||
title: "cagr",
|
||||
type: "Baseline",
|
||||
colors: [colors.lime, colors.pink],
|
||||
unit: "percentage",
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
createPriceLine({ unit: "percentage" }),
|
||||
],
|
||||
};
|
||||
}),
|
||||
},
|
||||
|
||||
// Indicators
|
||||
{
|
||||
name: "Indicators",
|
||||
tree: [
|
||||
// Volatility
|
||||
{
|
||||
name: "Volatility",
|
||||
title: "Bitcoin Price Volatility Index",
|
||||
bottom: [
|
||||
s({ metric: volatility.price1wVolatility, name: "1w", color: colors.red, unit: "percentage" }),
|
||||
s({ metric: volatility.price1mVolatility, name: "1m", color: colors.orange, unit: "percentage" }),
|
||||
s({ metric: volatility.price1yVolatility, name: "1y", color: colors.lime, unit: "percentage" }),
|
||||
],
|
||||
},
|
||||
|
||||
// MinMax
|
||||
{
|
||||
name: "MinMax",
|
||||
tree: [
|
||||
{ id: "1w", title: "1 Week", min: range.price1wMin, max: range.price1wMax },
|
||||
{ id: "2w", title: "2 Week", min: range.price2wMin, max: range.price2wMax },
|
||||
{ id: "1m", title: "1 Month", min: range.price1mMin, max: range.price1mMax },
|
||||
{ id: "1y", title: "1 Year", min: range.price1yMin, max: range.price1yMax },
|
||||
].map(({ id, title, min, max }) => ({
|
||||
name: id,
|
||||
title: `Bitcoin Price ${title} MinMax Bands`,
|
||||
top: [
|
||||
s({ metric: min, name: "min", color: colors.red, unit: "usd" }),
|
||||
s({ metric: max, name: "max", color: colors.green, unit: "usd" }),
|
||||
],
|
||||
})),
|
||||
},
|
||||
|
||||
// True range
|
||||
{
|
||||
name: "True range",
|
||||
title: "Bitcoin Price True Range",
|
||||
bottom: [s({ metric: range.priceTrueRange, name: "value", color: colors.yellow, unit: "usd" })],
|
||||
},
|
||||
|
||||
// Choppiness
|
||||
{
|
||||
name: "Choppiness",
|
||||
title: "Bitcoin Price Choppiness Index",
|
||||
bottom: [
|
||||
s({ metric: range.price2wChoppinessIndex, name: "2w", color: colors.red, unit: "index" }),
|
||||
createPriceLine({ unit: "index", number: 61.8 }),
|
||||
createPriceLine({ unit: "index", number: 38.2 }),
|
||||
],
|
||||
},
|
||||
|
||||
// Mayer multiple
|
||||
{
|
||||
name: "Mayer multiple",
|
||||
title: "Mayer multiple",
|
||||
top: [
|
||||
s({ metric: movingAverage.price200dSma.price, name: "200d sma", color: colors.yellow, unit: "usd" }),
|
||||
s({ metric: movingAverage.price200dSmaX24, name: "200d sma x2.4", color: colors.green, unit: "usd" }),
|
||||
s({ metric: movingAverage.price200dSmaX08, name: "200d sma x0.8", color: colors.red, unit: "usd" }),
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// Investing
|
||||
{
|
||||
name: "Investing",
|
||||
tree: [
|
||||
// DCA vs Lump sum
|
||||
{
|
||||
name: "DCA vs Lump sum",
|
||||
tree: [
|
||||
.../** @type {const} */ ([
|
||||
["1w", dca._1wDcaAvgPrice, lookback.price1wAgo, dca._1wDcaReturns, returns._1wPriceReturns],
|
||||
["1m", dca._1mDcaAvgPrice, lookback.price1mAgo, dca._1mDcaReturns, returns._1mPriceReturns],
|
||||
["3m", dca._3mDcaAvgPrice, lookback.price3mAgo, dca._3mDcaReturns, returns._3mPriceReturns],
|
||||
["6m", dca._6mDcaAvgPrice, lookback.price6mAgo, dca._6mDcaReturns, returns._6mPriceReturns],
|
||||
["1y", dca._1yDcaAvgPrice, lookback.price1yAgo, dca._1yDcaReturns, returns._1yPriceReturns],
|
||||
]).map(([id, dcaAvgPrice, priceAgo, dcaReturns, priceReturns]) => {
|
||||
const name = periodIdToName(id, true);
|
||||
return {
|
||||
name,
|
||||
tree: [
|
||||
{
|
||||
name: "price",
|
||||
title: `${name} DCA vs Lump Sum (Price)`,
|
||||
top: [
|
||||
s({ metric: dcaAvgPrice, name: "DCA avg", color: colors.green, unit: "usd" }),
|
||||
s({ metric: priceAgo, name: "Lump sum", color: colors.orange, unit: "usd" }),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "returns",
|
||||
title: `${name} DCA vs Lump Sum (Returns)`,
|
||||
bottom: [
|
||||
/** @type {AnyFetchedSeriesBlueprint} */ ({
|
||||
metric: dcaReturns,
|
||||
title: "DCA",
|
||||
type: "Baseline",
|
||||
unit: "percentage",
|
||||
}),
|
||||
/** @type {AnyFetchedSeriesBlueprint} */ ({
|
||||
metric: priceReturns,
|
||||
title: "Lump sum",
|
||||
type: "Baseline",
|
||||
colors: [colors.lime, colors.red],
|
||||
unit: "percentage",
|
||||
}),
|
||||
createPriceLine({ unit: "percentage" }),
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
}),
|
||||
],
|
||||
},
|
||||
|
||||
// DCA classes
|
||||
{
|
||||
name: "DCA classes",
|
||||
tree: [
|
||||
{
|
||||
name: "Average price",
|
||||
title: "DCA Average Price by Year",
|
||||
top: dcaClasses.map(({ year, color, defaultActive, avgPrice }) =>
|
||||
s({ metric: avgPrice, name: `${year}`, color, defaultActive, unit: "usd" }),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "Returns",
|
||||
title: "DCA Returns by Year",
|
||||
bottom: dcaClasses.map(({ year, color, defaultActive, returns }) =>
|
||||
/** @type {AnyFetchedSeriesBlueprint} */ ({
|
||||
metric: returns,
|
||||
title: `${year}`,
|
||||
type: "Baseline",
|
||||
color,
|
||||
defaultActive,
|
||||
unit: "percentage",
|
||||
}),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "Stack",
|
||||
title: "DCA Stack by Year",
|
||||
bottom: dcaClasses.map(({ year, color, defaultActive, stack }) =>
|
||||
s({ metric: stack, name: `${year}`, color, defaultActive, unit: "sats" }),
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
/** Series helpers for creating chart series blueprints */
|
||||
|
||||
/**
|
||||
* Create a single series from a tree accessor
|
||||
* @param {Object} args
|
||||
* @param {MetricAccessor<any>} args.metric - Tree accessor with .by property
|
||||
* @param {string} args.name - Display name for the series
|
||||
* @param {Color} [args.color]
|
||||
* @param {Unit} [args.unit]
|
||||
* @param {boolean} [args.defaultActive]
|
||||
* @param {LineSeriesPartialOptions} [args.options]
|
||||
* @returns {AnyFetchedSeriesBlueprint}
|
||||
*/
|
||||
export function s({ metric, name, color, defaultActive, unit, options }) {
|
||||
return {
|
||||
metric,
|
||||
title: name,
|
||||
color,
|
||||
unit,
|
||||
defaultActive,
|
||||
options,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create series from a BlockCountPattern ({ base, sum, cumulative })
|
||||
* @param {Colors} colors
|
||||
* @param {BlockCountPattern<any>} pattern
|
||||
* @param {string} title
|
||||
* @param {Color} [color]
|
||||
* @returns {AnyFetchedSeriesBlueprint[]}
|
||||
*/
|
||||
export function fromBlockCount(colors, pattern, title, color) {
|
||||
return [
|
||||
{ metric: pattern.base, title, color: color ?? colors.default },
|
||||
{
|
||||
metric: pattern.sum,
|
||||
title: `${title} (sum)`,
|
||||
color: colors.red,
|
||||
defaultActive: false,
|
||||
},
|
||||
{
|
||||
metric: pattern.cumulative,
|
||||
title: `${title} (cum.)`,
|
||||
color: colors.cyan,
|
||||
defaultActive: false,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create series from a BitcoinPattern ({ base, sum, cumulative, average, min, max, median, pct* })
|
||||
* @param {Colors} colors
|
||||
* @param {BitcoinPattern<any>} pattern
|
||||
* @param {string} title
|
||||
* @param {Color} [color]
|
||||
* @returns {AnyFetchedSeriesBlueprint[]}
|
||||
*/
|
||||
export function fromBitcoin(colors, pattern, title, color) {
|
||||
return [
|
||||
{ metric: pattern.base, title, color: color ?? colors.default },
|
||||
{ metric: pattern.average, title: "Average", defaultActive: false },
|
||||
{
|
||||
metric: pattern.sum,
|
||||
title: `${title} (sum)`,
|
||||
color: colors.red,
|
||||
defaultActive: false,
|
||||
},
|
||||
{
|
||||
metric: pattern.cumulative,
|
||||
title: `${title} (cum.)`,
|
||||
color: colors.cyan,
|
||||
defaultActive: false,
|
||||
},
|
||||
{
|
||||
metric: pattern.max,
|
||||
title: "Max",
|
||||
color: colors.pink,
|
||||
defaultActive: false,
|
||||
},
|
||||
{
|
||||
metric: pattern.min,
|
||||
title: "Min",
|
||||
color: colors.green,
|
||||
defaultActive: false,
|
||||
},
|
||||
{
|
||||
metric: pattern.median,
|
||||
title: "Median",
|
||||
color: colors.amber,
|
||||
defaultActive: false,
|
||||
},
|
||||
{
|
||||
metric: pattern.pct75,
|
||||
title: "pct75",
|
||||
color: colors.red,
|
||||
defaultActive: false,
|
||||
},
|
||||
{
|
||||
metric: pattern.pct25,
|
||||
title: "pct25",
|
||||
color: colors.yellow,
|
||||
defaultActive: false,
|
||||
},
|
||||
{
|
||||
metric: pattern.pct90,
|
||||
title: "pct90",
|
||||
color: colors.rose,
|
||||
defaultActive: false,
|
||||
},
|
||||
{
|
||||
metric: pattern.pct10,
|
||||
title: "pct10",
|
||||
color: colors.lime,
|
||||
defaultActive: false,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create series from a BlockSizePattern ({ sum, cumulative, average, min, max, median, pct* })
|
||||
* @param {Colors} colors
|
||||
* @param {BlockSizePattern<any>} pattern
|
||||
* @param {string} title
|
||||
* @param {Color} [color]
|
||||
* @returns {AnyFetchedSeriesBlueprint[]}
|
||||
*/
|
||||
export function fromBlockSize(colors, pattern, title, color) {
|
||||
return [
|
||||
{ metric: pattern.sum, title, color: color ?? colors.default },
|
||||
{ metric: pattern.average, title: "Average", defaultActive: false },
|
||||
{
|
||||
metric: pattern.cumulative,
|
||||
title: `${title} (cum.)`,
|
||||
color: colors.cyan,
|
||||
defaultActive: false,
|
||||
},
|
||||
{
|
||||
metric: pattern.max,
|
||||
title: "Max",
|
||||
color: colors.pink,
|
||||
defaultActive: false,
|
||||
},
|
||||
{
|
||||
metric: pattern.min,
|
||||
title: "Min",
|
||||
color: colors.green,
|
||||
defaultActive: false,
|
||||
},
|
||||
{
|
||||
metric: pattern.median,
|
||||
title: "Median",
|
||||
color: colors.amber,
|
||||
defaultActive: false,
|
||||
},
|
||||
{
|
||||
metric: pattern.pct75,
|
||||
title: "pct75",
|
||||
color: colors.red,
|
||||
defaultActive: false,
|
||||
},
|
||||
{
|
||||
metric: pattern.pct25,
|
||||
title: "pct25",
|
||||
color: colors.yellow,
|
||||
defaultActive: false,
|
||||
},
|
||||
{
|
||||
metric: pattern.pct90,
|
||||
title: "pct90",
|
||||
color: colors.rose,
|
||||
defaultActive: false,
|
||||
},
|
||||
{
|
||||
metric: pattern.pct10,
|
||||
title: "pct10",
|
||||
color: colors.lime,
|
||||
defaultActive: false,
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
/**
|
||||
* @typedef {Object} BaseSeriesBlueprint
|
||||
* @property {string} title
|
||||
* @property {boolean} [defaultActive]
|
||||
*
|
||||
* @typedef {Object} BaselineSeriesBlueprintSpecific
|
||||
* @property {"Baseline"} type
|
||||
* @property {Color} [color]
|
||||
* @property {[Color, Color]} [colors]
|
||||
* @property {BaselineSeriesPartialOptions} [options]
|
||||
* @property {Accessor<BaselineData[]>} [data]
|
||||
* @typedef {BaseSeriesBlueprint & BaselineSeriesBlueprintSpecific} BaselineSeriesBlueprint
|
||||
*
|
||||
* @typedef {Object} CandlestickSeriesBlueprintSpecific
|
||||
* @property {"Candlestick"} type
|
||||
* @property {Color} [color]
|
||||
* @property {CandlestickSeriesPartialOptions} [options]
|
||||
* @property {Accessor<CandlestickData[]>} [data]
|
||||
* @typedef {BaseSeriesBlueprint & CandlestickSeriesBlueprintSpecific} CandlestickSeriesBlueprint
|
||||
*
|
||||
* @typedef {Object} LineSeriesBlueprintSpecific
|
||||
* @property {"Line"} [type]
|
||||
* @property {Color} [color]
|
||||
* @property {LineSeriesPartialOptions} [options]
|
||||
* @property {Accessor<LineData[]>} [data]
|
||||
* @typedef {BaseSeriesBlueprint & LineSeriesBlueprintSpecific} LineSeriesBlueprint
|
||||
*
|
||||
* @typedef {Object} HistogramSeriesBlueprintSpecific
|
||||
* @property {"Histogram"} type
|
||||
* @property {Color} color
|
||||
* @property {HistogramSeriesPartialOptions} [options]
|
||||
* @property {Accessor<HistogramData[]>} [data]
|
||||
* @typedef {BaseSeriesBlueprint & HistogramSeriesBlueprintSpecific} HistogramSeriesBlueprint
|
||||
*
|
||||
* @typedef {BaselineSeriesBlueprint | CandlestickSeriesBlueprint | LineSeriesBlueprint | HistogramSeriesBlueprint} AnySeriesBlueprint
|
||||
*
|
||||
* @typedef {AnySeriesBlueprint["type"]} SeriesType
|
||||
*
|
||||
* @typedef {{ metric: MetricAccessor<any>, unit?: Unit }} FetchedAnySeriesOptions
|
||||
*
|
||||
* @typedef {BaselineSeriesBlueprint & FetchedAnySeriesOptions} FetchedBaselineSeriesBlueprint
|
||||
* @typedef {CandlestickSeriesBlueprint & FetchedAnySeriesOptions} FetchedCandlestickSeriesBlueprint
|
||||
* @typedef {LineSeriesBlueprint & FetchedAnySeriesOptions} FetchedLineSeriesBlueprint
|
||||
* @typedef {HistogramSeriesBlueprint & FetchedAnySeriesOptions} FetchedHistogramSeriesBlueprint
|
||||
* @typedef {AnySeriesBlueprint & FetchedAnySeriesOptions} AnyFetchedSeriesBlueprint
|
||||
*
|
||||
* @typedef {Object} PartialOption
|
||||
* @property {string} name
|
||||
*
|
||||
* @typedef {Object} ProcessedOptionAddons
|
||||
* @property {string} title
|
||||
* @property {string[]} path
|
||||
*
|
||||
* @typedef {Object} PartialExplorerOptionSpecific
|
||||
* @property {"explorer"} kind
|
||||
* @property {string} title
|
||||
*
|
||||
* @typedef {PartialOption & PartialExplorerOptionSpecific} PartialExplorerOption
|
||||
*
|
||||
* @typedef {Required<PartialExplorerOption> & ProcessedOptionAddons} ExplorerOption
|
||||
*
|
||||
* @typedef {Object} PartialChartOptionSpecific
|
||||
* @property {"chart"} [kind]
|
||||
* @property {string} title
|
||||
* @property {AnyFetchedSeriesBlueprint[]} [top]
|
||||
* @property {AnyFetchedSeriesBlueprint[]} [bottom]
|
||||
*
|
||||
* @typedef {PartialOption & PartialChartOptionSpecific} PartialChartOption
|
||||
*
|
||||
* @typedef {Object} ProcessedChartOptionAddons
|
||||
* @property {Record<Unit, AnyFetchedSeriesBlueprint[]>} top
|
||||
* @property {Record<Unit, AnyFetchedSeriesBlueprint[]>} bottom
|
||||
*
|
||||
* @typedef {Required<Omit<PartialChartOption, "top" | "bottom">> & ProcessedChartOptionAddons & ProcessedOptionAddons} ChartOption
|
||||
*
|
||||
* @typedef {Object} PartialTableOptionSpecific
|
||||
* @property {"table"} kind
|
||||
* @property {string} title
|
||||
*
|
||||
* @typedef {PartialOption & PartialTableOptionSpecific} PartialTableOption
|
||||
*
|
||||
* @typedef {Required<PartialTableOption> & ProcessedOptionAddons} TableOption
|
||||
*
|
||||
* @typedef {Object} PartialSimulationOptionSpecific
|
||||
* @property {"simulation"} kind
|
||||
* @property {string} title
|
||||
*
|
||||
* @typedef {PartialOption & PartialSimulationOptionSpecific} PartialSimulationOption
|
||||
*
|
||||
* @typedef {Required<PartialSimulationOption> & ProcessedOptionAddons} SimulationOption
|
||||
*
|
||||
* @typedef {Object} PartialUrlOptionSpecific
|
||||
* @property {"url"} [kind]
|
||||
* @property {() => string} url
|
||||
* @property {string} title
|
||||
* @property {boolean} [qrcode]
|
||||
*
|
||||
* @typedef {PartialOption & PartialUrlOptionSpecific} PartialUrlOption
|
||||
*
|
||||
* @typedef {Required<PartialUrlOption> & ProcessedOptionAddons} UrlOption
|
||||
*
|
||||
* @typedef {PartialExplorerOption | PartialChartOption | PartialTableOption | PartialSimulationOption | PartialUrlOption} AnyPartialOption
|
||||
*
|
||||
* @typedef {ExplorerOption | ChartOption | TableOption | SimulationOption | UrlOption} Option
|
||||
*
|
||||
* @typedef {(AnyPartialOption | PartialOptionsGroup)[]} PartialOptionsTree
|
||||
*
|
||||
* @typedef {Object} PartialOptionsGroup
|
||||
* @property {string} name
|
||||
* @property {PartialOptionsTree} tree
|
||||
*
|
||||
* @typedef {Object} OptionsGroup
|
||||
* @property {string} name
|
||||
* @property {OptionsTree} tree
|
||||
*
|
||||
* @typedef {(Option | OptionsGroup)[]} OptionsTree
|
||||
*
|
||||
* @typedef {Object} UtxoCohortObject
|
||||
* @property {string} name
|
||||
* @property {string} title
|
||||
* @property {Color} color
|
||||
* @property {UtxoCohortPattern} tree
|
||||
*
|
||||
* Age cohorts (term, maxAge, minAge, ageRange, epoch) - have price percentiles
|
||||
* @typedef {Object} AgeCohortObject
|
||||
* @property {string} name
|
||||
* @property {string} title
|
||||
* @property {Color} color
|
||||
* @property {PatternWithPricePercentiles} tree
|
||||
*
|
||||
* Amount cohorts (geAmount, ltAmount, amountRange, type) - no price percentiles
|
||||
* @typedef {Object} AmountCohortObject
|
||||
* @property {string} name
|
||||
* @property {string} title
|
||||
* @property {Color} color
|
||||
* @property {UtxoAmountPattern} tree
|
||||
*
|
||||
* @typedef {Object} AddressCohortObject
|
||||
* @property {string} name
|
||||
* @property {string} title
|
||||
* @property {Color} color
|
||||
* @property {AddressCohortPattern} tree
|
||||
*
|
||||
* @typedef {UtxoCohortObject | AddressCohortObject} CohortObject
|
||||
*
|
||||
* @typedef {Object} UtxoCohortGroupObject
|
||||
* @property {string} name
|
||||
* @property {string} title
|
||||
* @property {readonly UtxoCohortObject[]} list
|
||||
*
|
||||
* @typedef {Object} AgeCohortGroupObject
|
||||
* @property {string} name
|
||||
* @property {string} title
|
||||
* @property {readonly AgeCohortObject[]} list
|
||||
*
|
||||
* @typedef {Object} AmountCohortGroupObject
|
||||
* @property {string} name
|
||||
* @property {string} title
|
||||
* @property {readonly AmountCohortObject[]} list
|
||||
*
|
||||
* @typedef {Object} AddressCohortGroupObject
|
||||
* @property {string} name
|
||||
* @property {string} title
|
||||
* @property {readonly AddressCohortObject[]} list
|
||||
*
|
||||
* @typedef {UtxoCohortGroupObject | AddressCohortGroupObject} CohortGroupObject
|
||||
*
|
||||
* @typedef {Object} PartialContext
|
||||
* @property {Colors} colors
|
||||
* @property {BrkClient} brk
|
||||
* @property {BrkClient["tree"]["computed"]["constants"]} constants
|
||||
* @property {(args: { metric: MetricAccessor<any>, name: string, color?: Color, defaultActive?: boolean, unit?: Unit, options?: LineSeriesPartialOptions }) => AnyFetchedSeriesBlueprint} s
|
||||
* @property {(pattern: BlockCountPattern<any>, title: string, color?: Color) => AnyFetchedSeriesBlueprint[]} fromBlockCount
|
||||
* @property {(pattern: BitcoinPattern<any>, title: string, color?: Color) => AnyFetchedSeriesBlueprint[]} fromBitcoin
|
||||
* @property {(pattern: BlockSizePattern<any>, title: string, color?: Color) => AnyFetchedSeriesBlueprint[]} fromBlockSize
|
||||
* @property {(num: number) => Constant0Pattern<any>} getConstant
|
||||
* @property {(pattern: Constant0Pattern<any>) => MetricAccessor<any>} flattenConstant
|
||||
* @property {(args: { number?: number, name?: string, defaultActive?: boolean, lineStyle?: number, color?: Color, unit: Unit }) => FetchedLineSeriesBlueprint} createPriceLine
|
||||
* @property {(args: { numbers: number[], unit: Unit }) => FetchedLineSeriesBlueprint[]} createPriceLines
|
||||
* @property {(args: { constant: Constant0Pattern<any>, name: string, unit: Unit, color?: Color, lineStyle?: number, defaultActive?: boolean }) => FetchedLineSeriesBlueprint} line
|
||||
* @property {MetricAccessor<any>} constant100
|
||||
*/
|
||||
|
||||
// Re-export for type consumers
|
||||
export {};
|
||||
Reference in New Issue
Block a user