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.
Files changed (236) hide show
  1. package/README.en.md +90 -10
  2. package/README.md +76 -10
  3. package/dist/account/private-websocket.js +4 -4
  4. package/dist/account/private-websocket.js.map +1 -1
  5. package/dist/account/service.d.ts +12 -1
  6. package/dist/account/service.js +18 -0
  7. package/dist/account/service.js.map +1 -1
  8. package/dist/bars/rate-limiter.d.ts +18 -0
  9. package/dist/bars/rate-limiter.js +84 -0
  10. package/dist/bars/rate-limiter.js.map +1 -0
  11. package/dist/bars/schema.d.ts +36 -0
  12. package/dist/bars/schema.js +134 -0
  13. package/dist/bars/schema.js.map +1 -0
  14. package/dist/bars/service.d.ts +60 -0
  15. package/dist/bars/service.js +120 -0
  16. package/dist/bars/service.js.map +1 -0
  17. package/dist/bars/store.d.ts +105 -0
  18. package/dist/bars/store.js +415 -0
  19. package/dist/bars/store.js.map +1 -0
  20. package/dist/bars/timeframe.d.ts +40 -0
  21. package/dist/bars/timeframe.js +146 -0
  22. package/dist/bars/timeframe.js.map +1 -0
  23. package/dist/bars/types.d.ts +68 -0
  24. package/dist/bars/types.js +13 -0
  25. package/dist/bars/types.js.map +1 -0
  26. package/dist/cli/data-render.d.ts +37 -0
  27. package/dist/cli/data-render.js +143 -0
  28. package/dist/cli/data-render.js.map +1 -0
  29. package/dist/cli/doctor.js +7 -3
  30. package/dist/cli/doctor.js.map +1 -1
  31. package/dist/cli/index.js +757 -26
  32. package/dist/cli/index.js.map +1 -1
  33. package/dist/cli/live-render.d.ts +24 -0
  34. package/dist/cli/live-render.js +85 -0
  35. package/dist/cli/live-render.js.map +1 -0
  36. package/dist/cli/range.d.ts +28 -0
  37. package/dist/cli/range.js +63 -0
  38. package/dist/cli/range.js.map +1 -0
  39. package/dist/cli/render.js +3 -0
  40. package/dist/cli/render.js.map +1 -1
  41. package/dist/cli/strategy-render.d.ts +36 -0
  42. package/dist/cli/strategy-render.js +391 -0
  43. package/dist/cli/strategy-render.js.map +1 -0
  44. package/dist/cli/width.d.ts +18 -0
  45. package/dist/cli/width.js +71 -0
  46. package/dist/cli/width.js.map +1 -0
  47. package/dist/config/loader.js +1 -1
  48. package/dist/config/schema.d.ts +7 -0
  49. package/dist/config/schema.js +24 -0
  50. package/dist/config/schema.js.map +1 -1
  51. package/dist/core/okx-client.d.ts +9 -1
  52. package/dist/core/okx-client.js +14 -5
  53. package/dist/core/okx-client.js.map +1 -1
  54. package/dist/i18n/locale.d.ts +24 -0
  55. package/dist/i18n/locale.js +65 -0
  56. package/dist/i18n/locale.js.map +1 -0
  57. package/dist/i18n/messages.d.ts +333 -0
  58. package/dist/i18n/messages.js +660 -0
  59. package/dist/i18n/messages.js.map +1 -0
  60. package/dist/live/account-snapshot.d.ts +30 -0
  61. package/dist/live/account-snapshot.js +130 -0
  62. package/dist/live/account-snapshot.js.map +1 -0
  63. package/dist/live/cutoff-queue.d.ts +42 -0
  64. package/dist/live/cutoff-queue.js +69 -0
  65. package/dist/live/cutoff-queue.js.map +1 -0
  66. package/dist/live/execution-key.d.ts +23 -0
  67. package/dist/live/execution-key.js +31 -0
  68. package/dist/live/execution-key.js.map +1 -0
  69. package/dist/live/failures.d.ts +37 -0
  70. package/dist/live/failures.js +57 -0
  71. package/dist/live/failures.js.map +1 -0
  72. package/dist/live/gates.d.ts +65 -0
  73. package/dist/live/gates.js +136 -0
  74. package/dist/live/gates.js.map +1 -0
  75. package/dist/live/loop.d.ts +56 -0
  76. package/dist/live/loop.js +197 -0
  77. package/dist/live/loop.js.map +1 -0
  78. package/dist/live/preconditions.d.ts +48 -0
  79. package/dist/live/preconditions.js +69 -0
  80. package/dist/live/preconditions.js.map +1 -0
  81. package/dist/live/reconcile.d.ts +46 -0
  82. package/dist/live/reconcile.js +104 -0
  83. package/dist/live/reconcile.js.map +1 -0
  84. package/dist/live/runner.d.ts +57 -0
  85. package/dist/live/runner.js +160 -0
  86. package/dist/live/runner.js.map +1 -0
  87. package/dist/live/schema.d.ts +18 -0
  88. package/dist/live/schema.js +91 -0
  89. package/dist/live/schema.js.map +1 -0
  90. package/dist/live/service.d.ts +144 -0
  91. package/dist/live/service.js +303 -0
  92. package/dist/live/service.js.map +1 -0
  93. package/dist/live/session.d.ts +85 -0
  94. package/dist/live/session.js +234 -0
  95. package/dist/live/session.js.map +1 -0
  96. package/dist/live/sizing.d.ts +62 -0
  97. package/dist/live/sizing.js +79 -0
  98. package/dist/live/sizing.js.map +1 -0
  99. package/dist/live/store.d.ts +123 -0
  100. package/dist/live/store.js +350 -0
  101. package/dist/live/store.js.map +1 -0
  102. package/dist/live/types.d.ts +82 -0
  103. package/dist/live/types.js +2 -0
  104. package/dist/live/types.js.map +1 -0
  105. package/dist/market/websocket.d.ts +16 -1
  106. package/dist/market/websocket.js +60 -5
  107. package/dist/market/websocket.js.map +1 -1
  108. package/dist/mcp/server.d.ts +1 -0
  109. package/dist/mcp/server.js +15 -1
  110. package/dist/mcp/server.js.map +1 -1
  111. package/dist/network/connectivity.d.ts +9 -1
  112. package/dist/network/connectivity.js +28 -1
  113. package/dist/network/connectivity.js.map +1 -1
  114. package/dist/report/chart-script.d.ts +12 -0
  115. package/dist/report/chart-script.js +146 -0
  116. package/dist/report/chart-script.js.map +1 -0
  117. package/dist/report/compare-html.d.ts +8 -0
  118. package/dist/report/compare-html.js +254 -0
  119. package/dist/report/compare-html.js.map +1 -0
  120. package/dist/report/compare-script.d.ts +12 -0
  121. package/dist/report/compare-script.js +109 -0
  122. package/dist/report/compare-script.js.map +1 -0
  123. package/dist/report/compare.d.ts +61 -0
  124. package/dist/report/compare.js +205 -0
  125. package/dist/report/compare.js.map +1 -0
  126. package/dist/report/fetch.d.ts +20 -0
  127. package/dist/report/fetch.js +56 -0
  128. package/dist/report/fetch.js.map +1 -0
  129. package/dist/report/html.d.ts +54 -0
  130. package/dist/report/html.js +641 -0
  131. package/dist/report/html.js.map +1 -0
  132. package/dist/report/open.d.ts +42 -0
  133. package/dist/report/open.js +114 -0
  134. package/dist/report/open.js.map +1 -0
  135. package/dist/runtime/server.d.ts +16 -1
  136. package/dist/runtime/server.js +112 -9
  137. package/dist/runtime/server.js.map +1 -1
  138. package/dist/setup/installer.d.ts +1 -0
  139. package/dist/setup/installer.js +8 -0
  140. package/dist/setup/installer.js.map +1 -1
  141. package/dist/setup/wizard.js +19 -22
  142. package/dist/setup/wizard.js.map +1 -1
  143. package/dist/strategy/constants.d.ts +23 -0
  144. package/dist/strategy/constants.js +24 -0
  145. package/dist/strategy/constants.js.map +1 -0
  146. package/dist/strategy/environment.d.ts +52 -0
  147. package/dist/strategy/environment.js +187 -0
  148. package/dist/strategy/environment.js.map +1 -0
  149. package/dist/strategy/instrument.d.ts +29 -0
  150. package/dist/strategy/instrument.js +39 -0
  151. package/dist/strategy/instrument.js.map +1 -0
  152. package/dist/strategy/optimize.d.ts +73 -0
  153. package/dist/strategy/optimize.js +113 -0
  154. package/dist/strategy/optimize.js.map +1 -0
  155. package/dist/strategy/parameter-space.d.ts +59 -0
  156. package/dist/strategy/parameter-space.js +221 -0
  157. package/dist/strategy/parameter-space.js.map +1 -0
  158. package/dist/strategy/python-bridge.d.ts +24 -0
  159. package/dist/strategy/python-bridge.js +114 -0
  160. package/dist/strategy/python-bridge.js.map +1 -0
  161. package/dist/strategy/schema.d.ts +9 -0
  162. package/dist/strategy/schema.js +91 -0
  163. package/dist/strategy/schema.js.map +1 -0
  164. package/dist/strategy/service.d.ts +138 -0
  165. package/dist/strategy/service.js +745 -0
  166. package/dist/strategy/service.js.map +1 -0
  167. package/dist/strategy/settings.d.ts +162 -0
  168. package/dist/strategy/settings.js +243 -0
  169. package/dist/strategy/settings.js.map +1 -0
  170. package/dist/strategy/store.d.ts +96 -0
  171. package/dist/strategy/store.js +367 -0
  172. package/dist/strategy/store.js.map +1 -0
  173. package/dist/strategy/templates.d.ts +11 -0
  174. package/dist/strategy/templates.js +134 -0
  175. package/dist/strategy/templates.js.map +1 -0
  176. package/dist/strategy/types.d.ts +111 -0
  177. package/dist/strategy/types.js +2 -0
  178. package/dist/strategy/types.js.map +1 -0
  179. package/dist/tools/catalog.d.ts +16 -0
  180. package/dist/tools/catalog.js +118 -17
  181. package/dist/tools/catalog.js.map +1 -1
  182. package/dist/trade/service.d.ts +12 -0
  183. package/dist/trade/service.js +24 -7
  184. package/dist/trade/service.js.map +1 -1
  185. package/dist/tui/app.d.ts +23 -0
  186. package/dist/tui/app.js +322 -0
  187. package/dist/tui/app.js.map +1 -0
  188. package/dist/tui/commands.d.ts +70 -0
  189. package/dist/tui/commands.js +313 -0
  190. package/dist/tui/commands.js.map +1 -0
  191. package/dist/tui/entries.d.ts +17 -0
  192. package/dist/tui/entries.js +24 -0
  193. package/dist/tui/entries.js.map +1 -0
  194. package/dist/tui/execute.d.ts +26 -0
  195. package/dist/tui/execute.js +664 -0
  196. package/dist/tui/execute.js.map +1 -0
  197. package/dist/tui/history.d.ts +17 -0
  198. package/dist/tui/history.js +48 -0
  199. package/dist/tui/history.js.map +1 -0
  200. package/dist/tui/index.d.ts +8 -0
  201. package/dist/tui/index.js +48 -0
  202. package/dist/tui/index.js.map +1 -0
  203. package/dist/tui/line-editor.d.ts +44 -0
  204. package/dist/tui/line-editor.js +98 -0
  205. package/dist/tui/line-editor.js.map +1 -0
  206. package/dist/tui/progress.d.ts +23 -0
  207. package/dist/tui/progress.js +46 -0
  208. package/dist/tui/progress.js.map +1 -0
  209. package/dist/tui/settings-editor.d.ts +18 -0
  210. package/dist/tui/settings-editor.js +115 -0
  211. package/dist/tui/settings-editor.js.map +1 -0
  212. package/docs/live-trading.md +455 -0
  213. package/docs/strategy-research.md +597 -0
  214. package/package.json +10 -1
  215. package/python/desic_strategy/__init__.py +34 -0
  216. package/python/desic_strategy/actions.py +158 -0
  217. package/python/desic_strategy/context.py +164 -0
  218. package/python/desic_strategy/engine.py +614 -0
  219. package/python/desic_strategy/indicators.py +159 -0
  220. package/python/desic_strategy/live.py +253 -0
  221. package/python/desic_strategy/policy.py +193 -0
  222. package/python/desic_strategy/portfolio.py +152 -0
  223. package/python/desic_strategy/report.py +319 -0
  224. package/python/desic_strategy/runner.py +574 -0
  225. package/python/desic_strategy/timeframe.py +150 -0
  226. package/python/main.py +18 -0
  227. package/skills/okx-live-trading/SKILL.md +117 -0
  228. package/skills/okx-live-trading/agents/openai.yaml +9 -0
  229. package/skills/okx-live-trading/references/lifecycle.md +128 -0
  230. package/skills/okx-strategy-research/SKILL.md +113 -0
  231. package/skills/okx-strategy-research/agents/openai.yaml +9 -0
  232. package/skills/okx-strategy-research/references/execution-semantics.md +107 -0
  233. package/skills/okx-strategy-research/references/field-traps.md +142 -0
  234. package/skills/okx-strategy-research/references/python-api.md +121 -0
  235. package/skills/okx-strategy-research/references/tools-and-data.md +192 -0
  236. 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