Files
brk/website/scripts/options/distribution/cost-basis.js
2026-02-12 22:52:57 +01:00

366 lines
10 KiB
JavaScript

/**
* Cost Basis section builders
*
* Structure:
* - Summary: Key stats (avg + median active, quartiles/extremes available)
* - By Coin: BTC-weighted percentiles (IQR active: p25, p50, p75)
* - By Capital: USD-weighted percentiles (IQR active: p25, p50, p75)
* - Price Position: Spot percentile (both perspectives active)
*
* For cohorts WITHOUT percentiles: Summary only
*/
import { colors } from "../../utils/colors.js";
import { entries } from "../../utils/array.js";
import { Unit } from "../../utils/units.js";
import { priceLines } from "../constants.js";
import { line, price } from "../series.js";
import { mapCohortsWithAll } from "../shared.js";
const ACTIVE_PCTS = new Set(["pct75", "pct50", "pct25"]);
/**
* @param {PercentilesPattern} p
* @param {(name: string) => string} [n]
* @returns {FetchedPriceSeriesBlueprint[]}
*/
function createCorePercentileSeries(p, n = (x) => x) {
return entries(p)
.reverse()
.map(([key, metric], i, arr) =>
price({
metric,
name: n(key.replace("pct", "p")),
color: colors.at(i, arr.length),
...(ACTIVE_PCTS.has(key) ? {} : { defaultActive: false }),
}),
);
}
/**
* @param {UtxoCohortObject | CohortWithoutRelative} cohort
* @returns {FetchedPriceSeriesBlueprint[]}
*/
function createSingleSummarySeriesBasic(cohort) {
const { color, tree } = cohort;
return [
price({ metric: tree.realized.realizedPrice, name: "Average", color }),
price({
metric: tree.costBasis.max,
name: "Max",
color: colors.stat.max,
defaultActive: false,
}),
price({
metric: tree.costBasis.min,
name: "Min",
color: colors.stat.min,
defaultActive: false,
}),
];
}
/**
* @param {CohortAll | CohortFull | CohortWithPercentiles} cohort
* @returns {FetchedPriceSeriesBlueprint[]}
*/
function createSingleSummarySeriesWithPercentiles(cohort) {
const { color, tree } = cohort;
const p = tree.costBasis.percentiles;
return [
price({ metric: tree.realized.realizedPrice, name: "Average", color }),
price({
metric: tree.costBasis.max,
name: "Max (p100)",
color: colors.stat.max,
defaultActive: false,
}),
price({
metric: p.pct75,
name: "Q3 (p75)",
color: colors.stat.pct75,
defaultActive: false,
}),
price({ metric: p.pct50, name: "Median (p50)", color: colors.stat.median }),
price({
metric: p.pct25,
name: "Q1 (p25)",
color: colors.stat.pct25,
defaultActive: false,
}),
price({
metric: tree.costBasis.min,
name: "Min (p0)",
color: colors.stat.min,
defaultActive: false,
}),
];
}
/**
* @param {readonly CohortObject[]} list
* @param {CohortAll} all
* @returns {FetchedPriceSeriesBlueprint[]}
*/
function createGroupedSummarySeries(list, all) {
return mapCohortsWithAll(list, all, ({ name, color, tree }) =>
price({ metric: tree.realized.realizedPrice, name, color }),
);
}
/**
* @param {CohortAll | CohortFull | CohortWithPercentiles} cohort
* @returns {FetchedPriceSeriesBlueprint[]}
*/
function createSingleByCoinSeries(cohort) {
const { color, tree } = cohort;
const cb = tree.costBasis;
return [
price({ metric: tree.realized.realizedPrice, name: "Average", color }),
price({
metric: cb.max,
name: "p100",
color: colors.stat.max,
defaultActive: false,
}),
...createCorePercentileSeries(cb.percentiles),
price({
metric: cb.min,
name: "p0",
color: colors.stat.min,
defaultActive: false,
}),
];
}
/**
* @param {CohortAll | CohortFull | CohortWithPercentiles} cohort
* @returns {FetchedPriceSeriesBlueprint[]}
*/
function createSingleByCapitalSeries(cohort) {
const { color, tree } = cohort;
return [
price({ metric: tree.realized.investorPrice, name: "Average", color }),
...createCorePercentileSeries(tree.costBasis.investedCapital),
];
}
/**
* @param {CohortAll | CohortFull | CohortWithPercentiles} cohort
* @returns {AnyFetchedSeriesBlueprint[]}
*/
function createSinglePricePositionSeries(cohort) {
const { tree } = cohort;
return [
line({
metric: tree.costBasis.spotCostBasisPercentile,
name: "By Coin",
color: colors.bitcoin,
unit: Unit.percentage,
}),
line({
metric: tree.costBasis.spotInvestedCapitalPercentile,
name: "By Capital",
color: colors.usd,
unit: Unit.percentage,
}),
...priceLines({ numbers: [100, 50, 0], unit: Unit.percentage }),
];
}
/**
* @param {{ cohort: UtxoCohortObject | CohortWithoutRelative, title: (metric: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createCostBasisSection({ cohort, title }) {
return {
name: "Cost Basis",
tree: [
{
name: "Summary",
title: title("Cost Basis Summary"),
top: createSingleSummarySeriesBasic(cohort),
},
],
};
}
/**
* @param {{ cohort: CohortAll | CohortFull | CohortWithPercentiles, title: (metric: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createCostBasisSectionWithPercentiles({ cohort, title }) {
return {
name: "Cost Basis",
tree: [
{
name: "Summary",
title: title("Cost Basis Summary"),
top: createSingleSummarySeriesWithPercentiles(cohort),
},
{
name: "By Coin",
title: title("Cost Basis Distribution (BTC-weighted)"),
top: createSingleByCoinSeries(cohort),
},
{
name: "By Capital",
title: title("Cost Basis Distribution (USD-weighted)"),
top: createSingleByCapitalSeries(cohort),
},
{
name: "Price Position",
title: title("Current Price Position"),
bottom: createSinglePricePositionSeries(cohort),
},
],
};
}
/**
* @param {{ list: readonly (UtxoCohortObject | CohortWithoutRelative)[], all: CohortAll, title: (metric: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createGroupedCostBasisSection({ list, all, title }) {
return {
name: "Cost Basis",
tree: [
{
name: "Summary",
title: title("Cost Basis Summary"),
top: createGroupedSummarySeries(list, all),
},
],
};
}
/**
* @param {{ list: readonly (CohortAll | CohortFull | CohortWithPercentiles)[], all: CohortAll, title: (metric: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createGroupedCostBasisSectionWithPercentiles({ list, all, title }) {
return {
name: "Cost Basis",
tree: [
{
name: "Summary",
title: title("Cost Basis Summary"),
top: createGroupedSummarySeries(list, all),
},
{
name: "By Coin",
tree: [
{
name: "Average",
title: title("Realized Price Comparison"),
top: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
price({ metric: tree.realized.realizedPrice, name, color }),
),
},
{
name: "Median",
title: title("Cost Basis Median (BTC-weighted)"),
top: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
price({ metric: tree.costBasis.percentiles.pct50, name, color }),
),
},
{
name: "Q3",
title: title("Cost Basis Q3 (BTC-weighted)"),
top: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
price({ metric: tree.costBasis.percentiles.pct75, name, color }),
),
},
{
name: "Q1",
title: title("Cost Basis Q1 (BTC-weighted)"),
top: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
price({ metric: tree.costBasis.percentiles.pct25, name, color }),
),
},
],
},
{
name: "By Capital",
tree: [
{
name: "Average",
title: title("Investor Price Comparison"),
top: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
price({ metric: tree.realized.investorPrice, name, color }),
),
},
{
name: "Median",
title: title("Cost Basis Median (USD-weighted)"),
top: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
price({
metric: tree.costBasis.investedCapital.pct50,
name,
color,
}),
),
},
{
name: "Q3",
title: title("Cost Basis Q3 (USD-weighted)"),
top: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
price({
metric: tree.costBasis.investedCapital.pct75,
name,
color,
}),
),
},
{
name: "Q1",
title: title("Cost Basis Q1 (USD-weighted)"),
top: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
price({
metric: tree.costBasis.investedCapital.pct25,
name,
color,
}),
),
},
],
},
{
name: "Price Position",
tree: [
{
name: "By Coin",
title: title("Price Position (BTC-weighted)"),
bottom: [
...mapCohortsWithAll(list, all, ({ name, color, tree }) =>
line({
metric: tree.costBasis.spotCostBasisPercentile,
name,
color,
unit: Unit.percentage,
}),
),
...priceLines({ numbers: [100, 50, 0], unit: Unit.percentage }),
],
},
{
name: "By Capital",
title: title("Price Position (USD-weighted)"),
bottom: [
...mapCohortsWithAll(list, all, ({ name, color, tree }) =>
line({
metric: tree.costBasis.spotInvestedCapitalPercentile,
name,
color,
unit: Unit.percentage,
}),
),
...priceLines({ numbers: [100, 50, 0], unit: Unit.percentage }),
],
},
],
},
],
};
}