mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-04-24 22:59:58 -07:00
982 lines
40 KiB
JavaScript
982 lines
40 KiB
JavaScript
/**
|
|
* Activity section builders
|
|
*
|
|
* Capabilities by cohort type:
|
|
* - All/STH: activity (full), SOPR (rolling + adjusted), sell side risk, value (flows + breakdown), coins
|
|
* - LTH: activity (full), SOPR (rolling), sell side risk, value (flows + breakdown), coins
|
|
* - AgeRange/MaxAge: activity (basic), SOPR (24h only), value (no flows/breakdown), coins
|
|
* - Others (UtxoAmount, Empty, Address): no activity, value only
|
|
*/
|
|
|
|
import { Unit } from "../../utils/units.js";
|
|
import { line, baseline, dotsBaseline, percentRatio, percentRatioDots } from "../series.js";
|
|
import {
|
|
mapCohortsWithAll,
|
|
flatMapCohortsWithAll,
|
|
} from "../shared.js";
|
|
import { colors } from "../../utils/colors.js";
|
|
|
|
// ============================================================================
|
|
// Shared Volume Helpers
|
|
// ============================================================================
|
|
|
|
/**
|
|
* @param {{ transferVolume: TransferVolumePattern, coindaysDestroyed: CountPattern<number> }} activity
|
|
* @param {Color} color
|
|
* @param {(name: string) => string} title
|
|
* @returns {PartialOptionsTree}
|
|
*/
|
|
function volumeAndCoinsTree(activity, color, title) {
|
|
return [
|
|
{
|
|
name: "Volume",
|
|
tree: [
|
|
{
|
|
name: "Sum",
|
|
title: title("Sent Volume"),
|
|
bottom: [
|
|
line({ series: activity.transferVolume.base.sats, name: "Sum", color, unit: Unit.sats }),
|
|
line({ series: activity.transferVolume.sum._24h.sats, name: "24h", color: colors.time._24h, unit: Unit.sats, defaultActive: false }),
|
|
line({ series: activity.transferVolume.sum._1w.sats, name: "1w", color: colors.time._1w, unit: Unit.sats, defaultActive: false }),
|
|
line({ series: activity.transferVolume.sum._1m.sats, name: "1m", color: colors.time._1m, unit: Unit.sats, defaultActive: false }),
|
|
line({ series: activity.transferVolume.sum._1y.sats, name: "1y", color: colors.time._1y, unit: Unit.sats, defaultActive: false }),
|
|
],
|
|
},
|
|
{
|
|
name: "Cumulative",
|
|
title: title("Sent Volume (Total)"),
|
|
bottom: [
|
|
line({ series: activity.transferVolume.cumulative.sats, name: "All-time", color, unit: Unit.sats }),
|
|
],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
name: "Coins Destroyed",
|
|
tree: [
|
|
{
|
|
name: "Base",
|
|
title: title("Coindays Destroyed"),
|
|
bottom: [
|
|
line({ series: activity.coindaysDestroyed.base, name: "Base", color, unit: Unit.coindays }),
|
|
line({ series: activity.coindaysDestroyed.sum._24h, name: "24h", color: colors.time._24h, unit: Unit.coindays, defaultActive: false }),
|
|
line({ series: activity.coindaysDestroyed.sum._1w, name: "1w", color: colors.time._1w, unit: Unit.coindays, defaultActive: false }),
|
|
line({ series: activity.coindaysDestroyed.sum._1m, name: "1m", color: colors.time._1m, unit: Unit.coindays, defaultActive: false }),
|
|
line({ series: activity.coindaysDestroyed.sum._1y, name: "1y", color: colors.time._1y, unit: Unit.coindays, defaultActive: false }),
|
|
],
|
|
},
|
|
{
|
|
name: "Cumulative",
|
|
title: title("Cumulative Coindays Destroyed"),
|
|
bottom: [
|
|
line({ series: activity.coindaysDestroyed.cumulative, name: "All-time", color, unit: Unit.coindays }),
|
|
],
|
|
},
|
|
],
|
|
},
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Sent in profit/loss breakdown tree (shared by full and mid-level activity)
|
|
* @param {TransferVolumePattern} sent
|
|
* @param {(name: string) => string} title
|
|
* @returns {PartialOptionsTree}
|
|
*/
|
|
function sentProfitLossTree(sent, title) {
|
|
return [
|
|
{
|
|
name: "Sent In Profit",
|
|
tree: [
|
|
{
|
|
name: "USD",
|
|
title: title("Sent Volume In Profit"),
|
|
bottom: [
|
|
line({ series: sent.inProfit.base.usd, name: "Base", color: colors.profit, unit: Unit.usd }),
|
|
line({ series: sent.inProfit.sum._24h.usd, name: "24h", color: colors.time._24h, unit: Unit.usd, defaultActive: false }),
|
|
line({ series: sent.inProfit.sum._1w.usd, name: "1w", color: colors.time._1w, unit: Unit.usd, defaultActive: false }),
|
|
line({ series: sent.inProfit.sum._1m.usd, name: "1m", color: colors.time._1m, unit: Unit.usd, defaultActive: false }),
|
|
line({ series: sent.inProfit.sum._1y.usd, name: "1y", color: colors.time._1y, unit: Unit.usd, defaultActive: false }),
|
|
],
|
|
},
|
|
{
|
|
name: "BTC",
|
|
title: title("Sent Volume In Profit (BTC)"),
|
|
bottom: [
|
|
line({ series: sent.inProfit.base.btc, name: "Base", color: colors.profit, unit: Unit.btc }),
|
|
line({ series: sent.inProfit.sum._24h.btc, name: "24h", color: colors.time._24h, unit: Unit.btc, defaultActive: false }),
|
|
line({ series: sent.inProfit.sum._1w.btc, name: "1w", color: colors.time._1w, unit: Unit.btc, defaultActive: false }),
|
|
line({ series: sent.inProfit.sum._1m.btc, name: "1m", color: colors.time._1m, unit: Unit.btc, defaultActive: false }),
|
|
line({ series: sent.inProfit.sum._1y.btc, name: "1y", color: colors.time._1y, unit: Unit.btc, defaultActive: false }),
|
|
],
|
|
},
|
|
{
|
|
name: "Sats",
|
|
title: title("Sent Volume In Profit (Sats)"),
|
|
bottom: [
|
|
line({ series: sent.inProfit.base.sats, name: "Base", color: colors.profit, unit: Unit.sats }),
|
|
line({ series: sent.inProfit.sum._24h.sats, name: "24h", color: colors.time._24h, unit: Unit.sats, defaultActive: false }),
|
|
line({ series: sent.inProfit.sum._1w.sats, name: "1w", color: colors.time._1w, unit: Unit.sats, defaultActive: false }),
|
|
line({ series: sent.inProfit.sum._1m.sats, name: "1m", color: colors.time._1m, unit: Unit.sats, defaultActive: false }),
|
|
line({ series: sent.inProfit.sum._1y.sats, name: "1y", color: colors.time._1y, unit: Unit.sats, defaultActive: false }),
|
|
],
|
|
},
|
|
{ name: "Cumulative", title: title("Cumulative Sent In Profit"), bottom: [
|
|
line({ series: sent.inProfit.cumulative.usd, name: "USD", color: colors.profit, unit: Unit.usd }),
|
|
line({ series: sent.inProfit.cumulative.btc, name: "BTC", color: colors.profit, unit: Unit.btc, defaultActive: false }),
|
|
line({ series: sent.inProfit.cumulative.sats, name: "Sats", color: colors.profit, unit: Unit.sats, defaultActive: false }),
|
|
]},
|
|
],
|
|
},
|
|
{
|
|
name: "Sent In Loss",
|
|
tree: [
|
|
{
|
|
name: "USD",
|
|
title: title("Sent Volume In Loss"),
|
|
bottom: [
|
|
line({ series: sent.inLoss.base.usd, name: "Base", color: colors.loss, unit: Unit.usd }),
|
|
line({ series: sent.inLoss.sum._24h.usd, name: "24h", color: colors.time._24h, unit: Unit.usd, defaultActive: false }),
|
|
line({ series: sent.inLoss.sum._1w.usd, name: "1w", color: colors.time._1w, unit: Unit.usd, defaultActive: false }),
|
|
line({ series: sent.inLoss.sum._1m.usd, name: "1m", color: colors.time._1m, unit: Unit.usd, defaultActive: false }),
|
|
line({ series: sent.inLoss.sum._1y.usd, name: "1y", color: colors.time._1y, unit: Unit.usd, defaultActive: false }),
|
|
],
|
|
},
|
|
{
|
|
name: "BTC",
|
|
title: title("Sent Volume In Loss (BTC)"),
|
|
bottom: [
|
|
line({ series: sent.inLoss.base.btc, name: "Base", color: colors.loss, unit: Unit.btc }),
|
|
line({ series: sent.inLoss.sum._24h.btc, name: "24h", color: colors.time._24h, unit: Unit.btc, defaultActive: false }),
|
|
line({ series: sent.inLoss.sum._1w.btc, name: "1w", color: colors.time._1w, unit: Unit.btc, defaultActive: false }),
|
|
line({ series: sent.inLoss.sum._1m.btc, name: "1m", color: colors.time._1m, unit: Unit.btc, defaultActive: false }),
|
|
line({ series: sent.inLoss.sum._1y.btc, name: "1y", color: colors.time._1y, unit: Unit.btc, defaultActive: false }),
|
|
],
|
|
},
|
|
{
|
|
name: "Sats",
|
|
title: title("Sent Volume In Loss (Sats)"),
|
|
bottom: [
|
|
line({ series: sent.inLoss.base.sats, name: "Base", color: colors.loss, unit: Unit.sats }),
|
|
line({ series: sent.inLoss.sum._24h.sats, name: "24h", color: colors.time._24h, unit: Unit.sats, defaultActive: false }),
|
|
line({ series: sent.inLoss.sum._1w.sats, name: "1w", color: colors.time._1w, unit: Unit.sats, defaultActive: false }),
|
|
line({ series: sent.inLoss.sum._1m.sats, name: "1m", color: colors.time._1m, unit: Unit.sats, defaultActive: false }),
|
|
line({ series: sent.inLoss.sum._1y.sats, name: "1y", color: colors.time._1y, unit: Unit.sats, defaultActive: false }),
|
|
],
|
|
},
|
|
{ name: "Cumulative", title: title("Cumulative Sent In Loss"), bottom: [
|
|
line({ series: sent.inLoss.cumulative.usd, name: "USD", color: colors.loss, unit: Unit.usd }),
|
|
line({ series: sent.inLoss.cumulative.btc, name: "BTC", color: colors.loss, unit: Unit.btc, defaultActive: false }),
|
|
line({ series: sent.inLoss.cumulative.sats, name: "Sats", color: colors.loss, unit: Unit.sats, defaultActive: false }),
|
|
]},
|
|
],
|
|
},
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Volume and coins tree with coinyears, dormancy, and sent in profit/loss (All/STH/LTH)
|
|
* @param {FullActivityPattern} activity
|
|
* @param {Color} color
|
|
* @param {(name: string) => string} title
|
|
* @returns {PartialOptionsTree}
|
|
*/
|
|
function fullVolumeTree(activity, color, title) {
|
|
return [
|
|
...volumeAndCoinsTree(activity, color, title),
|
|
...sentProfitLossTree(activity.transferVolume, title),
|
|
{
|
|
name: "Coinyears Destroyed",
|
|
title: title("Coinyears Destroyed"),
|
|
bottom: [line({ series: activity.coinyearsDestroyed, name: "CYD", color, unit: Unit.years })],
|
|
},
|
|
{
|
|
name: "Dormancy",
|
|
title: title("Dormancy"),
|
|
bottom: [line({ series: activity.dormancy, name: "Dormancy", color, unit: Unit.days })],
|
|
},
|
|
];
|
|
}
|
|
|
|
// ============================================================================
|
|
// Shared SOPR Helpers
|
|
// ============================================================================
|
|
|
|
/**
|
|
* @param {RollingWindowPattern<number>} ratio
|
|
* @param {(name: string) => string} title
|
|
* @param {string} [prefix]
|
|
* @returns {PartialOptionsTree}
|
|
*/
|
|
function singleRollingSoprTree(ratio, title, prefix = "") {
|
|
return [
|
|
{
|
|
name: "Compare",
|
|
title: title(`Rolling ${prefix}SOPR`),
|
|
bottom: [
|
|
baseline({ series: ratio._24h, name: "24h", color: colors.time._24h, unit: Unit.ratio, base: 1 }),
|
|
baseline({ series: ratio._1w, name: "7d", color: colors.time._1w, unit: Unit.ratio, base: 1 }),
|
|
baseline({ series: ratio._1m, name: "30d", color: colors.time._1m, unit: Unit.ratio, base: 1 }),
|
|
baseline({ series: ratio._1y, name: "1y", color: colors.time._1y, unit: Unit.ratio, base: 1 }),
|
|
],
|
|
},
|
|
{
|
|
name: "24h",
|
|
title: title(`${prefix}SOPR (24h)`),
|
|
bottom: [dotsBaseline({ series: ratio._24h, name: "24h", unit: Unit.ratio, base: 1 })],
|
|
},
|
|
{
|
|
name: "7d",
|
|
title: title(`${prefix}SOPR (7d)`),
|
|
bottom: [baseline({ series: ratio._1w, name: "SOPR", unit: Unit.ratio, base: 1 })],
|
|
},
|
|
{
|
|
name: "30d",
|
|
title: title(`${prefix}SOPR (30d)`),
|
|
bottom: [baseline({ series: ratio._1m, name: "SOPR", unit: Unit.ratio, base: 1 })],
|
|
},
|
|
{
|
|
name: "1y",
|
|
title: title(`${prefix}SOPR (1y)`),
|
|
bottom: [baseline({ series: ratio._1y, name: "SOPR", unit: Unit.ratio, base: 1 })],
|
|
},
|
|
];
|
|
}
|
|
|
|
// ============================================================================
|
|
// Shared Sell Side Risk Helpers
|
|
// ============================================================================
|
|
|
|
/**
|
|
* @param {SellSideRiskPattern} sellSideRisk
|
|
* @param {(name: string) => string} title
|
|
* @returns {PartialOptionsTree}
|
|
*/
|
|
function singleSellSideRiskTree(sellSideRisk, title) {
|
|
return [
|
|
{
|
|
name: "Compare",
|
|
title: title("Rolling Sell Side Risk"),
|
|
bottom: [
|
|
...percentRatioDots({ pattern: sellSideRisk._24h, name: "24h", color: colors.time._24h }),
|
|
...percentRatio({ pattern: sellSideRisk._1w, name: "7d", color: colors.time._1w }),
|
|
...percentRatio({ pattern: sellSideRisk._1m, name: "30d", color: colors.time._1m }),
|
|
...percentRatio({ pattern: sellSideRisk._1y, name: "1y", color: colors.time._1y }),
|
|
],
|
|
},
|
|
{
|
|
name: "24h",
|
|
title: title("Sell Side Risk (24h)"),
|
|
bottom: percentRatioDots({ pattern: sellSideRisk._24h, name: "Raw", color: colors.bitcoin }),
|
|
},
|
|
{
|
|
name: "7d",
|
|
title: title("Sell Side Risk (7d)"),
|
|
bottom: percentRatio({ pattern: sellSideRisk._1w, name: "Risk" }),
|
|
},
|
|
{
|
|
name: "30d",
|
|
title: title("Sell Side Risk (30d)"),
|
|
bottom: percentRatio({ pattern: sellSideRisk._1m, name: "Risk" }),
|
|
},
|
|
{
|
|
name: "1y",
|
|
title: title("Sell Side Risk (1y)"),
|
|
bottom: percentRatio({ pattern: sellSideRisk._1y, name: "Risk" }),
|
|
},
|
|
];
|
|
}
|
|
|
|
// ============================================================================
|
|
// Shared Value Helpers
|
|
// ============================================================================
|
|
|
|
/**
|
|
* @param {CountPattern<number>} valueCreated
|
|
* @param {CountPattern<number>} valueDestroyed
|
|
* @param {(name: string) => string} title
|
|
* @param {string} [prefix]
|
|
* @returns {PartialOptionsTree}
|
|
*/
|
|
function singleRollingValueTree(valueCreated, valueDestroyed, title, prefix = "") {
|
|
return [
|
|
{
|
|
name: "Compare",
|
|
tree: [
|
|
{
|
|
name: "Created",
|
|
title: title(`Rolling ${prefix}Value Created`),
|
|
bottom: [
|
|
line({ series: valueCreated.sum._24h, name: "24h", color: colors.time._24h, unit: Unit.usd }),
|
|
line({ series: valueCreated.sum._1w, name: "7d", color: colors.time._1w, unit: Unit.usd }),
|
|
line({ series: valueCreated.sum._1m, name: "30d", color: colors.time._1m, unit: Unit.usd }),
|
|
line({ series: valueCreated.sum._1y, name: "1y", color: colors.time._1y, unit: Unit.usd }),
|
|
],
|
|
},
|
|
{
|
|
name: "Destroyed",
|
|
title: title(`Rolling ${prefix}Value Destroyed`),
|
|
bottom: [
|
|
line({ series: valueDestroyed.sum._24h, name: "24h", color: colors.time._24h, unit: Unit.usd }),
|
|
line({ series: valueDestroyed.sum._1w, name: "7d", color: colors.time._1w, unit: Unit.usd }),
|
|
line({ series: valueDestroyed.sum._1m, name: "30d", color: colors.time._1m, unit: Unit.usd }),
|
|
line({ series: valueDestroyed.sum._1y, name: "1y", color: colors.time._1y, unit: Unit.usd }),
|
|
],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
name: "24h",
|
|
title: title(`${prefix}Value Created & Destroyed (24h)`),
|
|
bottom: [
|
|
line({ series: valueCreated.sum._24h, name: "Created", color: colors.usd, unit: Unit.usd }),
|
|
line({ series: valueDestroyed.sum._24h, name: "Destroyed", color: colors.loss, unit: Unit.usd }),
|
|
],
|
|
},
|
|
{
|
|
name: "7d",
|
|
title: title(`${prefix}Value Created & Destroyed (7d)`),
|
|
bottom: [
|
|
line({ series: valueCreated.sum._1w, name: "Created", color: colors.usd, unit: Unit.usd }),
|
|
line({ series: valueDestroyed.sum._1w, name: "Destroyed", color: colors.loss, unit: Unit.usd }),
|
|
],
|
|
},
|
|
{
|
|
name: "30d",
|
|
title: title(`${prefix}Value Created & Destroyed (30d)`),
|
|
bottom: [
|
|
line({ series: valueCreated.sum._1m, name: "Created", color: colors.usd, unit: Unit.usd }),
|
|
line({ series: valueDestroyed.sum._1m, name: "Destroyed", color: colors.loss, unit: Unit.usd }),
|
|
],
|
|
},
|
|
{
|
|
name: "1y",
|
|
title: title(`${prefix}Value Created & Destroyed (1y)`),
|
|
bottom: [
|
|
line({ series: valueCreated.sum._1y, name: "Created", color: colors.usd, unit: Unit.usd }),
|
|
line({ series: valueDestroyed.sum._1y, name: "Destroyed", color: colors.loss, unit: Unit.usd }),
|
|
],
|
|
},
|
|
{
|
|
name: "Cumulative",
|
|
title: title(`${prefix}Value Created & Destroyed (Total)`),
|
|
bottom: [
|
|
line({ series: valueCreated.cumulative, name: "Created", color: colors.usd, unit: Unit.usd }),
|
|
line({ series: valueDestroyed.cumulative, name: "Destroyed", color: colors.loss, unit: Unit.usd }),
|
|
],
|
|
},
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Value section for cohorts with full realized (flows + breakdown)
|
|
* @param {ProfitDetailPattern} profit
|
|
* @param {LossDetailPattern} loss
|
|
* @param {CountPattern<number>} valueCreated
|
|
* @param {CountPattern<number>} valueDestroyed
|
|
* @param {AnyFetchedSeriesBlueprint[]} extraValueSeries
|
|
* @param {PartialOptionsTree} rollingTree
|
|
* @param {(name: string) => string} title
|
|
* @returns {PartialOptionsGroup}
|
|
*/
|
|
function fullValueSection(profit, loss, valueCreated, valueDestroyed, extraValueSeries, rollingTree, title) {
|
|
return {
|
|
name: "Value",
|
|
tree: [
|
|
{
|
|
name: "Flows",
|
|
title: title("Profit & Capitulation Flows"),
|
|
bottom: [
|
|
line({ series: profit.distributionFlow, name: "Distribution Flow", color: colors.profit, unit: Unit.usd }),
|
|
line({ series: loss.capitulationFlow, name: "Capitulation Flow", color: colors.loss, unit: Unit.usd }),
|
|
],
|
|
},
|
|
{
|
|
name: "Created & Destroyed",
|
|
title: title("Value Created & Destroyed"),
|
|
bottom: [
|
|
line({ series: valueCreated.base, name: "Created", color: colors.usd, unit: Unit.usd }),
|
|
line({ series: valueDestroyed.base, name: "Destroyed", color: colors.loss, unit: Unit.usd }),
|
|
...extraValueSeries,
|
|
],
|
|
},
|
|
{
|
|
name: "Breakdown",
|
|
tree: [
|
|
{
|
|
name: "Profit",
|
|
title: title("Profit Value Created & Destroyed"),
|
|
bottom: [
|
|
line({ series: profit.valueCreated.base, name: "Created", color: colors.profit, unit: Unit.usd }),
|
|
line({ series: profit.valueDestroyed.base, name: "Destroyed", color: colors.loss, unit: Unit.usd }),
|
|
],
|
|
},
|
|
{
|
|
name: "Loss",
|
|
title: title("Loss Value Created & Destroyed"),
|
|
bottom: [
|
|
line({ series: loss.valueCreated.base, name: "Created", color: colors.profit, unit: Unit.usd }),
|
|
line({ series: loss.valueDestroyed.base, name: "Destroyed", color: colors.loss, unit: Unit.usd }),
|
|
],
|
|
},
|
|
],
|
|
},
|
|
{ name: "Rolling", tree: rollingTree },
|
|
],
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Simple value section (created & destroyed + rolling)
|
|
* @param {CountPattern<number>} valueCreated
|
|
* @param {CountPattern<number>} valueDestroyed
|
|
* @param {(name: string) => string} title
|
|
* @returns {PartialOptionsGroup}
|
|
*/
|
|
function simpleValueSection(valueCreated, valueDestroyed, title) {
|
|
return {
|
|
name: "Value",
|
|
tree: [
|
|
{
|
|
name: "Created & Destroyed",
|
|
title: title("Value Created & Destroyed"),
|
|
bottom: [
|
|
line({ series: valueCreated.base, name: "Created", color: colors.usd, unit: Unit.usd }),
|
|
line({ series: valueDestroyed.base, name: "Destroyed", color: colors.loss, unit: Unit.usd }),
|
|
],
|
|
},
|
|
{
|
|
name: "Rolling",
|
|
tree: singleRollingValueTree(valueCreated, valueDestroyed, title),
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
// ============================================================================
|
|
// Single Cohort Activity Sections
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Full activity with adjusted SOPR (All/STH)
|
|
* @param {{ cohort: CohortAll | CohortFull, title: (name: string) => string }} args
|
|
* @returns {PartialOptionsGroup}
|
|
*/
|
|
export function createActivitySectionWithAdjusted({ cohort, title }) {
|
|
const { tree, color } = cohort;
|
|
const r = tree.realized;
|
|
const sopr = r.sopr;
|
|
|
|
return {
|
|
name: "Activity",
|
|
tree: [
|
|
...fullVolumeTree(tree.activity, color, title),
|
|
{
|
|
name: "SOPR",
|
|
tree: [
|
|
{
|
|
name: "Normal",
|
|
tree: singleRollingSoprTree(sopr.ratio, title),
|
|
},
|
|
{
|
|
name: "Adjusted",
|
|
tree: singleRollingSoprTree(sopr.adjusted.ratio, title, "Adjusted "),
|
|
},
|
|
],
|
|
},
|
|
{ name: "Sell Side Risk", tree: singleSellSideRiskTree(r.sellSideRiskRatio, title) },
|
|
fullValueSection(
|
|
r.profit, r.loss,
|
|
sopr.valueCreated, sopr.valueDestroyed,
|
|
[
|
|
line({ series: sopr.adjusted.valueCreated.base, name: "Adjusted Created", color: colors.adjustedCreated, unit: Unit.usd, defaultActive: false }),
|
|
line({ series: sopr.adjusted.valueDestroyed.base, name: "Adjusted Destroyed", color: colors.adjustedDestroyed, unit: Unit.usd, defaultActive: false }),
|
|
],
|
|
[
|
|
{
|
|
name: "Normal",
|
|
tree: singleRollingValueTree(sopr.valueCreated, sopr.valueDestroyed, title),
|
|
},
|
|
{
|
|
name: "Adjusted",
|
|
tree: singleRollingValueTree(sopr.adjusted.valueCreated, sopr.adjusted.valueDestroyed, title, "Adjusted "),
|
|
},
|
|
],
|
|
title,
|
|
),
|
|
],
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Activity section for cohorts with rolling SOPR + sell side risk (LTH, also CohortFull | CohortLongTerm)
|
|
* @param {{ cohort: CohortFull | CohortLongTerm, title: (name: string) => string }} args
|
|
* @returns {PartialOptionsGroup}
|
|
*/
|
|
export function createActivitySection({ cohort, title }) {
|
|
const { tree, color } = cohort;
|
|
const r = tree.realized;
|
|
const sopr = r.sopr;
|
|
|
|
return {
|
|
name: "Activity",
|
|
tree: [
|
|
...fullVolumeTree(tree.activity, color, title),
|
|
{
|
|
name: "SOPR",
|
|
tree: singleRollingSoprTree(sopr.ratio, title),
|
|
},
|
|
{ name: "Sell Side Risk", tree: singleSellSideRiskTree(r.sellSideRiskRatio, title) },
|
|
fullValueSection(
|
|
r.profit, r.loss,
|
|
sopr.valueCreated, sopr.valueDestroyed,
|
|
[],
|
|
singleRollingValueTree(sopr.valueCreated, sopr.valueDestroyed, title),
|
|
title,
|
|
),
|
|
],
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Activity section for cohorts with activity but basic realized (AgeRange/MaxAge — 24h SOPR only)
|
|
* @param {{ cohort: CohortAgeRange | CohortWithAdjusted, title: (name: string) => string }} args
|
|
* @returns {PartialOptionsGroup}
|
|
*/
|
|
export function createActivitySectionWithActivity({ cohort, title }) {
|
|
const { tree, color } = cohort;
|
|
const sopr = tree.realized.sopr;
|
|
|
|
return {
|
|
name: "Activity",
|
|
tree: [
|
|
...volumeAndCoinsTree(tree.activity, color, title),
|
|
...sentProfitLossTree(tree.activity.transferVolume, title),
|
|
{
|
|
name: "SOPR",
|
|
title: title("SOPR (24h)"),
|
|
bottom: [dotsBaseline({ series: sopr.ratio._24h, name: "SOPR", unit: Unit.ratio, base: 1 })],
|
|
},
|
|
simpleValueSection(sopr.valueCreated, sopr.valueDestroyed, title),
|
|
],
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Minimal activity section for cohorts without activity field (value only)
|
|
* @param {{ cohort: CohortBasicWithMarketCap | CohortBasicWithoutMarketCap | CohortWithoutRelative | CohortAddr | AddrCohortObject, title: (name: string) => string }} args
|
|
* @returns {PartialOptionsGroup}
|
|
*/
|
|
export function createActivitySectionMinimal({ cohort, title }) {
|
|
const sopr = cohort.tree.realized.sopr;
|
|
|
|
return {
|
|
name: "Activity",
|
|
tree: [
|
|
simpleValueSection(sopr.valueCreated, sopr.valueDestroyed, title),
|
|
],
|
|
};
|
|
}
|
|
|
|
// ============================================================================
|
|
// Grouped SOPR Helpers
|
|
// ============================================================================
|
|
|
|
/**
|
|
* @template {{ color: Color, name: string }} T
|
|
* @template {{ color: Color, name: string }} A
|
|
* @param {readonly T[]} list
|
|
* @param {A} all
|
|
* @param {(item: T | A) => AnySeriesPattern} getRaw
|
|
* @param {(item: T | A) => AnySeriesPattern} get7d
|
|
* @param {(item: T | A) => AnySeriesPattern} get30d
|
|
* @param {(name: string) => string} title
|
|
* @param {string} [prefix]
|
|
* @returns {PartialOptionsTree}
|
|
*/
|
|
function groupedSoprCharts(list, all, getRaw, get7d, get30d, title, prefix = "") {
|
|
return [
|
|
{
|
|
name: "Raw",
|
|
title: title(`${prefix}SOPR`),
|
|
bottom: mapCohortsWithAll(list, all, (item) =>
|
|
baseline({ series: getRaw(item), name: item.name, color: item.color, unit: Unit.ratio, base: 1 }),
|
|
),
|
|
},
|
|
{
|
|
name: "7d",
|
|
title: title(`${prefix}SOPR (7d)`),
|
|
bottom: mapCohortsWithAll(list, all, (item) =>
|
|
baseline({ series: get7d(item), name: item.name, color: item.color, unit: Unit.ratio, base: 1 }),
|
|
),
|
|
},
|
|
{
|
|
name: "30d",
|
|
title: title(`${prefix}SOPR (30d)`),
|
|
bottom: mapCohortsWithAll(list, all, (item) =>
|
|
baseline({ series: get30d(item), name: item.name, color: item.color, unit: Unit.ratio, base: 1 }),
|
|
),
|
|
},
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @template {{ color: Color, name: string }} T
|
|
* @template {{ color: Color, name: string }} A
|
|
* @param {readonly T[]} list
|
|
* @param {A} all
|
|
* @param {(item: T | A) => AnySeriesPattern} get24h
|
|
* @param {(item: T | A) => AnySeriesPattern} get7d
|
|
* @param {(item: T | A) => AnySeriesPattern} get30d
|
|
* @param {(item: T | A) => AnySeriesPattern} get1y
|
|
* @param {(name: string) => string} title
|
|
* @param {string} [prefix]
|
|
* @returns {PartialOptionsTree}
|
|
*/
|
|
function groupedRollingSoprCharts(list, all, get24h, get7d, get30d, get1y, title, prefix = "") {
|
|
return [
|
|
{
|
|
name: "24h",
|
|
title: title(`${prefix}SOPR (24h)`),
|
|
bottom: mapCohortsWithAll(list, all, (c) =>
|
|
baseline({ series: get24h(c), name: c.name, color: c.color, unit: Unit.ratio, base: 1 }),
|
|
),
|
|
},
|
|
{
|
|
name: "7d",
|
|
title: title(`${prefix}SOPR (7d)`),
|
|
bottom: mapCohortsWithAll(list, all, (c) =>
|
|
baseline({ series: get7d(c), name: c.name, color: c.color, unit: Unit.ratio, base: 1 }),
|
|
),
|
|
},
|
|
{
|
|
name: "30d",
|
|
title: title(`${prefix}SOPR (30d)`),
|
|
bottom: mapCohortsWithAll(list, all, (c) =>
|
|
baseline({ series: get30d(c), name: c.name, color: c.color, unit: Unit.ratio, base: 1 }),
|
|
),
|
|
},
|
|
{
|
|
name: "1y",
|
|
title: title(`${prefix}SOPR (1y)`),
|
|
bottom: mapCohortsWithAll(list, all, (c) =>
|
|
baseline({ series: get1y(c), name: c.name, color: c.color, unit: Unit.ratio, base: 1 }),
|
|
),
|
|
},
|
|
];
|
|
}
|
|
|
|
// ============================================================================
|
|
// Grouped Value/Flow Helpers
|
|
// ============================================================================
|
|
|
|
/**
|
|
* @template {{ color: Color, name: string }} T
|
|
* @template {{ color: Color, name: string }} A
|
|
* @param {readonly T[]} list
|
|
* @param {A} all
|
|
* @param {readonly { name: string, getCreated: (item: T | A) => AnySeriesPattern, getDestroyed: (item: T | A) => AnySeriesPattern }[]} windows
|
|
* @param {(name: string) => string} title
|
|
* @param {string} [prefix]
|
|
* @returns {PartialOptionsTree}
|
|
*/
|
|
function groupedRollingValueCharts(list, all, windows, title, prefix = "") {
|
|
return [
|
|
{
|
|
name: "Created",
|
|
tree: windows.map((w) => ({
|
|
name: w.name,
|
|
title: title(`${prefix}Value Created (${w.name})`),
|
|
bottom: mapCohortsWithAll(list, all, (item) =>
|
|
line({ series: w.getCreated(item), name: item.name, color: item.color, unit: Unit.usd }),
|
|
),
|
|
})),
|
|
},
|
|
{
|
|
name: "Destroyed",
|
|
tree: windows.map((w) => ({
|
|
name: w.name,
|
|
title: title(`${prefix}Value Destroyed (${w.name})`),
|
|
bottom: mapCohortsWithAll(list, all, (item) =>
|
|
line({ series: w.getDestroyed(item), name: item.name, color: item.color, unit: Unit.usd }),
|
|
),
|
|
})),
|
|
},
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param {readonly (CohortFull | CohortLongTerm | CohortAll)[]} list
|
|
* @param {CohortAll} all
|
|
*/
|
|
function valueWindows(list, all) {
|
|
return [
|
|
{ name: "24h", getCreated: (/** @type {typeof list[number] | typeof all} */ c) => c.tree.realized.sopr.valueCreated.sum._24h, getDestroyed: (/** @type {typeof list[number] | typeof all} */ c) => c.tree.realized.sopr.valueDestroyed.sum._24h },
|
|
{ name: "7d", getCreated: (/** @type {typeof list[number] | typeof all} */ c) => c.tree.realized.sopr.valueCreated.sum._1w, getDestroyed: (/** @type {typeof list[number] | typeof all} */ c) => c.tree.realized.sopr.valueDestroyed.sum._1w },
|
|
{ name: "30d", getCreated: (/** @type {typeof list[number] | typeof all} */ c) => c.tree.realized.sopr.valueCreated.sum._1m, getDestroyed: (/** @type {typeof list[number] | typeof all} */ c) => c.tree.realized.sopr.valueDestroyed.sum._1m },
|
|
{ name: "1y", getCreated: (/** @type {typeof list[number] | typeof all} */ c) => c.tree.realized.sopr.valueCreated.sum._1y, getDestroyed: (/** @type {typeof list[number] | typeof all} */ c) => c.tree.realized.sopr.valueDestroyed.sum._1y },
|
|
];
|
|
}
|
|
|
|
// ============================================================================
|
|
// Grouped Activity Sections
|
|
// ============================================================================
|
|
|
|
/**
|
|
* @param {{ list: readonly CohortFull[], all: CohortAll, title: (name: string) => string }} args
|
|
* @returns {PartialOptionsGroup}
|
|
*/
|
|
export function createGroupedActivitySectionWithAdjusted({ list, all, title }) {
|
|
return {
|
|
name: "Activity",
|
|
tree: [
|
|
{
|
|
name: "Volume",
|
|
title: title("Sent Volume"),
|
|
bottom: flatMapCohortsWithAll(list, all, ({ name, color, tree }) => [
|
|
line({ series: tree.activity.transferVolume.sum._24h.sats, name, color, unit: Unit.sats }),
|
|
]),
|
|
},
|
|
{
|
|
name: "SOPR",
|
|
tree: [
|
|
{
|
|
name: "Normal",
|
|
tree: [
|
|
...groupedSoprCharts(
|
|
list, all,
|
|
(c) => c.tree.realized.sopr.ratio._24h,
|
|
(c) => c.tree.realized.sopr.ratio._1w,
|
|
(c) => c.tree.realized.sopr.ratio._1m,
|
|
title,
|
|
),
|
|
{
|
|
name: "Rolling",
|
|
tree: groupedRollingSoprCharts(
|
|
list, all,
|
|
(c) => c.tree.realized.sopr.ratio._24h,
|
|
(c) => c.tree.realized.sopr.ratio._1w,
|
|
(c) => c.tree.realized.sopr.ratio._1m,
|
|
(c) => c.tree.realized.sopr.ratio._1y,
|
|
title,
|
|
),
|
|
},
|
|
],
|
|
},
|
|
{
|
|
name: "Adjusted",
|
|
tree: [
|
|
...groupedSoprCharts(
|
|
list, all,
|
|
(c) => c.tree.realized.sopr.adjusted.ratio._24h,
|
|
(c) => c.tree.realized.sopr.adjusted.ratio._1w,
|
|
(c) => c.tree.realized.sopr.adjusted.ratio._1m,
|
|
title,
|
|
"Adjusted ",
|
|
),
|
|
{
|
|
name: "Rolling",
|
|
tree: groupedRollingSoprCharts(
|
|
list, all,
|
|
(c) => c.tree.realized.sopr.adjusted.ratio._24h,
|
|
(c) => c.tree.realized.sopr.adjusted.ratio._1w,
|
|
(c) => c.tree.realized.sopr.adjusted.ratio._1m,
|
|
(c) => c.tree.realized.sopr.adjusted.ratio._1y,
|
|
title,
|
|
"Adjusted ",
|
|
),
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
name: "Sell Side Risk",
|
|
tree: [
|
|
{ name: "24h", title: title("Sell Side Risk (24h)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ series: tree.realized.sellSideRiskRatio._24h.ratio, name, color, unit: Unit.ratio })) },
|
|
{ name: "7d", title: title("Sell Side Risk (7d)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ series: tree.realized.sellSideRiskRatio._1w.ratio, name, color, unit: Unit.ratio })) },
|
|
{ name: "30d", title: title("Sell Side Risk (30d)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ series: tree.realized.sellSideRiskRatio._1m.ratio, name, color, unit: Unit.ratio })) },
|
|
{ name: "1y", title: title("Sell Side Risk (1y)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ series: tree.realized.sellSideRiskRatio._1y.ratio, name, color, unit: Unit.ratio })) },
|
|
],
|
|
},
|
|
{
|
|
name: "Value",
|
|
tree: [
|
|
{
|
|
name: "Flows",
|
|
tree: [
|
|
{ name: "Distribution", title: title("Distribution Flow"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ series: tree.realized.profit.distributionFlow, name, color, unit: Unit.usd })) },
|
|
{ name: "Capitulation", title: title("Capitulation Flow"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ series: tree.realized.loss.capitulationFlow, name, color, unit: Unit.usd })) },
|
|
],
|
|
},
|
|
{ name: "Created", title: title("Value Created"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ series: tree.realized.sopr.valueCreated.base, name, color, unit: Unit.usd })) },
|
|
{ name: "Destroyed", title: title("Value Destroyed"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ series: tree.realized.sopr.valueDestroyed.base, name, color, unit: Unit.usd })) },
|
|
{
|
|
name: "Rolling",
|
|
tree: [
|
|
{
|
|
name: "Normal",
|
|
tree: groupedRollingValueCharts(list, all, valueWindows(list, all), title),
|
|
},
|
|
{
|
|
name: "Adjusted",
|
|
tree: groupedRollingValueCharts(
|
|
list, all,
|
|
[
|
|
{ name: "24h", getCreated: (c) => c.tree.realized.sopr.adjusted.valueCreated.sum._24h, getDestroyed: (c) => c.tree.realized.sopr.adjusted.valueDestroyed.sum._24h },
|
|
{ name: "7d", getCreated: (c) => c.tree.realized.sopr.adjusted.valueCreated.sum._1w, getDestroyed: (c) => c.tree.realized.sopr.adjusted.valueDestroyed.sum._1w },
|
|
{ name: "30d", getCreated: (c) => c.tree.realized.sopr.adjusted.valueCreated.sum._1m, getDestroyed: (c) => c.tree.realized.sopr.adjusted.valueDestroyed.sum._1m },
|
|
{ name: "1y", getCreated: (c) => c.tree.realized.sopr.adjusted.valueCreated.sum._1y, getDestroyed: (c) => c.tree.realized.sopr.adjusted.valueDestroyed.sum._1y },
|
|
],
|
|
title,
|
|
"Adjusted ",
|
|
),
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
name: "Coins Destroyed",
|
|
title: title("Coindays Destroyed"),
|
|
bottom: flatMapCohortsWithAll(list, all, ({ name, color, tree }) => [
|
|
line({ series: tree.activity.coindaysDestroyed.sum._24h, name, color, unit: Unit.coindays }),
|
|
]),
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Grouped activity for cohorts with rolling SOPR + sell side risk (LTH-like)
|
|
* @param {{ list: readonly (CohortFull | CohortLongTerm)[], all: CohortAll, title: (name: string) => string }} args
|
|
* @returns {PartialOptionsGroup}
|
|
*/
|
|
export function createGroupedActivitySection({ list, all, title }) {
|
|
return {
|
|
name: "Activity",
|
|
tree: [
|
|
{
|
|
name: "Volume",
|
|
title: title("Sent Volume"),
|
|
bottom: flatMapCohortsWithAll(list, all, ({ name, color, tree }) => [
|
|
line({ series: tree.activity.transferVolume.sum._24h.sats, name, color, unit: Unit.sats }),
|
|
]),
|
|
},
|
|
{
|
|
name: "SOPR",
|
|
tree: [
|
|
...groupedSoprCharts(
|
|
list, all,
|
|
(c) => c.tree.realized.sopr.ratio._24h,
|
|
(c) => c.tree.realized.sopr.ratio._1w,
|
|
(c) => c.tree.realized.sopr.ratio._1m,
|
|
title,
|
|
),
|
|
{
|
|
name: "Rolling",
|
|
tree: groupedRollingSoprCharts(
|
|
list, all,
|
|
(c) => c.tree.realized.sopr.ratio._24h,
|
|
(c) => c.tree.realized.sopr.ratio._1w,
|
|
(c) => c.tree.realized.sopr.ratio._1m,
|
|
(c) => c.tree.realized.sopr.ratio._1y,
|
|
title,
|
|
),
|
|
},
|
|
],
|
|
},
|
|
{
|
|
name: "Sell Side Risk",
|
|
tree: [
|
|
{ name: "24h", title: title("Sell Side Risk (24h)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ series: tree.realized.sellSideRiskRatio._24h.ratio, name, color, unit: Unit.ratio })) },
|
|
{ name: "7d", title: title("Sell Side Risk (7d)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ series: tree.realized.sellSideRiskRatio._1w.ratio, name, color, unit: Unit.ratio })) },
|
|
{ name: "30d", title: title("Sell Side Risk (30d)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ series: tree.realized.sellSideRiskRatio._1m.ratio, name, color, unit: Unit.ratio })) },
|
|
{ name: "1y", title: title("Sell Side Risk (1y)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ series: tree.realized.sellSideRiskRatio._1y.ratio, name, color, unit: Unit.ratio })) },
|
|
],
|
|
},
|
|
{
|
|
name: "Value",
|
|
tree: [
|
|
{
|
|
name: "Flows",
|
|
tree: [
|
|
{ name: "Distribution", title: title("Distribution Flow"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ series: tree.realized.profit.distributionFlow, name, color, unit: Unit.usd })) },
|
|
{ name: "Capitulation", title: title("Capitulation Flow"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ series: tree.realized.loss.capitulationFlow, name, color, unit: Unit.usd })) },
|
|
],
|
|
},
|
|
{ name: "Created", title: title("Value Created"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ series: tree.realized.sopr.valueCreated.base, name, color, unit: Unit.usd })) },
|
|
{ name: "Destroyed", title: title("Value Destroyed"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ series: tree.realized.sopr.valueDestroyed.base, name, color, unit: Unit.usd })) },
|
|
{
|
|
name: "Rolling",
|
|
tree: groupedRollingValueCharts(list, all, valueWindows(list, all), title),
|
|
},
|
|
],
|
|
},
|
|
{
|
|
name: "Coins Destroyed",
|
|
title: title("Coindays Destroyed"),
|
|
bottom: flatMapCohortsWithAll(list, all, ({ name, color, tree }) => [
|
|
line({ series: tree.activity.coindaysDestroyed.sum._24h, name, color, unit: Unit.coindays }),
|
|
]),
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Grouped activity for cohorts with activity but basic realized (AgeRange/MaxAge)
|
|
* @param {{ list: readonly (CohortAgeRange | CohortWithAdjusted)[], all: CohortAll, title: (name: string) => string }} args
|
|
* @returns {PartialOptionsGroup}
|
|
*/
|
|
export function createGroupedActivitySectionWithActivity({ list, all, title }) {
|
|
return {
|
|
name: "Activity",
|
|
tree: [
|
|
{
|
|
name: "Volume",
|
|
title: title("Sent Volume"),
|
|
bottom: flatMapCohortsWithAll(list, all, ({ name, color, tree }) => [
|
|
line({ series: tree.activity.transferVolume.sum._24h.sats, name, color, unit: Unit.sats }),
|
|
]),
|
|
},
|
|
{
|
|
name: "SOPR",
|
|
title: title("SOPR (24h)"),
|
|
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
|
baseline({ series: tree.realized.sopr.ratio._24h, name, color, unit: Unit.ratio, base: 1 }),
|
|
),
|
|
},
|
|
{
|
|
name: "Value",
|
|
tree: [
|
|
{ name: "Created", title: title("Value Created"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ series: tree.realized.sopr.valueCreated.base, name, color, unit: Unit.usd })) },
|
|
{ name: "Destroyed", title: title("Value Destroyed"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ series: tree.realized.sopr.valueDestroyed.base, name, color, unit: Unit.usd })) },
|
|
],
|
|
},
|
|
{
|
|
name: "Coins Destroyed",
|
|
title: title("Coindays Destroyed"),
|
|
bottom: flatMapCohortsWithAll(list, all, ({ name, color, tree }) => [
|
|
line({ series: tree.activity.coindaysDestroyed.sum._24h, name, color, unit: Unit.coindays }),
|
|
]),
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Grouped minimal activity (value only, no activity field)
|
|
* @param {{ list: readonly (UtxoCohortObject | CohortWithoutRelative | CohortAddr | AddrCohortObject)[], all: CohortAll, title: (name: string) => string }} args
|
|
* @returns {PartialOptionsGroup}
|
|
*/
|
|
export function createGroupedActivitySectionMinimal({ list, all, title }) {
|
|
return {
|
|
name: "Activity",
|
|
tree: [
|
|
{ name: "Value Created", title: title("Value Created"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ series: tree.realized.sopr.valueCreated.base, name, color, unit: Unit.usd })) },
|
|
{ name: "Value Destroyed", title: title("Value Destroyed"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ series: tree.realized.sopr.valueDestroyed.base, name, color, unit: Unit.usd })) },
|
|
],
|
|
};
|
|
}
|