desic-okx-agent 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +90 -10
- package/README.md +76 -10
- package/dist/account/private-websocket.js +4 -4
- package/dist/account/private-websocket.js.map +1 -1
- package/dist/account/service.d.ts +12 -1
- package/dist/account/service.js +18 -0
- package/dist/account/service.js.map +1 -1
- package/dist/bars/rate-limiter.d.ts +18 -0
- package/dist/bars/rate-limiter.js +84 -0
- package/dist/bars/rate-limiter.js.map +1 -0
- package/dist/bars/schema.d.ts +36 -0
- package/dist/bars/schema.js +134 -0
- package/dist/bars/schema.js.map +1 -0
- package/dist/bars/service.d.ts +60 -0
- package/dist/bars/service.js +120 -0
- package/dist/bars/service.js.map +1 -0
- package/dist/bars/store.d.ts +105 -0
- package/dist/bars/store.js +415 -0
- package/dist/bars/store.js.map +1 -0
- package/dist/bars/timeframe.d.ts +40 -0
- package/dist/bars/timeframe.js +146 -0
- package/dist/bars/timeframe.js.map +1 -0
- package/dist/bars/types.d.ts +68 -0
- package/dist/bars/types.js +13 -0
- package/dist/bars/types.js.map +1 -0
- package/dist/cli/data-render.d.ts +37 -0
- package/dist/cli/data-render.js +143 -0
- package/dist/cli/data-render.js.map +1 -0
- package/dist/cli/doctor.js +7 -3
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/index.js +757 -26
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/live-render.d.ts +24 -0
- package/dist/cli/live-render.js +85 -0
- package/dist/cli/live-render.js.map +1 -0
- package/dist/cli/range.d.ts +28 -0
- package/dist/cli/range.js +63 -0
- package/dist/cli/range.js.map +1 -0
- package/dist/cli/render.js +3 -0
- package/dist/cli/render.js.map +1 -1
- package/dist/cli/strategy-render.d.ts +36 -0
- package/dist/cli/strategy-render.js +391 -0
- package/dist/cli/strategy-render.js.map +1 -0
- package/dist/cli/width.d.ts +18 -0
- package/dist/cli/width.js +71 -0
- package/dist/cli/width.js.map +1 -0
- package/dist/config/loader.js +1 -1
- package/dist/config/schema.d.ts +7 -0
- package/dist/config/schema.js +24 -0
- package/dist/config/schema.js.map +1 -1
- package/dist/core/okx-client.d.ts +9 -1
- package/dist/core/okx-client.js +14 -5
- package/dist/core/okx-client.js.map +1 -1
- package/dist/i18n/locale.d.ts +24 -0
- package/dist/i18n/locale.js +65 -0
- package/dist/i18n/locale.js.map +1 -0
- package/dist/i18n/messages.d.ts +333 -0
- package/dist/i18n/messages.js +660 -0
- package/dist/i18n/messages.js.map +1 -0
- package/dist/live/account-snapshot.d.ts +30 -0
- package/dist/live/account-snapshot.js +130 -0
- package/dist/live/account-snapshot.js.map +1 -0
- package/dist/live/cutoff-queue.d.ts +42 -0
- package/dist/live/cutoff-queue.js +69 -0
- package/dist/live/cutoff-queue.js.map +1 -0
- package/dist/live/execution-key.d.ts +23 -0
- package/dist/live/execution-key.js +31 -0
- package/dist/live/execution-key.js.map +1 -0
- package/dist/live/failures.d.ts +37 -0
- package/dist/live/failures.js +57 -0
- package/dist/live/failures.js.map +1 -0
- package/dist/live/gates.d.ts +65 -0
- package/dist/live/gates.js +136 -0
- package/dist/live/gates.js.map +1 -0
- package/dist/live/loop.d.ts +56 -0
- package/dist/live/loop.js +197 -0
- package/dist/live/loop.js.map +1 -0
- package/dist/live/preconditions.d.ts +48 -0
- package/dist/live/preconditions.js +69 -0
- package/dist/live/preconditions.js.map +1 -0
- package/dist/live/reconcile.d.ts +46 -0
- package/dist/live/reconcile.js +104 -0
- package/dist/live/reconcile.js.map +1 -0
- package/dist/live/runner.d.ts +57 -0
- package/dist/live/runner.js +160 -0
- package/dist/live/runner.js.map +1 -0
- package/dist/live/schema.d.ts +18 -0
- package/dist/live/schema.js +91 -0
- package/dist/live/schema.js.map +1 -0
- package/dist/live/service.d.ts +144 -0
- package/dist/live/service.js +303 -0
- package/dist/live/service.js.map +1 -0
- package/dist/live/session.d.ts +85 -0
- package/dist/live/session.js +234 -0
- package/dist/live/session.js.map +1 -0
- package/dist/live/sizing.d.ts +62 -0
- package/dist/live/sizing.js +79 -0
- package/dist/live/sizing.js.map +1 -0
- package/dist/live/store.d.ts +123 -0
- package/dist/live/store.js +350 -0
- package/dist/live/store.js.map +1 -0
- package/dist/live/types.d.ts +82 -0
- package/dist/live/types.js +2 -0
- package/dist/live/types.js.map +1 -0
- package/dist/market/websocket.d.ts +16 -1
- package/dist/market/websocket.js +60 -5
- package/dist/market/websocket.js.map +1 -1
- package/dist/mcp/server.d.ts +1 -0
- package/dist/mcp/server.js +15 -1
- package/dist/mcp/server.js.map +1 -1
- package/dist/network/connectivity.d.ts +9 -1
- package/dist/network/connectivity.js +28 -1
- package/dist/network/connectivity.js.map +1 -1
- package/dist/report/chart-script.d.ts +12 -0
- package/dist/report/chart-script.js +146 -0
- package/dist/report/chart-script.js.map +1 -0
- package/dist/report/compare-html.d.ts +8 -0
- package/dist/report/compare-html.js +254 -0
- package/dist/report/compare-html.js.map +1 -0
- package/dist/report/compare-script.d.ts +12 -0
- package/dist/report/compare-script.js +109 -0
- package/dist/report/compare-script.js.map +1 -0
- package/dist/report/compare.d.ts +61 -0
- package/dist/report/compare.js +205 -0
- package/dist/report/compare.js.map +1 -0
- package/dist/report/fetch.d.ts +20 -0
- package/dist/report/fetch.js +56 -0
- package/dist/report/fetch.js.map +1 -0
- package/dist/report/html.d.ts +54 -0
- package/dist/report/html.js +641 -0
- package/dist/report/html.js.map +1 -0
- package/dist/report/open.d.ts +42 -0
- package/dist/report/open.js +114 -0
- package/dist/report/open.js.map +1 -0
- package/dist/runtime/server.d.ts +16 -1
- package/dist/runtime/server.js +112 -9
- package/dist/runtime/server.js.map +1 -1
- package/dist/setup/installer.d.ts +1 -0
- package/dist/setup/installer.js +8 -0
- package/dist/setup/installer.js.map +1 -1
- package/dist/setup/wizard.js +19 -22
- package/dist/setup/wizard.js.map +1 -1
- package/dist/strategy/constants.d.ts +23 -0
- package/dist/strategy/constants.js +24 -0
- package/dist/strategy/constants.js.map +1 -0
- package/dist/strategy/environment.d.ts +52 -0
- package/dist/strategy/environment.js +187 -0
- package/dist/strategy/environment.js.map +1 -0
- package/dist/strategy/instrument.d.ts +29 -0
- package/dist/strategy/instrument.js +39 -0
- package/dist/strategy/instrument.js.map +1 -0
- package/dist/strategy/optimize.d.ts +73 -0
- package/dist/strategy/optimize.js +113 -0
- package/dist/strategy/optimize.js.map +1 -0
- package/dist/strategy/parameter-space.d.ts +59 -0
- package/dist/strategy/parameter-space.js +221 -0
- package/dist/strategy/parameter-space.js.map +1 -0
- package/dist/strategy/python-bridge.d.ts +24 -0
- package/dist/strategy/python-bridge.js +114 -0
- package/dist/strategy/python-bridge.js.map +1 -0
- package/dist/strategy/schema.d.ts +9 -0
- package/dist/strategy/schema.js +91 -0
- package/dist/strategy/schema.js.map +1 -0
- package/dist/strategy/service.d.ts +138 -0
- package/dist/strategy/service.js +745 -0
- package/dist/strategy/service.js.map +1 -0
- package/dist/strategy/settings.d.ts +162 -0
- package/dist/strategy/settings.js +243 -0
- package/dist/strategy/settings.js.map +1 -0
- package/dist/strategy/store.d.ts +96 -0
- package/dist/strategy/store.js +367 -0
- package/dist/strategy/store.js.map +1 -0
- package/dist/strategy/templates.d.ts +11 -0
- package/dist/strategy/templates.js +134 -0
- package/dist/strategy/templates.js.map +1 -0
- package/dist/strategy/types.d.ts +111 -0
- package/dist/strategy/types.js +2 -0
- package/dist/strategy/types.js.map +1 -0
- package/dist/tools/catalog.d.ts +16 -0
- package/dist/tools/catalog.js +118 -17
- package/dist/tools/catalog.js.map +1 -1
- package/dist/trade/service.d.ts +12 -0
- package/dist/trade/service.js +24 -7
- package/dist/trade/service.js.map +1 -1
- package/dist/tui/app.d.ts +23 -0
- package/dist/tui/app.js +322 -0
- package/dist/tui/app.js.map +1 -0
- package/dist/tui/commands.d.ts +70 -0
- package/dist/tui/commands.js +313 -0
- package/dist/tui/commands.js.map +1 -0
- package/dist/tui/entries.d.ts +17 -0
- package/dist/tui/entries.js +24 -0
- package/dist/tui/entries.js.map +1 -0
- package/dist/tui/execute.d.ts +26 -0
- package/dist/tui/execute.js +664 -0
- package/dist/tui/execute.js.map +1 -0
- package/dist/tui/history.d.ts +17 -0
- package/dist/tui/history.js +48 -0
- package/dist/tui/history.js.map +1 -0
- package/dist/tui/index.d.ts +8 -0
- package/dist/tui/index.js +48 -0
- package/dist/tui/index.js.map +1 -0
- package/dist/tui/line-editor.d.ts +44 -0
- package/dist/tui/line-editor.js +98 -0
- package/dist/tui/line-editor.js.map +1 -0
- package/dist/tui/progress.d.ts +23 -0
- package/dist/tui/progress.js +46 -0
- package/dist/tui/progress.js.map +1 -0
- package/dist/tui/settings-editor.d.ts +18 -0
- package/dist/tui/settings-editor.js +115 -0
- package/dist/tui/settings-editor.js.map +1 -0
- package/docs/live-trading.md +455 -0
- package/docs/strategy-research.md +597 -0
- package/package.json +10 -1
- package/python/desic_strategy/__init__.py +34 -0
- package/python/desic_strategy/actions.py +158 -0
- package/python/desic_strategy/context.py +164 -0
- package/python/desic_strategy/engine.py +614 -0
- package/python/desic_strategy/indicators.py +159 -0
- package/python/desic_strategy/live.py +253 -0
- package/python/desic_strategy/policy.py +193 -0
- package/python/desic_strategy/portfolio.py +152 -0
- package/python/desic_strategy/report.py +319 -0
- package/python/desic_strategy/runner.py +574 -0
- package/python/desic_strategy/timeframe.py +150 -0
- package/python/main.py +18 -0
- package/skills/okx-live-trading/SKILL.md +117 -0
- package/skills/okx-live-trading/agents/openai.yaml +9 -0
- package/skills/okx-live-trading/references/lifecycle.md +128 -0
- package/skills/okx-strategy-research/SKILL.md +113 -0
- package/skills/okx-strategy-research/agents/openai.yaml +9 -0
- package/skills/okx-strategy-research/references/execution-semantics.md +107 -0
- package/skills/okx-strategy-research/references/field-traps.md +142 -0
- package/skills/okx-strategy-research/references/python-api.md +121 -0
- package/skills/okx-strategy-research/references/tools-and-data.md +192 -0
- package/skills/okx-trading/SKILL.md +11 -10
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { activeLocale, activeMessages } from "../i18n/locale.js";
|
|
2
|
+
import { COMPARE_SCRIPT } from "./compare-script.js";
|
|
3
|
+
import { COMPARE_STYLES, escapeAttribute, escapeHtml } from "./html.js";
|
|
4
|
+
/** One colour per curve. Distinguishable in both colour schemes and when printed. */
|
|
5
|
+
const SERIES_COLOURS = ["#4f9cf9", "#f2777a", "#3fb950", "#d29922", "#a371f7", "#22b8cf"];
|
|
6
|
+
/**
|
|
7
|
+
* Renders a self-contained comparison of two or more runs.
|
|
8
|
+
*
|
|
9
|
+
* Same constraints as a single report: everything inlined, nothing fetched at view
|
|
10
|
+
* time, so it opens offline and never sends a strategy anywhere.
|
|
11
|
+
*/
|
|
12
|
+
export function renderComparisonHtml(report) {
|
|
13
|
+
const text = activeMessages();
|
|
14
|
+
const title = report.runs.map((run) => run.id).join(" · ");
|
|
15
|
+
return `<!DOCTYPE html>
|
|
16
|
+
<html lang="${activeLocale()}">
|
|
17
|
+
<head>
|
|
18
|
+
<meta charset="utf-8">
|
|
19
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
20
|
+
<title>${escapeHtml(text.comparisonHeading)} · ${escapeHtml(title)}</title>
|
|
21
|
+
<style>${COMPARE_STYLES}</style>
|
|
22
|
+
</head>
|
|
23
|
+
<body>
|
|
24
|
+
<main>
|
|
25
|
+
<header>
|
|
26
|
+
<div>
|
|
27
|
+
<h1>${escapeHtml(text.comparisonHeading)}</h1>
|
|
28
|
+
<p class="sub">${escapeHtml(report.runs.map((run) => run.strategyName).join(" · "))}</p>
|
|
29
|
+
</div>
|
|
30
|
+
</header>
|
|
31
|
+
|
|
32
|
+
${renderWarnings(report, text)}
|
|
33
|
+
${renderLegend(report)}
|
|
34
|
+
|
|
35
|
+
<section>
|
|
36
|
+
<h2>${escapeHtml(text.normalizedEquityHeading)}</h2>
|
|
37
|
+
${renderOverlay(report, text)}
|
|
38
|
+
</section>
|
|
39
|
+
|
|
40
|
+
${renderMetrics(report, text)}
|
|
41
|
+
${renderAssumptions(report, text)}
|
|
42
|
+
|
|
43
|
+
<footer>
|
|
44
|
+
<p>${escapeHtml(text.compareNormalizedNote)}</p>
|
|
45
|
+
<p>${escapeHtml(text.simulationNote)}</p>
|
|
46
|
+
</footer>
|
|
47
|
+
</main>
|
|
48
|
+
<script>${COMPARE_SCRIPT}</script>
|
|
49
|
+
</body>
|
|
50
|
+
</html>`;
|
|
51
|
+
}
|
|
52
|
+
function renderWarnings(report, text) {
|
|
53
|
+
if (report.warnings.length === 0)
|
|
54
|
+
return "";
|
|
55
|
+
// Placed above the chart on purpose: a reader who scrolls straight to the curves
|
|
56
|
+
// should not have to look for the reason they may not be comparable.
|
|
57
|
+
return `<div class="warnings">${report.warnings
|
|
58
|
+
.map((warning) => `<p>${escapeHtml(warning.message)}</p>`)
|
|
59
|
+
.join("")}</div>`;
|
|
60
|
+
}
|
|
61
|
+
function renderLegend(report) {
|
|
62
|
+
return `<div class="legend">${report.series
|
|
63
|
+
.map((series, index) => `<div><i style="background:${colour(index)}"></i><span>${escapeHtml(series.label)}</span></div>`)
|
|
64
|
+
.join("")}</div>`;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Overlays every curve on one indexed axis.
|
|
68
|
+
*
|
|
69
|
+
* The x axis is wall-clock time spanning the union of all runs, so two runs over
|
|
70
|
+
* different windows sit where they actually happened rather than being stretched
|
|
71
|
+
* to the same width — which would imply they covered the same period.
|
|
72
|
+
*/
|
|
73
|
+
function renderOverlay(report, text) {
|
|
74
|
+
const plotted = report.series.filter((series) => series.indexed.length > 1);
|
|
75
|
+
if (plotted.length === 0)
|
|
76
|
+
return `<p class="empty">${escapeHtml(text.notEnoughPoints)}</p>`;
|
|
77
|
+
const width = 1000;
|
|
78
|
+
const height = 300;
|
|
79
|
+
const startMs = Math.min(...plotted.map((series) => series.timeMs[0]));
|
|
80
|
+
const endMs = Math.max(...plotted.map((series) => series.timeMs.at(-1)));
|
|
81
|
+
const span = endMs - startMs || 1;
|
|
82
|
+
const values = plotted.flatMap((series) => series.indexed);
|
|
83
|
+
const minimum = Math.min(...values, 100);
|
|
84
|
+
const maximum = Math.max(...values, 100);
|
|
85
|
+
const range = maximum - minimum || 1;
|
|
86
|
+
const x = (timeMs) => ((timeMs - startMs) / span) * width;
|
|
87
|
+
const y = (value) => height - ((value - minimum) / range) * height;
|
|
88
|
+
const paths = plotted
|
|
89
|
+
.map((series, index) => {
|
|
90
|
+
const path = series.indexed
|
|
91
|
+
.map((value, point) => `${point === 0 ? "M" : "L"}${x(series.timeMs[point]).toFixed(1)},${y(value).toFixed(1)}`)
|
|
92
|
+
.join("");
|
|
93
|
+
return `<path d="${path}" fill="none" stroke="${colour(index)}" stroke-width="1.6" vector-effect="non-scaling-stroke"/>`;
|
|
94
|
+
})
|
|
95
|
+
.join("");
|
|
96
|
+
// Index 100 is break-even. Without it, "did this make money" is unanswerable
|
|
97
|
+
// from a curve scaled to its own range.
|
|
98
|
+
const baseline = minimum < 100 && maximum > 100
|
|
99
|
+
? `<line class="baseline" x1="0" y1="${y(100).toFixed(1)}" x2="${width}" y2="${y(100).toFixed(1)}"/>`
|
|
100
|
+
: "";
|
|
101
|
+
const payload = {
|
|
102
|
+
startMs,
|
|
103
|
+
endMs,
|
|
104
|
+
series: plotted.map((series, index) => ({
|
|
105
|
+
label: series.label,
|
|
106
|
+
colour: colour(index),
|
|
107
|
+
t: series.timeMs,
|
|
108
|
+
v: series.indexed.map((value) => Number(value.toFixed(3)))
|
|
109
|
+
})),
|
|
110
|
+
labels: { time: text.hoverTime, equity: text.normalizedEquityHeading }
|
|
111
|
+
};
|
|
112
|
+
return `
|
|
113
|
+
<div class="chart" id="compare-chart" data-series='${escapeAttribute(JSON.stringify(payload))}'>
|
|
114
|
+
<svg viewBox="0 -8 ${width} ${height + 16}" preserveAspectRatio="none" role="img" aria-label="normalized equity comparison">
|
|
115
|
+
${baseline}
|
|
116
|
+
${paths}
|
|
117
|
+
<line class="crosshair" x1="0" y1="-8" x2="0" y2="${height}" hidden/>
|
|
118
|
+
</svg>
|
|
119
|
+
<div class="axis"><span>${maximum.toFixed(1)}</span><span>${minimum.toFixed(1)}</span></div>
|
|
120
|
+
<div class="tooltip" hidden></div>
|
|
121
|
+
</div>
|
|
122
|
+
<p class="range mono">${escapeHtml(isoMinute(startMs))} → ${escapeHtml(isoMinute(endMs))} UTC · ${escapeHtml(text.hoverPrompt)}</p>`;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Metrics table, one column per run.
|
|
126
|
+
*
|
|
127
|
+
* The best value in each row is marked. On a six-run table that is the difference
|
|
128
|
+
* between reading the comparison and doing arithmetic across columns — and the
|
|
129
|
+
* direction differs per metric, since a low drawdown is good and a low return is
|
|
130
|
+
* not.
|
|
131
|
+
*/
|
|
132
|
+
function renderMetrics(report, text) {
|
|
133
|
+
const rows = COMPARED_ROWS.map(([label, key, better, kind]) => {
|
|
134
|
+
const values = report.runs.map((run) => num(run.metrics?.[key]));
|
|
135
|
+
const best = bestIndex(values, better);
|
|
136
|
+
return `<tr>
|
|
137
|
+
<th scope="row">${escapeHtml(label(text))}</th>
|
|
138
|
+
${values
|
|
139
|
+
.map((value, index) => `<td class="n${index === best ? " best" : ""}">${escapeHtml(format(value, kind))}</td>`)
|
|
140
|
+
.join("")}
|
|
141
|
+
</tr>`;
|
|
142
|
+
}).join("");
|
|
143
|
+
return `
|
|
144
|
+
<section>
|
|
145
|
+
<h2>${escapeHtml(text.comparedRunsHeading)}</h2>
|
|
146
|
+
<div class="scroll">
|
|
147
|
+
<table class="matrix">
|
|
148
|
+
<thead><tr><th></th>${report.runs
|
|
149
|
+
.map((run, index) => `<th class="n"><i style="background:${colour(index)}"></i>${escapeHtml(run.id)}<span class="dim">${escapeHtml(run.instId)}</span></th>`)
|
|
150
|
+
.join("")}</tr></thead>
|
|
151
|
+
<tbody>${rows}</tbody>
|
|
152
|
+
</table>
|
|
153
|
+
</div>
|
|
154
|
+
</section>`;
|
|
155
|
+
}
|
|
156
|
+
/** `higher` and `lower` say which direction is an improvement for each metric. */
|
|
157
|
+
const COMPARED_ROWS = [
|
|
158
|
+
[(text) => text.returnLabel, "returnPct", "higher", "signed"],
|
|
159
|
+
[(text) => text.maxDrawdown, "maxDrawdownPct", "lower", "pct"],
|
|
160
|
+
[(text) => text.calmarLabel, "calmar", "higher", "plain"],
|
|
161
|
+
[(text) => text.sharpeLabel, "sharpe", "higher", "plain"],
|
|
162
|
+
[(text) => text.sortinoLabel, "sortino", "higher", "plain"],
|
|
163
|
+
[(text) => text.tradesLabel, "tradeCount", "none", "plain"],
|
|
164
|
+
[(text) => text.winRate, "winRatePct", "higher", "pct"],
|
|
165
|
+
[(text) => text.profitFactor, "profitFactor", "higher", "plain"],
|
|
166
|
+
[(text) => text.expectancy, "expectancyUsdt", "higher", "signed"],
|
|
167
|
+
[(text) => text.feesLabel, "totalFeesUsdt", "lower", "plain"],
|
|
168
|
+
[(text) => text.feesOfGross, "feesPctOfGross", "lower", "pct"],
|
|
169
|
+
[(text) => text.turnover, "turnoverUsdt", "none", "plain"],
|
|
170
|
+
[(text) => text.exposure, "exposurePct", "none", "pct"]
|
|
171
|
+
];
|
|
172
|
+
/**
|
|
173
|
+
* Assumption rows, differences first.
|
|
174
|
+
*
|
|
175
|
+
* The shared ones are listed after and collapsed to a single column, because the
|
|
176
|
+
* question a comparison answers is what changed.
|
|
177
|
+
*/
|
|
178
|
+
function renderAssumptions(report, text) {
|
|
179
|
+
if (report.differingAssumptions.length === 0) {
|
|
180
|
+
return `<section><h2>${escapeHtml(text.differingAssumptionsHeading)}</h2><p class="empty">${escapeHtml(text.assumptionsIdentical)}</p></section>`;
|
|
181
|
+
}
|
|
182
|
+
const rows = report.differingAssumptions
|
|
183
|
+
.map((entry) => `<tr>
|
|
184
|
+
<th scope="row">${escapeHtml(entry.key)}</th>
|
|
185
|
+
${entry.values.map((value) => `<td class="n differs">${escapeHtml(value ?? "-")}</td>`).join("")}
|
|
186
|
+
</tr>`)
|
|
187
|
+
.join("");
|
|
188
|
+
const shared = report.sharedAssumptions.length > 0
|
|
189
|
+
? `<p class="empty">${escapeHtml(text.sharedAssumptionsNote(String(report.sharedAssumptions.length)))}</p>`
|
|
190
|
+
: "";
|
|
191
|
+
return `
|
|
192
|
+
<section>
|
|
193
|
+
<h2>${escapeHtml(text.differingAssumptionsHeading)}</h2>
|
|
194
|
+
<div class="scroll">
|
|
195
|
+
<table class="matrix">
|
|
196
|
+
<thead><tr><th></th>${report.runs.map((run) => `<th class="n">${escapeHtml(run.id)}</th>`).join("")}</tr></thead>
|
|
197
|
+
<tbody>${rows}</tbody>
|
|
198
|
+
</table>
|
|
199
|
+
</div>
|
|
200
|
+
${shared}
|
|
201
|
+
</section>`;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Index of the best value in a row, or -1 when there is no winner.
|
|
205
|
+
*
|
|
206
|
+
* A tie is left unmarked: highlighting one of two identical numbers invents a
|
|
207
|
+
* distinction that is not in the data.
|
|
208
|
+
*/
|
|
209
|
+
function bestIndex(values, better) {
|
|
210
|
+
if (better === "none")
|
|
211
|
+
return -1;
|
|
212
|
+
let best = -1;
|
|
213
|
+
let bestValue = better === "higher" ? -Infinity : Infinity;
|
|
214
|
+
let ties = 0;
|
|
215
|
+
values.forEach((value, index) => {
|
|
216
|
+
if (value === null)
|
|
217
|
+
return;
|
|
218
|
+
if (value === bestValue) {
|
|
219
|
+
ties += 1;
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
if (better === "higher" ? value > bestValue : value < bestValue) {
|
|
223
|
+
bestValue = value;
|
|
224
|
+
best = index;
|
|
225
|
+
ties = 0;
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
return ties > 0 ? -1 : best;
|
|
229
|
+
}
|
|
230
|
+
function num(value) {
|
|
231
|
+
if (value === null || value === undefined)
|
|
232
|
+
return null;
|
|
233
|
+
const parsed = Number(value);
|
|
234
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
235
|
+
}
|
|
236
|
+
function format(value, kind) {
|
|
237
|
+
if (value === null)
|
|
238
|
+
return "-";
|
|
239
|
+
const magnitude = Math.abs(value);
|
|
240
|
+
const body = magnitude >= 1_000_000
|
|
241
|
+
? `${(value / 1_000_000).toFixed(2)}M`
|
|
242
|
+
: magnitude >= 1_000
|
|
243
|
+
? value.toFixed(0)
|
|
244
|
+
: value.toFixed(2);
|
|
245
|
+
const sign = kind === "signed" && value > 0 ? "+" : "";
|
|
246
|
+
return `${sign}${body}${kind === "plain" ? "" : "%"}`;
|
|
247
|
+
}
|
|
248
|
+
function colour(index) {
|
|
249
|
+
return SERIES_COLOURS[index % SERIES_COLOURS.length];
|
|
250
|
+
}
|
|
251
|
+
function isoMinute(ms) {
|
|
252
|
+
return new Date(ms).toISOString().slice(0, 16).replace("T", " ");
|
|
253
|
+
}
|
|
254
|
+
//# sourceMappingURL=compare-html.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compare-html.js","sourceRoot":"","sources":["../../src/report/compare-html.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hover readout for the comparison overlay.
|
|
3
|
+
*
|
|
4
|
+
* Shows every curve's value at one instant, which is the question an overlay
|
|
5
|
+
* raises and a static image cannot answer: two lines crossing tells you they
|
|
6
|
+
* crossed, not by how much.
|
|
7
|
+
*
|
|
8
|
+
* Same constraints as the single-run script — one IIFE, no network, no storage,
|
|
9
|
+
* data read from a `data-series` attribute — so the report stays a self-contained
|
|
10
|
+
* file that sends nothing.
|
|
11
|
+
*/
|
|
12
|
+
export declare const COMPARE_SCRIPT: string;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hover readout for the comparison overlay.
|
|
3
|
+
*
|
|
4
|
+
* Shows every curve's value at one instant, which is the question an overlay
|
|
5
|
+
* raises and a static image cannot answer: two lines crossing tells you they
|
|
6
|
+
* crossed, not by how much.
|
|
7
|
+
*
|
|
8
|
+
* Same constraints as the single-run script — one IIFE, no network, no storage,
|
|
9
|
+
* data read from a `data-series` attribute — so the report stays a self-contained
|
|
10
|
+
* file that sends nothing.
|
|
11
|
+
*/
|
|
12
|
+
export const COMPARE_SCRIPT = String.raw `
|
|
13
|
+
(function () {
|
|
14
|
+
var chart = document.getElementById("compare-chart");
|
|
15
|
+
if (!chart) return;
|
|
16
|
+
|
|
17
|
+
var payload;
|
|
18
|
+
try {
|
|
19
|
+
payload = JSON.parse(chart.getAttribute("data-series") || "");
|
|
20
|
+
} catch (error) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (!payload || !payload.series || !payload.series.length) return;
|
|
24
|
+
|
|
25
|
+
var svg = chart.querySelector("svg");
|
|
26
|
+
var tooltip = chart.querySelector(".tooltip");
|
|
27
|
+
var crosshair = chart.querySelector(".crosshair");
|
|
28
|
+
if (!svg || !tooltip) return;
|
|
29
|
+
|
|
30
|
+
var span = payload.endMs - payload.startMs || 1;
|
|
31
|
+
|
|
32
|
+
function formatTime(ms) {
|
|
33
|
+
return new Date(ms).toISOString().slice(0, 16).replace("T", " ");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Nearest sample at or before the cursor's time, by binary search: a linear scan
|
|
37
|
+
// per mouse move is visibly slow once six series carry 900 points each.
|
|
38
|
+
function sampleAt(series, timeMs) {
|
|
39
|
+
var times = series.t;
|
|
40
|
+
if (timeMs < times[0] || timeMs > times[times.length - 1]) return null;
|
|
41
|
+
var low = 0;
|
|
42
|
+
var high = times.length - 1;
|
|
43
|
+
while (low < high) {
|
|
44
|
+
var mid = (low + high + 1) >> 1;
|
|
45
|
+
if (times[mid] <= timeMs) low = mid;
|
|
46
|
+
else high = mid - 1;
|
|
47
|
+
}
|
|
48
|
+
return { time: times[low], value: series.v[low] };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function show(clientX) {
|
|
52
|
+
var box = svg.getBoundingClientRect();
|
|
53
|
+
if (box.width <= 0) return;
|
|
54
|
+
var ratio = Math.min(1, Math.max(0, (clientX - box.left) / box.width));
|
|
55
|
+
var timeMs = payload.startMs + ratio * span;
|
|
56
|
+
|
|
57
|
+
var rows = '<div class="row head"><span>' + payload.labels.time + "</span><b>" + formatTime(timeMs) + "</b></div>";
|
|
58
|
+
for (var index = 0; index < payload.series.length; index += 1) {
|
|
59
|
+
var series = payload.series[index];
|
|
60
|
+
var sample = sampleAt(series, timeMs);
|
|
61
|
+
// A run whose window does not cover this instant shows a dash rather than
|
|
62
|
+
// its nearest endpoint, which would imply data it does not have.
|
|
63
|
+
var reading = sample === null ? "-" : sample.value.toFixed(2);
|
|
64
|
+
rows +=
|
|
65
|
+
'<div class="row"><span><i class="swatch" style="background:' +
|
|
66
|
+
series.colour +
|
|
67
|
+
'"></i>' +
|
|
68
|
+
series.label +
|
|
69
|
+
"</span><b>" +
|
|
70
|
+
reading +
|
|
71
|
+
"</b></div>";
|
|
72
|
+
}
|
|
73
|
+
tooltip.innerHTML = rows;
|
|
74
|
+
tooltip.hidden = false;
|
|
75
|
+
|
|
76
|
+
// Flip the tooltip to the other side near the right edge so it stays on screen.
|
|
77
|
+
var offset = ratio > 0.6 ? 12 : box.width * ratio + 18;
|
|
78
|
+
if (ratio > 0.6) {
|
|
79
|
+
tooltip.style.left = "auto";
|
|
80
|
+
tooltip.style.right = offset + "px";
|
|
81
|
+
} else {
|
|
82
|
+
tooltip.style.right = "auto";
|
|
83
|
+
tooltip.style.left = offset + "px";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (crosshair) {
|
|
87
|
+
var viewBox = svg.viewBox.baseVal;
|
|
88
|
+
crosshair.setAttribute("x1", String(ratio * viewBox.width));
|
|
89
|
+
crosshair.setAttribute("x2", String(ratio * viewBox.width));
|
|
90
|
+
crosshair.hidden = false;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function hide() {
|
|
95
|
+
tooltip.hidden = true;
|
|
96
|
+
if (crosshair) crosshair.hidden = true;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
chart.addEventListener("mousemove", function (event) {
|
|
100
|
+
show(event.clientX);
|
|
101
|
+
});
|
|
102
|
+
chart.addEventListener("mouseleave", hide);
|
|
103
|
+
chart.addEventListener("touchmove", function (event) {
|
|
104
|
+
if (event.touches && event.touches.length) show(event.touches[0].clientX);
|
|
105
|
+
});
|
|
106
|
+
chart.addEventListener("touchend", hide);
|
|
107
|
+
})();
|
|
108
|
+
`;
|
|
109
|
+
//# sourceMappingURL=compare-script.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compare-script.js","sourceRoot":"","sources":["../../src/report/compare-script.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgGvC,CAAC"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { EquityPoint, RunSummary } from "../strategy/types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Upper bound on runs in one comparison.
|
|
4
|
+
*
|
|
5
|
+
* Beyond a handful the overlay stops being readable: six curves already crowd a
|
|
6
|
+
* chart, and the metric table needs a column per run.
|
|
7
|
+
*/
|
|
8
|
+
export declare const MAX_COMPARED_RUNS = 6;
|
|
9
|
+
export declare const MIN_COMPARED_RUNS = 2;
|
|
10
|
+
export interface ComparedRun {
|
|
11
|
+
run: RunSummary;
|
|
12
|
+
equity: EquityPoint[];
|
|
13
|
+
}
|
|
14
|
+
/** One reason two runs may not be directly comparable. */
|
|
15
|
+
export interface ComparisonWarning {
|
|
16
|
+
code: "data_snapshot" | "instrument" | "window" | "assumptions" | "incomplete";
|
|
17
|
+
message: string;
|
|
18
|
+
}
|
|
19
|
+
export interface ComparisonInput {
|
|
20
|
+
runs: ComparedRun[];
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* A normalized curve, indexed to 100 at its own first point.
|
|
24
|
+
*
|
|
25
|
+
* Two runs starting from different equity cannot be read off one axis: the larger
|
|
26
|
+
* balance simply draws higher. Indexing makes the shapes comparable, which is the
|
|
27
|
+
* only thing an overlay is good for.
|
|
28
|
+
*/
|
|
29
|
+
export interface NormalizedSeries {
|
|
30
|
+
runId: string;
|
|
31
|
+
label: string;
|
|
32
|
+
/** Wall-clock time of each sample, so runs over different windows still align. */
|
|
33
|
+
timeMs: number[];
|
|
34
|
+
/** Equity as a percentage of that run's own starting equity. */
|
|
35
|
+
indexed: number[];
|
|
36
|
+
}
|
|
37
|
+
export interface ComparisonReport {
|
|
38
|
+
runs: RunSummary[];
|
|
39
|
+
warnings: ComparisonWarning[];
|
|
40
|
+
/** Assumption keys whose values are not identical across every run. */
|
|
41
|
+
differingAssumptions: Array<{
|
|
42
|
+
key: string;
|
|
43
|
+
values: Array<string | null>;
|
|
44
|
+
}>;
|
|
45
|
+
/** Assumption keys every run agrees on, collapsed to one value each. */
|
|
46
|
+
sharedAssumptions: Array<{
|
|
47
|
+
key: string;
|
|
48
|
+
value: string;
|
|
49
|
+
}>;
|
|
50
|
+
series: NormalizedSeries[];
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Builds a comparison from already-collected runs.
|
|
54
|
+
*
|
|
55
|
+
* Warnings come first in the output because the most common mistake this guards
|
|
56
|
+
* against is comparing runs whose numbers were never comparable — different data,
|
|
57
|
+
* different instrument, or different costs. Refusing outright would be worse:
|
|
58
|
+
* sometimes that difference is exactly what is being examined. Stating it is the
|
|
59
|
+
* useful middle.
|
|
60
|
+
*/
|
|
61
|
+
export declare function buildComparison(input: ComparisonInput): ComparisonReport;
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { RuntimeError } from "../core/errors.js";
|
|
2
|
+
import { activeMessages } from "../i18n/locale.js";
|
|
3
|
+
/**
|
|
4
|
+
* Upper bound on runs in one comparison.
|
|
5
|
+
*
|
|
6
|
+
* Beyond a handful the overlay stops being readable: six curves already crowd a
|
|
7
|
+
* chart, and the metric table needs a column per run.
|
|
8
|
+
*/
|
|
9
|
+
export const MAX_COMPARED_RUNS = 6;
|
|
10
|
+
export const MIN_COMPARED_RUNS = 2;
|
|
11
|
+
/**
|
|
12
|
+
* Assumption keys never shown in the difference table.
|
|
13
|
+
*
|
|
14
|
+
* The contract specification is a property of the instrument rather than a choice
|
|
15
|
+
* the user made, so surfacing it as a "difference" between two instruments repeats
|
|
16
|
+
* what the instrument warning already said.
|
|
17
|
+
*
|
|
18
|
+
* `params` is deliberately *not* here. Strategy parameters are the most common
|
|
19
|
+
* thing that varies between two runs — it is the whole point of comparing them —
|
|
20
|
+
* and they are flattened below so each one gets its own row instead of appearing
|
|
21
|
+
* as one opaque JSON blob.
|
|
22
|
+
*/
|
|
23
|
+
const OPAQUE_ASSUMPTIONS = new Set(["contract"]);
|
|
24
|
+
/**
|
|
25
|
+
* Builds a comparison from already-collected runs.
|
|
26
|
+
*
|
|
27
|
+
* Warnings come first in the output because the most common mistake this guards
|
|
28
|
+
* against is comparing runs whose numbers were never comparable — different data,
|
|
29
|
+
* different instrument, or different costs. Refusing outright would be worse:
|
|
30
|
+
* sometimes that difference is exactly what is being examined. Stating it is the
|
|
31
|
+
* useful middle.
|
|
32
|
+
*/
|
|
33
|
+
export function buildComparison(input) {
|
|
34
|
+
const runs = input.runs.map((item) => item.run);
|
|
35
|
+
if (runs.length < MIN_COMPARED_RUNS) {
|
|
36
|
+
throw new RuntimeError("VALIDATION", `A comparison needs at least ${MIN_COMPARED_RUNS} runs`);
|
|
37
|
+
}
|
|
38
|
+
if (runs.length > MAX_COMPARED_RUNS) {
|
|
39
|
+
throw new RuntimeError("VALIDATION", `A comparison holds at most ${MAX_COMPARED_RUNS} runs; ${runs.length} were given`);
|
|
40
|
+
}
|
|
41
|
+
const duplicate = runs.find((run, index) => runs.findIndex((item) => item.id === run.id) !== index);
|
|
42
|
+
if (duplicate) {
|
|
43
|
+
throw new RuntimeError("VALIDATION", `${duplicate.id} appears twice; a run cannot be compared with itself`);
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
runs,
|
|
47
|
+
warnings: collectWarnings(input.runs),
|
|
48
|
+
...partitionAssumptions(runs),
|
|
49
|
+
series: input.runs.map(normalize)
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function collectWarnings(items) {
|
|
53
|
+
const warnings = [];
|
|
54
|
+
const runs = items.map((item) => item.run);
|
|
55
|
+
// Warnings are prose shown to a person, so they follow the display language like
|
|
56
|
+
// the rest of the shell. The codes stay English for programmatic callers.
|
|
57
|
+
const text = activeMessages();
|
|
58
|
+
const unfinished = runs.filter((run) => run.status !== "completed");
|
|
59
|
+
if (unfinished.length > 0) {
|
|
60
|
+
warnings.push({
|
|
61
|
+
code: "incomplete",
|
|
62
|
+
message: text.warnIncompleteRuns(unfinished.map((run) => `${run.id} (${run.status})`).join(", "))
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
const instruments = unique(runs.map((run) => run.instId));
|
|
66
|
+
if (instruments.length > 1) {
|
|
67
|
+
warnings.push({
|
|
68
|
+
code: "instrument",
|
|
69
|
+
message: text.warnDifferentInstruments(instruments.join(", "))
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
// The one that matters most. Two runs over different bars are not two results
|
|
73
|
+
// for one question, and this is the easiest mistake to make by accident —
|
|
74
|
+
// downloading more history between runs is enough to cause it.
|
|
75
|
+
const snapshots = unique(runs.map((run) => run.dataSnapshotId ?? "none"));
|
|
76
|
+
if (snapshots.length > 1) {
|
|
77
|
+
warnings.push({
|
|
78
|
+
code: "data_snapshot",
|
|
79
|
+
message: text.warnDifferentData
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
// Identical bars are not implied by an identical snapshot when the evaluated
|
|
84
|
+
// window differs, because the snapshot covers the loaded range.
|
|
85
|
+
const windows = unique(runs.map((run) => `${run.assumptions?.evaluationStartMs ?? "?"}-${run.assumptions?.evaluationEndMs ?? "?"}`));
|
|
86
|
+
if (windows.length > 1) {
|
|
87
|
+
warnings.push({
|
|
88
|
+
code: "window",
|
|
89
|
+
message: text.warnDifferentWindows
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const costKeys = ["leverage", "takerFeeRate", "makerFeeRate", "entrySlippageBps", "exitSlippageBps", "entryBudgetPct"];
|
|
94
|
+
const differingCosts = costKeys.filter((key) => unique(runs.map((run) => String(run.assumptions?.[key] ?? "?"))).length > 1);
|
|
95
|
+
if (differingCosts.length > 0) {
|
|
96
|
+
warnings.push({
|
|
97
|
+
code: "assumptions",
|
|
98
|
+
message: text.warnDifferentAssumptions(differingCosts.join(", "))
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
return warnings;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Splits assumptions into the ones that differ and the ones every run shares.
|
|
105
|
+
*
|
|
106
|
+
* Collapsing the shared ones is the point: with twenty assumption rows per run, a
|
|
107
|
+
* full matrix buries the two values that actually changed.
|
|
108
|
+
*/
|
|
109
|
+
function partitionAssumptions(runs) {
|
|
110
|
+
const flattened = runs.map(flattenAssumptions);
|
|
111
|
+
const keys = unique(flattened.flatMap((entry) => Object.keys(entry)));
|
|
112
|
+
const differing = [];
|
|
113
|
+
const shared = [];
|
|
114
|
+
for (const key of keys.sort()) {
|
|
115
|
+
const values = flattened.map((entry) => entry[key] ?? null);
|
|
116
|
+
// Null folds to a sentinel rather than being compared directly, so "absent"
|
|
117
|
+
// and "absent" count as agreement while "absent" and "0" do not.
|
|
118
|
+
if (unique(values.map((value) => value ?? "\u0000")).length === 1) {
|
|
119
|
+
shared.push({ key, value: values[0] ?? "-" });
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
differing.push({ key, values });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return { differingAssumptions: differing, sharedAssumptions: shared };
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* One flat map of comparable values per run.
|
|
129
|
+
*
|
|
130
|
+
* Strategy parameters are lifted to `params.<name>` so each takes its own row. Left
|
|
131
|
+
* as a single JSON blob, changing `fastPeriod` from 20 to 40 shows up as two
|
|
132
|
+
* unreadable strings the reader has to diff by eye — for the value most likely to
|
|
133
|
+
* be the reason the two runs differ at all.
|
|
134
|
+
*/
|
|
135
|
+
function flattenAssumptions(run) {
|
|
136
|
+
const flat = {};
|
|
137
|
+
for (const [key, value] of Object.entries(run.assumptions ?? {})) {
|
|
138
|
+
if (OPAQUE_ASSUMPTIONS.has(key))
|
|
139
|
+
continue;
|
|
140
|
+
if (key === "params" && value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
141
|
+
for (const [name, param] of Object.entries(value)) {
|
|
142
|
+
flat[`params.${name}`] = stringify(param);
|
|
143
|
+
}
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
flat[key] = stringify(value, key);
|
|
147
|
+
}
|
|
148
|
+
return flat;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Indexes one curve to 100 at its own first point.
|
|
152
|
+
*
|
|
153
|
+
* Downsampled to a fixed budget so an overlay of six year-long runs stays a
|
|
154
|
+
* readable file rather than several megabytes of path data.
|
|
155
|
+
*/
|
|
156
|
+
function normalize(item, index) {
|
|
157
|
+
const sampled = downsample(item.equity, 900);
|
|
158
|
+
const base = sampled[0]?.equityUsdt ?? 0;
|
|
159
|
+
return {
|
|
160
|
+
runId: item.run.id,
|
|
161
|
+
label: `${item.run.strategyName} · ${item.run.id.slice(0, 10)}`,
|
|
162
|
+
timeMs: sampled.map((point) => point.timeMs),
|
|
163
|
+
// A zero or negative starting equity has no meaningful index; those points
|
|
164
|
+
// read as flat rather than as an infinite spike.
|
|
165
|
+
indexed: sampled.map((point) => (base > 0 ? (point.equityUsdt / base) * 100 : 100))
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
function downsample(points, budget) {
|
|
169
|
+
if (points.length <= budget)
|
|
170
|
+
return points;
|
|
171
|
+
const step = (points.length - 1) / (budget - 1);
|
|
172
|
+
const out = [];
|
|
173
|
+
for (let index = 0; index < budget; index += 1) {
|
|
174
|
+
out.push(points[Math.round(index * step)]);
|
|
175
|
+
}
|
|
176
|
+
return out;
|
|
177
|
+
}
|
|
178
|
+
function unique(values) {
|
|
179
|
+
return [...new Set(values)];
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Keys whose numeric value is an epoch millisecond.
|
|
183
|
+
*
|
|
184
|
+
* Rendered as a UTC timestamp rather than the raw number: every other window in
|
|
185
|
+
* the tool is shown that way, and `1786605120000` beside `1784937600000` cannot be
|
|
186
|
+
* read as "eight days earlier" by anyone.
|
|
187
|
+
*/
|
|
188
|
+
const TIME_ASSUMPTIONS = new Set(["evaluationStartMs", "evaluationEndMs"]);
|
|
189
|
+
function stringify(value, key) {
|
|
190
|
+
if (value === null || value === undefined)
|
|
191
|
+
return null;
|
|
192
|
+
if (typeof value === "number" && key !== undefined && TIME_ASSUMPTIONS.has(key)) {
|
|
193
|
+
return new Date(value).toISOString().slice(0, 16).replace("T", " ");
|
|
194
|
+
}
|
|
195
|
+
if (typeof value === "number")
|
|
196
|
+
return String(Number(value.toFixed(10)));
|
|
197
|
+
if (typeof value === "boolean")
|
|
198
|
+
return value ? "on" : "off";
|
|
199
|
+
if (Array.isArray(value))
|
|
200
|
+
return value.join(",");
|
|
201
|
+
if (typeof value === "object")
|
|
202
|
+
return JSON.stringify(value);
|
|
203
|
+
return String(value);
|
|
204
|
+
}
|
|
205
|
+
//# sourceMappingURL=compare.js.map
|