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,641 @@
|
|
|
1
|
+
import { activeLocale, activeMessages } from "../i18n/locale.js";
|
|
2
|
+
import { MINIMUM_ANNUALIZATION_DAYS } from "../strategy/constants.js";
|
|
3
|
+
import { CHART_SCRIPT } from "./chart-script.js";
|
|
4
|
+
/**
|
|
5
|
+
* Renders a self-contained HTML report.
|
|
6
|
+
*
|
|
7
|
+
* Everything is inlined — no CDN, no fonts, no scripts fetched at view time — so
|
|
8
|
+
* the file opens offline, keeps working when archived next to its run, and never
|
|
9
|
+
* sends the contents of a strategy anywhere.
|
|
10
|
+
*
|
|
11
|
+
* The chart is drawn as SVG paths computed here rather than by a charting
|
|
12
|
+
* library: a 50,000-point curve as JSON plus a bundled library would be
|
|
13
|
+
* megabytes, and the useful information survives downsampling.
|
|
14
|
+
*/
|
|
15
|
+
export function renderReportHtml(input) {
|
|
16
|
+
const { run, equity, trades } = input;
|
|
17
|
+
const text = activeMessages();
|
|
18
|
+
const metrics = (run.metrics ?? {});
|
|
19
|
+
const sampled = downsample(equity, 900);
|
|
20
|
+
const drawdown = drawdownSeries(sampled, num(metrics.initialEquityUsdt) ?? 10_000);
|
|
21
|
+
return `<!DOCTYPE html>
|
|
22
|
+
<html lang="${activeLang()}">
|
|
23
|
+
<head>
|
|
24
|
+
<meta charset="utf-8">
|
|
25
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
26
|
+
<title>${escapeHtml(run.id)} · ${escapeHtml(run.strategyName)}</title>
|
|
27
|
+
<style>${STYLES}</style>
|
|
28
|
+
</head>
|
|
29
|
+
<body>
|
|
30
|
+
<main>
|
|
31
|
+
<header>
|
|
32
|
+
<div>
|
|
33
|
+
<h1>${escapeHtml(run.strategyName)}</h1>
|
|
34
|
+
<p class="sub">${escapeHtml(run.instId)} · ${escapeHtml(run.id)}</p>
|
|
35
|
+
</div>
|
|
36
|
+
<div class="headline ${sign(num(metrics.returnPct))}">
|
|
37
|
+
<span class="value">${signed(num(metrics.returnPct), "%")}</span>
|
|
38
|
+
<span class="label">${escapeHtml(text.returnLabel)}</span>
|
|
39
|
+
</div>
|
|
40
|
+
</header>
|
|
41
|
+
|
|
42
|
+
${renderFacts(run, metrics, text)}
|
|
43
|
+
${renderWarnings(metrics, text)}
|
|
44
|
+
|
|
45
|
+
<section>
|
|
46
|
+
<h2>${escapeHtml(text.equityHeading)}</h2>
|
|
47
|
+
${renderChart(sampled, drawdown, num(metrics.initialEquityUsdt) ?? 10_000, text)}
|
|
48
|
+
</section>
|
|
49
|
+
|
|
50
|
+
${renderMetricGroups(metrics, text)}
|
|
51
|
+
${renderAssumptions(run, text)}
|
|
52
|
+
${renderMonthly(equity, text)}
|
|
53
|
+
${renderDistribution(trades, text)}
|
|
54
|
+
${renderTrades(trades, text)}
|
|
55
|
+
|
|
56
|
+
<footer>
|
|
57
|
+
<p>${escapeHtml(text.simulationNote)}</p>
|
|
58
|
+
<p class="mono">${escapeHtml(run.dataSnapshotId ?? "-")}</p>
|
|
59
|
+
</footer>
|
|
60
|
+
</main>
|
|
61
|
+
<script>${CHART_SCRIPT}</script>
|
|
62
|
+
</body>
|
|
63
|
+
</html>`;
|
|
64
|
+
}
|
|
65
|
+
function renderFacts(run, metrics, text) {
|
|
66
|
+
const days = num(metrics.evaluationDays);
|
|
67
|
+
const items = [
|
|
68
|
+
[text.statusLabel, run.status],
|
|
69
|
+
[text.barsLabel, `${formatNumber(run.barCount)} (${formatNumber(run.preloadBars)} preload)`],
|
|
70
|
+
["", days === null ? "-" : `${days.toFixed(1)}d`],
|
|
71
|
+
[
|
|
72
|
+
text.elapsedLabel,
|
|
73
|
+
run.startedAt !== null && run.finishedAt !== null
|
|
74
|
+
? `${((run.finishedAt - run.startedAt) / 1000).toFixed(1)}s`
|
|
75
|
+
: "-"
|
|
76
|
+
]
|
|
77
|
+
];
|
|
78
|
+
return `<div class="facts">${items
|
|
79
|
+
.filter(([label]) => label !== "")
|
|
80
|
+
.map(([label, value]) => `<div><span>${escapeHtml(label)}</span><strong>${escapeHtml(value)}</strong></div>`)
|
|
81
|
+
.join("")}</div>`;
|
|
82
|
+
}
|
|
83
|
+
function renderWarnings(metrics, text) {
|
|
84
|
+
const warnings = [];
|
|
85
|
+
const feeShare = num(metrics.feesPctOfGross);
|
|
86
|
+
// Costs above the gross edge are the usual reason a plausible signal loses
|
|
87
|
+
// money, so state it rather than leaving it to be inferred.
|
|
88
|
+
if (feeShare !== null && feeShare > 100) {
|
|
89
|
+
warnings.push(`${text.feesLabel} ${feeShare.toFixed(1)}% ${text.feesOfGross}`);
|
|
90
|
+
}
|
|
91
|
+
if (metrics.marginExhausted === true) {
|
|
92
|
+
warnings.push(`${text.marginExhaustedWarning} ${text.marginExhaustedDetail}`);
|
|
93
|
+
}
|
|
94
|
+
if (metrics.annualizationReliable === false) {
|
|
95
|
+
warnings.push(`${text.annualized}: ${text.annualizedWithheld(MINIMUM_ANNUALIZATION_DAYS)}`);
|
|
96
|
+
}
|
|
97
|
+
if (warnings.length === 0)
|
|
98
|
+
return "";
|
|
99
|
+
return `<div class="warnings">${warnings.map((item) => `<p>${escapeHtml(item)}</p>`).join("")}</div>`;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Equity line plus a shaded drawdown band beneath it.
|
|
103
|
+
*
|
|
104
|
+
* Drawdown is the question a bare equity line answers badly: two runs ending at
|
|
105
|
+
* the same number can differ entirely in how much they gave back on the way.
|
|
106
|
+
*/
|
|
107
|
+
function renderChart(points, drawdown, initialEquity, text) {
|
|
108
|
+
if (points.length < 2)
|
|
109
|
+
return `<p class="empty">${escapeHtml(text.notEnoughPoints)}</p>`;
|
|
110
|
+
const width = 1000;
|
|
111
|
+
const equityHeight = 260;
|
|
112
|
+
const drawdownHeight = 90;
|
|
113
|
+
const values = points.map((point) => point.equityUsdt);
|
|
114
|
+
const minimum = Math.min(...values);
|
|
115
|
+
const maximum = Math.max(...values);
|
|
116
|
+
const span = maximum - minimum || 1;
|
|
117
|
+
const worst = Math.max(...drawdown, 0.0001);
|
|
118
|
+
const x = (index) => (index / (points.length - 1)) * width;
|
|
119
|
+
const y = (value) => equityHeight - ((value - minimum) / span) * equityHeight;
|
|
120
|
+
const line = values.map((value, index) => `${index === 0 ? "M" : "L"}${x(index).toFixed(1)},${y(value).toFixed(1)}`).join("");
|
|
121
|
+
const area = `${line}L${width},${equityHeight}L0,${equityHeight}Z`;
|
|
122
|
+
const ddPath = drawdown
|
|
123
|
+
.map((value, index) => `${index === 0 ? "M" : "L"}${x(index).toFixed(1)},${((value / worst) * drawdownHeight).toFixed(1)}`)
|
|
124
|
+
.join("");
|
|
125
|
+
const ddArea = `${ddPath}L${width},0L0,0Z`;
|
|
126
|
+
const first = points[0];
|
|
127
|
+
const last = points.at(-1);
|
|
128
|
+
// The break-even line makes "is this above water" answerable at a glance,
|
|
129
|
+
// which a curve scaled to its own range cannot show.
|
|
130
|
+
const breakEven = initialEquity > minimum && initialEquity < maximum
|
|
131
|
+
? `<line class="baseline" x1="0" y1="${y(initialEquity).toFixed(1)}" x2="${width}" y2="${y(initialEquity).toFixed(1)}"/>`
|
|
132
|
+
: "";
|
|
133
|
+
// Series data for the hover readout. Sent as three parallel arrays rather than
|
|
134
|
+
// an array of objects: on a 900-point curve that is roughly a third of the
|
|
135
|
+
// bytes, and the report is read as one file.
|
|
136
|
+
const series = {
|
|
137
|
+
t: points.map((point) => point.timeMs),
|
|
138
|
+
e: points.map((point) => Number(point.equityUsdt.toFixed(4))),
|
|
139
|
+
d: drawdown.map((value) => Number(value.toFixed(4))),
|
|
140
|
+
initial: initialEquity,
|
|
141
|
+
labels: {
|
|
142
|
+
time: text.hoverTime,
|
|
143
|
+
equity: text.hoverEquity,
|
|
144
|
+
change: text.hoverChange,
|
|
145
|
+
drawdown: text.hoverDrawdown
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
return `
|
|
149
|
+
<div class="chart" id="equity-chart" data-series='${escapeAttribute(JSON.stringify(series))}'>
|
|
150
|
+
<svg viewBox="0 -8 ${width} ${equityHeight + 16}" preserveAspectRatio="none" role="img" aria-label="equity curve">
|
|
151
|
+
<defs>
|
|
152
|
+
<linearGradient id="fill" x1="0" y1="0" x2="0" y2="1">
|
|
153
|
+
<stop offset="0%" stop-color="#4f9cf9" stop-opacity="0.28"/>
|
|
154
|
+
<stop offset="100%" stop-color="#4f9cf9" stop-opacity="0"/>
|
|
155
|
+
</linearGradient>
|
|
156
|
+
</defs>
|
|
157
|
+
<path d="${area}" fill="url(#fill)"/>
|
|
158
|
+
${breakEven}
|
|
159
|
+
<path d="${line}" fill="none" stroke="#4f9cf9" stroke-width="1.6" vector-effect="non-scaling-stroke"/>
|
|
160
|
+
<line class="crosshair" x1="0" y1="-8" x2="0" y2="${equityHeight}" hidden/>
|
|
161
|
+
<circle class="marker" r="3.5" cx="0" cy="0" hidden/>
|
|
162
|
+
</svg>
|
|
163
|
+
<div class="axis"><span>${formatNumber(maximum)}</span><span>${formatNumber(minimum)}</span></div>
|
|
164
|
+
<div class="tooltip" hidden></div>
|
|
165
|
+
</div>
|
|
166
|
+
<div class="chart drawdown" id="drawdown-chart">
|
|
167
|
+
<svg viewBox="0 0 ${width} ${drawdownHeight}" preserveAspectRatio="none" role="img" aria-label="drawdown">
|
|
168
|
+
<path d="${ddArea}" fill="#f2777a" fill-opacity="0.22"/>
|
|
169
|
+
<path d="${ddPath}" fill="none" stroke="#f2777a" stroke-width="1.2" vector-effect="non-scaling-stroke"/>
|
|
170
|
+
<line class="crosshair" x1="0" y1="0" x2="0" y2="${drawdownHeight}" hidden/>
|
|
171
|
+
</svg>
|
|
172
|
+
<div class="axis"><span>0%</span><span>-${worst.toFixed(1)}%</span></div>
|
|
173
|
+
</div>
|
|
174
|
+
<p class="range mono">${escapeHtml(isoMinute(first.timeMs))} → ${escapeHtml(isoMinute(last.timeMs))} UTC · ${formatNumber(points.length)} · ${escapeHtml(text.hoverPrompt)}</p>`;
|
|
175
|
+
}
|
|
176
|
+
function renderMetricGroups(metrics, text) {
|
|
177
|
+
const groups = [
|
|
178
|
+
[
|
|
179
|
+
text.performance,
|
|
180
|
+
[
|
|
181
|
+
[text.netProfit, `${signed(num(metrics.netProfitUsdt), "")} USDT`],
|
|
182
|
+
[text.returnLabel, signed(num(metrics.returnPct), "%")],
|
|
183
|
+
[
|
|
184
|
+
text.annualized,
|
|
185
|
+
metrics.annualizedReturnPct === null || metrics.annualizedReturnPct === undefined
|
|
186
|
+
? "-"
|
|
187
|
+
: signed(num(metrics.annualizedReturnPct), "%")
|
|
188
|
+
],
|
|
189
|
+
[text.maxDrawdown, `${plain(num(metrics.maxDrawdownPct), "%")} (${plain(num(metrics.maxDrawdownUsdt), "")})`],
|
|
190
|
+
[text.drawdownRecovery, `${formatNumber(num(metrics.longestDrawdownBars) ?? 0)} bars`]
|
|
191
|
+
]
|
|
192
|
+
],
|
|
193
|
+
[
|
|
194
|
+
text.riskAdjusted,
|
|
195
|
+
[
|
|
196
|
+
[text.sharpeLabel, plain(num(metrics.sharpe), "")],
|
|
197
|
+
[text.sortinoLabel, plain(num(metrics.sortino), "")],
|
|
198
|
+
[text.calmarLabel, plain(num(metrics.calmar), "")]
|
|
199
|
+
]
|
|
200
|
+
],
|
|
201
|
+
[
|
|
202
|
+
text.activity,
|
|
203
|
+
[
|
|
204
|
+
[
|
|
205
|
+
text.tradesLabel,
|
|
206
|
+
`${formatNumber(num(metrics.tradeCount) ?? 0)} (${text.longShort(num(metrics.longTradeCount) ?? 0, num(metrics.shortTradeCount) ?? 0)})`
|
|
207
|
+
],
|
|
208
|
+
[text.winRate, plain(num(metrics.winRatePct), "%")],
|
|
209
|
+
[text.profitFactor, plain(num(metrics.profitFactor), "")],
|
|
210
|
+
[text.expectancy, `${signed(num(metrics.expectancyUsdt), "")} USDT`],
|
|
211
|
+
[
|
|
212
|
+
text.averageWinLoss,
|
|
213
|
+
`${signed(num(metrics.averageWinUsdt), "")} / ${signed(num(metrics.averageLossUsdt), "")}`
|
|
214
|
+
],
|
|
215
|
+
[
|
|
216
|
+
text.largestWinLoss,
|
|
217
|
+
`${signed(num(metrics.largestWinUsdt), "")} / ${signed(num(metrics.largestLossUsdt), "")}`
|
|
218
|
+
],
|
|
219
|
+
[
|
|
220
|
+
text.streaks,
|
|
221
|
+
`${formatNumber(num(metrics.maxConsecutiveWins) ?? 0)}W / ${formatNumber(num(metrics.maxConsecutiveLosses) ?? 0)}L`
|
|
222
|
+
],
|
|
223
|
+
[text.holdingTime, `${plain(num(metrics.averageHoldingBars), "")} bars`],
|
|
224
|
+
[text.tradesPerDay, plain(num(metrics.tradesPerDay), "")]
|
|
225
|
+
]
|
|
226
|
+
],
|
|
227
|
+
[
|
|
228
|
+
text.costs,
|
|
229
|
+
[
|
|
230
|
+
[text.feesLabel, `${plain(num(metrics.totalFeesUsdt), "")} USDT`],
|
|
231
|
+
[text.feesOfGross, plain(num(metrics.feesPctOfGross), "%")],
|
|
232
|
+
[text.turnover, `${plain(num(metrics.turnoverUsdt), "")} USDT`],
|
|
233
|
+
[text.exposure, plain(num(metrics.exposurePct), "%")]
|
|
234
|
+
]
|
|
235
|
+
]
|
|
236
|
+
];
|
|
237
|
+
return groups
|
|
238
|
+
.map(([title, rows]) => `
|
|
239
|
+
<section>
|
|
240
|
+
<h2>${escapeHtml(title)}</h2>
|
|
241
|
+
<dl>${rows
|
|
242
|
+
.map(([name, value]) => `<div><dt>${escapeHtml(name)}</dt><dd>${escapeHtml(value)}</dd></div>`)
|
|
243
|
+
.join("")}</dl>
|
|
244
|
+
</section>`)
|
|
245
|
+
.join("");
|
|
246
|
+
}
|
|
247
|
+
function renderTrades(trades, text) {
|
|
248
|
+
if (trades.length === 0) {
|
|
249
|
+
return `<section><h2>${escapeHtml(text.tradesHeading)}</h2><p class="empty">${escapeHtml(text.noClosedTrades)}</p></section>`;
|
|
250
|
+
}
|
|
251
|
+
// Running equity per trade answers "when did this go wrong", which a list of
|
|
252
|
+
// individual results cannot.
|
|
253
|
+
let cumulative = 0;
|
|
254
|
+
const rows = trades
|
|
255
|
+
.map((trade, index) => {
|
|
256
|
+
const net = num(trade.netPnlUsdt ?? trade.realizedPnlUsdt);
|
|
257
|
+
cumulative += net ?? 0;
|
|
258
|
+
const opened = num(trade.openedAtMs) ?? 0;
|
|
259
|
+
const closed = num(trade.closedAtMs) ?? 0;
|
|
260
|
+
return `<tr class="${sign(net)}">
|
|
261
|
+
<td class="n dim">${formatNumber(index + 1)}</td>
|
|
262
|
+
<td>${escapeHtml(String(trade.side ?? "-"))}</td>
|
|
263
|
+
<td class="n">${plain(num(trade.quantity), "")}</td>
|
|
264
|
+
<td class="n">${plain(num(trade.entryPrice), "")}</td>
|
|
265
|
+
<td class="n">${plain(num(trade.exitPrice), "")}</td>
|
|
266
|
+
<td class="n">${signed(net, "")}</td>
|
|
267
|
+
<td class="n">${signed(num(trade.returnPct), "%")}</td>
|
|
268
|
+
<td class="n dim">${signed(cumulative, "")}</td>
|
|
269
|
+
<td class="n dim">${plain(num(trade.feesUsdt), "")}</td>
|
|
270
|
+
<td class="n">${escapeHtml(formatHolding(num(trade.holdingBars) ?? 0))}</td>
|
|
271
|
+
<td class="mono">${escapeHtml(isoMinute(opened))}</td>
|
|
272
|
+
<td class="mono">${escapeHtml(closed > 0 ? isoMinute(closed) : "-")}</td>
|
|
273
|
+
</tr>`;
|
|
274
|
+
})
|
|
275
|
+
.join("");
|
|
276
|
+
return `
|
|
277
|
+
<section>
|
|
278
|
+
<h2>${escapeHtml(text.tradesHeading)} <span class="count">${formatNumber(trades.length)}</span></h2>
|
|
279
|
+
<div class="scroll tall">
|
|
280
|
+
<table>
|
|
281
|
+
<thead><tr>
|
|
282
|
+
<th class="n">#</th><th>${escapeHtml(text.colSide)}</th><th class="n">${escapeHtml(text.colQty)}</th>
|
|
283
|
+
<th class="n">${escapeHtml(text.colEntry)}</th><th class="n">${escapeHtml(text.colExit)}</th>
|
|
284
|
+
<th class="n">${escapeHtml(text.colNetPnl)}</th><th class="n">${escapeHtml(text.colReturn)}</th>
|
|
285
|
+
<th class="n">${escapeHtml(text.colCumulative)}</th><th class="n">${escapeHtml(text.colFees)}</th>
|
|
286
|
+
<th class="n">${escapeHtml(text.colHeld)}</th>
|
|
287
|
+
<th>${escapeHtml(text.colOpened)}</th><th>${escapeHtml(text.colClosed)}</th>
|
|
288
|
+
</tr></thead>
|
|
289
|
+
<tbody>${rows}</tbody>
|
|
290
|
+
</table>
|
|
291
|
+
</div>
|
|
292
|
+
</section>`;
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* The assumptions the run used.
|
|
296
|
+
*
|
|
297
|
+
* A return figure means nothing without the leverage, budget, and costs behind
|
|
298
|
+
* it, and two runs are only comparable when these match.
|
|
299
|
+
*/
|
|
300
|
+
function renderAssumptions(run, text) {
|
|
301
|
+
const assumptions = run.assumptions;
|
|
302
|
+
if (!assumptions)
|
|
303
|
+
return "";
|
|
304
|
+
const contract = (assumptions.contract ?? {});
|
|
305
|
+
const fixedBudget = num(assumptions.entryBudgetUsdt);
|
|
306
|
+
const rows = [
|
|
307
|
+
[text.settingEquity, `${plain(num(assumptions.initialEquityUsdt), "")} USDT`],
|
|
308
|
+
[text.settingLeverage, `${plain(num(assumptions.leverage), "")}x`],
|
|
309
|
+
[text.settingMarginSafety, `${plain(num(assumptions.marginSafetyMultiplier), "")}x`],
|
|
310
|
+
[
|
|
311
|
+
text.settingBudgetPct,
|
|
312
|
+
fixedBudget === null
|
|
313
|
+
? asPercent(num(assumptions.entryBudgetPct))
|
|
314
|
+
: `${plain(fixedBudget, "")} USDT`
|
|
315
|
+
],
|
|
316
|
+
[text.settingTakerFee, asBps(num(assumptions.takerFeeRate))],
|
|
317
|
+
[text.settingMakerFee, asBps(num(assumptions.makerFeeRate))],
|
|
318
|
+
[text.settingEntrySlippage, `${plain(num(assumptions.entrySlippageBps), "")} bps`],
|
|
319
|
+
[text.settingExitSlippage, `${plain(num(assumptions.exitSlippageBps), "")} bps`],
|
|
320
|
+
[text.settingPreload, `${formatNumber(num(assumptions.preloadBars) ?? 0)}`],
|
|
321
|
+
[text.settingCloseAtEnd, assumptions.closeAtEnd === false ? text.no : text.yes],
|
|
322
|
+
[
|
|
323
|
+
"contract",
|
|
324
|
+
`${plain(num(contract.contractValue), "")} ${String(contract.contractValueCurrency ?? "")} x${plain(num(contract.contractMultiplier), "")}`
|
|
325
|
+
],
|
|
326
|
+
["lot / min", `${plain(num(contract.lotSize), "")} / ${plain(num(contract.minSize), "")}`]
|
|
327
|
+
];
|
|
328
|
+
const params = assumptions.params;
|
|
329
|
+
const paramRow = params && Object.keys(params).length
|
|
330
|
+
? `<div><dt>params</dt><dd class="mono">${escapeHtml(Object.entries(params).map(([key, value]) => `${key}=${String(value)}`).join(" "))}</dd></div>`
|
|
331
|
+
: "";
|
|
332
|
+
const intervals = Array.isArray(assumptions.intervals) ? assumptions.intervals : [];
|
|
333
|
+
const intervalRow = intervals.length
|
|
334
|
+
? `<div><dt>timeframes</dt><dd class="mono">${escapeHtml(intervals.join(", "))}</dd></div>`
|
|
335
|
+
: "";
|
|
336
|
+
return `
|
|
337
|
+
<section>
|
|
338
|
+
<h2>${escapeHtml(text.assumptionsHeading)}</h2>
|
|
339
|
+
<dl>${rows
|
|
340
|
+
.map(([name, value]) => `<div><dt>${escapeHtml(name)}</dt><dd>${escapeHtml(value)}</dd></div>`)
|
|
341
|
+
.join("")}${intervalRow}${paramRow}</dl>
|
|
342
|
+
</section>`;
|
|
343
|
+
}
|
|
344
|
+
function asBps(value) {
|
|
345
|
+
return value === null ? "-" : `${(value * 10_000).toFixed(2)} bps`;
|
|
346
|
+
}
|
|
347
|
+
function asPercent(value) {
|
|
348
|
+
return value === null ? "-" : `${(value * 100).toFixed(2)}%`;
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Monthly result table.
|
|
352
|
+
*
|
|
353
|
+
* A single total hides whether an edge was steady or came from one month, which
|
|
354
|
+
* is the difference between a strategy and a lucky window.
|
|
355
|
+
*/
|
|
356
|
+
function renderMonthly(equity, text) {
|
|
357
|
+
if (equity.length < 2)
|
|
358
|
+
return "";
|
|
359
|
+
const months = new Map();
|
|
360
|
+
for (const point of equity) {
|
|
361
|
+
const key = new Date(point.timeMs).toISOString().slice(0, 7);
|
|
362
|
+
const found = months.get(key);
|
|
363
|
+
if (!found) {
|
|
364
|
+
months.set(key, { start: point.equityUsdt, end: point.equityUsdt, low: point.equityUsdt, high: point.equityUsdt });
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
found.end = point.equityUsdt;
|
|
368
|
+
found.low = Math.min(found.low, point.equityUsdt);
|
|
369
|
+
found.high = Math.max(found.high, point.equityUsdt);
|
|
370
|
+
}
|
|
371
|
+
if (months.size < 2)
|
|
372
|
+
return "";
|
|
373
|
+
const rows = [...months.entries()]
|
|
374
|
+
.map(([month, values]) => {
|
|
375
|
+
const change = values.start > 0 ? (values.end / values.start - 1) * 100 : 0;
|
|
376
|
+
const depth = values.high > 0 ? ((values.high - values.low) / values.high) * 100 : 0;
|
|
377
|
+
return `<tr class="${sign(change)}">
|
|
378
|
+
<td class="mono">${escapeHtml(month)}</td>
|
|
379
|
+
<td class="n">${plain(values.start, "")}</td>
|
|
380
|
+
<td class="n">${plain(values.end, "")}</td>
|
|
381
|
+
<td class="n">${signed(change, "%")}</td>
|
|
382
|
+
<td class="n dim">${plain(depth, "%")}</td>
|
|
383
|
+
</tr>`;
|
|
384
|
+
})
|
|
385
|
+
.join("");
|
|
386
|
+
return `
|
|
387
|
+
<section>
|
|
388
|
+
<h2>${escapeHtml(text.monthlyHeading)}</h2>
|
|
389
|
+
<div class="scroll">
|
|
390
|
+
<table>
|
|
391
|
+
<thead><tr><th>${escapeHtml(text.colMonth)}</th><th class="n">${escapeHtml(text.colOpenEquity)}</th><th class="n">${escapeHtml(text.colCloseEquity)}</th><th class="n">${escapeHtml(text.colChange)}</th><th class="n">${escapeHtml(text.colWorstDrop)}</th></tr></thead>
|
|
392
|
+
<tbody>${rows}</tbody>
|
|
393
|
+
</table>
|
|
394
|
+
</div>
|
|
395
|
+
</section>`;
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Histogram of per-trade results.
|
|
399
|
+
*
|
|
400
|
+
* Shows whether an average came from many similar trades or a few outliers — the
|
|
401
|
+
* same mean can describe either.
|
|
402
|
+
*/
|
|
403
|
+
function renderDistribution(trades, text) {
|
|
404
|
+
const values = trades
|
|
405
|
+
.map((trade) => num(trade.netPnlUsdt ?? trade.realizedPnlUsdt))
|
|
406
|
+
.filter((value) => value !== null);
|
|
407
|
+
if (values.length < 5)
|
|
408
|
+
return "";
|
|
409
|
+
const minimum = Math.min(...values);
|
|
410
|
+
const maximum = Math.max(...values);
|
|
411
|
+
if (maximum === minimum)
|
|
412
|
+
return "";
|
|
413
|
+
const buckets = 11;
|
|
414
|
+
const width = (maximum - minimum) / buckets;
|
|
415
|
+
const counts = new Array(buckets).fill(0);
|
|
416
|
+
for (const value of values) {
|
|
417
|
+
const index = Math.min(buckets - 1, Math.floor((value - minimum) / width));
|
|
418
|
+
counts[index] += 1;
|
|
419
|
+
}
|
|
420
|
+
const peak = Math.max(...counts);
|
|
421
|
+
const bars = counts
|
|
422
|
+
.map((count, index) => {
|
|
423
|
+
const from = minimum + index * width;
|
|
424
|
+
const to = from + width;
|
|
425
|
+
const share = peak > 0 ? (count / peak) * 100 : 0;
|
|
426
|
+
const losing = to <= 0;
|
|
427
|
+
return `<div class="bin">
|
|
428
|
+
<span class="range mono">${escapeHtml(compact(from))}…${escapeHtml(compact(to))}</span>
|
|
429
|
+
<span class="track"><span class="fill ${losing ? "down" : "up"}" style="width:${share.toFixed(1)}%"></span></span>
|
|
430
|
+
<span class="count">${formatNumber(count)}</span>
|
|
431
|
+
</div>`;
|
|
432
|
+
})
|
|
433
|
+
.join("");
|
|
434
|
+
return `
|
|
435
|
+
<section>
|
|
436
|
+
<h2>${escapeHtml(text.distributionHeading)}</h2>
|
|
437
|
+
<div class="histogram">${bars}</div>
|
|
438
|
+
</section>`;
|
|
439
|
+
}
|
|
440
|
+
/** Bars as a duration, since "4210 bars" is harder to judge than "2d 22h". */
|
|
441
|
+
function formatHolding(bars) {
|
|
442
|
+
if (bars < 60)
|
|
443
|
+
return `${Math.round(bars)}m`;
|
|
444
|
+
if (bars < 1_440)
|
|
445
|
+
return `${Math.floor(bars / 60)}h${String(Math.round(bars % 60)).padStart(2, "0")}m`;
|
|
446
|
+
const days = Math.floor(bars / 1_440);
|
|
447
|
+
const hours = Math.floor((bars % 1_440) / 60);
|
|
448
|
+
return `${days}d${String(hours).padStart(2, "0")}h`;
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Reduces a curve for plotting while keeping its shape.
|
|
452
|
+
*
|
|
453
|
+
* Each bucket contributes both its highest and lowest point, in the order they
|
|
454
|
+
* occurred. Picking one point per bucket — even alternating high and low — drops
|
|
455
|
+
* a spike whenever it lands in a bucket that happens to be looking for the other
|
|
456
|
+
* extreme, which erases exactly the moves a reader is scanning for.
|
|
457
|
+
*/
|
|
458
|
+
export function downsample(points, target) {
|
|
459
|
+
if (points.length <= target)
|
|
460
|
+
return points;
|
|
461
|
+
const buckets = Math.max(1, Math.floor(target / 2));
|
|
462
|
+
const size = points.length / buckets;
|
|
463
|
+
const output = [];
|
|
464
|
+
for (let index = 0; index < buckets; index += 1) {
|
|
465
|
+
const start = Math.floor(index * size);
|
|
466
|
+
const end = Math.min(points.length, Math.floor((index + 1) * size));
|
|
467
|
+
if (end <= start)
|
|
468
|
+
continue;
|
|
469
|
+
let lowest = points[start];
|
|
470
|
+
let highest = points[start];
|
|
471
|
+
for (let inner = start; inner < end; inner += 1) {
|
|
472
|
+
const candidate = points[inner];
|
|
473
|
+
if (candidate.equityUsdt < lowest.equityUsdt)
|
|
474
|
+
lowest = candidate;
|
|
475
|
+
if (candidate.equityUsdt > highest.equityUsdt)
|
|
476
|
+
highest = candidate;
|
|
477
|
+
}
|
|
478
|
+
// Preserve chronology so the line does not zig-zag artificially.
|
|
479
|
+
const [first, second] = lowest.timeMs <= highest.timeMs ? [lowest, highest] : [highest, lowest];
|
|
480
|
+
output.push(first);
|
|
481
|
+
if (second !== first)
|
|
482
|
+
output.push(second);
|
|
483
|
+
}
|
|
484
|
+
return output;
|
|
485
|
+
}
|
|
486
|
+
/** Percentage below the running peak at each point. */
|
|
487
|
+
export function drawdownSeries(points, initialEquity) {
|
|
488
|
+
let peak = initialEquity;
|
|
489
|
+
return points.map((point) => {
|
|
490
|
+
peak = Math.max(peak, point.equityUsdt);
|
|
491
|
+
return peak > 0 ? ((peak - point.equityUsdt) / peak) * 100 : 0;
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
/** The document's `lang` attribute, so a browser picks the right font stack. */
|
|
495
|
+
function activeLang() {
|
|
496
|
+
return activeLocale();
|
|
497
|
+
}
|
|
498
|
+
function num(value) {
|
|
499
|
+
if (value === null || value === undefined)
|
|
500
|
+
return null;
|
|
501
|
+
const parsed = Number(value);
|
|
502
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
503
|
+
}
|
|
504
|
+
function sign(value) {
|
|
505
|
+
if (value === null || value === 0)
|
|
506
|
+
return "flat";
|
|
507
|
+
return value > 0 ? "up" : "down";
|
|
508
|
+
}
|
|
509
|
+
function signed(value, suffix) {
|
|
510
|
+
if (value === null)
|
|
511
|
+
return "-";
|
|
512
|
+
return `${value > 0 ? "+" : ""}${compact(value)}${suffix}`;
|
|
513
|
+
}
|
|
514
|
+
function plain(value, suffix) {
|
|
515
|
+
if (value === null)
|
|
516
|
+
return "-";
|
|
517
|
+
return `${compact(value)}${suffix}`;
|
|
518
|
+
}
|
|
519
|
+
function compact(value) {
|
|
520
|
+
const magnitude = Math.abs(value);
|
|
521
|
+
if (magnitude >= 1_000_000)
|
|
522
|
+
return `${(value / 1_000_000).toFixed(2)}M`;
|
|
523
|
+
if (magnitude >= 1_000)
|
|
524
|
+
return value.toFixed(0);
|
|
525
|
+
return value.toFixed(2);
|
|
526
|
+
}
|
|
527
|
+
function formatNumber(value) {
|
|
528
|
+
return Math.round(value).toLocaleString("en-US");
|
|
529
|
+
}
|
|
530
|
+
function isoMinute(ms) {
|
|
531
|
+
return new Date(ms).toISOString().slice(0, 16).replace("T", " ");
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* Escapes text for HTML.
|
|
535
|
+
*
|
|
536
|
+
* Strategy names and error messages come from user input, so anything
|
|
537
|
+
* interpolated into the document is escaped rather than trusted.
|
|
538
|
+
*/
|
|
539
|
+
export function escapeHtml(value) {
|
|
540
|
+
return value
|
|
541
|
+
.replace(/&/g, "&")
|
|
542
|
+
.replace(/</g, "<")
|
|
543
|
+
.replace(/>/g, ">")
|
|
544
|
+
.replace(/"/g, """)
|
|
545
|
+
.replace(/'/g, "'");
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* Escapes a value for a single-quoted HTML attribute.
|
|
549
|
+
*
|
|
550
|
+
* The series JSON is carried in an attribute rather than a script literal, so a
|
|
551
|
+
* quote or an angle bracket inside it cannot terminate the attribute or open a
|
|
552
|
+
* tag. Kept separate from `escapeHtml` because that one escapes double quotes
|
|
553
|
+
* and would mangle the JSON's own syntax.
|
|
554
|
+
*/
|
|
555
|
+
export function escapeAttribute(value) {
|
|
556
|
+
return value.replace(/&/g, "&").replace(/'/g, "'").replace(/</g, "<").replace(/>/g, ">");
|
|
557
|
+
}
|
|
558
|
+
const STYLES = `
|
|
559
|
+
:root{--bg:#0f1116;--panel:#171a21;--line:#242833;--text:#e6e8ee;--dim:#8b90a0;--up:#3fb950;--down:#f2777a;--accent:#4f9cf9}
|
|
560
|
+
@media(prefers-color-scheme:light){:root{--bg:#f7f8fa;--panel:#fff;--line:#e3e6ec;--text:#1a1d24;--dim:#6b7280;--up:#1a7f37;--down:#c9372c}}
|
|
561
|
+
*{box-sizing:border-box}
|
|
562
|
+
body{margin:0;background:var(--bg);color:var(--text);font:14px/1.55 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue","PingFang SC","Microsoft YaHei",sans-serif}
|
|
563
|
+
main{max-width:1080px;margin:0 auto;padding:32px 24px 64px}
|
|
564
|
+
header{display:flex;justify-content:space-between;align-items:flex-end;gap:24px;flex-wrap:wrap;padding-bottom:20px;border-bottom:1px solid var(--line)}
|
|
565
|
+
h1{margin:0;font-size:26px;font-weight:600;letter-spacing:-.01em}
|
|
566
|
+
.sub{margin:4px 0 0;color:var(--dim);font-size:13px}
|
|
567
|
+
.headline{text-align:right}
|
|
568
|
+
.headline .value{display:block;font-size:34px;font-weight:650;letter-spacing:-.02em;font-variant-numeric:tabular-nums}
|
|
569
|
+
.headline .label{color:var(--dim);font-size:12px;text-transform:uppercase;letter-spacing:.08em}
|
|
570
|
+
.up{color:var(--up)}.down{color:var(--down)}.flat{color:var(--dim)}
|
|
571
|
+
.facts{display:flex;gap:28px;flex-wrap:wrap;margin:20px 0 8px;color:var(--dim);font-size:13px}
|
|
572
|
+
.facts span{display:block;text-transform:uppercase;letter-spacing:.06em;font-size:11px}
|
|
573
|
+
.facts strong{color:var(--text);font-weight:550;font-variant-numeric:tabular-nums}
|
|
574
|
+
.warnings{margin:20px 0 0;padding:12px 16px;border-left:3px solid var(--down);background:color-mix(in srgb,var(--down) 8%,transparent);border-radius:0 6px 6px 0}
|
|
575
|
+
.warnings p{margin:4px 0;font-size:13px}
|
|
576
|
+
section{margin-top:32px}
|
|
577
|
+
h2{margin:0 0 14px;font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:.1em;color:var(--dim)}
|
|
578
|
+
h2 .count{color:var(--dim);font-weight:400}
|
|
579
|
+
dl{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:1px;margin:0;background:var(--line);border:1px solid var(--line);border-radius:8px;overflow:hidden}
|
|
580
|
+
dl>div{background:var(--panel);padding:12px 14px}
|
|
581
|
+
dt{color:var(--dim);font-size:11px;text-transform:uppercase;letter-spacing:.06em}
|
|
582
|
+
dd{margin:3px 0 0;font-size:16px;font-weight:550;font-variant-numeric:tabular-nums}
|
|
583
|
+
.chart{background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:10px 12px;display:flex;gap:10px;align-items:stretch}
|
|
584
|
+
.chart.drawdown{margin-top:8px;border-radius:8px}
|
|
585
|
+
.chart svg{flex:1;height:240px;width:100%;display:block}
|
|
586
|
+
.chart.drawdown svg{height:80px}
|
|
587
|
+
.axis{display:flex;flex-direction:column;justify-content:space-between;color:var(--dim);font-size:11px;font-variant-numeric:tabular-nums;min-width:62px;text-align:right}
|
|
588
|
+
.range{margin:8px 0 0;color:var(--dim);font-size:12px}
|
|
589
|
+
.chart{position:relative}
|
|
590
|
+
.chart svg{cursor:crosshair}
|
|
591
|
+
.chart:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
|
|
592
|
+
.baseline{stroke:var(--dim);stroke-width:1;stroke-dasharray:3 4;opacity:.5;vector-effect:non-scaling-stroke}
|
|
593
|
+
.crosshair{stroke:var(--dim);stroke-width:1;vector-effect:non-scaling-stroke;pointer-events:none}
|
|
594
|
+
.marker{fill:var(--accent);stroke:var(--panel);stroke-width:1.5;pointer-events:none}
|
|
595
|
+
.tooltip{position:absolute;top:10px;pointer-events:none;background:var(--panel);border:1px solid var(--line);border-radius:6px;padding:8px 10px;font-size:12px;min-width:180px;box-shadow:0 4px 16px rgba(0,0,0,.28);z-index:2}
|
|
596
|
+
.tooltip .row{display:flex;justify-content:space-between;gap:14px;padding:1px 0}
|
|
597
|
+
.tooltip .row span{color:var(--dim)}
|
|
598
|
+
.tooltip .row b{font-weight:550;font-variant-numeric:tabular-nums}
|
|
599
|
+
.scroll{overflow-x:auto;border:1px solid var(--line);border-radius:8px}
|
|
600
|
+
table{width:100%;border-collapse:collapse;font-size:13px}
|
|
601
|
+
th,td{padding:8px 12px;text-align:left;border-bottom:1px solid var(--line);white-space:nowrap}
|
|
602
|
+
th{background:var(--panel);color:var(--dim);font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.06em;position:sticky;top:0}
|
|
603
|
+
tbody tr:last-child td{border-bottom:none}
|
|
604
|
+
td.n{text-align:right;font-variant-numeric:tabular-nums}
|
|
605
|
+
tr.up td:nth-child(5),tr.up td:nth-child(6){color:var(--up)}
|
|
606
|
+
tr.down td:nth-child(5),tr.down td:nth-child(6){color:var(--down)}
|
|
607
|
+
.mono{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px}
|
|
608
|
+
td.dim{color:var(--dim)}
|
|
609
|
+
.scroll.tall{max-height:520px;overflow-y:auto}
|
|
610
|
+
.histogram{background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:12px 14px}
|
|
611
|
+
.bin{display:flex;align-items:center;gap:10px;padding:3px 0}
|
|
612
|
+
.bin .range{min-width:132px;color:var(--dim);text-align:right;flex-shrink:0}
|
|
613
|
+
.bin .track{flex:1;height:12px;background:color-mix(in srgb,var(--line) 60%,transparent);border-radius:3px;overflow:hidden}
|
|
614
|
+
.bin .fill{display:block;height:100%;border-radius:3px}
|
|
615
|
+
.bin .fill.up{background:var(--up)}
|
|
616
|
+
.bin .fill.down{background:var(--down)}
|
|
617
|
+
.bin>.count{min-width:44px;text-align:right;color:var(--dim);font-size:12px;font-variant-numeric:tabular-nums;flex-shrink:0}
|
|
618
|
+
.empty{color:var(--dim);font-size:13px}
|
|
619
|
+
footer{margin-top:40px;padding-top:16px;border-top:1px solid var(--line);color:var(--dim);font-size:12px}
|
|
620
|
+
footer p{margin:4px 0}
|
|
621
|
+
`;
|
|
622
|
+
/**
|
|
623
|
+
* Styles for the comparison document.
|
|
624
|
+
*
|
|
625
|
+
* The single-run sheet plus the few rules a matrix table needs, rather than a
|
|
626
|
+
* second copy: the two documents should not drift apart visually, and a duplicated
|
|
627
|
+
* sheet is exactly how that happens.
|
|
628
|
+
*/
|
|
629
|
+
export const COMPARE_STYLES = `${STYLES}
|
|
630
|
+
.legend{display:flex;gap:20px;flex-wrap:wrap;margin:20px 0 0;font-size:13px}
|
|
631
|
+
.legend div{display:flex;align-items:center;gap:7px}
|
|
632
|
+
.legend i,th i{display:inline-block;width:10px;height:10px;border-radius:2px;flex-shrink:0}
|
|
633
|
+
th i{margin-right:6px;vertical-align:middle}
|
|
634
|
+
table.matrix th[scope=row]{background:var(--panel);color:var(--dim);text-transform:none;letter-spacing:0;font-size:12px;font-weight:500;text-align:left;position:sticky;left:0}
|
|
635
|
+
table.matrix thead th{text-align:right}
|
|
636
|
+
table.matrix thead th span.dim{display:block;font-weight:400;text-transform:none;letter-spacing:0;font-size:10px}
|
|
637
|
+
td.best{color:var(--up);font-weight:600}
|
|
638
|
+
.chart .tooltip .swatch{display:inline-block;width:8px;height:8px;border-radius:2px;margin-right:6px}
|
|
639
|
+
.chart .tooltip .row.head{border-bottom:1px solid var(--line);margin-bottom:3px;padding-bottom:3px}
|
|
640
|
+
`;
|
|
641
|
+
//# sourceMappingURL=html.js.map
|