mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-06-17 10:19:44 -07:00
global: snapshot
This commit is contained in:
+163
-16
@@ -9,7 +9,7 @@ import {
|
||||
import { createLegend } from "./legend.js";
|
||||
import { capture } from "./capture.js";
|
||||
import { colors } from "../utils/colors.js";
|
||||
import { createRadios, createSelect } from "../utils/dom.js";
|
||||
import { createRadios, createSelect, getElementById } from "../utils/dom.js";
|
||||
import { createPersistedValue } from "../utils/persisted.js";
|
||||
import { onChange as onThemeChange } from "../utils/theme.js";
|
||||
import { throttle, debounce } from "../utils/timing.js";
|
||||
@@ -74,6 +74,71 @@ const lineWidth = /** @type {1} */ (/** @type {unknown} */ (1.5));
|
||||
|
||||
const MAX_SIZE = 10_000;
|
||||
|
||||
/** @typedef {{ label: string, index: IndexLabel, from: number }} RangePreset */
|
||||
|
||||
/** @returns {RangePreset[]} */
|
||||
function getRangePresets() {
|
||||
const nowSec = Math.floor(Date.now() / 1000);
|
||||
const now = new Date();
|
||||
const y = now.getUTCFullYear();
|
||||
const m = now.getUTCMonth();
|
||||
const d = now.getUTCDate();
|
||||
/** @param {number} n */
|
||||
const monthsAgo = (n) => Math.floor(Date.UTC(y, m - n, d) / 1000);
|
||||
|
||||
/** @type {RangePreset[]} */
|
||||
const presets = [
|
||||
{
|
||||
label: "1w",
|
||||
index: /** @type {IndexLabel} */ ("30mn"),
|
||||
from: nowSec - 7 * 86_400,
|
||||
},
|
||||
{
|
||||
label: "1m",
|
||||
index: /** @type {IndexLabel} */ ("1h"),
|
||||
from: monthsAgo(1),
|
||||
},
|
||||
{
|
||||
label: "3m",
|
||||
index: /** @type {IndexLabel} */ ("4h"),
|
||||
from: monthsAgo(3),
|
||||
},
|
||||
{
|
||||
label: "6m",
|
||||
index: /** @type {IndexLabel} */ ("12h"),
|
||||
from: monthsAgo(6),
|
||||
},
|
||||
{
|
||||
label: "1y",
|
||||
index: /** @type {IndexLabel} */ ("1d"),
|
||||
from: monthsAgo(12),
|
||||
},
|
||||
{
|
||||
label: "4y",
|
||||
index: /** @type {IndexLabel} */ ("3d"),
|
||||
from: monthsAgo(48),
|
||||
},
|
||||
];
|
||||
|
||||
// Insert ytd at the right position
|
||||
const ytdFrom = Math.floor(Date.UTC(y, 0, 1) / 1000);
|
||||
const ri = presets.findIndex((e) => e.from <= ytdFrom);
|
||||
const insertAt = ri === -1 ? presets.length : ri;
|
||||
presets.splice(insertAt, 0, {
|
||||
label: "ytd",
|
||||
index: presets[ri === -1 ? presets.length - 1 : ri].index,
|
||||
from: ytdFrom,
|
||||
});
|
||||
|
||||
presets.push({
|
||||
label: "all",
|
||||
index: /** @type {IndexLabel} */ ("1w"),
|
||||
from: -Infinity,
|
||||
});
|
||||
|
||||
return presets;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} args
|
||||
* @param {HTMLElement} args.parent
|
||||
@@ -89,8 +154,8 @@ export function createChart({ parent, brk, fitContent }) {
|
||||
/** @param {ChartableIndex} idx */
|
||||
const getTimeEndpoint = (idx) =>
|
||||
idx === "height"
|
||||
? brk.series.blocks.time.timestampMonotonic.by[idx]
|
||||
: brk.series.blocks.time.timestamp.by[idx];
|
||||
? brk.series.indexes.timestamp.monotonic.by[idx]
|
||||
: brk.series.indexes.timestamp.resolutions.by[idx];
|
||||
|
||||
const index = {
|
||||
/** @type {Set<(index: ChartableIndex) => void>} */
|
||||
@@ -204,10 +269,6 @@ export function createChart({ parent, brk, fitContent }) {
|
||||
enableResize: false,
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
vertLines: { visible: false },
|
||||
horzLines: { visible: false },
|
||||
},
|
||||
rightPriceScale: {
|
||||
borderVisible: false,
|
||||
},
|
||||
@@ -290,6 +351,8 @@ export function createChart({ parent, brk, fitContent }) {
|
||||
const defaultColor = colors.default();
|
||||
const offColor = colors.gray();
|
||||
const borderColor = colors.border();
|
||||
const offBorderColor = colors.offBorder();
|
||||
console.log(borderColor);
|
||||
ichart.applyOptions({
|
||||
layout: {
|
||||
textColor: offColor,
|
||||
@@ -307,6 +370,14 @@ export function createChart({ parent, brk, fitContent }) {
|
||||
labelBackgroundColor: defaultColor,
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
horzLines: {
|
||||
color: offBorderColor,
|
||||
},
|
||||
vertLines: {
|
||||
color: offBorderColor,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
applyColors();
|
||||
@@ -402,8 +473,9 @@ export function createChart({ parent, brk, fitContent }) {
|
||||
const pane = ichart.panes().at(paneIndex);
|
||||
if (!pane) return;
|
||||
if (this.isAllHidden(paneIndex)) {
|
||||
const collapsedHeight = paneIndex === 0 ? 32 : 64;
|
||||
const chartHeight = ichart.chartElement().clientHeight;
|
||||
pane.setStretchFactor(chartHeight > 0 ? 48 / (chartHeight - 48) : 0);
|
||||
pane.setStretchFactor(chartHeight > 0 ? collapsedHeight / (chartHeight - collapsedHeight) : 0);
|
||||
} else {
|
||||
pane.setStretchFactor(1);
|
||||
}
|
||||
@@ -885,6 +957,7 @@ export function createChart({ parent, brk, fitContent }) {
|
||||
* @param {Unit} args.unit
|
||||
* @param {string} [args.key] - Optional key for persistence (derived from name if not provided)
|
||||
* @param {Color | [Color, Color]} [args.color] - Single color or [positive, negative] colors
|
||||
* @param {(value: number) => Color} [args.colorFn]
|
||||
* @param {number} [args.paneIndex]
|
||||
* @param {boolean} [args.defaultActive]
|
||||
* @param {HistogramSeriesPartialOptions} [args.options]
|
||||
@@ -894,6 +967,7 @@ export function createChart({ parent, brk, fitContent }) {
|
||||
name,
|
||||
key,
|
||||
color = colors.bi.p1,
|
||||
colorFn,
|
||||
order,
|
||||
unit,
|
||||
paneIndex = 0,
|
||||
@@ -930,7 +1004,17 @@ export function createChart({ parent, brk, fitContent }) {
|
||||
});
|
||||
},
|
||||
setData: (data) => {
|
||||
if (isDualColor) {
|
||||
if (colorFn) {
|
||||
iseries.setData(
|
||||
data.map((d) => ({
|
||||
...d,
|
||||
color:
|
||||
"value" in d
|
||||
? (colorFn(d.value) ?? (() => "transparent"))()
|
||||
: "transparent",
|
||||
})),
|
||||
);
|
||||
} else if (isDualColor) {
|
||||
iseries.setData(
|
||||
data.map((d) => ({
|
||||
...d,
|
||||
@@ -957,6 +1041,7 @@ export function createChart({ parent, brk, fitContent }) {
|
||||
* @param {Unit} args.unit
|
||||
* @param {string} [args.key] - Optional key for persistence (derived from name if not provided)
|
||||
* @param {Color} args.color
|
||||
* @param {(value: number) => Color} [args.colorFn]
|
||||
* @param {number} [args.paneIndex]
|
||||
* @param {boolean} [args.defaultActive]
|
||||
* @param {LineSeriesPartialOptions} [args.options]
|
||||
@@ -967,6 +1052,7 @@ export function createChart({ parent, brk, fitContent }) {
|
||||
key,
|
||||
order,
|
||||
color,
|
||||
colorFn,
|
||||
unit,
|
||||
paneIndex = 0,
|
||||
defaultActive,
|
||||
@@ -999,7 +1085,18 @@ export function createChart({ parent, brk, fitContent }) {
|
||||
color: color.highlight(highlighted),
|
||||
});
|
||||
},
|
||||
setData: (data) => iseries.setData(data),
|
||||
setData: (data) => {
|
||||
if (colorFn) {
|
||||
iseries.setData(
|
||||
data.map((d) => ({
|
||||
...d,
|
||||
color: "value" in d ? (colorFn(d.value) ?? color)() : color(),
|
||||
})),
|
||||
);
|
||||
} else {
|
||||
iseries.setData(data);
|
||||
}
|
||||
},
|
||||
update: (data) => iseries.update(data),
|
||||
getData: () => iseries.data(),
|
||||
onRemove: () => ichart.removeSeries(iseries),
|
||||
@@ -1373,7 +1470,11 @@ export function createChart({ parent, brk, fitContent }) {
|
||||
break;
|
||||
case "Histogram":
|
||||
pane.series.push(
|
||||
serieses.addHistogram({ ...common, color: blueprint.color }),
|
||||
serieses.addHistogram({
|
||||
...common,
|
||||
color: blueprint.color,
|
||||
colorFn: blueprint.colorFn,
|
||||
}),
|
||||
);
|
||||
break;
|
||||
case "Candlestick":
|
||||
@@ -1414,6 +1515,7 @@ export function createChart({ parent, brk, fitContent }) {
|
||||
serieses.addLine({
|
||||
...common,
|
||||
color: blueprint.color ?? defaultColor,
|
||||
colorFn: blueprint.colorFn,
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -1443,9 +1545,35 @@ export function createChart({ parent, brk, fitContent }) {
|
||||
/** @type {HTMLElement | null} */
|
||||
let indexField = null;
|
||||
|
||||
const lastTd = ichart
|
||||
.chartElement()
|
||||
.querySelector("table > tr:last-child > td:last-child");
|
||||
/** @param {RangePreset} preset */
|
||||
function applyPreset(preset) {
|
||||
preferredIndex = preset.index;
|
||||
/** @type {HTMLSelectElement} */ (getElementById("index")).value =
|
||||
preset.index;
|
||||
index.name.set(preset.index);
|
||||
|
||||
const targetGen = generation;
|
||||
const waitAndApply = () => {
|
||||
if (generation !== targetGen) return;
|
||||
if (!initialLoadComplete) {
|
||||
requestAnimationFrame(waitAndApply);
|
||||
return;
|
||||
}
|
||||
const data = blueprints.panes[0].series[0]?.getData();
|
||||
if (!data?.length) return;
|
||||
const from = isFinite(preset.from)
|
||||
? (data.findIndex(
|
||||
(d) => /** @type {number} */ (d.time) >= preset.from,
|
||||
) ?? 0)
|
||||
: 0;
|
||||
const padding = Math.round((data.length - from) * 0.025);
|
||||
ichart.timeScale().setVisibleLogicalRange({
|
||||
from: from - padding,
|
||||
to: data.length + padding,
|
||||
});
|
||||
};
|
||||
requestAnimationFrame(waitAndApply);
|
||||
}
|
||||
|
||||
const chart = {
|
||||
get panes() {
|
||||
@@ -1464,7 +1592,13 @@ export function createChart({ parent, brk, fitContent }) {
|
||||
index.name.set(currentValue);
|
||||
}
|
||||
|
||||
indexField = createSelect({
|
||||
indexField = window.document.createElement("div");
|
||||
indexField.classList.add("index-bar");
|
||||
|
||||
const scroller = window.document.createElement("div");
|
||||
indexField.append(scroller);
|
||||
|
||||
const selectField = createSelect({
|
||||
initialValue: currentValue,
|
||||
onChange: (v) => {
|
||||
preferredIndex = v;
|
||||
@@ -1474,7 +1608,20 @@ export function createChart({ parent, brk, fitContent }) {
|
||||
groups,
|
||||
id: "index",
|
||||
});
|
||||
if (lastTd) lastTd.append(indexField);
|
||||
scroller.append(selectField);
|
||||
|
||||
const sep = window.document.createElement("span");
|
||||
sep.textContent = "|";
|
||||
scroller.append(sep);
|
||||
|
||||
for (const preset of getRangePresets()) {
|
||||
const btn = window.document.createElement("button");
|
||||
btn.textContent = preset.label;
|
||||
btn.addEventListener("click", () => applyPreset(preset));
|
||||
scroller.append(btn);
|
||||
}
|
||||
|
||||
chartEl.append(indexField);
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -69,19 +69,19 @@ const ALL_YEARS = /** @type {const} */ ([...YEARS_2020S, ...YEARS_2010S]);
|
||||
|
||||
/**
|
||||
* Build DCA class entry from year
|
||||
* @param {MarketDca} dca
|
||||
* @param {Investing} investing
|
||||
* @param {DcaYear} year
|
||||
* @param {number} i
|
||||
* @returns {BaseEntryItem}
|
||||
*/
|
||||
function buildYearEntry(dca, year, i) {
|
||||
function buildYearEntry(investing, year, i) {
|
||||
const key = /** @type {DcaYearKey} */ (`from${year}`);
|
||||
return {
|
||||
name: `${year}`,
|
||||
color: colors.at(i, ALL_YEARS.length),
|
||||
costBasis: dca.class.costBasis[key],
|
||||
returns: dca.class.return[key],
|
||||
stack: dca.class.stack[key],
|
||||
costBasis: investing.class.costBasis[key],
|
||||
returns: investing.class.return[key],
|
||||
stack: investing.class.stack[key],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -90,16 +90,16 @@ function buildYearEntry(dca, year, i) {
|
||||
* @returns {PartialOptionsGroup}
|
||||
*/
|
||||
export function createInvestingSection() {
|
||||
const { market } = brk.series;
|
||||
const { dca, lookback, returns } = market;
|
||||
const { market, investing } = brk.series;
|
||||
const { lookback, returns } = market;
|
||||
|
||||
return {
|
||||
name: "Investing",
|
||||
tree: [
|
||||
createDcaVsLumpSumSection({ dca, lookback, returns }),
|
||||
createDcaByPeriodSection({ dca, returns }),
|
||||
createLumpSumByPeriodSection({ dca, lookback, returns }),
|
||||
createDcaByStartYearSection({ dca }),
|
||||
createDcaVsLumpSumSection({ investing, lookback, returns }),
|
||||
createDcaByPeriodSection({ investing, returns }),
|
||||
createLumpSumByPeriodSection({ investing, lookback, returns }),
|
||||
createDcaByStartYearSection({ investing }),
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -270,15 +270,15 @@ function createLongSingleEntry(item) {
|
||||
/**
|
||||
* Create DCA vs Lump Sum section
|
||||
* @param {Object} args
|
||||
* @param {Market["dca"]} args.dca
|
||||
* @param {Investing} args.investing
|
||||
* @param {Market["lookback"]} args.lookback
|
||||
* @param {Market["returns"]} args.returns
|
||||
*/
|
||||
export function createDcaVsLumpSumSection({ dca, lookback, returns }) {
|
||||
export function createDcaVsLumpSumSection({ investing, lookback, returns }) {
|
||||
/** @param {AllPeriodKey} key */
|
||||
const topPane = (key) => [
|
||||
price({
|
||||
series: dca.period.costBasis[key],
|
||||
series: investing.period.costBasis[key],
|
||||
name: "DCA",
|
||||
color: colors.profit,
|
||||
}),
|
||||
@@ -299,11 +299,11 @@ export function createDcaVsLumpSumSection({ dca, lookback, returns }) {
|
||||
top: topPane(key),
|
||||
bottom: [
|
||||
...percentRatioBaseline({
|
||||
pattern: dca.period.return[key],
|
||||
pattern: investing.period.return[key],
|
||||
name: "DCA",
|
||||
}),
|
||||
...percentRatioBaseline({
|
||||
pattern: dca.period.lumpSumReturn[key],
|
||||
pattern: investing.period.lumpSumReturn[key],
|
||||
name: "Lump Sum",
|
||||
color: colors.bi.p2,
|
||||
}),
|
||||
@@ -317,7 +317,7 @@ export function createDcaVsLumpSumSection({ dca, lookback, returns }) {
|
||||
top: topPane(key),
|
||||
bottom: [
|
||||
...percentRatioBaseline({
|
||||
pattern: dca.period.cagr[key],
|
||||
pattern: investing.period.cagr[key],
|
||||
name: "DCA",
|
||||
}),
|
||||
...percentRatioBaseline({
|
||||
@@ -335,12 +335,12 @@ export function createDcaVsLumpSumSection({ dca, lookback, returns }) {
|
||||
top: topPane(key),
|
||||
bottom: [
|
||||
...satsBtcUsd({
|
||||
pattern: dca.period.stack[key],
|
||||
pattern: investing.period.stack[key],
|
||||
name: "DCA",
|
||||
color: colors.profit,
|
||||
}),
|
||||
...satsBtcUsd({
|
||||
pattern: dca.period.lumpSumStack[key],
|
||||
pattern: investing.period.lumpSumStack[key],
|
||||
name: "Lump Sum",
|
||||
color: colors.bitcoin,
|
||||
}),
|
||||
@@ -395,11 +395,11 @@ export function createDcaVsLumpSumSection({ dca, lookback, returns }) {
|
||||
/**
|
||||
* Create period-based section (DCA or Lump Sum)
|
||||
* @param {Object} args
|
||||
* @param {Market["dca"]} args.dca
|
||||
* @param {Investing} args.investing
|
||||
* @param {Market["lookback"]} [args.lookback]
|
||||
* @param {Market["returns"]} args.returns
|
||||
*/
|
||||
function createPeriodSection({ dca, lookback, returns }) {
|
||||
function createPeriodSection({ investing, lookback, returns }) {
|
||||
const isLumpSum = !!lookback;
|
||||
const suffix = isLumpSum ? "Lump Sum" : "DCA";
|
||||
|
||||
@@ -409,20 +409,20 @@ function createPeriodSection({ dca, lookback, returns }) {
|
||||
const buildBaseEntry = (key, i) => ({
|
||||
name: periodName(key),
|
||||
color: colors.at(i, allPeriods.length),
|
||||
costBasis: isLumpSum ? lookback[key] : dca.period.costBasis[key],
|
||||
costBasis: isLumpSum ? lookback[key] : investing.period.costBasis[key],
|
||||
returns: isLumpSum
|
||||
? dca.period.lumpSumReturn[key]
|
||||
: dca.period.return[key],
|
||||
? investing.period.lumpSumReturn[key]
|
||||
: investing.period.return[key],
|
||||
stack: isLumpSum
|
||||
? dca.period.lumpSumStack[key]
|
||||
: dca.period.stack[key],
|
||||
? investing.period.lumpSumStack[key]
|
||||
: investing.period.stack[key],
|
||||
});
|
||||
|
||||
/** @param {LongPeriodKey} key @param {number} i @returns {LongEntryItem} */
|
||||
const buildLongEntry = (key, i) =>
|
||||
withCagr(
|
||||
buildBaseEntry(key, i),
|
||||
isLumpSum ? returns.cagr[key] : dca.period.cagr[key],
|
||||
isLumpSum ? returns.cagr[key] : investing.period.cagr[key],
|
||||
);
|
||||
|
||||
/** @param {BaseEntryItem} entry */
|
||||
@@ -471,30 +471,30 @@ function createPeriodSection({ dca, lookback, returns }) {
|
||||
/**
|
||||
* Create DCA by Period section
|
||||
* @param {Object} args
|
||||
* @param {Market["dca"]} args.dca
|
||||
* @param {Investing} args.investing
|
||||
* @param {Market["returns"]} args.returns
|
||||
*/
|
||||
export function createDcaByPeriodSection({ dca, returns }) {
|
||||
return createPeriodSection({ dca, returns });
|
||||
export function createDcaByPeriodSection({ investing, returns }) {
|
||||
return createPeriodSection({ investing, returns });
|
||||
}
|
||||
|
||||
/**
|
||||
* Create Lump Sum by Period section
|
||||
* @param {Object} args
|
||||
* @param {Market["dca"]} args.dca
|
||||
* @param {Investing} args.investing
|
||||
* @param {Market["lookback"]} args.lookback
|
||||
* @param {Market["returns"]} args.returns
|
||||
*/
|
||||
export function createLumpSumByPeriodSection({ dca, lookback, returns }) {
|
||||
return createPeriodSection({ dca, lookback, returns });
|
||||
export function createLumpSumByPeriodSection({ investing, lookback, returns }) {
|
||||
return createPeriodSection({ investing, lookback, returns });
|
||||
}
|
||||
|
||||
/**
|
||||
* Create DCA by Start Year section
|
||||
* @param {Object} args
|
||||
* @param {Market["dca"]} args.dca
|
||||
* @param {Investing} args.investing
|
||||
*/
|
||||
export function createDcaByStartYearSection({ dca }) {
|
||||
export function createDcaByStartYearSection({ investing }) {
|
||||
/** @param {string} name @param {string} title @param {BaseEntryItem[]} entries */
|
||||
const createDecadeGroup = (name, title, entries) => ({
|
||||
name,
|
||||
@@ -511,10 +511,10 @@ export function createDcaByStartYearSection({ dca }) {
|
||||
});
|
||||
|
||||
const entries2020s = YEARS_2020S.map((year, i) =>
|
||||
buildYearEntry(dca, year, i),
|
||||
buildYearEntry(investing, year, i),
|
||||
);
|
||||
const entries2010s = YEARS_2010S.map((year, i) =>
|
||||
buildYearEntry(dca, year, YEARS_2020S.length + i),
|
||||
buildYearEntry(investing, year, YEARS_2020S.length + i),
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
ROLLING_WINDOWS,
|
||||
ROLLING_WINDOWS_TO_1M,
|
||||
} from "./series.js";
|
||||
import { simplePriceRatioTree } from "./shared.js";
|
||||
import { simplePriceRatioTree, percentileBands, priceBands } from "./shared.js";
|
||||
import { periodIdToName } from "./utils.js";
|
||||
|
||||
/**
|
||||
@@ -165,7 +165,10 @@ function returnsSubSectionWithCagr(name, periods) {
|
||||
...periods.map((p) => ({
|
||||
name: periodIdToName(p.id, true),
|
||||
title: `${periodIdToName(p.id, true)} Total Price Returns`,
|
||||
bottom: percentRatioBaseline({ pattern: p.returns, name: "Return" }),
|
||||
bottom: percentRatioBaseline({
|
||||
pattern: p.returns,
|
||||
name: "Return",
|
||||
}),
|
||||
})),
|
||||
],
|
||||
},
|
||||
@@ -1084,6 +1087,46 @@ export function createMarketSection() {
|
||||
color: colors.loss,
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "Thermometer",
|
||||
tree: [
|
||||
{
|
||||
name: "Bands",
|
||||
title: "Thermometer",
|
||||
top: priceBands(percentileBands(indicators.thermometer), { defaultActive: true }),
|
||||
},
|
||||
{
|
||||
name: "Score",
|
||||
title: "Thermometer",
|
||||
top: priceBands(percentileBands(indicators.thermometer)),
|
||||
bottom: [
|
||||
histogram({
|
||||
series: indicators.thermometer.zone,
|
||||
name: "Zone",
|
||||
unit: Unit.count,
|
||||
colorFn: (v) => /** @type {const} */ ([
|
||||
colors.ratioPct._0_5,
|
||||
colors.ratioPct._1,
|
||||
colors.ratioPct._2,
|
||||
colors.ratioPct._5,
|
||||
colors.transparent,
|
||||
colors.ratioPct._95,
|
||||
colors.ratioPct._98,
|
||||
colors.ratioPct._99,
|
||||
colors.ratioPct._99_5,
|
||||
])[v + 4],
|
||||
}),
|
||||
baseline({
|
||||
series: indicators.thermometer.score,
|
||||
name: "Score",
|
||||
unit: Unit.count,
|
||||
color: [colors.ratioPct._99, colors.ratioPct._1],
|
||||
defaultActive: false,
|
||||
}),
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -147,6 +147,7 @@ function percentileSeries({ pattern, unit, title = "" }) {
|
||||
* @param {string} [args.key] - Optional key for persistence (derived from name if not provided)
|
||||
* @param {LineStyle} [args.style]
|
||||
* @param {Color} [args.color]
|
||||
* @param {(value: number) => Color} [args.colorFn]
|
||||
* @param {boolean} [args.defaultActive]
|
||||
* @param {LineSeriesPartialOptions} [args.options]
|
||||
* @returns {FetchedLineSeriesBlueprint}
|
||||
@@ -157,6 +158,7 @@ export function line({
|
||||
key,
|
||||
style,
|
||||
color,
|
||||
colorFn,
|
||||
defaultActive,
|
||||
unit,
|
||||
options,
|
||||
@@ -166,6 +168,7 @@ export function line({
|
||||
title: name,
|
||||
key,
|
||||
color,
|
||||
colorFn,
|
||||
unit,
|
||||
defaultActive,
|
||||
options: {
|
||||
@@ -370,6 +373,7 @@ export function dotsBaseline({
|
||||
* @param {Unit} args.unit
|
||||
* @param {string} [args.key] - Optional key for persistence (derived from name if not provided)
|
||||
* @param {Color | [Color, Color]} [args.color]
|
||||
* @param {(value: number) => Color} [args.colorFn]
|
||||
* @param {boolean} [args.defaultActive]
|
||||
* @param {HistogramSeriesPartialOptions} [args.options]
|
||||
* @returns {FetchedHistogramSeriesBlueprint}
|
||||
@@ -379,6 +383,7 @@ export function histogram({
|
||||
name,
|
||||
key,
|
||||
color,
|
||||
colorFn,
|
||||
defaultActive,
|
||||
unit,
|
||||
options,
|
||||
@@ -389,6 +394,7 @@ export function histogram({
|
||||
title: name,
|
||||
key,
|
||||
color,
|
||||
colorFn,
|
||||
unit,
|
||||
defaultActive,
|
||||
options,
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
/** Shared helpers for options */
|
||||
|
||||
import { Unit } from "../utils/units.js";
|
||||
import { ROLLING_WINDOWS, line, baseline, price, sumsAndAveragesCumulativeWith } from "./series.js";
|
||||
import {
|
||||
ROLLING_WINDOWS,
|
||||
line,
|
||||
baseline,
|
||||
price,
|
||||
sumsAndAveragesCumulativeWith,
|
||||
} from "./series.js";
|
||||
import { priceLine, priceLines } from "./constants.js";
|
||||
import { colors } from "../utils/colors.js";
|
||||
|
||||
@@ -270,11 +276,19 @@ export function simplePriceRatioTree({ pattern, title, legend, color }) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param {InvestorPercentilesPattern} p
|
||||
* @param {(entry: InvestorPercentileEntry) => T} extract
|
||||
* @param {{ pct95: AnyPricePattern, pct5: AnyPricePattern, pct98: AnyPricePattern, pct2: AnyPricePattern, pct99: AnyPricePattern, pct1: AnyPricePattern, pct995: AnyPricePattern, pct05: AnyPricePattern }} p
|
||||
*/
|
||||
function percentileBands(p, extract) {
|
||||
export function percentileBands(p) {
|
||||
return percentileBandsWith(p, (e) => e);
|
||||
}
|
||||
|
||||
/**
|
||||
* @template E
|
||||
* @template T
|
||||
* @param {{ pct95: E, pct5: E, pct98: E, pct2: E, pct99: E, pct1: E, pct995: E, pct05: E }} p
|
||||
* @param {(entry: E) => T} extract
|
||||
*/
|
||||
export function percentileBandsWith(p, extract) {
|
||||
return [
|
||||
{ name: "P95", prop: extract(p.pct95), color: colors.ratioPct._95 },
|
||||
{ name: "P5", prop: extract(p.pct5), color: colors.ratioPct._5 },
|
||||
@@ -282,20 +296,38 @@ function percentileBands(p, extract) {
|
||||
{ name: "P2", prop: extract(p.pct2), color: colors.ratioPct._2 },
|
||||
{ name: "P99", prop: extract(p.pct99), color: colors.ratioPct._99 },
|
||||
{ name: "P1", prop: extract(p.pct1), color: colors.ratioPct._1 },
|
||||
{ name: "P99.5", prop: extract(p.pct995), color: colors.ratioPct._99_5 },
|
||||
{ name: "P0.5", prop: extract(p.pct05), color: colors.ratioPct._0_5 },
|
||||
];
|
||||
}
|
||||
|
||||
/** @param {{ name: string, prop: AnyPricePattern, color: Color }[]} bands */
|
||||
function priceBands(bands) {
|
||||
/**
|
||||
* @param {{ name: string, prop: AnyPricePattern, color: Color }[]} bands
|
||||
* @param {{ defaultActive?: boolean }} [opts]
|
||||
*/
|
||||
export function priceBands(bands, opts) {
|
||||
return bands.map(({ name, prop, color }) =>
|
||||
price({ series: prop, name, color, defaultActive: false, options: { lineStyle: 1 } }),
|
||||
price({
|
||||
series: prop,
|
||||
name,
|
||||
color,
|
||||
defaultActive: opts?.defaultActive ?? false,
|
||||
options: { lineStyle: 1 },
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/** @param {{ name: string, prop: AnySeriesPattern, color: Color }[]} bands */
|
||||
function ratioBands(bands) {
|
||||
return bands.map(({ name, prop, color }) =>
|
||||
line({ series: prop, name, color, defaultActive: false, unit: Unit.ratio, options: { lineStyle: 1 } }),
|
||||
line({
|
||||
series: prop,
|
||||
name,
|
||||
color,
|
||||
defaultActive: false,
|
||||
unit: Unit.ratio,
|
||||
options: { lineStyle: 1 },
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -319,8 +351,8 @@ export function priceRatioPercentilesTree({
|
||||
priceReferences,
|
||||
}) {
|
||||
const p = pattern.percentiles;
|
||||
const pctUsd = percentileBands(p, (e) => e.price);
|
||||
const pctRatio = percentileBands(p, (e) => e.ratio);
|
||||
const pctUsd = percentileBandsWith(p, (e) => e.price);
|
||||
const pctRatio = percentileBandsWith(p, (e) => e.ratio);
|
||||
return [
|
||||
{
|
||||
name: "Price",
|
||||
@@ -500,7 +532,11 @@ export function ratioSmas(ratio) {
|
||||
{ name: "1y SMA", series: ratio.sma._1y.ratio },
|
||||
{ name: "2y SMA", series: ratio.sma._2y.ratio },
|
||||
{ name: "4y SMA", series: ratio.sma._4y.ratio },
|
||||
{ name: "All Time SMA", series: ratio.sma.all.ratio, color: colors.time.all },
|
||||
{
|
||||
name: "All Time SMA",
|
||||
series: ratio.sma.all.ratio,
|
||||
color: colors.time.all,
|
||||
},
|
||||
].map((s, i, arr) => ({ color: colors.at(i, arr.length), ...s }));
|
||||
}
|
||||
|
||||
@@ -543,7 +579,14 @@ export function ratioBottomSeries(ratio) {
|
||||
* @param {string} [args.legend]
|
||||
* @returns {PartialChartOption}
|
||||
*/
|
||||
export function createRatioChart({ title, pricePattern, ratio, color, name, legend }) {
|
||||
export function createRatioChart({
|
||||
title,
|
||||
pricePattern,
|
||||
ratio,
|
||||
color,
|
||||
name,
|
||||
legend,
|
||||
}) {
|
||||
return {
|
||||
name: name ?? "Ratio",
|
||||
title: title(name ?? "Ratio"),
|
||||
@@ -727,7 +770,7 @@ export function createPriceRatioCharts({
|
||||
priceReferences,
|
||||
}) {
|
||||
const titleFn = formatCohortTitle(context);
|
||||
const pctUsd = percentileBands(ratio.percentiles, (e) => e.price);
|
||||
const pctUsd = percentileBandsWith(ratio.percentiles, (e) => e.price);
|
||||
return [
|
||||
{
|
||||
name: "Price",
|
||||
@@ -775,20 +818,39 @@ export function createPriceRatioCharts({
|
||||
* @param {Unit} args.unit
|
||||
* @returns {PartialOptionsTree}
|
||||
*/
|
||||
export function groupedWindowsCumulative({ list, all, title, metricTitle, getWindowSeries, getCumulativeSeries, seriesFn, unit }) {
|
||||
export function groupedWindowsCumulative({
|
||||
list,
|
||||
all,
|
||||
title,
|
||||
metricTitle,
|
||||
getWindowSeries,
|
||||
getCumulativeSeries,
|
||||
seriesFn,
|
||||
unit,
|
||||
}) {
|
||||
return [
|
||||
...ROLLING_WINDOWS.map((w) => ({
|
||||
name: w.name,
|
||||
title: title(`${w.title} ${metricTitle}`),
|
||||
bottom: mapCohortsWithAll(list, all, (c) =>
|
||||
seriesFn({ series: getWindowSeries(c, w.key), name: c.name, color: c.color, unit }),
|
||||
seriesFn({
|
||||
series: getWindowSeries(c, w.key),
|
||||
name: c.name,
|
||||
color: c.color,
|
||||
unit,
|
||||
}),
|
||||
),
|
||||
})),
|
||||
{
|
||||
name: "Cumulative",
|
||||
title: title(`Cumulative ${metricTitle}`),
|
||||
bottom: mapCohortsWithAll(list, all, (c) =>
|
||||
seriesFn({ series: getCumulativeSeries(c), name: c.name, color: c.color, unit }),
|
||||
seriesFn({
|
||||
series: getCumulativeSeries(c),
|
||||
name: c.name,
|
||||
color: c.color,
|
||||
unit,
|
||||
}),
|
||||
),
|
||||
},
|
||||
];
|
||||
@@ -807,9 +869,21 @@ export function groupedWindowsCumulative({ list, all, title, metricTitle, getWin
|
||||
* @param {(args: { series: AnySeriesPattern, name: string, color: Color, unit: Unit }) => AnyFetchedSeriesBlueprint} [args.seriesFn]
|
||||
* @returns {PartialOptionsTree}
|
||||
*/
|
||||
export function groupedWindowsCumulativeUsd({ list, all, title, metricTitle, getMetric, seriesFn = line }) {
|
||||
export function groupedWindowsCumulativeUsd({
|
||||
list,
|
||||
all,
|
||||
title,
|
||||
metricTitle,
|
||||
getMetric,
|
||||
seriesFn = line,
|
||||
}) {
|
||||
return groupedWindowsCumulative({
|
||||
list, all, title, metricTitle, seriesFn, unit: Unit.usd,
|
||||
list,
|
||||
all,
|
||||
title,
|
||||
metricTitle,
|
||||
seriesFn,
|
||||
unit: Unit.usd,
|
||||
getWindowSeries: (c, key) => getMetric(c).sum[key].usd,
|
||||
getCumulativeSeries: (c) => getMetric(c).cumulative.usd,
|
||||
});
|
||||
@@ -827,20 +901,34 @@ export function groupedWindowsCumulativeUsd({ list, all, title, metricTitle, get
|
||||
* @param {(c: T | A) => { sum: Record<string, AnyValuePattern>, cumulative: AnyValuePattern }} args.getMetric
|
||||
* @returns {PartialOptionsTree}
|
||||
*/
|
||||
export function groupedWindowsCumulativeSatsBtcUsd({ list, all, title, metricTitle, getMetric }) {
|
||||
export function groupedWindowsCumulativeSatsBtcUsd({
|
||||
list,
|
||||
all,
|
||||
title,
|
||||
metricTitle,
|
||||
getMetric,
|
||||
}) {
|
||||
return [
|
||||
...ROLLING_WINDOWS.map((w) => ({
|
||||
name: w.name,
|
||||
title: title(`${w.title} ${metricTitle}`),
|
||||
bottom: flatMapCohortsWithAll(list, all, (c) =>
|
||||
satsBtcUsd({ pattern: getMetric(c).sum[w.key], name: c.name, color: c.color }),
|
||||
satsBtcUsd({
|
||||
pattern: getMetric(c).sum[w.key],
|
||||
name: c.name,
|
||||
color: c.color,
|
||||
}),
|
||||
),
|
||||
})),
|
||||
{
|
||||
name: "Cumulative",
|
||||
title: title(`Cumulative ${metricTitle}`),
|
||||
bottom: flatMapCohortsWithAll(list, all, (c) =>
|
||||
satsBtcUsd({ pattern: getMetric(c).cumulative, name: c.name, color: c.color }),
|
||||
satsBtcUsd({
|
||||
pattern: getMetric(c).cumulative,
|
||||
name: c.name,
|
||||
color: c.color,
|
||||
}),
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -20,12 +20,14 @@
|
||||
* @typedef {Object} LineSeriesBlueprintSpecific
|
||||
* @property {"Line"} [type]
|
||||
* @property {Color} [color]
|
||||
* @property {(value: number) => Color} [colorFn]
|
||||
* @property {LineSeriesPartialOptions} [options]
|
||||
* @typedef {BaseSeriesBlueprint & LineSeriesBlueprintSpecific} LineSeriesBlueprint
|
||||
*
|
||||
* @typedef {Object} HistogramSeriesBlueprintSpecific
|
||||
* @property {"Histogram"} type
|
||||
* @property {Color | [Color, Color]} [color] - Single color or [positive, negative] colors (defaults to green/red)
|
||||
* @property {(value: number) => Color} [colorFn]
|
||||
* @property {HistogramSeriesPartialOptions} [options]
|
||||
* @typedef {BaseSeriesBlueprint & HistogramSeriesBlueprintSpecific} HistogramSeriesBlueprint
|
||||
*
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
searchLabelElement,
|
||||
searchResultsElement,
|
||||
} from "../utils/elements.js";
|
||||
import { QuickMatch } from "../modules/quickmatch-js/0.4.0/src/index.js";
|
||||
import { QuickMatch } from "../modules/quickmatch-js/0.4.1/src/index.js";
|
||||
|
||||
/**
|
||||
* @param {Options} options
|
||||
|
||||
@@ -180,7 +180,7 @@
|
||||
* Tree branch types
|
||||
* @typedef {Brk.SeriesTree_Market} Market
|
||||
* @typedef {Brk.SeriesTree_Market_MovingAverage} MarketMovingAverage
|
||||
* @typedef {Brk.SeriesTree_Market_Dca} MarketDca
|
||||
* @typedef {Brk.SeriesTree_Investing} Investing
|
||||
* @typedef {Brk._10y2y3y4y5y6y8yPattern} PeriodCagrPattern
|
||||
* @typedef {FullStatsPattern} AnyFullStatsPattern
|
||||
*
|
||||
@@ -243,7 +243,7 @@
|
||||
* @typedef {Brk.AbsoluteRatePattern2} FiatDeltaPattern
|
||||
*
|
||||
* Investor price percentiles (pct1/2/5/95/98/99)
|
||||
* @typedef {Brk.Pct1Pct2Pct5Pct95Pct98Pct99Pattern} InvestorPercentilesPattern
|
||||
* @typedef {Brk.Pct0Pct1Pct2Pct5Pct95Pct98Pct99Pattern} InvestorPercentilesPattern
|
||||
* @typedef {Brk.BpsPriceRatioPattern} InvestorPercentileEntry
|
||||
*
|
||||
* Generic tree node type for walking
|
||||
|
||||
@@ -124,9 +124,11 @@ function seq(keys) {
|
||||
}
|
||||
|
||||
export const colors = {
|
||||
transparent: createColor(() => "transparent"),
|
||||
default: createColor(() => getLightDarkValue("--color")),
|
||||
gray: createColor(() => getColor("gray")),
|
||||
border: createColor(() => getLightDarkValue("--border-color")),
|
||||
offBorder: createColor(() => getLightDarkValue("--off-border-color")),
|
||||
|
||||
// Directional
|
||||
profit: palette.green,
|
||||
@@ -214,12 +216,14 @@ export const colors = {
|
||||
|
||||
// Ratio percentile bands (extreme values)
|
||||
ratioPct: {
|
||||
_99: palette.rose,
|
||||
_98: palette.pink,
|
||||
_95: palette.fuchsia,
|
||||
_5: palette.teal,
|
||||
_99_5: palette.red,
|
||||
_99: palette.orange,
|
||||
_98: palette.amber,
|
||||
_95: palette.yellow,
|
||||
_5: palette.cyan,
|
||||
_2: palette.sky,
|
||||
_1: palette.indigo,
|
||||
_1: palette.blue,
|
||||
_0_5: palette.indigo,
|
||||
},
|
||||
|
||||
// Standard deviation bands (warm = positive, cool = negative)
|
||||
|
||||
Reference in New Issue
Block a user