desic-okx-agent 0.2.0 → 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 (237) hide show
  1. package/README.en.md +353 -0
  2. package/README.md +194 -190
  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 +758 -27
  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.d.ts +3 -0
  142. package/dist/setup/wizard.js +131 -6
  143. package/dist/setup/wizard.js.map +1 -1
  144. package/dist/strategy/constants.d.ts +23 -0
  145. package/dist/strategy/constants.js +24 -0
  146. package/dist/strategy/constants.js.map +1 -0
  147. package/dist/strategy/environment.d.ts +52 -0
  148. package/dist/strategy/environment.js +187 -0
  149. package/dist/strategy/environment.js.map +1 -0
  150. package/dist/strategy/instrument.d.ts +29 -0
  151. package/dist/strategy/instrument.js +39 -0
  152. package/dist/strategy/instrument.js.map +1 -0
  153. package/dist/strategy/optimize.d.ts +73 -0
  154. package/dist/strategy/optimize.js +113 -0
  155. package/dist/strategy/optimize.js.map +1 -0
  156. package/dist/strategy/parameter-space.d.ts +59 -0
  157. package/dist/strategy/parameter-space.js +221 -0
  158. package/dist/strategy/parameter-space.js.map +1 -0
  159. package/dist/strategy/python-bridge.d.ts +24 -0
  160. package/dist/strategy/python-bridge.js +114 -0
  161. package/dist/strategy/python-bridge.js.map +1 -0
  162. package/dist/strategy/schema.d.ts +9 -0
  163. package/dist/strategy/schema.js +91 -0
  164. package/dist/strategy/schema.js.map +1 -0
  165. package/dist/strategy/service.d.ts +138 -0
  166. package/dist/strategy/service.js +745 -0
  167. package/dist/strategy/service.js.map +1 -0
  168. package/dist/strategy/settings.d.ts +162 -0
  169. package/dist/strategy/settings.js +243 -0
  170. package/dist/strategy/settings.js.map +1 -0
  171. package/dist/strategy/store.d.ts +96 -0
  172. package/dist/strategy/store.js +367 -0
  173. package/dist/strategy/store.js.map +1 -0
  174. package/dist/strategy/templates.d.ts +11 -0
  175. package/dist/strategy/templates.js +134 -0
  176. package/dist/strategy/templates.js.map +1 -0
  177. package/dist/strategy/types.d.ts +111 -0
  178. package/dist/strategy/types.js +2 -0
  179. package/dist/strategy/types.js.map +1 -0
  180. package/dist/tools/catalog.d.ts +16 -0
  181. package/dist/tools/catalog.js +118 -17
  182. package/dist/tools/catalog.js.map +1 -1
  183. package/dist/trade/service.d.ts +12 -0
  184. package/dist/trade/service.js +24 -7
  185. package/dist/trade/service.js.map +1 -1
  186. package/dist/tui/app.d.ts +23 -0
  187. package/dist/tui/app.js +322 -0
  188. package/dist/tui/app.js.map +1 -0
  189. package/dist/tui/commands.d.ts +70 -0
  190. package/dist/tui/commands.js +313 -0
  191. package/dist/tui/commands.js.map +1 -0
  192. package/dist/tui/entries.d.ts +17 -0
  193. package/dist/tui/entries.js +24 -0
  194. package/dist/tui/entries.js.map +1 -0
  195. package/dist/tui/execute.d.ts +26 -0
  196. package/dist/tui/execute.js +664 -0
  197. package/dist/tui/execute.js.map +1 -0
  198. package/dist/tui/history.d.ts +17 -0
  199. package/dist/tui/history.js +48 -0
  200. package/dist/tui/history.js.map +1 -0
  201. package/dist/tui/index.d.ts +8 -0
  202. package/dist/tui/index.js +48 -0
  203. package/dist/tui/index.js.map +1 -0
  204. package/dist/tui/line-editor.d.ts +44 -0
  205. package/dist/tui/line-editor.js +98 -0
  206. package/dist/tui/line-editor.js.map +1 -0
  207. package/dist/tui/progress.d.ts +23 -0
  208. package/dist/tui/progress.js +46 -0
  209. package/dist/tui/progress.js.map +1 -0
  210. package/dist/tui/settings-editor.d.ts +18 -0
  211. package/dist/tui/settings-editor.js +115 -0
  212. package/dist/tui/settings-editor.js.map +1 -0
  213. package/docs/live-trading.md +455 -0
  214. package/docs/strategy-research.md +597 -0
  215. package/package.json +11 -1
  216. package/python/desic_strategy/__init__.py +34 -0
  217. package/python/desic_strategy/actions.py +158 -0
  218. package/python/desic_strategy/context.py +164 -0
  219. package/python/desic_strategy/engine.py +614 -0
  220. package/python/desic_strategy/indicators.py +159 -0
  221. package/python/desic_strategy/live.py +253 -0
  222. package/python/desic_strategy/policy.py +193 -0
  223. package/python/desic_strategy/portfolio.py +152 -0
  224. package/python/desic_strategy/report.py +319 -0
  225. package/python/desic_strategy/runner.py +574 -0
  226. package/python/desic_strategy/timeframe.py +150 -0
  227. package/python/main.py +18 -0
  228. package/skills/okx-live-trading/SKILL.md +117 -0
  229. package/skills/okx-live-trading/agents/openai.yaml +9 -0
  230. package/skills/okx-live-trading/references/lifecycle.md +128 -0
  231. package/skills/okx-strategy-research/SKILL.md +113 -0
  232. package/skills/okx-strategy-research/agents/openai.yaml +9 -0
  233. package/skills/okx-strategy-research/references/execution-semantics.md +107 -0
  234. package/skills/okx-strategy-research/references/field-traps.md +142 -0
  235. package/skills/okx-strategy-research/references/python-api.md +121 -0
  236. package/skills/okx-strategy-research/references/tools-and-data.md +192 -0
  237. package/skills/okx-trading/SKILL.md +11 -10
@@ -0,0 +1,664 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { publicError } from "../core/errors.js";
4
+ import { TEMPLATE_NAMES, isTemplateName, template } from "../strategy/templates.js";
5
+ import { DEFAULT_BUDGET } from "../strategy/parameter-space.js";
6
+ import { describeEnvironment, formatCoverageDetail, formatInstrumentTable, formatSyncReport } from "../cli/data-render.js";
7
+ import { formatComparison, formatEquitySparkline, formatOptimizationReport, formatRunDetail, formatRunTable, formatTradeTable, formatValidation } from "../cli/strategy-render.js";
8
+ import { formatCommandDetail, formatCommandList, commandByName, suggest } from "./commands.js";
9
+ import { entry } from "./entries.js";
10
+ import { padEndDisplay } from "../cli/width.js";
11
+ import { openInBrowser, writeComparison, writeReport } from "../report/open.js";
12
+ import { describeRange, parseRangeArgument } from "../cli/range.js";
13
+ import { loadConfig, saveConfig } from "../config/loader.js";
14
+ import { CONFIG_PATH } from "../config/paths.js";
15
+ import { collectComparison, collectReport } from "../report/fetch.js";
16
+ import { formatProfileTable, formatSignalTable } from "../cli/live-render.js";
17
+ import { buildComparison, MAX_COMPARED_RUNS, MIN_COMPARED_RUNS } from "../report/compare.js";
18
+ import { activeLocale, activeMessages, saveLocale } from "../i18n/locale.js";
19
+ import { LOCALES, isLocale } from "../i18n/messages.js";
20
+ const DEFAULT_INSTRUMENT = "BTC-USDT-SWAP";
21
+ const DEFAULT_DAYS = 30;
22
+ /**
23
+ * Longer than a backtest's default because the window is split 70/30. Thirty days
24
+ * would leave nine to validate against, which is too short to contradict the
25
+ * searched segment.
26
+ */
27
+ const DEFAULT_OPTIMIZE_DAYS = 90;
28
+ const DEFAULT_SPACE_FILE = "space.json";
29
+ /**
30
+ * Runs one parsed command and returns the entries to append.
31
+ *
32
+ * Every command returns text rather than rendering: the shell owns presentation,
33
+ * and this stays testable without a terminal.
34
+ */
35
+ export async function execute(name, args, context) {
36
+ try {
37
+ switch (name) {
38
+ case "help":
39
+ return { entries: [helpEntry(args[0])] };
40
+ case "clear":
41
+ return { entries: [], effect: "clear" };
42
+ case "exit":
43
+ case "quit":
44
+ return { entries: [], effect: "exit" };
45
+ case "data":
46
+ return { entries: [await dataEntry(context)] };
47
+ case "download":
48
+ return { entries: await downloadEntries(args, context) };
49
+ case "new":
50
+ return { entries: [newStrategyEntry(args)] };
51
+ case "validate":
52
+ return { entries: [await validateEntry(args, context)] };
53
+ case "backtest":
54
+ return { entries: await backtestEntries(args, context) };
55
+ case "optimize":
56
+ return { entries: await optimizeEntries(args, context) };
57
+ case "compare":
58
+ return { entries: await compareEntries(args, context) };
59
+ case "market":
60
+ return marketResult(args, context.configPath);
61
+ case "live":
62
+ return { entries: await liveEntries(args, context) };
63
+ case "runs":
64
+ return { entries: [await runsEntry(args, context)] };
65
+ case "report":
66
+ return { entries: await reportEntries(args, context) };
67
+ case "cancel":
68
+ return { entries: [await cancelEntry(args, context)] };
69
+ case "coverage":
70
+ return { entries: [await coverageEntry(args, context)] };
71
+ case "delete":
72
+ return { entries: [await deleteEntry(args, context)] };
73
+ case "env":
74
+ return { entries: [await envEntry(args, context)] };
75
+ case "status":
76
+ return { entries: [await statusEntry(context)] };
77
+ case "settings":
78
+ // Handled by the shell, which opens the form; reaching here means the
79
+ // caller was not the shell.
80
+ return { entries: [entry("note", ` ${activeMessages().settingsHintCommand}`)] };
81
+ case "lang":
82
+ case "language":
83
+ return langResult(args);
84
+ default: {
85
+ const text = activeMessages();
86
+ const closest = suggest(name);
87
+ const hint = closest ? text.didYouMean(closest) : text.seeHelp;
88
+ return { entries: [entry("error", ` ${text.unknownCommand(name)}${hint}`)] };
89
+ }
90
+ }
91
+ }
92
+ catch (error) {
93
+ const detail = publicError(error);
94
+ return { entries: [entry("error", ` ${String(detail.message)}`)] };
95
+ }
96
+ finally {
97
+ context.onActivity(null);
98
+ }
99
+ }
100
+ function helpEntry(topic) {
101
+ const text = activeMessages();
102
+ if (!topic) {
103
+ return entry("output", `${formatCommandList()}\n\n ${text.argumentsAsked}`);
104
+ }
105
+ const spec = commandByName(topic.replace(/^\//, ""));
106
+ if (!spec)
107
+ return entry("error", ` ${text.noCommandNamed(topic)}`);
108
+ return entry("output", formatCommandDetail(spec));
109
+ }
110
+ /**
111
+ * Lists live profiles, reads their decisions, or stops one.
112
+ *
113
+ * Read-only apart from stopping. Creating and starting stay in the CLI: they are the two
114
+ * actions that commit money to a strategy, and a shell where one keystroke could begin
115
+ * trading is not a shell anyone should trust.
116
+ */
117
+ async function liveEntries(args, context) {
118
+ const text = activeMessages();
119
+ const action = (args[0] ?? "").toLowerCase();
120
+ if (!action) {
121
+ context.onActivity(text.readingRuns);
122
+ const result = (await context.client.call("live_list_profiles", {}));
123
+ return [entry("output", indent(formatProfileTable(result.data)))];
124
+ }
125
+ if (action === "signals") {
126
+ const profileId = args[1];
127
+ if (!profileId)
128
+ return [entry("error", ` ${text.giveRunId("live signals")}`)];
129
+ context.onActivity(text.readingRun(profileId));
130
+ const result = (await context.client.call("live_signals", { profileId, limit: 30 }));
131
+ return [entry("output", indent(formatSignalTable(result.data)))];
132
+ }
133
+ if (action === "stop") {
134
+ const profileId = args[1];
135
+ if (!profileId)
136
+ return [entry("error", ` ${text.giveRunId("live stop")}`)];
137
+ const result = (await context.client.call("live_stop", { profileId }));
138
+ return [entry("success", ` ${text.liveProfileStopped(result.data.name)}`)];
139
+ }
140
+ return [entry("error", ` ${text.argLiveAction}`)];
141
+ }
142
+ function indent(text) {
143
+ return text
144
+ .split("\n")
145
+ .map((line) => ` ${line}`)
146
+ .join("\n");
147
+ }
148
+ /**
149
+ * Shows or switches the market data environment.
150
+ *
151
+ * The switch is a config write, so it cannot take effect in this session — the runtime
152
+ * holds its sockets open and reads this once at startup. Reporting success without
153
+ * saying that would leave someone believing the shell in front of them had changed
154
+ * venue when it had not.
155
+ */
156
+ function marketResult(args, configPath = CONFIG_PATH) {
157
+ const text = activeMessages();
158
+ const config = loadConfig(configPath);
159
+ const current = config.market.environment;
160
+ const requested = (args[0] ?? "").trim().toLowerCase();
161
+ if (!requested) {
162
+ const lines = [` ${text.marketEnvironmentLabel} ${describeEnvironment(current) ?? current}`];
163
+ if (current === "demo")
164
+ lines.push(` ${text.demoDataWarning}`);
165
+ lines.push(` ${text.environmentUsageShell}`);
166
+ return { entries: [entry("output", lines.join("\n"))] };
167
+ }
168
+ if (requested !== "demo" && requested !== "live") {
169
+ // The shell's own syntax, not the CLI's: telling someone at a `/` prompt to run a
170
+ // `desic-okx` command is an instruction they cannot follow where they are.
171
+ return { entries: [entry("error", ` ${text.environmentUsageShell}`)] };
172
+ }
173
+ if (requested === current) {
174
+ return { entries: [entry("note", ` ${text.environmentAlready(requested)}`)] };
175
+ }
176
+ saveConfig({ ...config, market: { ...config.market, environment: requested } }, configPath);
177
+ const lines = [
178
+ ` ${text.environmentSwitched(describeEnvironment(requested) ?? requested)}`,
179
+ ` ${text.environmentRestartHint}`
180
+ ];
181
+ if (requested === "demo")
182
+ lines.push(` ${text.demoDataWarning}`);
183
+ return { entries: [entry("success", lines.join("\n"))] };
184
+ }
185
+ /**
186
+ * Changes the display language and remembers it.
187
+ *
188
+ * The preference is written to disk rather than kept per session: a user who
189
+ * chose a language should not have to choose again on every start.
190
+ */
191
+ function langResult(args) {
192
+ const text = activeMessages();
193
+ const requested = (args[0] ?? "").toLowerCase();
194
+ if (!requested) {
195
+ return { entries: [entry("output", ` ${text.languageCurrent(activeLocale())}\n ${text.languageUsage}`)] };
196
+ }
197
+ if (!isLocale(requested)) {
198
+ return {
199
+ entries: [entry("error", ` ${text.languageUsage} (${LOCALES.join(", ")})`)]
200
+ };
201
+ }
202
+ saveLocale(requested);
203
+ // Read the bundle again so the confirmation is already in the new language.
204
+ return { entries: [entry("success", ` ${activeMessages().languageChanged(requested)}`)] };
205
+ }
206
+ async function dataEntry(context) {
207
+ const text = activeMessages();
208
+ context.onActivity(text.readingCoverage);
209
+ const result = (await context.client.call("data_list_instruments", {}));
210
+ const rows = result.data;
211
+ const environment = describeEnvironment(result.meta?.environment);
212
+ if (rows.length === 0) {
213
+ return entry("note", ` ${text.noLocalHistory}\n ${text.tryDownload}`);
214
+ }
215
+ return entry("output", formatInstrumentTable(rows, environment)
216
+ .split("\n")
217
+ .map((line) => ` ${line}`)
218
+ .join("\n"));
219
+ }
220
+ async function downloadEntries(args, context) {
221
+ const text = activeMessages();
222
+ const instId = (args[0] ?? DEFAULT_INSTRUMENT).toUpperCase();
223
+ let range;
224
+ try {
225
+ range = parseRangeArgument(args[1] ?? String(DEFAULT_DAYS));
226
+ }
227
+ catch (failure) {
228
+ return [entry("error", ` ${failure instanceof Error ? failure.message : String(failure)}`)];
229
+ }
230
+ context.onActivity(text.requestingWindow(instId, describeRange(range)));
231
+ // The download call holds one request open for its whole duration, so progress
232
+ // is polled from the runtime rather than streamed back on that request.
233
+ const startedAt = Date.now();
234
+ const poll = setInterval(() => {
235
+ void context.client
236
+ .call("data_download_progress", { instId })
237
+ .then((value) => {
238
+ const state = value.data;
239
+ if (!state || !state.active)
240
+ return;
241
+ context.onProgress?.({
242
+ label: `${instId} ${text.requestingWindow(instId, describeRange(range))}`,
243
+ done: state.present,
244
+ total: state.expected,
245
+ startedAt
246
+ });
247
+ })
248
+ .catch(() => undefined);
249
+ }, 250);
250
+ try {
251
+ const result = (await context.client.call("data_download", { instId, ...range }));
252
+ const report = result.data;
253
+ return [
254
+ entry(report.status === "completed" ? "success" : "note", formatSyncReport(report)
255
+ .split("\n")
256
+ .map((line) => ` ${line}`)
257
+ .join("\n"))
258
+ ];
259
+ }
260
+ finally {
261
+ clearInterval(poll);
262
+ context.onProgress?.(null);
263
+ }
264
+ }
265
+ function newStrategyEntry(args) {
266
+ const text = activeMessages();
267
+ const file = args[0];
268
+ if (!file)
269
+ return entry("error", ` ${text.giveFileNew} (${TEMPLATE_NAMES.join(", ")})`);
270
+ const name = args[1] ?? "ema-trend";
271
+ if (!isTemplateName(name)) {
272
+ return entry("error", ` ${text.unknownTemplate(name, TEMPLATE_NAMES.join(", "))}`);
273
+ }
274
+ if (fs.existsSync(file)) {
275
+ return entry("error", ` ${text.fileExists(file)}`);
276
+ }
277
+ fs.writeFileSync(file, template(name), "utf8");
278
+ return entry("success", ` ${text.wroteTemplate(name, file)}\n ${text.nextValidate(file)}`);
279
+ }
280
+ async function validateEntry(args, context) {
281
+ const text = activeMessages();
282
+ const file = args[0];
283
+ if (!file)
284
+ return entry("error", ` ${text.giveFileValidate}`);
285
+ if (!fs.existsSync(file))
286
+ return entry("error", ` ${text.fileMissing(file)}`);
287
+ context.onActivity(text.validating(path.basename(file)));
288
+ const source = fs.readFileSync(file, "utf8");
289
+ const result = (await context.client.call("strategy_validate_source", { source }));
290
+ const rendered = formatValidation(result.data, file)
291
+ .split("\n")
292
+ .map((line) => ` ${line}`)
293
+ .join("\n");
294
+ return entry(result.data.valid ? "success" : "error", rendered);
295
+ }
296
+ async function backtestEntries(args, context) {
297
+ const text = activeMessages();
298
+ const file = args[0];
299
+ if (!file)
300
+ return [entry("error", ` ${text.giveFileBacktest}`)];
301
+ if (!fs.existsSync(file))
302
+ return [entry("error", ` ${text.fileMissing(file)}`)];
303
+ const instId = (args[1] ?? DEFAULT_INSTRUMENT).toUpperCase();
304
+ // Accepts a day count or an explicit range; the same question at different
305
+ // precision, so it is one argument rather than two.
306
+ let range;
307
+ try {
308
+ range = parseRangeArgument(args[2] ?? String(DEFAULT_DAYS));
309
+ }
310
+ catch (failure) {
311
+ return [entry("error", ` ${failure instanceof Error ? failure.message : String(failure)}`)];
312
+ }
313
+ // Read the real settings rather than printing a fixed string: the old line
314
+ // claimed "10,000 USDT · 10x · 120 preload bars" no matter what a run used.
315
+ const settings = (await context.client.call("strategy_settings", {})).data;
316
+ const entries = [
317
+ entry("note", ` ${text.backtestWindow(path.basename(file), instId, describeRange(range))} · ${text.backtestSettings(settings.initialEquityUsdt.toLocaleString("en-US"), settings.leverage, settings.preloadBars)}\n ${text.settingsHintCommand}`)
318
+ ];
319
+ context.onActivity(text.queuing(instId));
320
+ const source = fs.readFileSync(file, "utf8");
321
+ // No preloadBars override: the service already applies the saved setting, and
322
+ // sending a literal here would use a different value than the line above just
323
+ // reported — the same mismatch the hardcoded config string used to cause.
324
+ const queued = (await context.client.call("strategy_run_backtest", {
325
+ instId,
326
+ source,
327
+ strategyName: path.basename(file, ".py"),
328
+ ...range
329
+ }));
330
+ const runId = queued.data.runId;
331
+ const startedAt = Date.now();
332
+ const run = await waitForRun(context, runId, (summary) => {
333
+ if (summary.barCount > 0) {
334
+ // The engine reports a bar count, so the remaining work is measurable and
335
+ // a bar is more useful than a spinner.
336
+ context.onProgress?.({
337
+ label: `${runId} ${summary.instId}`,
338
+ done: Math.round((summary.progressPct / 100) * summary.barCount),
339
+ total: summary.barCount,
340
+ startedAt,
341
+ detail: `${summary.barCount.toLocaleString("en-US")} ${text.barsColumn.toLowerCase()}`
342
+ });
343
+ }
344
+ else {
345
+ context.onActivity(`${runId} ${summary.status}`);
346
+ }
347
+ });
348
+ context.onProgress?.(null);
349
+ if (run.status !== "completed") {
350
+ entries.push(entry("error", ` ${runId} ${run.status}\n ${run.error ?? "-"}`));
351
+ return entries;
352
+ }
353
+ entries.push(entry("success", formatRunDetail(run)
354
+ .split("\n")
355
+ .map((line) => ` ${line}`)
356
+ .join("\n") + `\n\n ${text.forCurveAndTrades(runId)}`));
357
+ return entries;
358
+ }
359
+ /**
360
+ * Queues a parameter search, or reprints a finished one.
361
+ *
362
+ * The default window is longer than a backtest's: the window is split 70/30, so a
363
+ * 30-day request would leave nine days to validate against, which is too few to
364
+ * disagree with the searched segment about anything.
365
+ */
366
+ async function optimizeEntries(args, context) {
367
+ const text = activeMessages();
368
+ const positional = args.filter((item) => !item.startsWith("-"));
369
+ const first = positional[0];
370
+ if (!first)
371
+ return [entry("error", ` ${text.giveFileOptimize}`)];
372
+ if (first.toLowerCase() === "report") {
373
+ return optimizeReportEntries(positional.slice(1), args, context);
374
+ }
375
+ if (!fs.existsSync(first))
376
+ return [entry("error", ` ${text.fileMissing(first)}`)];
377
+ const instId = (positional[1] ?? DEFAULT_INSTRUMENT).toUpperCase();
378
+ let range;
379
+ try {
380
+ range = parseRangeArgument(positional[2] ?? String(DEFAULT_OPTIMIZE_DAYS));
381
+ }
382
+ catch (failure) {
383
+ return [entry("error", ` ${failure instanceof Error ? failure.message : String(failure)}`)];
384
+ }
385
+ const spaceFile = positional[3] ?? DEFAULT_SPACE_FILE;
386
+ if (!fs.existsSync(spaceFile))
387
+ return [entry("error", ` ${text.spaceFileMissing(spaceFile)}`)];
388
+ let space;
389
+ try {
390
+ space = JSON.parse(fs.readFileSync(spaceFile, "utf8"));
391
+ }
392
+ catch {
393
+ return [entry("error", ` ${text.spaceFileInvalid(spaceFile)}`)];
394
+ }
395
+ const budget = Number(positional[4] ?? DEFAULT_BUDGET);
396
+ if (!Number.isInteger(budget) || budget < 2) {
397
+ return [entry("error", ` ${text.giveSpaceFile}`)];
398
+ }
399
+ // A parameters file beside the strategy supplies the defaults the space is
400
+ // checked against, so a range naming an unread parameter is caught here.
401
+ const params = readParamsBeside(first);
402
+ const entries = [
403
+ entry("note", ` ${text.optimizeWindow(path.basename(first), instId, describeRange(range))}`)
404
+ ];
405
+ context.onActivity(text.queuing(instId));
406
+ const queued = (await context.client.call("strategy_run_optimize", {
407
+ instId,
408
+ source: fs.readFileSync(first, "utf8"),
409
+ strategyName: path.basename(first, ".py"),
410
+ space,
411
+ budget,
412
+ ...(params ? { params } : {}),
413
+ ...range
414
+ }));
415
+ const runId = queued.data.runId;
416
+ const startedAt = Date.now();
417
+ const run = await waitForRun(context, runId, (summary) => {
418
+ const evaluated = Number(summary.metrics?.candidatesEvaluated ?? 0);
419
+ context.onProgress?.({
420
+ label: `${runId} ${summary.instId}`,
421
+ // Progress is measured in candidate-segments, which the run reports as a
422
+ // percentage; scaling it onto the budget keeps the bar in candidate units.
423
+ done: Math.round((summary.progressPct / 100) * budget),
424
+ total: budget,
425
+ startedAt,
426
+ detail: `${evaluated > 0 ? `${evaluated} done · ` : ""}${budget} candidates`
427
+ });
428
+ });
429
+ context.onProgress?.(null);
430
+ if (run.status !== "completed") {
431
+ entries.push(entry("error", ` ${runId} ${run.status}\n ${run.error ?? "-"}`));
432
+ return entries;
433
+ }
434
+ const report = (await context.client.call("strategy_get_optimization", { runId, top: 20 }));
435
+ entries.push(entry("success", formatOptimizationReport(report.data)
436
+ .split("\n")
437
+ .map((line) => ` ${line}`)
438
+ .join("\n") + `\n\n ${text.forCandidateTable(runId)}`));
439
+ return entries;
440
+ }
441
+ async function optimizeReportEntries(positional, args, context) {
442
+ const text = activeMessages();
443
+ const runId = positional[0];
444
+ if (!runId)
445
+ return [entry("error", ` ${text.giveRunId("optimize report")}`)];
446
+ context.onActivity(text.readingRun(runId));
447
+ const report = (await context.client.call("strategy_get_optimization", { runId, top: 50 }));
448
+ const entries = [
449
+ entry("output", formatOptimizationReport(report.data)
450
+ .split("\n")
451
+ .map((line) => ` ${line}`)
452
+ .join("\n"))
453
+ ];
454
+ const adoptIndex = args.findIndex((item) => item === "--adopt");
455
+ if (adoptIndex === -1)
456
+ return entries;
457
+ const target = args[adoptIndex + 1];
458
+ if (!target)
459
+ return [...entries, entry("error", ` ${text.giveSpaceFile}`)];
460
+ const best = report.data.candidates.find((candidate) => candidate.status === "completed");
461
+ if (!best)
462
+ return [...entries, entry("note", ` ${text.noBestCandidate}`)];
463
+ // Written to its own file rather than into the strategy: the source belongs to
464
+ // the user, and rewriting it would erase what they wrote.
465
+ fs.writeFileSync(target, `${JSON.stringify(best.params, null, 2)}\n`, "utf8");
466
+ entries.push(entry("success", ` ${text.adoptedParams(target)}\n ${text.adoptHint(runId)}`));
467
+ return entries;
468
+ }
469
+ /**
470
+ * Puts two or more runs side by side.
471
+ *
472
+ * Opens in the browser by default, like `/report`: an overlay of several curves is
473
+ * the thing a terminal renders worst. Warnings are printed to the transcript
474
+ * either way, since they are the reason a comparison may be meaningless and should
475
+ * not live only in a window the user might not switch to.
476
+ */
477
+ async function compareEntries(args, context) {
478
+ const text = activeMessages();
479
+ const runIds = args.filter((item) => !item.startsWith("-"));
480
+ if (runIds.length < MIN_COMPARED_RUNS)
481
+ return [entry("error", ` ${text.compareNeedsTwo}`)];
482
+ if (runIds.length > MAX_COMPARED_RUNS) {
483
+ return [entry("error", ` ${text.compareTooMany(MAX_COMPARED_RUNS)}`)];
484
+ }
485
+ const terminalOnly = args.some((item) => item === "--no-open" || item === "-n");
486
+ context.onActivity(text.readingRun(runIds.join(", ")));
487
+ const report = buildComparison(await collectComparison(context.client, runIds));
488
+ const entries = [];
489
+ for (const warning of report.warnings) {
490
+ entries.push(entry("note", ` ! ${warning.message}`));
491
+ }
492
+ if (terminalOnly) {
493
+ entries.push(entry("output", formatComparison(report)
494
+ .split("\n")
495
+ // Warnings already stand on their own above, so they are not repeated
496
+ // inside the table block.
497
+ .filter((line) => !line.trimStart().startsWith("! "))
498
+ .map((line) => ` ${line}`)
499
+ .join("\n")));
500
+ return entries;
501
+ }
502
+ const file = writeComparison(report);
503
+ const opened = openInBrowser(file);
504
+ entries.push(entry(opened ? "success" : "note", ` ${opened ? text.reportOpened : text.reportWritten}\n ${file}`));
505
+ return entries;
506
+ }
507
+ /**
508
+ * Reads `<strategy>.params.json` if it exists.
509
+ *
510
+ * The parameter space is validated against the strategy's own parameters, and
511
+ * those live inside `ctx.params.get(...)` calls where the host cannot see them.
512
+ * This file is the declared set, and it is also where `--adopt` writes.
513
+ */
514
+ function readParamsBeside(strategyFile) {
515
+ const candidate = strategyFile.replace(/\.py$/, "") + ".params.json";
516
+ if (!fs.existsSync(candidate))
517
+ return null;
518
+ try {
519
+ const parsed = JSON.parse(fs.readFileSync(candidate, "utf8"));
520
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed)
521
+ ? parsed
522
+ : null;
523
+ }
524
+ catch {
525
+ return null;
526
+ }
527
+ }
528
+ async function runsEntry(args, context) {
529
+ const text = activeMessages();
530
+ const limit = Number(args[0] ?? 10);
531
+ context.onActivity(text.readingRuns);
532
+ const result = (await context.client.call("strategy_list_runs", {
533
+ limit: Number.isFinite(limit) && limit > 0 ? Math.min(limit, 200) : 10
534
+ }));
535
+ return entry("output", formatRunTable(result.data)
536
+ .split("\n")
537
+ .map((line) => ` ${line}`)
538
+ .join("\n"));
539
+ }
540
+ async function reportEntries(args, context) {
541
+ const text = activeMessages();
542
+ const runId = args.find((item) => !item.startsWith("-"));
543
+ if (!runId)
544
+ return [entry("error", ` ${text.giveRunId("report")}`)];
545
+ const terminalOnly = args.some((item) => item === "--no-open" || item === "-n");
546
+ context.onActivity(text.readingRun(runId));
547
+ const run = (await context.client.call("strategy_get_run", { runId }));
548
+ const entries = [
549
+ entry("output", formatRunDetail(run.data)
550
+ .split("\n")
551
+ .map((line) => ` ${line}`)
552
+ .join("\n"))
553
+ ];
554
+ if (run.data.status !== "completed")
555
+ return entries;
556
+ // One reader for both paths. Callers must not choose a page size: the tool
557
+ // rejects a limit above its own cap, and a limit below the row count silently
558
+ // returns a prefix — `collectReport` follows pagination instead.
559
+ const collected = await collectReport(context.client, runId);
560
+ if (terminalOnly) {
561
+ if (collected.equity.length > 1) {
562
+ entries.push(entry("output", ` ${text.equityHeading}\n${formatEquitySparkline(collected.equity)}`));
563
+ }
564
+ entries.push(entry("output", ` ${text.tradesHeading}\n${formatTradeTable(collected.trades, 12)}`));
565
+ return entries;
566
+ }
567
+ // A browser can show the full curve, a drawdown band, and every trade at once;
568
+ // a terminal cannot. The sparkline stays available behind --no-open.
569
+ const file = writeReport(collected);
570
+ const opened = openInBrowser(file);
571
+ entries.push(entry(opened ? "success" : "note", ` ${opened ? text.reportOpened : text.reportWritten}\n ${file}`));
572
+ return entries;
573
+ }
574
+ /** One instrument's stored range and the gaps that would fail a backtest. */
575
+ async function coverageEntry(args, context) {
576
+ const text = activeMessages();
577
+ const instId = (args[0] ?? DEFAULT_INSTRUMENT).toUpperCase();
578
+ context.onActivity(text.readingCoverage);
579
+ const result = (await context.client.call("data_coverage", { instId }));
580
+ return entry(result.data === null || result.data.missingCount > 0 ? "note" : "output", formatCoverageDetail(result.data, instId)
581
+ .split("\n")
582
+ .map((line) => ` ${line}`)
583
+ .join("\n"));
584
+ }
585
+ async function deleteEntry(args, context) {
586
+ const text = activeMessages();
587
+ const runId = args.find((item) => !item.startsWith("-"));
588
+ if (!runId)
589
+ return entry("error", ` ${text.giveRunId("delete")}`);
590
+ // Deletion is irreversible, so it is confirmed rather than assumed.
591
+ if (!args.includes("--yes") && !args.includes("-y")) {
592
+ return entry("note", ` ${text.deleteConfirm(runId)}`);
593
+ }
594
+ const result = (await context.client.call("strategy_delete_run", { runId }));
595
+ return result.data.deleted
596
+ ? entry("success", ` ${text.deleted(runId)}`)
597
+ : entry("note", ` ${text.alreadyFinished(runId)}`);
598
+ }
599
+ async function cancelEntry(args, context) {
600
+ const text = activeMessages();
601
+ const runId = args[0];
602
+ if (!runId)
603
+ return entry("error", ` ${text.giveRunId("cancel")}`);
604
+ const result = (await context.client.call("strategy_cancel_run", { runId }));
605
+ return result.data.cancelled
606
+ ? entry("success", ` ${text.cancelled(runId)}`)
607
+ : entry("note", ` ${text.alreadyFinished(runId)}`);
608
+ }
609
+ async function envEntry(args, context) {
610
+ const text = activeMessages();
611
+ if ((args[0] ?? "").toLowerCase() === "setup") {
612
+ context.onActivity(text.preparingEnvironment);
613
+ const result = (await context.client.call("strategy_environment_setup", {}));
614
+ const log = (result.data.log ?? []).map((line) => ` ${line}`).join("\n");
615
+ return entry("success", `${log}\n ${text.environmentReady}`);
616
+ }
617
+ context.onActivity(text.checkingEnvironment);
618
+ const result = (await context.client.call("strategy_environment", {}));
619
+ const status = result.data;
620
+ const lines = [
621
+ ` ${padEndDisplay(text.pythonLabel, 8)} ${status.interpreter ?? text.notFound}${status.version ? ` ${status.version}` : ""}`,
622
+ ` ${padEndDisplay(text.readyLabel, 8)} ${status.ready ? text.yes : text.no}`
623
+ ];
624
+ for (const problem of status.problems)
625
+ lines.push(` ${"!".padEnd(8)} ${problem}`);
626
+ if (!status.ready)
627
+ lines.push(` ${padEndDisplay(text.fixLabel, 8)} /env setup`);
628
+ return entry(status.ready ? "output" : "note", lines.join("\n"));
629
+ }
630
+ async function statusEntry(context) {
631
+ const text = activeMessages();
632
+ context.onActivity(text.readingStatus);
633
+ const health = (await context.client.health());
634
+ const websocket = (health.websocket ?? {});
635
+ const database = (health.database ?? {});
636
+ const subscriptions = Array.isArray(health.subscriptions) ? health.subscriptions.length : 0;
637
+ const megabytes = ((database.sizeBytes ?? 0) / 1_048_576).toFixed(1);
638
+ return entry("output", [
639
+ ` ${padEndDisplay(text.runtimeLabel, 12)} ${String(health.version ?? "?")} pid ${String(health.pid ?? "?")}`,
640
+ ` ${padEndDisplay(text.publicWs, 12)} ${websocket.public?.state ?? "?"}`,
641
+ ` ${padEndDisplay(text.businessWs, 12)} ${websocket.business?.state ?? "?"}`,
642
+ ` ${padEndDisplay(text.subscribed(subscriptions), 12)}`,
643
+ ` ${padEndDisplay(text.databaseLabel, 12)} ${megabytes} MB`,
644
+ // Stated on every status read: demo and live are different markets, and a
645
+ // reader who cannot tell which one is streaming may trust the wrong prices.
646
+ ` ${padEndDisplay(text.marketEnvironmentLabel, 12)} ${describeEnvironment(String(health.marketEnvironment ?? "")) ?? "?"}`
647
+ ].join("\n"));
648
+ }
649
+ /** Polls a run until it settles, reporting progress through `onProgress`. */
650
+ async function waitForRun(context, runId, onProgress) {
651
+ for (;;) {
652
+ const response = (await context.client.call("strategy_get_run", { runId }));
653
+ const run = response.data;
654
+ if (run.status === "completed" || run.status === "failed" || run.status === "cancelled")
655
+ return run;
656
+ onProgress(run);
657
+ if (context.signal?.aborted) {
658
+ await context.client.call("strategy_cancel_run", { runId }).catch(() => undefined);
659
+ return { ...run, status: "cancelled", error: "Cancelled from the shell" };
660
+ }
661
+ await new Promise((resolve) => setTimeout(resolve, 400));
662
+ }
663
+ }
664
+ //# sourceMappingURL=execute.js.map