mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-04-25 07:09:59 -07:00
1151 lines
32 KiB
JavaScript
1151 lines
32 KiB
JavaScript
/**
|
|
* Address cohort folder builder
|
|
* Creates option trees for address-based cohorts (has addrCount)
|
|
* Address cohorts use _0satsPattern which has CostBasisPattern (no percentiles)
|
|
*/
|
|
|
|
import { Unit } from "../../utils/units.js";
|
|
import { priceLine } from "../constants.js";
|
|
import { line, baseline, price } from "../series.js";
|
|
import { formatCohortTitle, satsBtcUsd } from "../shared.js";
|
|
import {
|
|
createSingleSupplySeries,
|
|
createGroupedSupplySection,
|
|
createUtxoCountSeries,
|
|
createAddressCountSeries,
|
|
createRealizedPriceSeries,
|
|
createRealizedPriceRatioSeries,
|
|
createSingleCoinsDestroyedSeries,
|
|
createGroupedCoinblocksDestroyedSeries,
|
|
createGroupedCoindaysDestroyedSeries,
|
|
createSingleSentSeries,
|
|
groupedSupplyRelativeGenerators,
|
|
createSingleSupplyRelativeOptions,
|
|
createSingleSellSideRiskSeries,
|
|
createSingleValueCreatedDestroyedSeries,
|
|
createSingleValueFlowBreakdownSeries,
|
|
createSingleCapitulationProfitFlowSeries,
|
|
createSingleSoprSeries,
|
|
createSingleInvestorPriceSeries,
|
|
createSingleInvestorPriceRatioSeries,
|
|
createInvestorPriceSeries,
|
|
createInvestorPriceRatioSeries,
|
|
} 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 = formatCohortTitle(args.name);
|
|
|
|
return {
|
|
name: args.name || "all",
|
|
tree: [
|
|
// Supply section
|
|
isSingle
|
|
? {
|
|
name: "Supply",
|
|
title: title("Supply"),
|
|
bottom: createSingleSupplySeries(
|
|
ctx,
|
|
/** @type {AddressCohortObject} */ (args),
|
|
createSingleSupplyRelativeOptions(
|
|
ctx,
|
|
/** @type {AddressCohortObject} */ (args),
|
|
),
|
|
),
|
|
}
|
|
: createGroupedSupplySection(
|
|
list,
|
|
title,
|
|
groupedSupplyRelativeGenerators,
|
|
),
|
|
|
|
// UTXO count
|
|
{
|
|
name: "UTXO Count",
|
|
title: title("UTXO Count"),
|
|
bottom: createUtxoCountSeries(list, useGroupName),
|
|
},
|
|
|
|
// Address count (ADDRESS COHORTS ONLY - fully type safe!)
|
|
{
|
|
name: "Address Count",
|
|
title: title("Address Count"),
|
|
bottom: createAddressCountSeries(ctx, list, useGroupName),
|
|
},
|
|
|
|
// Realized section
|
|
{
|
|
name: "Realized",
|
|
tree: [
|
|
...(useGroupName
|
|
? [
|
|
{
|
|
name: "Price",
|
|
title: title("Realized Price"),
|
|
top: createRealizedPriceSeries(list),
|
|
},
|
|
{
|
|
name: "Ratio",
|
|
title: title("Realized Price Ratio"),
|
|
bottom: createRealizedPriceRatioSeries(list),
|
|
},
|
|
{
|
|
name: "Investor Price",
|
|
tree: [
|
|
{
|
|
name: "Price",
|
|
title: title("Investor Price"),
|
|
top: createInvestorPriceSeries(list),
|
|
},
|
|
{
|
|
name: "Ratio",
|
|
title: title("Investor Price Ratio"),
|
|
bottom: createInvestorPriceRatioSeries(list),
|
|
},
|
|
],
|
|
},
|
|
]
|
|
: createRealizedPriceOptions(
|
|
/** @type {AddressCohortObject} */ (args),
|
|
title,
|
|
)),
|
|
{
|
|
name: "Capitalization",
|
|
title: title("Realized Cap"),
|
|
bottom: createRealizedCapWithExtras(ctx, list, args, useGroupName),
|
|
},
|
|
{
|
|
name: "Value",
|
|
title: title("Realized Value"),
|
|
bottom: list.map(({ color, name, tree }) =>
|
|
line({
|
|
metric: tree.realized.realizedValue,
|
|
name: useGroupName ? name : "Realized Value",
|
|
color,
|
|
unit: Unit.usd,
|
|
}),
|
|
),
|
|
},
|
|
...(useGroupName
|
|
? createGroupedRealizedPnlSection(ctx, list, title)
|
|
: createRealizedPnlSection(
|
|
ctx,
|
|
/** @type {AddressCohortObject} */ (args),
|
|
title,
|
|
)),
|
|
],
|
|
},
|
|
|
|
// Unrealized section
|
|
...createUnrealizedSection(ctx, list, useGroupName, title),
|
|
|
|
// Cost basis section (no percentiles for address cohorts)
|
|
...createCostBasisSection(list, useGroupName, title),
|
|
|
|
// Activity section
|
|
...createActivitySection(args, title),
|
|
],
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Create realized price options for single cohort
|
|
* @param {AddressCohortObject} args
|
|
* @param {(metric: string) => string} title
|
|
* @returns {PartialOptionsTree}
|
|
*/
|
|
function createRealizedPriceOptions(args, title) {
|
|
const { tree, color } = args;
|
|
|
|
return [
|
|
{
|
|
name: "Price",
|
|
title: title("Realized Price"),
|
|
top: [
|
|
price({
|
|
metric: tree.realized.realizedPrice,
|
|
name: "Realized",
|
|
color,
|
|
}),
|
|
],
|
|
},
|
|
{
|
|
name: "Investor Price",
|
|
tree: [
|
|
{
|
|
name: "Price",
|
|
title: title("Investor Price"),
|
|
top: createSingleInvestorPriceSeries(tree, color),
|
|
},
|
|
{
|
|
name: "Ratio",
|
|
title: title("Investor Price Ratio"),
|
|
bottom: createSingleInvestorPriceRatioSeries(tree, color),
|
|
},
|
|
],
|
|
},
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Create realized cap with extras
|
|
* @param {PartialContext} ctx
|
|
* @param {readonly AddressCohortObject[]} list
|
|
* @param {AddressCohortObject | AddressCohortGroupObject} args
|
|
* @param {boolean} useGroupName
|
|
* @returns {AnyFetchedSeriesBlueprint[]}
|
|
*/
|
|
function createRealizedCapWithExtras(ctx, list, args, useGroupName) {
|
|
const isSingle = !("list" in args);
|
|
|
|
return list.flatMap(({ color, name, tree }) => [
|
|
line({
|
|
metric: tree.realized.realizedCap,
|
|
name: useGroupName ? name : "Capitalization",
|
|
color,
|
|
unit: Unit.usd,
|
|
}),
|
|
...(isSingle
|
|
? [
|
|
baseline({
|
|
metric: tree.realized.realizedCap30dDelta,
|
|
name: "30d Change",
|
|
unit: 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 {(metric: string) => string} title
|
|
* @returns {PartialOptionsTree}
|
|
*/
|
|
function createRealizedPnlSection(ctx, args, title) {
|
|
const { colors } = ctx;
|
|
const { realized } = args.tree;
|
|
|
|
return [
|
|
{
|
|
name: "P&L",
|
|
title: title("Realized P&L"),
|
|
bottom: [
|
|
line({
|
|
metric: realized.realizedProfit.sum,
|
|
name: "Profit",
|
|
color: colors.green,
|
|
unit: Unit.usd,
|
|
}),
|
|
line({
|
|
metric: realized.realizedProfit7dEma,
|
|
name: "Profit 7d EMA",
|
|
color: colors.green,
|
|
unit: Unit.usd,
|
|
}),
|
|
line({
|
|
metric: realized.realizedProfit.cumulative,
|
|
name: "Profit Cumulative",
|
|
color: colors.green,
|
|
unit: Unit.usd,
|
|
defaultActive: false,
|
|
}),
|
|
line({
|
|
metric: realized.realizedLoss.sum,
|
|
name: "Loss",
|
|
color: colors.red,
|
|
unit: Unit.usd,
|
|
}),
|
|
line({
|
|
metric: realized.realizedLoss7dEma,
|
|
name: "Loss 7d EMA",
|
|
color: colors.red,
|
|
unit: Unit.usd,
|
|
}),
|
|
line({
|
|
metric: realized.realizedLoss.cumulative,
|
|
name: "Loss Cumulative",
|
|
color: colors.red,
|
|
unit: Unit.usd,
|
|
defaultActive: false,
|
|
}),
|
|
line({
|
|
metric: realized.negRealizedLoss.sum,
|
|
name: "Negative Loss",
|
|
color: colors.red,
|
|
unit: Unit.usd,
|
|
defaultActive: false,
|
|
}),
|
|
line({
|
|
metric: realized.negRealizedLoss.cumulative,
|
|
name: "Negative Loss Cumulative",
|
|
color: colors.red,
|
|
unit: Unit.usd,
|
|
defaultActive: false,
|
|
}),
|
|
line({
|
|
metric: realized.totalRealizedPnl,
|
|
name: "Total",
|
|
color: colors.default,
|
|
unit: Unit.usd,
|
|
defaultActive: false,
|
|
}),
|
|
baseline({
|
|
metric: realized.realizedProfitRelToRealizedCap.sum,
|
|
name: "Profit",
|
|
color: colors.green,
|
|
unit: Unit.pctRcap,
|
|
}),
|
|
baseline({
|
|
metric: realized.realizedProfitRelToRealizedCap.cumulative,
|
|
name: "Profit Cumulative",
|
|
color: colors.green,
|
|
unit: Unit.pctRcap,
|
|
defaultActive: false,
|
|
}),
|
|
baseline({
|
|
metric: realized.realizedLossRelToRealizedCap.sum,
|
|
name: "Loss",
|
|
color: colors.red,
|
|
unit: Unit.pctRcap,
|
|
}),
|
|
baseline({
|
|
metric: realized.realizedLossRelToRealizedCap.cumulative,
|
|
name: "Loss Cumulative",
|
|
color: colors.red,
|
|
unit: Unit.pctRcap,
|
|
defaultActive: false,
|
|
}),
|
|
],
|
|
},
|
|
{
|
|
name: "Net pnl",
|
|
title: title("Net Realized P&L"),
|
|
bottom: [
|
|
baseline({
|
|
metric: realized.netRealizedPnl.sum,
|
|
name: "Net",
|
|
unit: Unit.usd,
|
|
}),
|
|
baseline({
|
|
metric: realized.netRealizedPnl7dEma,
|
|
name: "Net 7d EMA",
|
|
unit: Unit.usd,
|
|
}),
|
|
baseline({
|
|
metric: realized.netRealizedPnl.cumulative,
|
|
name: "Net Cumulative",
|
|
unit: Unit.usd,
|
|
defaultActive: false,
|
|
}),
|
|
baseline({
|
|
metric: realized.netRealizedPnlCumulative30dDelta,
|
|
name: "Cumulative 30d Change",
|
|
unit: Unit.usd,
|
|
defaultActive: false,
|
|
}),
|
|
baseline({
|
|
metric: realized.netRealizedPnlRelToRealizedCap.sum,
|
|
name: "Net",
|
|
unit: Unit.pctRcap,
|
|
}),
|
|
baseline({
|
|
metric: realized.netRealizedPnlRelToRealizedCap.cumulative,
|
|
name: "Net Cumulative",
|
|
unit: Unit.pctRcap,
|
|
defaultActive: false,
|
|
}),
|
|
baseline({
|
|
metric: realized.netRealizedPnlCumulative30dDeltaRelToRealizedCap,
|
|
name: "Cumulative 30d Change",
|
|
unit: Unit.pctRcap,
|
|
defaultActive: false,
|
|
}),
|
|
baseline({
|
|
metric: realized.netRealizedPnlCumulative30dDeltaRelToMarketCap,
|
|
name: "Cumulative 30d Change",
|
|
unit: Unit.pctMcap,
|
|
}),
|
|
priceLine({
|
|
ctx,
|
|
unit: Unit.usd,
|
|
number: 1,
|
|
}),
|
|
priceLine({
|
|
ctx,
|
|
unit: Unit.pctMcap,
|
|
}),
|
|
priceLine({
|
|
ctx,
|
|
unit: Unit.pctRcap,
|
|
}),
|
|
],
|
|
},
|
|
{
|
|
name: "SOPR",
|
|
title: title("SOPR"),
|
|
bottom: [
|
|
...createSingleSoprSeries(colors, args.tree),
|
|
priceLine({
|
|
ctx,
|
|
unit: Unit.ratio,
|
|
number: 1,
|
|
}),
|
|
],
|
|
},
|
|
{
|
|
name: "Sell Side Risk",
|
|
title: title("Sell Side Risk Ratio"),
|
|
bottom: createSingleSellSideRiskSeries(colors, args.tree),
|
|
},
|
|
{
|
|
name: "Value",
|
|
tree: [
|
|
{
|
|
name: "Created & Destroyed",
|
|
title: title("Value Created & Destroyed"),
|
|
bottom: createSingleValueCreatedDestroyedSeries(colors, args.tree),
|
|
},
|
|
{
|
|
name: "Breakdown",
|
|
title: title("Value Flow Breakdown"),
|
|
bottom: createSingleValueFlowBreakdownSeries(colors, args.tree),
|
|
},
|
|
{
|
|
name: "Flow",
|
|
title: title("Capitulation & Profit Flow"),
|
|
bottom: createSingleCapitulationProfitFlowSeries(colors, args.tree),
|
|
},
|
|
],
|
|
},
|
|
{
|
|
name: "Peak Regret",
|
|
title: title("Peak Regret"),
|
|
bottom: [
|
|
line({
|
|
metric: realized.peakRegret.sum,
|
|
name: "Sum",
|
|
color: colors.red,
|
|
unit: Unit.usd,
|
|
}),
|
|
line({
|
|
metric: realized.peakRegret.cumulative,
|
|
name: "Cumulative",
|
|
color: colors.red,
|
|
unit: Unit.usd,
|
|
defaultActive: false,
|
|
}),
|
|
baseline({
|
|
metric: realized.peakRegretRelToRealizedCap,
|
|
name: "Rel. to Realized Cap",
|
|
color: colors.orange,
|
|
unit: Unit.pctRcap,
|
|
}),
|
|
],
|
|
},
|
|
{
|
|
name: "Sent In P/L",
|
|
tree: [
|
|
{
|
|
name: "In Profit",
|
|
title: title("Sent In Profit"),
|
|
bottom: [
|
|
line({
|
|
metric: realized.sentInProfit.bitcoin.sum,
|
|
name: "Sum",
|
|
color: colors.green,
|
|
unit: Unit.btc,
|
|
}),
|
|
line({
|
|
metric: realized.sentInProfit.bitcoin.cumulative,
|
|
name: "Cumulative",
|
|
color: colors.green,
|
|
unit: Unit.btc,
|
|
defaultActive: false,
|
|
}),
|
|
line({
|
|
metric: realized.sentInProfit.sats.sum,
|
|
name: "Sum",
|
|
color: colors.green,
|
|
unit: Unit.sats,
|
|
}),
|
|
line({
|
|
metric: realized.sentInProfit.sats.cumulative,
|
|
name: "Cumulative",
|
|
color: colors.green,
|
|
unit: Unit.sats,
|
|
defaultActive: false,
|
|
}),
|
|
line({
|
|
metric: realized.sentInProfit.dollars.sum,
|
|
name: "Sum",
|
|
color: colors.green,
|
|
unit: Unit.usd,
|
|
}),
|
|
line({
|
|
metric: realized.sentInProfit.dollars.cumulative,
|
|
name: "Cumulative",
|
|
color: colors.green,
|
|
unit: Unit.usd,
|
|
defaultActive: false,
|
|
}),
|
|
],
|
|
},
|
|
{
|
|
name: "In Loss",
|
|
title: title("Sent In Loss"),
|
|
bottom: [
|
|
line({
|
|
metric: realized.sentInLoss.bitcoin.sum,
|
|
name: "Sum",
|
|
color: colors.red,
|
|
unit: Unit.btc,
|
|
}),
|
|
line({
|
|
metric: realized.sentInLoss.bitcoin.cumulative,
|
|
name: "Cumulative",
|
|
color: colors.red,
|
|
unit: Unit.btc,
|
|
defaultActive: false,
|
|
}),
|
|
line({
|
|
metric: realized.sentInLoss.sats.sum,
|
|
name: "Sum",
|
|
color: colors.red,
|
|
unit: Unit.sats,
|
|
}),
|
|
line({
|
|
metric: realized.sentInLoss.sats.cumulative,
|
|
name: "Cumulative",
|
|
color: colors.red,
|
|
unit: Unit.sats,
|
|
defaultActive: false,
|
|
}),
|
|
line({
|
|
metric: realized.sentInLoss.dollars.sum,
|
|
name: "Sum",
|
|
color: colors.red,
|
|
unit: Unit.usd,
|
|
}),
|
|
line({
|
|
metric: realized.sentInLoss.dollars.cumulative,
|
|
name: "Cumulative",
|
|
color: colors.red,
|
|
unit: Unit.usd,
|
|
defaultActive: false,
|
|
}),
|
|
],
|
|
},
|
|
{
|
|
name: "In Profit 14d EMA",
|
|
title: title("Sent In Profit 14d EMA"),
|
|
bottom: satsBtcUsd({
|
|
pattern: realized.sentInProfit14dEma,
|
|
name: "14d EMA",
|
|
color: colors.green,
|
|
}),
|
|
},
|
|
{
|
|
name: "In Loss 14d EMA",
|
|
title: title("Sent In Loss 14d EMA"),
|
|
bottom: satsBtcUsd({
|
|
pattern: realized.sentInLoss14dEma,
|
|
name: "14d EMA",
|
|
color: colors.red,
|
|
}),
|
|
},
|
|
],
|
|
},
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Create grouped realized P&L section for address cohorts (for compare view)
|
|
* @param {PartialContext} ctx
|
|
* @param {readonly AddressCohortObject[]} list
|
|
* @param {(metric: string) => string} title
|
|
* @returns {PartialOptionsTree}
|
|
*/
|
|
function createGroupedRealizedPnlSection(ctx, list, title) {
|
|
const pnlConfigs = /** @type {const} */ ([
|
|
{
|
|
name: "Profit",
|
|
sum: "realizedProfit",
|
|
ema: "realizedProfit7dEma",
|
|
rel: "realizedProfitRelToRealizedCap",
|
|
isNet: false,
|
|
},
|
|
{
|
|
name: "Loss",
|
|
sum: "realizedLoss",
|
|
ema: "realizedLoss7dEma",
|
|
rel: "realizedLossRelToRealizedCap",
|
|
isNet: false,
|
|
},
|
|
{
|
|
name: "Net P&L",
|
|
sum: "netRealizedPnl",
|
|
ema: "netRealizedPnl7dEma",
|
|
rel: "netRealizedPnlRelToRealizedCap",
|
|
isNet: true,
|
|
},
|
|
]);
|
|
|
|
return [
|
|
...pnlConfigs.map(({ name, sum, ema, rel, isNet }) => ({
|
|
name,
|
|
tree: [
|
|
{
|
|
name: "Sum",
|
|
title: title(`Realized ${name}`),
|
|
bottom: [
|
|
...list.flatMap(({ color, name, tree }) => [
|
|
(isNet ? baseline : line)({
|
|
metric: tree.realized[sum].sum,
|
|
name,
|
|
color,
|
|
unit: Unit.usd,
|
|
}),
|
|
baseline({
|
|
metric: tree.realized[rel].sum,
|
|
name,
|
|
color,
|
|
unit: Unit.pctRcap,
|
|
}),
|
|
]),
|
|
],
|
|
},
|
|
{
|
|
name: "7d EMA",
|
|
title: title(`Realized ${name} 7d EMA`),
|
|
bottom: [
|
|
...list.map(({ color, name, tree }) =>
|
|
(isNet ? baseline : line)({
|
|
metric: tree.realized[ema],
|
|
name,
|
|
color,
|
|
unit: Unit.usd,
|
|
}),
|
|
),
|
|
],
|
|
},
|
|
],
|
|
})),
|
|
{
|
|
name: "Peak Regret",
|
|
tree: [
|
|
{
|
|
name: "Sum",
|
|
title: title("Peak Regret"),
|
|
bottom: list.flatMap(({ color, name, tree }) => [
|
|
line({
|
|
metric: tree.realized.peakRegret.sum,
|
|
name,
|
|
color,
|
|
unit: Unit.usd,
|
|
}),
|
|
]),
|
|
},
|
|
{
|
|
name: "Cumulative",
|
|
title: title("Peak Regret Cumulative"),
|
|
bottom: list.flatMap(({ color, name, tree }) => [
|
|
line({
|
|
metric: tree.realized.peakRegret.cumulative,
|
|
name,
|
|
color,
|
|
unit: Unit.usd,
|
|
}),
|
|
]),
|
|
},
|
|
{
|
|
name: "Rel. to Realized Cap",
|
|
title: title("Peak Regret Rel. to Realized Cap"),
|
|
bottom: list.flatMap(({ color, name, tree }) => [
|
|
baseline({
|
|
metric: tree.realized.peakRegretRelToRealizedCap,
|
|
name,
|
|
color,
|
|
unit: Unit.pctRcap,
|
|
}),
|
|
]),
|
|
},
|
|
],
|
|
},
|
|
{
|
|
name: "Sent In P/L",
|
|
tree: [
|
|
{
|
|
name: "In Profit",
|
|
title: title("Sent In Profit"),
|
|
bottom: list.flatMap(({ color, name, tree }) => [
|
|
line({
|
|
metric: tree.realized.sentInProfit.bitcoin.sum,
|
|
name,
|
|
color,
|
|
unit: Unit.btc,
|
|
}),
|
|
line({
|
|
metric: tree.realized.sentInProfit.sats.sum,
|
|
name,
|
|
color,
|
|
unit: Unit.sats,
|
|
}),
|
|
line({
|
|
metric: tree.realized.sentInProfit.dollars.sum,
|
|
name,
|
|
color,
|
|
unit: Unit.usd,
|
|
}),
|
|
]),
|
|
},
|
|
{
|
|
name: "In Profit Cumulative",
|
|
title: title("Sent In Profit Cumulative"),
|
|
bottom: list.flatMap(({ color, name, tree }) => [
|
|
line({
|
|
metric: tree.realized.sentInProfit.bitcoin.cumulative,
|
|
name,
|
|
color,
|
|
unit: Unit.btc,
|
|
}),
|
|
line({
|
|
metric: tree.realized.sentInProfit.sats.cumulative,
|
|
name,
|
|
color,
|
|
unit: Unit.sats,
|
|
}),
|
|
line({
|
|
metric: tree.realized.sentInProfit.dollars.cumulative,
|
|
name,
|
|
color,
|
|
unit: Unit.usd,
|
|
}),
|
|
]),
|
|
},
|
|
{
|
|
name: "In Loss",
|
|
title: title("Sent In Loss"),
|
|
bottom: list.flatMap(({ color, name, tree }) => [
|
|
line({
|
|
metric: tree.realized.sentInLoss.bitcoin.sum,
|
|
name,
|
|
color,
|
|
unit: Unit.btc,
|
|
}),
|
|
line({
|
|
metric: tree.realized.sentInLoss.sats.sum,
|
|
name,
|
|
color,
|
|
unit: Unit.sats,
|
|
}),
|
|
line({
|
|
metric: tree.realized.sentInLoss.dollars.sum,
|
|
name,
|
|
color,
|
|
unit: Unit.usd,
|
|
}),
|
|
]),
|
|
},
|
|
{
|
|
name: "In Loss Cumulative",
|
|
title: title("Sent In Loss Cumulative"),
|
|
bottom: list.flatMap(({ color, name, tree }) => [
|
|
line({
|
|
metric: tree.realized.sentInLoss.bitcoin.cumulative,
|
|
name,
|
|
color,
|
|
unit: Unit.btc,
|
|
}),
|
|
line({
|
|
metric: tree.realized.sentInLoss.sats.cumulative,
|
|
name,
|
|
color,
|
|
unit: Unit.sats,
|
|
}),
|
|
line({
|
|
metric: tree.realized.sentInLoss.dollars.cumulative,
|
|
name,
|
|
color,
|
|
unit: Unit.usd,
|
|
}),
|
|
]),
|
|
},
|
|
{
|
|
name: "In Profit 14d EMA",
|
|
title: title("Sent In Profit 14d EMA"),
|
|
bottom: list.flatMap(({ color, name, tree }) =>
|
|
satsBtcUsd({
|
|
pattern: tree.realized.sentInProfit14dEma,
|
|
name,
|
|
color,
|
|
}),
|
|
),
|
|
},
|
|
{
|
|
name: "In Loss 14d EMA",
|
|
title: title("Sent In Loss 14d EMA"),
|
|
bottom: list.flatMap(({ color, name, tree }) =>
|
|
satsBtcUsd({
|
|
pattern: tree.realized.sentInLoss14dEma,
|
|
name,
|
|
color,
|
|
}),
|
|
),
|
|
},
|
|
],
|
|
},
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Create unrealized section
|
|
* @param {PartialContext} ctx
|
|
* @param {readonly AddressCohortObject[]} list
|
|
* @param {boolean} useGroupName
|
|
* @param {(metric: string) => string} title
|
|
* @returns {PartialOptionsTree}
|
|
*/
|
|
function createUnrealizedSection(ctx, list, useGroupName, title) {
|
|
const { colors } = ctx;
|
|
|
|
return [
|
|
{
|
|
name: "Unrealized",
|
|
tree: [
|
|
{
|
|
name: "Profit",
|
|
title: title("Unrealized Profit"),
|
|
bottom: list.flatMap(({ color, name, tree }) => [
|
|
line({
|
|
metric: tree.unrealized.unrealizedProfit,
|
|
name: useGroupName ? name : "Profit",
|
|
color: useGroupName ? color : colors.green,
|
|
unit: Unit.usd,
|
|
}),
|
|
]),
|
|
},
|
|
{
|
|
name: "Loss",
|
|
title: title("Unrealized Loss"),
|
|
bottom: list.flatMap(({ color, name, tree }) => [
|
|
line({
|
|
metric: tree.unrealized.unrealizedLoss,
|
|
name: useGroupName ? name : "Loss",
|
|
color: useGroupName ? color : colors.red,
|
|
unit: Unit.usd,
|
|
}),
|
|
]),
|
|
},
|
|
{
|
|
name: "Total P&L",
|
|
title: title("Total Unrealized P&L"),
|
|
bottom: list.flatMap(({ color, name, tree }) => [
|
|
baseline({
|
|
metric: tree.unrealized.totalUnrealizedPnl,
|
|
name: useGroupName ? name : "Total",
|
|
color: useGroupName ? color : undefined,
|
|
unit: Unit.usd,
|
|
}),
|
|
]),
|
|
},
|
|
{
|
|
name: "Negative Loss",
|
|
title: title("Negative Unrealized Loss"),
|
|
bottom: list.flatMap(({ color, name, tree }) => [
|
|
line({
|
|
metric: tree.unrealized.negUnrealizedLoss,
|
|
name: useGroupName ? name : "Negative Loss",
|
|
color: useGroupName ? color : colors.red,
|
|
unit: Unit.usd,
|
|
}),
|
|
]),
|
|
},
|
|
{
|
|
name: "Invested Capital",
|
|
tree: [
|
|
{
|
|
name: "In Profit",
|
|
title: title("Invested Capital In Profit"),
|
|
bottom: list.flatMap(({ color, name, tree }) => [
|
|
line({
|
|
metric: tree.unrealized.investedCapitalInProfit,
|
|
name: useGroupName ? name : "In Profit",
|
|
color: useGroupName ? color : colors.green,
|
|
unit: Unit.usd,
|
|
}),
|
|
]),
|
|
},
|
|
{
|
|
name: "In Loss",
|
|
title: title("Invested Capital In Loss"),
|
|
bottom: list.flatMap(({ color, name, tree }) => [
|
|
line({
|
|
metric: tree.unrealized.investedCapitalInLoss,
|
|
name: useGroupName ? name : "In Loss",
|
|
color: useGroupName ? color : colors.red,
|
|
unit: Unit.usd,
|
|
}),
|
|
]),
|
|
},
|
|
],
|
|
},
|
|
{
|
|
name: "Relative",
|
|
tree: [
|
|
{
|
|
name: "NUPL",
|
|
title: title("NUPL (Rel to Market Cap)"),
|
|
bottom: list.flatMap(({ color, name, tree }) => [
|
|
baseline({
|
|
metric: tree.relative.nupl,
|
|
name: useGroupName ? name : "NUPL",
|
|
color: useGroupName ? color : undefined,
|
|
unit: Unit.ratio,
|
|
options: { baseValue: { price: 0 } },
|
|
}),
|
|
]),
|
|
},
|
|
{
|
|
name: "Profit",
|
|
title: title("Unrealized Profit (% of Market Cap)"),
|
|
bottom: list.flatMap(({ color, name, tree }) => [
|
|
line({
|
|
metric: tree.relative.unrealizedProfitRelToMarketCap,
|
|
name: useGroupName ? name : "Profit",
|
|
color: useGroupName ? color : colors.green,
|
|
unit: Unit.pctMcap,
|
|
}),
|
|
]),
|
|
},
|
|
{
|
|
name: "Loss",
|
|
title: title("Unrealized Loss (% of Market Cap)"),
|
|
bottom: list.flatMap(({ color, name, tree }) => [
|
|
line({
|
|
metric: tree.relative.unrealizedLossRelToMarketCap,
|
|
name: useGroupName ? name : "Loss",
|
|
color: useGroupName ? color : colors.red,
|
|
unit: Unit.pctMcap,
|
|
}),
|
|
]),
|
|
},
|
|
{
|
|
name: "Net P&L",
|
|
title: title("Net Unrealized P&L (% of Market Cap)"),
|
|
bottom: list.flatMap(({ color, name, tree }) => [
|
|
baseline({
|
|
metric: tree.relative.netUnrealizedPnlRelToMarketCap,
|
|
name: useGroupName ? name : "Net",
|
|
color: useGroupName ? color : undefined,
|
|
unit: Unit.pctMcap,
|
|
}),
|
|
]),
|
|
},
|
|
{
|
|
name: "Negative Loss",
|
|
title: title("Negative Unrealized Loss (% of Market Cap)"),
|
|
bottom: list.flatMap(({ color, name, tree }) => [
|
|
line({
|
|
metric: tree.relative.negUnrealizedLossRelToMarketCap,
|
|
name: useGroupName ? name : "Negative Loss",
|
|
color: useGroupName ? color : colors.red,
|
|
unit: Unit.pctMcap,
|
|
}),
|
|
]),
|
|
},
|
|
{
|
|
name: "Invested Capital In Profit",
|
|
title: title("Invested Capital In Profit"),
|
|
bottom: list.flatMap(({ color, name, tree }) => [
|
|
baseline({
|
|
metric: tree.relative.investedCapitalInProfitPct,
|
|
name: useGroupName ? name : "In Profit",
|
|
color: useGroupName ? color : colors.green,
|
|
unit: Unit.pctRcap,
|
|
}),
|
|
]),
|
|
},
|
|
{
|
|
name: "Invested Capital In Loss",
|
|
title: title("Invested Capital In Loss"),
|
|
bottom: list.flatMap(({ color, name, tree }) => [
|
|
baseline({
|
|
metric: tree.relative.investedCapitalInLossPct,
|
|
name: useGroupName ? name : "In Loss",
|
|
color: useGroupName ? color : colors.red,
|
|
unit: Unit.pctRcap,
|
|
}),
|
|
]),
|
|
},
|
|
],
|
|
},
|
|
{
|
|
name: "NUPL",
|
|
title: title("Net Unrealized P&L"),
|
|
bottom: list.flatMap(({ color, name, tree }) => [
|
|
baseline({
|
|
metric: tree.unrealized.netUnrealizedPnl,
|
|
name: useGroupName ? name : "NUPL",
|
|
color: useGroupName ? color : undefined,
|
|
unit: Unit.ratio,
|
|
}),
|
|
priceLine({
|
|
ctx,
|
|
unit: Unit.ratio,
|
|
}),
|
|
]),
|
|
},
|
|
{
|
|
name: "Net Sentiment",
|
|
title: title("Net Sentiment"),
|
|
bottom: list.flatMap(({ color, name, tree }) => [
|
|
baseline({
|
|
metric: tree.unrealized.netSentiment,
|
|
name: useGroupName ? name : "Net Sentiment",
|
|
color: useGroupName ? color : undefined,
|
|
unit: Unit.usd,
|
|
}),
|
|
priceLine({
|
|
ctx,
|
|
unit: Unit.usd,
|
|
}),
|
|
]),
|
|
},
|
|
],
|
|
},
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Create cost basis section (no percentiles for address cohorts)
|
|
* @param {readonly AddressCohortObject[]} list
|
|
* @param {boolean} useGroupName
|
|
* @param {(metric: string) => string} title
|
|
* @returns {PartialOptionsTree}
|
|
*/
|
|
function createCostBasisSection(list, useGroupName, title) {
|
|
return [
|
|
{
|
|
name: "Cost Basis",
|
|
tree: [
|
|
{
|
|
name: "Min",
|
|
title: title("Min Cost Basis"),
|
|
top: list.map(({ color, name, tree }) =>
|
|
price({
|
|
metric: tree.costBasis.min,
|
|
name: useGroupName ? name : "Min",
|
|
color,
|
|
}),
|
|
),
|
|
},
|
|
{
|
|
name: "Max",
|
|
title: title("Max Cost Basis"),
|
|
top: list.map(({ color, name, tree }) =>
|
|
price({
|
|
metric: tree.costBasis.max,
|
|
name: useGroupName ? name : "Max",
|
|
color,
|
|
}),
|
|
),
|
|
},
|
|
],
|
|
},
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Create activity section
|
|
* @param {AddressCohortObject | AddressCohortGroupObject} args
|
|
* @param {(metric: string) => string} title
|
|
* @returns {PartialOptionsTree}
|
|
*/
|
|
function createActivitySection(args, title) {
|
|
const list = "list" in args ? args.list : [args];
|
|
const isSingle = !("list" in args);
|
|
|
|
// Single cohort: all metrics on one chart
|
|
if (isSingle) {
|
|
const cohort = /** @type {AddressCohortObject} */ (args);
|
|
return [
|
|
{
|
|
name: "Activity",
|
|
tree: [
|
|
{
|
|
name: "Coins Destroyed",
|
|
title: title("Coins Destroyed"),
|
|
bottom: createSingleCoinsDestroyedSeries(cohort),
|
|
},
|
|
{
|
|
name: "Sent",
|
|
title: title("Sent"),
|
|
bottom: createSingleSentSeries(cohort),
|
|
},
|
|
],
|
|
},
|
|
];
|
|
}
|
|
|
|
// Grouped cohorts: split charts for comparison
|
|
return [
|
|
{
|
|
name: "Activity",
|
|
tree: [
|
|
{
|
|
name: "Coinblocks Destroyed",
|
|
title: title("Coinblocks Destroyed"),
|
|
bottom: createGroupedCoinblocksDestroyedSeries(list),
|
|
},
|
|
{
|
|
name: "Coindays Destroyed",
|
|
title: title("Coindays Destroyed"),
|
|
bottom: createGroupedCoindaysDestroyedSeries(list),
|
|
},
|
|
{
|
|
name: "Sent",
|
|
tree: [
|
|
{
|
|
name: "Sum",
|
|
title: title("Sent"),
|
|
bottom: list.flatMap(({ color, name, tree }) =>
|
|
satsBtcUsd({
|
|
pattern: {
|
|
sats: tree.activity.sent.sats.sum,
|
|
bitcoin: tree.activity.sent.bitcoin.sum,
|
|
dollars: tree.activity.sent.dollars.sum,
|
|
},
|
|
name,
|
|
color,
|
|
}),
|
|
),
|
|
},
|
|
{
|
|
name: "14d EMA",
|
|
title: title("Sent 14d EMA"),
|
|
bottom: list.flatMap(({ color, name, tree }) =>
|
|
satsBtcUsd({ pattern: tree.activity.sent14dEma, name, color }),
|
|
),
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
];
|
|
}
|