mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-07-13 03:58:12 -07:00
heatmaps: part 22
This commit is contained in:
+205
-57
@@ -1,96 +1,230 @@
|
||||
import { brk } from "../../scripts/utils/client.js";
|
||||
import { numberToShortUSFormat } from "../../scripts/utils/format.js";
|
||||
import { createAverageGrid } from "./grid.js";
|
||||
import { INFERNO_LUT, logIntensityColor } from "./lut.js";
|
||||
import {
|
||||
DIVERGING_NEGATIVE_LUT,
|
||||
DIVERGING_POSITIVE_LUT,
|
||||
INFERNO_LUT,
|
||||
divergingPowerIntensityColor,
|
||||
powerIntensityColor,
|
||||
} from "./lut.js";
|
||||
import { defaultTooltip } from "./tooltip/index.js";
|
||||
|
||||
const COHORT = "all";
|
||||
const AGGREGATION = "raw";
|
||||
/** @typedef {Brk.Cohort} UrpdCohort */
|
||||
/**
|
||||
* @typedef {Object} UrpdMetric
|
||||
* @property {string} name
|
||||
* @property {string} title
|
||||
* @property {(bucket: Urpd["buckets"][number]) => number} getValue
|
||||
* @property {HeatmapColorFn} color
|
||||
* @property {{ valueLabel: string, formatValue: (value: number) => string }} tooltip
|
||||
*/
|
||||
/** @typedef {{ name: string, cohort: UrpdCohort }} UrpdCohortFolder */
|
||||
|
||||
const AGGREGATION = "log2000";
|
||||
const MIN_LOG = -2;
|
||||
const MAX_LOG = 6;
|
||||
const DEFAULT_MIN_LOG = Math.log10(1_000);
|
||||
const DEFAULT_MAX_LOG = Math.log10(250_000);
|
||||
const PRICE_CHOICES = [
|
||||
{ label: "$0.01", value: Math.log10(0.01) },
|
||||
{ label: "$0.1", value: Math.log10(0.1) },
|
||||
{ label: "$1", value: 0 },
|
||||
{ label: "$10", value: 1 },
|
||||
{ label: "$100", value: 2 },
|
||||
{ label: "$250", value: Math.log10(250) },
|
||||
{ label: "$1k", value: Math.log10(1_000) },
|
||||
{ label: "$2.5k", value: Math.log10(2_500) },
|
||||
{ label: "$5k", value: Math.log10(5_000) },
|
||||
{ label: "$10k", value: Math.log10(10_000) },
|
||||
{ label: "$25k", value: Math.log10(25_000) },
|
||||
{ label: "$50k", value: Math.log10(50_000) },
|
||||
{ label: "$100k", value: Math.log10(100_000) },
|
||||
{ label: "$250k", value: Math.log10(250_000) },
|
||||
{ label: "$500k", value: Math.log10(500_000) },
|
||||
{ label: "$1M", value: Math.log10(1_000_000) },
|
||||
{ label: "$0.01", key: "0.01", value: Math.log10(0.01) },
|
||||
{ label: "$0.1", key: "0.1", value: Math.log10(0.1) },
|
||||
{ label: "$1", key: "1", value: 0 },
|
||||
{ label: "$10", key: "10", value: 1 },
|
||||
{ label: "$100", key: "100", value: 2 },
|
||||
{ label: "$250", key: "250", value: Math.log10(250) },
|
||||
{ label: "$1k", key: "1k", value: Math.log10(1_000) },
|
||||
{ label: "$2.5k", key: "2.5k", value: Math.log10(2_500) },
|
||||
{ label: "$5k", key: "5k", value: Math.log10(5_000) },
|
||||
{ label: "$10k", key: "10k", value: Math.log10(10_000) },
|
||||
{ label: "$25k", key: "25k", value: Math.log10(25_000) },
|
||||
{ label: "$50k", key: "50k", value: Math.log10(50_000) },
|
||||
{ label: "$100k", key: "100k", value: Math.log10(100_000) },
|
||||
{ label: "$250k", key: "250k", value: Math.log10(250_000) },
|
||||
{ label: "$500k", key: "500k", value: Math.log10(500_000) },
|
||||
{ label: "$1M", key: "1M", value: Math.log10(1_000_000) },
|
||||
];
|
||||
const VALUE_COLOR = powerIntensityColor(INFERNO_LUT, 0.4);
|
||||
const PNL_COLOR = divergingPowerIntensityColor(
|
||||
DIVERGING_NEGATIVE_LUT,
|
||||
DIVERGING_POSITIVE_LUT,
|
||||
0.4,
|
||||
);
|
||||
|
||||
/** @satisfies {PartialHeatmapOption} */
|
||||
export const urpdSupplyHeatmapOption = {
|
||||
kind: "heatmap",
|
||||
name: "Supply",
|
||||
title: "URPD Supply",
|
||||
points: {
|
||||
fetch: (date, signal, onPoints) =>
|
||||
fetchUrpdSupplyPoints(date, signal, onPoints),
|
||||
},
|
||||
grid: createAverageGrid({
|
||||
yMin: MIN_LOG,
|
||||
yMax: MAX_LOG,
|
||||
}),
|
||||
color: logIntensityColor(INFERNO_LUT),
|
||||
axis: {
|
||||
y: {
|
||||
label: "price",
|
||||
choices: PRICE_CHOICES,
|
||||
format: formatPrice,
|
||||
/** @type {UrpdMetric[]} */
|
||||
const METRICS = [
|
||||
{
|
||||
name: "supply",
|
||||
title: "Supply",
|
||||
getValue: (bucket) => bucket.supply,
|
||||
color: VALUE_COLOR,
|
||||
tooltip: {
|
||||
valueLabel: "Supply",
|
||||
formatValue: formatBitcoin,
|
||||
},
|
||||
},
|
||||
defaults: {
|
||||
from: "2017",
|
||||
to: "today",
|
||||
yMin: DEFAULT_MIN_LOG,
|
||||
yMax: DEFAULT_MAX_LOG,
|
||||
{
|
||||
name: "capital",
|
||||
title: "Capital",
|
||||
getValue: (bucket) => bucket.realizedCap,
|
||||
color: VALUE_COLOR,
|
||||
tooltip: {
|
||||
valueLabel: "Realized cap",
|
||||
formatValue: formatDollar,
|
||||
},
|
||||
},
|
||||
tooltip: defaultTooltip({
|
||||
valueLabel: "Supply",
|
||||
averageLabel: "Avg supply",
|
||||
formatValue: formatBitcoin,
|
||||
}),
|
||||
};
|
||||
{
|
||||
name: "profitability",
|
||||
title: "Profitability",
|
||||
getValue: (bucket) => bucket.unrealizedPnl,
|
||||
color: PNL_COLOR,
|
||||
tooltip: {
|
||||
valueLabel: "Unrealized PnL",
|
||||
formatValue: formatSignedDollar,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
/** @type {UrpdCohortFolder[]} */
|
||||
const AGE_BANDS = [
|
||||
{ name: "Up to 1h", cohort: "utxos_under_1h_old" },
|
||||
{ name: "1h to 1d", cohort: "utxos_1h_to_1d_old" },
|
||||
{ name: "1d to 1w", cohort: "utxos_1d_to_1w_old" },
|
||||
{ name: "1w to 1m", cohort: "utxos_1w_to_1m_old" },
|
||||
{ name: "1m to 2m", cohort: "utxos_1m_to_2m_old" },
|
||||
{ name: "2m to 3m", cohort: "utxos_2m_to_3m_old" },
|
||||
{ name: "3m to 4m", cohort: "utxos_3m_to_4m_old" },
|
||||
{ name: "4m to 5m", cohort: "utxos_4m_to_5m_old" },
|
||||
{ name: "5m to 6m", cohort: "utxos_5m_to_6m_old" },
|
||||
{ name: "6m to 1y", cohort: "utxos_6m_to_1y_old" },
|
||||
{ name: "1y to 2y", cohort: "utxos_1y_to_2y_old" },
|
||||
{ name: "2y to 3y", cohort: "utxos_2y_to_3y_old" },
|
||||
{ name: "3y to 4y", cohort: "utxos_3y_to_4y_old" },
|
||||
{ name: "4y to 5y", cohort: "utxos_4y_to_5y_old" },
|
||||
{ name: "5y to 6y", cohort: "utxos_5y_to_6y_old" },
|
||||
{ name: "6y to 7y", cohort: "utxos_6y_to_7y_old" },
|
||||
{ name: "7y to 8y", cohort: "utxos_7y_to_8y_old" },
|
||||
{ name: "8y to 10y", cohort: "utxos_8y_to_10y_old" },
|
||||
{ name: "10y to 12y", cohort: "utxos_10y_to_12y_old" },
|
||||
{ name: "12y to 15y", cohort: "utxos_12y_to_15y_old" },
|
||||
{ name: "Over 15y", cohort: "utxos_over_15y_old" },
|
||||
];
|
||||
|
||||
export const urpdAllHeatmapOptions = createCohortHeatmapOptions({
|
||||
cohort: "all",
|
||||
});
|
||||
export const urpdSthHeatmapOptions = createCohortHeatmapOptions({
|
||||
cohort: "sth",
|
||||
titlePrefix: "STH",
|
||||
});
|
||||
export const urpdLthHeatmapOptions = createCohortHeatmapOptions({
|
||||
cohort: "lth",
|
||||
titlePrefix: "LTH",
|
||||
});
|
||||
export const urpdAgeBandHeatmapFolders = AGE_BANDS.map(({ name, cohort }) => ({
|
||||
name,
|
||||
tree: createCohortHeatmapOptions({ cohort, titlePrefix: name }),
|
||||
}));
|
||||
|
||||
/**
|
||||
* @param {Object} args
|
||||
* @param {UrpdCohort} args.cohort
|
||||
* @param {string} [args.titlePrefix]
|
||||
* @returns {PartialHeatmapOption[]}
|
||||
*/
|
||||
function createCohortHeatmapOptions({ cohort, titlePrefix }) {
|
||||
return METRICS.map((metric) => {
|
||||
const title = titlePrefix
|
||||
? `${titlePrefix} ${metric.title} Distribution`
|
||||
: `${metric.title} Distribution`;
|
||||
|
||||
return createUrpdHeatmapOption({
|
||||
...metric,
|
||||
cohort,
|
||||
title,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} args
|
||||
* @param {UrpdCohort} args.cohort
|
||||
* @param {string} args.name
|
||||
* @param {string} args.title
|
||||
* @param {(bucket: Urpd["buckets"][number]) => number} args.getValue
|
||||
* @param {HeatmapColorFn} args.color
|
||||
* @param {{ valueLabel?: string, formatValue?: (value: number) => string }} args.tooltip
|
||||
* @returns {PartialHeatmapOption}
|
||||
*/
|
||||
function createUrpdHeatmapOption({
|
||||
cohort,
|
||||
name,
|
||||
title,
|
||||
getValue,
|
||||
color,
|
||||
tooltip,
|
||||
}) {
|
||||
return {
|
||||
kind: "heatmap",
|
||||
name,
|
||||
title,
|
||||
points: {
|
||||
fetch: (date, signal, onPoints) =>
|
||||
fetchUrpdPoints(cohort, date, signal, getValue, onPoints),
|
||||
},
|
||||
grid: createAverageGrid({
|
||||
yMin: MIN_LOG,
|
||||
yMax: MAX_LOG,
|
||||
}),
|
||||
color,
|
||||
axis: {
|
||||
y: {
|
||||
label: "price",
|
||||
choices: PRICE_CHOICES,
|
||||
format: formatPrice,
|
||||
},
|
||||
},
|
||||
defaults: {
|
||||
from: "2017",
|
||||
to: "today",
|
||||
yMin: DEFAULT_MIN_LOG,
|
||||
yMax: DEFAULT_MAX_LOG,
|
||||
},
|
||||
tooltip: defaultTooltip(tooltip),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {UrpdCohort} cohort
|
||||
* @param {string} date
|
||||
* @param {AbortSignal} signal
|
||||
* @param {(bucket: Urpd["buckets"][number]) => number} getValue
|
||||
* @param {(points: HeatmapPoints) => void} [onPoints]
|
||||
* @returns {Promise<HeatmapPoints>}
|
||||
*/
|
||||
async function fetchUrpdSupplyPoints(date, signal, onPoints) {
|
||||
async function fetchUrpdPoints(cohort, date, signal, getValue, onPoints) {
|
||||
/** @type {HeatmapPoints | undefined} */
|
||||
let points;
|
||||
const urpd = await brk.getUrpdAt(COHORT, date, AGGREGATION, {
|
||||
const urpd = await brk.getUrpdAt(cohort, date, AGGREGATION, {
|
||||
signal,
|
||||
cache: false,
|
||||
onValue: onPoints
|
||||
? (value) => {
|
||||
points = toSupplyPoints(value);
|
||||
? (urpd) => {
|
||||
points = toPoints(urpd, getValue);
|
||||
onPoints(points);
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
|
||||
return points ?? toSupplyPoints(urpd);
|
||||
return points ?? toPoints(urpd, getValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Urpd} urpd
|
||||
* @param {(bucket: Urpd["buckets"][number]) => number} getValue
|
||||
* @returns {HeatmapPoints}
|
||||
*/
|
||||
function toSupplyPoints(urpd) {
|
||||
function toPoints(urpd, getValue) {
|
||||
const buckets = urpd.buckets;
|
||||
const y = new Float64Array(buckets.length);
|
||||
const values = new Float64Array(buckets.length);
|
||||
@@ -98,9 +232,10 @@ function toSupplyPoints(urpd) {
|
||||
|
||||
for (let i = 0; i < buckets.length; i++) {
|
||||
const bucket = buckets[i];
|
||||
if (bucket.priceFloor <= 0 || !Number.isFinite(bucket.supply)) continue;
|
||||
const pointValue = getValue(bucket);
|
||||
if (bucket.priceFloor <= 0 || !Number.isFinite(pointValue)) continue;
|
||||
y[length] = Math.log10(bucket.priceFloor);
|
||||
values[length] = bucket.supply;
|
||||
values[length] = pointValue;
|
||||
length++;
|
||||
}
|
||||
|
||||
@@ -130,6 +265,19 @@ function formatBitcoin(value) {
|
||||
return `${numberToShortUSFormat(value)} BTC`;
|
||||
}
|
||||
|
||||
/** @param {number} value */
|
||||
function formatDollar(value) {
|
||||
return `$${numberToShortUSFormat(value)}`;
|
||||
}
|
||||
|
||||
/** @param {number} value */
|
||||
function formatSignedDollar(value) {
|
||||
const formatted = `$${numberToShortUSFormat(Math.abs(value))}`;
|
||||
if (value > 0) return `+${formatted}`;
|
||||
if (value < 0) return `-${formatted}`;
|
||||
return formatted;
|
||||
}
|
||||
|
||||
/** @param {number} value */
|
||||
function formatCompact(value) {
|
||||
if (value >= 1000) return `${formatNumber(value / 1000)}k`;
|
||||
|
||||
Reference in New Issue
Block a user