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
package/dist/cli/index.js CHANGED
@@ -8,7 +8,7 @@ import { publicError } from "../core/errors.js";
8
8
  import { PACKAGE_VERSION } from "../core/version.js";
9
9
  import { checkOkxConnectivity, OKX_REST_BASE_URL } from "../network/connectivity.js";
10
10
  import { displayProxy, resolveProxy } from "../network/proxy.js";
11
- import { AccountService } from "../account/service.js";
11
+ import { AccountService, detectAccountEnvironment } from "../account/service.js";
12
12
  import { runMcpServer } from "../mcp/server.js";
13
13
  import { RuntimeClient } from "../runtime/client.js";
14
14
  import { ask, askConfirm, askSecret } from "./prompts.js";
@@ -16,6 +16,20 @@ import { SetupCancelledError, inspectSetupTargets, parseSetupTargets, SETUP_TARG
16
16
  import { runSetupWizard } from "../setup/wizard.js";
17
17
  import { runDoctor, formatDoctor } from "./doctor.js";
18
18
  import { formatRuntimeStatus, formatToolHelp } from "./render.js";
19
+ import { describeEnvironment, formatCoverageDetail, formatInstrumentTable, formatSyncReport, parseBarTime } from "./data-render.js";
20
+ import fs from "node:fs";
21
+ import path from "node:path";
22
+ import { TEMPLATE_NAMES, isTemplateName, template } from "../strategy/templates.js";
23
+ import { formatComparison, formatEquitySparkline, formatOptimizationReport, formatRunDetail, formatRunTable, formatTradeTable, formatValidation } from "./strategy-render.js";
24
+ import { buildComparison, MAX_COMPARED_RUNS, MIN_COMPARED_RUNS } from "../report/compare.js";
25
+ import { formatProfileTable, formatReadiness, formatSignalTable } from "./live-render.js";
26
+ import { DEFAULT_BUDGET } from "../strategy/parameter-space.js";
27
+ import { activeLocale, activeMessages, saveLocale } from "../i18n/locale.js";
28
+ import { LOCALES, isLocale } from "../i18n/messages.js";
29
+ import { SETTINGS_PATH, SETTING_SPECS, coerceSetting, formatSettingValue, loadSettings, withSetting } from "../strategy/settings.js";
30
+ import { displayWidth, padEndDisplay } from "./width.js";
31
+ import { openInBrowser, writeComparison, writeReport } from "../report/open.js";
32
+ import { collectComparison, collectReport } from "../report/fetch.js";
19
33
  import { getToolHelp } from "../tools/help.js";
20
34
  import { checkForUpdate, installLatestVersion, OFFICIAL_NPM_REGISTRY, runInstalledCommand } from "../update/service.js";
21
35
  const program = new Command()
@@ -173,29 +187,670 @@ program.command("config-path")
173
187
  .action(() => {
174
188
  process.stdout.write(`${CONFIG_PATH}\n`);
175
189
  });
190
+ program.command("lang")
191
+ .description("Show or set the display language (en / zh); the choice is saved")
192
+ .argument("[locale]", `one of ${LOCALES.join(", ")}`)
193
+ .option("--json", "Print machine-readable JSON")
194
+ .action((locale, options) => {
195
+ if (!locale) {
196
+ const current = activeLocale();
197
+ if (options.json || !process.stdout.isTTY)
198
+ print({ locale: current, available: LOCALES });
199
+ else
200
+ process.stdout.write(`${activeMessages().languageCurrent(current)}\n`);
201
+ return;
202
+ }
203
+ const requested = locale.toLowerCase();
204
+ if (!isLocale(requested))
205
+ throw new Error(`Unsupported locale '${locale}'. Available: ${LOCALES.join(", ")}`);
206
+ saveLocale(requested);
207
+ if (options.json || !process.stdout.isTTY)
208
+ print({ locale: requested, saved: true });
209
+ else
210
+ process.stdout.write(`${activeMessages().languageChanged(requested)}\n`);
211
+ });
212
+ const live = program.command("live").description("Run a validated strategy against a real account");
213
+ live.command("list")
214
+ .description("List live profiles and their state")
215
+ .option("--json", "Print machine-readable JSON")
216
+ .action(async (options) => {
217
+ const client = await RuntimeClient.connect();
218
+ const result = await client.call("live_list_profiles", {});
219
+ if (options.json || !process.stdout.isTTY)
220
+ print(result);
221
+ else
222
+ process.stdout.write(`${formatProfileTable(result.data)}\n`);
223
+ });
224
+ live.command("create")
225
+ .description("Create a live profile. Creating one never starts it.")
226
+ .requiredOption("--file <file>", "strategy .py file")
227
+ .requiredOption("--inst <instId>", "OKX instrument ID")
228
+ .requiredOption("--account <name>", "configured account alias")
229
+ .requiredOption("--entry-budget <usdt>", "margin ceiling for one entry")
230
+ .requiredOption("--side-budget <usdt>", "margin ceiling for one direction")
231
+ .requiredOption("--daily-loss <usdt>", "realized loss for the UTC day at which entries stop")
232
+ .option("--name <name>", "profile name; defaults to the file name")
233
+ .option("--environment <env>", "demo | live", "demo")
234
+ .option("--leverage <x>", "leverage", "10")
235
+ .option("--margin-mode <mode>", "cross | isolated", "cross")
236
+ .option("--cooldown <seconds>", "seconds after an order before another may be sent", "0")
237
+ .option("--params <file>", "JSON file of strategy parameters")
238
+ .option("--no-long", "forbid opening long positions")
239
+ .option("--no-short", "forbid opening short positions")
240
+ .option("--json", "Print machine-readable JSON")
241
+ .action(async (options) => {
242
+ const file = String(options.file);
243
+ const client = await RuntimeClient.connect();
244
+ const result = await client.call("live_create_profile", {
245
+ name: options.name ? String(options.name) : path.basename(file, ".py"),
246
+ source: fs.readFileSync(file, "utf8"),
247
+ instId: String(options.inst),
248
+ account: String(options.account),
249
+ environment: String(options.environment),
250
+ leverage: Number(options.leverage),
251
+ marginMode: String(options.marginMode),
252
+ entryBudgetUsdt: Number(options.entryBudget),
253
+ sideBudgetUsdt: Number(options.sideBudget),
254
+ dailyLossLimitUsdt: Number(options.dailyLoss),
255
+ cooldownSeconds: Number(options.cooldown),
256
+ allowLong: options.long !== false,
257
+ allowShort: options.short !== false,
258
+ ...(options.params ? { params: readJsonFile(String(options.params), "--params") } : {})
259
+ });
260
+ if (options.json || !process.stdout.isTTY)
261
+ print(result);
262
+ else {
263
+ process.stdout.write(`${activeMessages().liveProfileCreated(result.data.id)}\n`);
264
+ process.stdout.write(` desic-okx live readiness --id ${result.data.id}\n`);
265
+ }
266
+ });
267
+ live.command("readiness")
268
+ .description("Show what still blocks this profile from being enabled")
269
+ .requiredOption("--id <profileId>", "profile id")
270
+ .option("--json", "Print machine-readable JSON")
271
+ .action(async (options) => {
272
+ const client = await RuntimeClient.connect();
273
+ const result = await client.call("live_readiness", { profileId: options.id });
274
+ if (options.json || !process.stdout.isTTY)
275
+ print(result);
276
+ else
277
+ process.stdout.write(`${formatReadiness(result.data)}\n`);
278
+ });
279
+ live.command("start")
280
+ .description("Enable a profile so it trades on every closed minute")
281
+ .requiredOption("--id <profileId>", "profile id")
282
+ .option("--json", "Print machine-readable JSON")
283
+ .action(async (options) => {
284
+ const client = await RuntimeClient.connect();
285
+ const result = await client.call("live_start", { profileId: options.id });
286
+ if (options.json || !process.stdout.isTTY)
287
+ print(result);
288
+ else {
289
+ const text = activeMessages();
290
+ process.stdout.write(`${text.liveProfileStarted(result.data.profile.name)}\n`);
291
+ process.stdout.write(` ${text.liveEvidence(result.data.backtestRunId ?? "-")}\n`);
292
+ process.stdout.write(` desic-okx live signals --id ${options.id}\n`);
293
+ }
294
+ });
295
+ live.command("stop")
296
+ .description("Disable a profile. Always allowed.")
297
+ .requiredOption("--id <profileId>", "profile id")
298
+ .option("--reason <text>", "recorded on the profile")
299
+ .option("--json", "Print machine-readable JSON")
300
+ .action(async (options) => {
301
+ const client = await RuntimeClient.connect();
302
+ const result = await client.call("live_stop", {
303
+ profileId: options.id,
304
+ ...(options.reason ? { reason: options.reason } : {})
305
+ });
306
+ if (options.json || !process.stdout.isTTY)
307
+ print(result);
308
+ else
309
+ process.stdout.write(`${activeMessages().liveProfileStopped(result.data.name)}\n`);
310
+ });
311
+ live.command("signals")
312
+ .description("Show what a profile decided, including every refusal")
313
+ .requiredOption("--id <profileId>", "profile id")
314
+ .option("--limit <count>", "rows to show", "30")
315
+ .option("--json", "Print machine-readable JSON")
316
+ .action(async (options) => {
317
+ const client = await RuntimeClient.connect();
318
+ const result = await client.call("live_signals", {
319
+ profileId: options.id,
320
+ limit: Number(options.limit)
321
+ });
322
+ if (options.json || !process.stdout.isTTY)
323
+ print(result);
324
+ else
325
+ process.stdout.write(`${formatSignalTable(result.data)}\n`);
326
+ });
327
+ live.command("agent-access")
328
+ .description("Show or set whether MCP clients may create and enable live profiles")
329
+ .argument("[state]", "on | off; omit to show the current setting")
330
+ .option("--json", "Print machine-readable JSON")
331
+ .action(async (state, options) => {
332
+ const text = activeMessages();
333
+ const config = loadConfig();
334
+ const current = config.live.agentAccess;
335
+ if (state === undefined) {
336
+ if (options.json || !process.stdout.isTTY)
337
+ print({ agentAccess: current });
338
+ else
339
+ process.stdout.write(`${text.agentAccessState(current ? "on" : "off")}\n`);
340
+ return;
341
+ }
342
+ const target = state.trim().toLowerCase();
343
+ if (target !== "on" && target !== "off")
344
+ throw new Error(text.agentAccessUsage);
345
+ saveConfig({ ...config, live: { ...config.live, agentAccess: target === "on" } });
346
+ if (options.json || !process.stdout.isTTY) {
347
+ print({ agentAccess: target === "on", restartRequired: true });
348
+ return;
349
+ }
350
+ process.stdout.write(`${text.agentAccessSet(target)}\n`);
351
+ process.stdout.write(` ${text.agentAccessRestart}\n`);
352
+ if (target === "on")
353
+ process.stdout.write(` ${text.agentAccessWarning}\n`);
354
+ });
355
+ live.command("delete")
356
+ .description("Delete a stopped profile and its signal history")
357
+ .requiredOption("--id <profileId>", "profile id")
358
+ .option("--yes", "confirm deletion")
359
+ .action(async (options) => {
360
+ if (!options.yes)
361
+ throw new Error(activeMessages().liveDeleteConfirm(options.id));
362
+ const client = await RuntimeClient.connect();
363
+ print(await client.call("live_delete_profile", { profileId: options.id }));
364
+ });
365
+ const market = program.command("market").description("Inspect and switch the market data environment");
366
+ market.command("env")
367
+ .description("Show or switch which OKX venue supplies market data")
368
+ .argument("[environment]", "demo | live; omit to show the current one")
369
+ .option("--json", "Print machine-readable JSON")
370
+ .action(async (requested, options) => {
371
+ const text = activeMessages();
372
+ const config = loadConfig();
373
+ const current = config.market.environment;
374
+ if (requested === undefined) {
375
+ if (options.json || !process.stdout.isTTY)
376
+ print({ environment: current });
377
+ else
378
+ process.stdout.write(`${text.marketEnvironmentLabel} ${describeEnvironment(current) ?? current}\n`);
379
+ return;
380
+ }
381
+ const target = requested.trim().toLowerCase();
382
+ if (target !== "demo" && target !== "live") {
383
+ throw new Error(text.environmentUsage);
384
+ }
385
+ if (target === current) {
386
+ process.stdout.write(`${text.environmentAlready(target)}\n`);
387
+ return;
388
+ }
389
+ saveConfig({ ...config, market: { ...config.market, environment: target } });
390
+ if (options.json || !process.stdout.isTTY) {
391
+ print({ environment: target, previous: current, restartRequired: true });
392
+ return;
393
+ }
394
+ process.stdout.write(`${text.environmentSwitched(describeEnvironment(target) ?? target)}\n`);
395
+ // The runtime reads this once at startup, so saying it changed without saying it
396
+ // has not taken effect yet would be worse than saying nothing.
397
+ process.stdout.write(`${text.environmentRestartHint}\n`);
398
+ if (target === "demo")
399
+ process.stdout.write(`${text.demoDataWarning}\n`);
400
+ });
401
+ const strategy = program.command("strategy").description("Write, validate, and backtest Python strategies");
402
+ strategy.command("new")
403
+ .description("Write a starting strategy file")
404
+ .requiredOption("--out <file>", "destination .py file")
405
+ .option("--template <name>", `one of ${TEMPLATE_NAMES.join(", ")}`, "ema-trend")
406
+ .option("--force", "overwrite an existing file")
407
+ .action((options) => {
408
+ if (!isTemplateName(options.template)) {
409
+ throw new Error(`Unknown template '${options.template}'. Available: ${TEMPLATE_NAMES.join(", ")}`);
410
+ }
411
+ if (fs.existsSync(options.out) && !options.force) {
412
+ throw new Error(`${options.out} already exists. Pass --force to overwrite it.`);
413
+ }
414
+ fs.writeFileSync(options.out, template(options.template), "utf8");
415
+ process.stdout.write(`Wrote ${options.template} template to ${options.out}\n`);
416
+ });
417
+ strategy.command("env")
418
+ .description("Check or prepare the local Python strategy environment")
419
+ .option("--setup", "Create the environment and install packages")
420
+ .option("--json", "Print machine-readable JSON")
421
+ .action(async (options) => {
422
+ const client = await RuntimeClient.connect();
423
+ if (options.setup) {
424
+ const result = await client.call("strategy_environment_setup", {});
425
+ if (options.json || !process.stdout.isTTY)
426
+ print(result);
427
+ else {
428
+ for (const line of result.data.log ?? [])
429
+ process.stdout.write(` ${line}\n`);
430
+ process.stdout.write("Environment ready\n");
431
+ }
432
+ return;
433
+ }
434
+ const result = await client.call("strategy_environment", {});
435
+ if (options.json || !process.stdout.isTTY)
436
+ print(result);
437
+ else {
438
+ const status = result.data;
439
+ process.stdout.write(`Python ${status.interpreter ?? "not found"}${status.version ? ` (${status.version})` : ""}\n`);
440
+ process.stdout.write(`Engine ${status.engineRoot}\n`);
441
+ process.stdout.write(`Ready ${status.ready ? "yes" : "no"}\n`);
442
+ for (const problem of status.problems ?? [])
443
+ process.stdout.write(` ! ${problem}\n`);
444
+ }
445
+ });
446
+ strategy.command("validate")
447
+ .description("Statically validate a strategy file")
448
+ .requiredOption("--file <file>", "strategy .py file")
449
+ .option("--json", "Print machine-readable JSON")
450
+ .action(async (options) => {
451
+ const source = fs.readFileSync(options.file, "utf8");
452
+ const client = await RuntimeClient.connect();
453
+ const result = await client.call("strategy_validate_source", { source });
454
+ if (options.json || !process.stdout.isTTY)
455
+ print(result);
456
+ else
457
+ process.stdout.write(`${formatValidation(result.data, options.file)}\n`);
458
+ if (!result.data.valid)
459
+ process.exitCode = 1;
460
+ });
461
+ const backtestCommand = strategy.command("backtest")
462
+ .description("Queue a backtest and print its run id")
463
+ .requiredOption("--file <file>", "strategy .py file")
464
+ .requiredOption("--inst <instId>", "OKX instrument ID")
465
+ .option("--days <days>", "trailing evaluation window in days", "30")
466
+ .option("--from <date>", "inclusive evaluation start, for example 2026-07-01")
467
+ .option("--to <date>", "inclusive evaluation end")
468
+ .option("--params <file>", "JSON file of strategy parameters")
469
+ .option("--intervals <list>", "extra timeframes to derive, for example 15m,1H")
470
+ .option("--follow", "wait for completion and print the report")
471
+ .option("--json", "Print machine-readable JSON")
472
+ .action(async (options) => {
473
+ const request = {
474
+ instId: String(options.inst),
475
+ source: fs.readFileSync(String(options.file), "utf8"),
476
+ strategyName: path.basename(String(options.file), ".py")
477
+ };
478
+ // Only flags the user actually passed are forwarded; the rest fall through to
479
+ // the saved settings, so `strategy settings` genuinely takes effect.
480
+ for (const spec of SETTING_SPECS) {
481
+ const provided = options[camelFlag(spec.flag)];
482
+ if (provided === undefined)
483
+ continue;
484
+ request[spec.key] = coerceSetting(spec, provided);
485
+ }
486
+ if (options.from)
487
+ request.fromMs = parseBarTime(String(options.from), "--from");
488
+ if (options.to)
489
+ request.toMs = parseBarTime(String(options.to), "--to");
490
+ if (!options.from)
491
+ request.days = Number(options.days);
492
+ if (options.params)
493
+ request.params = JSON.parse(fs.readFileSync(String(options.params), "utf8"));
494
+ if (options.intervals) {
495
+ request.intervals = String(options.intervals).split(",").map((item) => item.trim()).filter(Boolean);
496
+ }
497
+ const client = await RuntimeClient.connect();
498
+ const queued = await client.call("strategy_run_backtest", request);
499
+ const runId = queued.data.runId;
500
+ if (!options.follow) {
501
+ if (options.json || !process.stdout.isTTY)
502
+ print(queued);
503
+ else
504
+ process.stdout.write(`Queued ${runId}\n desic-okx strategy report --run ${runId}\n`);
505
+ return;
506
+ }
507
+ const final = await followRun(client, runId, Boolean(options.json));
508
+ if (options.json || !process.stdout.isTTY)
509
+ print({ data: final });
510
+ else
511
+ process.stdout.write(`${formatRunDetail(final)}\n`);
512
+ if (final.status !== "completed")
513
+ process.exitCode = 1;
514
+ });
515
+ const optimizeCommand = strategy.command("optimize")
516
+ .description("Search a parameter space, ranking candidates on a held-back segment")
517
+ .requiredOption("--file <file>", "strategy .py file")
518
+ .requiredOption("--inst <instId>", "OKX instrument ID")
519
+ .requiredOption("--space <file>", "JSON file of { param: { min, max, step } } ranges")
520
+ .option("--budget <count>", "candidates to evaluate", String(DEFAULT_BUDGET))
521
+ .option("--days <days>", "trailing evaluation window in days", "90")
522
+ .option("--from <date>", "inclusive evaluation start, for example 2026-05-01")
523
+ .option("--to <date>", "inclusive evaluation end")
524
+ .option("--params <file>", "JSON file of the strategy's base parameters")
525
+ .option("--intervals <list>", "extra timeframes to derive, for example 15m,1H")
526
+ .option("--top <count>", "candidates to print when following", "20")
527
+ .option("--follow", "wait for completion and print the candidate table")
528
+ .option("--json", "Print machine-readable JSON")
529
+ .action(async (options) => {
530
+ const request = {
531
+ instId: String(options.inst),
532
+ source: fs.readFileSync(String(options.file), "utf8"),
533
+ strategyName: path.basename(String(options.file), ".py"),
534
+ space: readJsonFile(String(options.space), "--space"),
535
+ budget: Number(options.budget)
536
+ };
537
+ for (const spec of SETTING_SPECS) {
538
+ const provided = options[camelFlag(spec.flag)];
539
+ if (provided === undefined)
540
+ continue;
541
+ request[spec.key] = coerceSetting(spec, provided);
542
+ }
543
+ if (options.from)
544
+ request.fromMs = parseBarTime(String(options.from), "--from");
545
+ if (options.to)
546
+ request.toMs = parseBarTime(String(options.to), "--to");
547
+ if (!options.from)
548
+ request.days = Number(options.days);
549
+ // The space is checked against these, so a strategy whose defaults live in
550
+ // `ctx.params.get(...)` calls needs them passed here for the check to work.
551
+ if (options.params)
552
+ request.params = readJsonFile(String(options.params), "--params");
553
+ if (options.intervals) {
554
+ request.intervals = String(options.intervals).split(",").map((item) => item.trim()).filter(Boolean);
555
+ }
556
+ const client = await RuntimeClient.connect();
557
+ const queued = await client.call("strategy_run_optimize", request);
558
+ const runId = queued.data.runId;
559
+ if (!options.follow) {
560
+ if (options.json || !process.stdout.isTTY)
561
+ print(queued);
562
+ else
563
+ process.stdout.write(`Queued ${runId}\n desic-okx strategy optimize-report --run ${runId}\n`);
564
+ return;
565
+ }
566
+ const final = await followRun(client, runId, Boolean(options.json));
567
+ const report = await client.call("strategy_get_optimization", {
568
+ runId,
569
+ top: Number(options.top)
570
+ });
571
+ if (options.json || !process.stdout.isTTY)
572
+ print(report);
573
+ else
574
+ process.stdout.write(`${formatOptimizationReport(report.data)}\n`);
575
+ if (final.status !== "completed")
576
+ process.exitCode = 1;
577
+ });
578
+ for (const spec of SETTING_SPECS) {
579
+ const exists = optimizeCommand.options.some((option) => option.long === `--${spec.flag}`);
580
+ if (!exists)
581
+ optimizeCommand.option(`--${spec.flag} <value>`, settingFlagHelp(spec));
582
+ }
583
+ strategy.command("optimize-report")
584
+ .description("Show an optimize run's candidates, ranked by the held-back segment")
585
+ .requiredOption("--run <runId>", "optimize run id")
586
+ .option("--top <count>", "candidates to print", "20")
587
+ .option("--adopt <file>", "write the best parameters to this JSON file")
588
+ .option("--json", "Print machine-readable JSON")
589
+ .action(async (options) => {
590
+ const client = await RuntimeClient.connect();
591
+ const report = await client.call("strategy_get_optimization", {
592
+ runId: options.run,
593
+ top: Number(options.top)
594
+ });
595
+ if (options.json || !process.stdout.isTTY)
596
+ print(report);
597
+ else
598
+ process.stdout.write(`${formatOptimizationReport(report.data)}\n`);
599
+ if (!options.adopt)
600
+ return;
601
+ // Written beside the strategy rather than into it: the source is the user's,
602
+ // and a tool that edits it takes away the ability to see what changed.
603
+ const best = report.data.candidates.find((candidate) => candidate.status === "completed");
604
+ if (!best) {
605
+ process.stderr.write(`${activeMessages().noBestCandidate}\n`);
606
+ process.exitCode = 1;
607
+ return;
608
+ }
609
+ fs.writeFileSync(options.adopt, `${JSON.stringify(best.params, null, 2)}\n`, "utf8");
610
+ process.stdout.write(`${activeMessages().adoptedParams(options.adopt)}\n ${activeMessages().adoptHint(options.run)}\n`);
611
+ });
612
+ strategy.command("runs")
613
+ .description("List recent strategy runs")
614
+ .option("--limit <count>", "maximum rows", "20")
615
+ .option("--status <status>", "queued|running|completed|failed|cancelled")
616
+ .option("--json", "Print machine-readable JSON")
617
+ .action(async (options) => {
618
+ const client = await RuntimeClient.connect();
619
+ const input = { limit: Number(options.limit) };
620
+ if (options.status)
621
+ input.status = options.status;
622
+ const result = await client.call("strategy_list_runs", input);
623
+ if (options.json || !process.stdout.isTTY)
624
+ print(result);
625
+ else
626
+ process.stdout.write(`${formatRunTable(result.data)}\n`);
627
+ });
628
+ strategy.command("report")
629
+ .description("Show one run's metrics, equity curve, and trades")
630
+ .requiredOption("--run <runId>", "strategy run id")
631
+ .option("--open", "Build an HTML report and open it in the browser")
632
+ .option("--equity", "include the equity sparkline")
633
+ .option("--trades", "include closed trades")
634
+ .option("--json", "Print machine-readable JSON")
635
+ .action(async (options) => {
636
+ const client = await RuntimeClient.connect();
637
+ if (options.open) {
638
+ const file = writeReport(await collectReport(client, options.run));
639
+ const opened = openInBrowser(file);
640
+ if (options.json || !process.stdout.isTTY)
641
+ print({ file, opened });
642
+ else
643
+ process.stdout.write(`${opened ? activeMessages().reportOpened : activeMessages().reportWritten}\n${file}\n`);
644
+ return;
645
+ }
646
+ // Read through the paginating collector rather than guessing a page size:
647
+ // a limit above the tool's cap is rejected, and one below the row count
648
+ // silently returns a prefix.
649
+ const needsDetail = Boolean(options.equity || options.trades);
650
+ const run = await client.call("strategy_get_run", { runId: options.run });
651
+ const collected = needsDetail ? await collectReport(client, options.run) : null;
652
+ if (options.json || !process.stdout.isTTY) {
653
+ const payload = { run: run.data };
654
+ if (options.equity)
655
+ payload.equity = collected?.equity ?? [];
656
+ if (options.trades)
657
+ payload.trades = collected?.trades ?? [];
658
+ print(payload);
659
+ return;
660
+ }
661
+ process.stdout.write(`${formatRunDetail(run.data)}\n`);
662
+ if (options.equity && collected) {
663
+ process.stdout.write(`\n${activeMessages().equityHeading}\n${formatEquitySparkline(collected.equity)}\n`);
664
+ }
665
+ if (options.trades && collected) {
666
+ process.stdout.write(`\n${activeMessages().tradesHeading}\n${formatTradeTable(collected.trades)}\n`);
667
+ }
668
+ });
669
+ strategy.command("compare")
670
+ .description("Put two or more runs side by side, normalizing their equity curves")
671
+ .requiredOption("--runs <ids>", "comma-separated run ids, for example bt_a,bt_b")
672
+ .option("--open", "Build an HTML comparison and open it in the browser")
673
+ .option("--json", "Print machine-readable JSON")
674
+ .action(async (options) => {
675
+ const runIds = options.runs.split(",").map((item) => item.trim()).filter(Boolean);
676
+ if (runIds.length < MIN_COMPARED_RUNS)
677
+ throw new Error(activeMessages().compareNeedsTwo);
678
+ if (runIds.length > MAX_COMPARED_RUNS)
679
+ throw new Error(activeMessages().compareTooMany(MAX_COMPARED_RUNS));
680
+ const client = await RuntimeClient.connect();
681
+ const report = buildComparison(await collectComparison(client, runIds));
682
+ if (options.open) {
683
+ const file = writeComparison(report);
684
+ const opened = openInBrowser(file);
685
+ if (options.json || !process.stdout.isTTY)
686
+ print({ file, opened, warnings: report.warnings });
687
+ else {
688
+ // Warnings are printed even when the document opens elsewhere: a reader who
689
+ // switches to the browser should not be the only one who sees them.
690
+ for (const warning of report.warnings)
691
+ process.stderr.write(`! ${warning.message}\n`);
692
+ process.stdout.write(`${opened ? activeMessages().reportOpened : activeMessages().reportWritten}\n${file}\n`);
693
+ }
694
+ return;
695
+ }
696
+ if (options.json || !process.stdout.isTTY) {
697
+ print({
698
+ runs: report.runs,
699
+ warnings: report.warnings,
700
+ differingAssumptions: report.differingAssumptions,
701
+ sharedAssumptions: report.sharedAssumptions
702
+ });
703
+ return;
704
+ }
705
+ process.stdout.write(`${formatComparison(report)}\n`);
706
+ });
707
+ strategy.command("delete")
708
+ .description("Delete a run and its stored equity curve, trades, and decisions")
709
+ .requiredOption("--run <runId>", "strategy run id")
710
+ .option("--yes", "skip the confirmation")
711
+ .action(async (options) => {
712
+ // Irreversible, so it is confirmed rather than assumed.
713
+ if (!options.yes) {
714
+ throw new Error(`This removes ${options.run} and its stored series. Pass --yes to confirm.`);
715
+ }
716
+ const client = await RuntimeClient.connect();
717
+ print(await client.call("strategy_delete_run", { runId: options.run }));
718
+ });
719
+ strategy.command("cancel")
720
+ .description("Cancel a queued or running backtest")
721
+ .requiredOption("--run <runId>", "strategy run id")
722
+ .action(async (options) => {
723
+ const client = await RuntimeClient.connect();
724
+ print(await client.call("strategy_cancel_run", { runId: options.run }));
725
+ });
726
+ for (const spec of SETTING_SPECS) {
727
+ // `--days` is already declared above with its own help text; re-registering a
728
+ // flag makes commander throw at startup.
729
+ const exists = backtestCommand.options.some((option) => option.long === `--${spec.flag}`);
730
+ if (!exists)
731
+ backtestCommand.option(`--${spec.flag} <value>`, settingFlagHelp(spec));
732
+ }
733
+ const settings = strategy.command("settings")
734
+ .description("Show or change the assumptions every backtest uses")
735
+ .option("--json", "Print machine-readable JSON")
736
+ .action(async (options) => {
737
+ const client = await RuntimeClient.connect();
738
+ const current = (await client.call("strategy_settings", {})).data;
739
+ if (options.json || !process.stdout.isTTY)
740
+ print({ settings: current, path: SETTINGS_PATH });
741
+ else
742
+ process.stdout.write(`${formatSettingsTable(current)}\n`);
743
+ });
744
+ settings.command("set")
745
+ .description("Change one or more settings, as key=value pairs")
746
+ .argument("<pairs...>", "for example leverage=5 budget-pct=0.1")
747
+ .option("--json", "Print machine-readable JSON")
748
+ .action(async (pairs, options) => {
749
+ // Validated locally for a fast, clear error, then applied through the
750
+ // runtime so both views stay in agreement.
751
+ let candidate = loadSettings();
752
+ const values = {};
753
+ const changed = [];
754
+ for (const pair of pairs) {
755
+ const separator = pair.indexOf("=");
756
+ if (separator <= 0)
757
+ throw new Error(`Expected key=value, received '${pair}'`);
758
+ const applied = withSetting(candidate, pair.slice(0, separator), pair.slice(separator + 1));
759
+ candidate = applied.settings;
760
+ values[applied.spec.key] = candidate[applied.spec.key];
761
+ changed.push(applied.spec.key);
762
+ }
763
+ const client = await RuntimeClient.connect();
764
+ const current = (await client.call("strategy_settings_update", { values })).data;
765
+ if (options.json || !process.stdout.isTTY)
766
+ print({ settings: current, changed });
767
+ else
768
+ process.stdout.write(`${activeMessages().settingsSaved}\n${formatSettingsTable(current)}\n`);
769
+ });
770
+ settings.command("reset")
771
+ .description("Restore every setting to its default")
772
+ .option("--json", "Print machine-readable JSON")
773
+ .action(async (options) => {
774
+ const client = await RuntimeClient.connect();
775
+ const restored = (await client.call("strategy_settings_update", { values: {}, reset: true })).data;
776
+ if (options.json || !process.stdout.isTTY)
777
+ print({ settings: restored, reset: true });
778
+ else
779
+ process.stdout.write(`${formatSettingsTable(restored)}\n`);
780
+ });
781
+ const data = program.command("data").description("Inspect and download local one-minute history");
782
+ data.command("list")
783
+ .description("List instruments with locally stored history and their covered range")
784
+ .option("--json", "Print machine-readable JSON")
785
+ .action(async (options) => {
786
+ const client = await RuntimeClient.connect();
787
+ const result = await client.call("data_list_instruments", {});
788
+ if (options.json || !process.stdout.isTTY)
789
+ print(result);
790
+ else
791
+ process.stdout.write(`${formatInstrumentTable(result.data, describeEnvironment(result.meta?.environment))}\n`);
792
+ });
793
+ data.command("coverage")
794
+ .description("Show the stored range, bar count, and gaps for one instrument")
795
+ .requiredOption("--inst <instId>", "OKX instrument ID, for example BTC-USDT-SWAP")
796
+ .option("--gap-limit <count>", "Maximum gaps to list", "50")
797
+ .option("--json", "Print machine-readable JSON")
798
+ .action(async (options) => {
799
+ const client = await RuntimeClient.connect();
800
+ const result = await client.call("data_coverage", {
801
+ instId: options.inst,
802
+ gapLimit: Number(options.gapLimit)
803
+ });
804
+ if (options.json || !process.stdout.isTTY)
805
+ print(result);
806
+ else
807
+ process.stdout.write(`${formatCoverageDetail(result.data, options.inst)}\n`);
808
+ });
809
+ data.command("download")
810
+ .description("Download or repair local one-minute history. Higher timeframes are derived from it.")
811
+ .requiredOption("--inst <instId>", "OKX instrument ID, for example BTC-USDT-SWAP")
812
+ .option("--days <days>", "Trailing window in days", "30")
813
+ .option("--from <date>", "Inclusive start, for example 2026-01-01")
814
+ .option("--to <date>", "Inclusive end; defaults to the newest closed minute")
815
+ .option("--json", "Print machine-readable JSON")
816
+ .action(async (options) => {
817
+ const request = { instId: options.inst };
818
+ if (options.from)
819
+ request.fromMs = parseBarTime(options.from, "--from");
820
+ if (options.to)
821
+ request.toMs = parseBarTime(options.to, "--to");
822
+ if (!options.from)
823
+ request.days = Number(options.days);
824
+ const client = await RuntimeClient.connect();
825
+ const result = await client.call("data_download", request);
826
+ if (options.json || !process.stdout.isTTY)
827
+ print(result);
828
+ else
829
+ process.stdout.write(`${formatSyncReport(result.data)}\n`);
830
+ });
176
831
  const account = program.command("account").description("Manage named OKX accounts");
177
832
  account.command("add")
178
833
  .description("Add or replace a named account")
179
834
  .option("--name <name>", "account alias")
180
- .option("--environment <environment>", "demo or live")
181
835
  .action(async (options) => {
182
836
  const name = options.name?.trim() || await ask("Account name", "default");
183
- const environmentValue = options.environment?.trim() || await ask("Environment (demo/live)", "demo");
184
- if (environmentValue !== "demo" && environmentValue !== "live")
185
- throw new Error("Environment must be demo or live");
186
837
  const apiKey = await askSecret("API Key");
187
838
  const secretKey = await askSecret("Secret Key");
188
839
  const passphrase = await askSecret("Passphrase");
189
840
  if (!apiKey || !secretKey || !passphrase)
190
841
  throw new Error("All three credential fields are required");
191
- const config = loadStoredConfig();
192
- const candidate = structuredClone(config);
193
- candidate.accounts[name] = { environment: environmentValue, apiKey, secretKey, passphrase };
842
+ const detected = await detectCredentials({ name, apiKey, secretKey, passphrase });
843
+ const candidate = loadStoredConfig();
844
+ candidate.accounts[name] = {
845
+ environment: detected.account.environment,
846
+ apiKey,
847
+ secretKey,
848
+ passphrase
849
+ };
194
850
  candidate.defaultAccount ??= name;
195
- const verified = await verifyAccount(candidate, name);
196
851
  saveConfig(candidate);
197
852
  await stopForReload();
198
- print({ saved: true, name, environment: environmentValue, permissions: verified.data, configPath: CONFIG_PATH });
853
+ print({ saved: true, name, environment: detected.account.environment, permissions: detected.config, configPath: CONFIG_PATH });
199
854
  });
200
855
  account.command("list")
201
856
  .description("List configured account aliases")
@@ -256,34 +911,34 @@ account.command("rename")
256
911
  print({ renamed: true, from: oldName, to: newName, default: config.defaultAccount === newName });
257
912
  });
258
913
  account.command("edit")
259
- .description("Edit an account environment or replace its credentials")
914
+ .description("Verify an account or replace its credentials")
260
915
  .argument("<name>", "account alias")
261
- .option("--environment <environment>", "demo or live")
262
916
  .option("--replace-credentials", "Prompt for replacement credentials")
263
917
  .action(async (name, options) => {
264
918
  const config = loadStoredConfig();
265
919
  const current = config.accounts[name];
266
920
  if (!current)
267
921
  throw new Error(`Account '${name}' was not found`);
268
- const environment = options.environment?.trim() || await ask("Environment (demo/live)", current.environment);
269
- if (environment !== "demo" && environment !== "live")
270
- throw new Error("Environment must be demo or live");
271
- const normalizedEnvironment = environment;
272
922
  const replace = options.replaceCredentials ?? await askConfirm("Replace API credentials", false);
273
- const updated = { ...current, environment: normalizedEnvironment };
923
+ const credentials = { name, apiKey: current.apiKey, secretKey: current.secretKey, passphrase: current.passphrase };
274
924
  if (replace) {
275
- updated.apiKey = await askSecret("API Key");
276
- updated.secretKey = await askSecret("Secret Key");
277
- updated.passphrase = await askSecret("Passphrase");
278
- if (!updated.apiKey || !updated.secretKey || !updated.passphrase)
925
+ credentials.apiKey = await askSecret("API Key");
926
+ credentials.secretKey = await askSecret("Secret Key");
927
+ credentials.passphrase = await askSecret("Passphrase");
928
+ if (!credentials.apiKey || !credentials.secretKey || !credentials.passphrase)
279
929
  throw new Error("All three credential fields are required");
280
930
  }
931
+ const detected = await detectCredentials(credentials, current.environment);
281
932
  const candidate = structuredClone(config);
282
- candidate.accounts[name] = updated;
283
- const verified = await verifyAccount(candidate, name);
933
+ candidate.accounts[name] = {
934
+ environment: detected.account.environment,
935
+ apiKey: credentials.apiKey,
936
+ secretKey: credentials.secretKey,
937
+ passphrase: credentials.passphrase
938
+ };
284
939
  saveConfig(candidate);
285
940
  await stopForReload();
286
- print({ saved: true, name, environment, permissions: verified.data });
941
+ print({ saved: true, name, environment: detected.account.environment, permissions: detected.config });
287
942
  });
288
943
  account.command("remove")
289
944
  .description("Remove one account")
@@ -299,6 +954,12 @@ account.command("remove")
299
954
  await stopForReload();
300
955
  print({ removed: true, name });
301
956
  });
957
+ program.command("shell", { isDefault: true })
958
+ .description("Open the interactive strategy research shell (default)")
959
+ .action(async () => {
960
+ const { runShell } = await import("../tui/index.js");
961
+ await runShell();
962
+ });
302
963
  try {
303
964
  await program.parseAsync(process.argv);
304
965
  }
@@ -315,9 +976,10 @@ async function stopForReload() {
315
976
  }
316
977
  catch { }
317
978
  }
318
- async function verifyAccount(config, name) {
979
+ async function detectCredentials(credentials, preferred) {
980
+ const config = loadStoredConfig();
319
981
  const client = new OkxClient(OKX_REST_BASE_URL, resolveProxy(config.proxy.url).url);
320
- return new AccountService(config, client).verify(name);
982
+ return detectAccountEnvironment(client, credentials, preferred);
321
983
  }
322
984
  function parseSkillTargets(value) {
323
985
  const targets = value.split(",").map((item) => item.trim()).filter(Boolean);
@@ -336,7 +998,76 @@ function resolveSkillTargets(value) {
336
998
  throw new Error("No installed Codex or Claude Code Skills were detected; pass --targets to install them");
337
999
  return detected;
338
1000
  }
1001
+ /**
1002
+ * Reads a JSON argument file, naming the flag in any failure.
1003
+ *
1004
+ * `JSON.parse` reports only a character offset, which is unhelpful when two flags
1005
+ * both take a file and either could be the one at fault.
1006
+ */
1007
+ function readJsonFile(file, flag) {
1008
+ let text;
1009
+ try {
1010
+ text = fs.readFileSync(file, "utf8");
1011
+ }
1012
+ catch {
1013
+ throw new Error(`${flag} file '${file}' could not be read`);
1014
+ }
1015
+ try {
1016
+ return JSON.parse(text);
1017
+ }
1018
+ catch (error) {
1019
+ throw new Error(`${flag} file '${file}' is not valid JSON: ${error instanceof Error ? error.message : error}`);
1020
+ }
1021
+ }
1022
+ /** `budget-pct` becomes `budgetPct`, which is how commander stores the option. */
1023
+ function camelFlag(flag) {
1024
+ return flag.replace(/-([a-z])/g, (_match, letter) => letter.toUpperCase());
1025
+ }
1026
+ function settingFlagHelp(spec) {
1027
+ const messages = activeMessages();
1028
+ const bounds = spec.minimum !== undefined && spec.maximum !== undefined ? ` (${spec.minimum}-${spec.maximum})` : "";
1029
+ return `${messages[spec.label]}${bounds}; defaults to the saved setting`;
1030
+ }
1031
+ function formatSettingsTable(settings) {
1032
+ const messages = activeMessages();
1033
+ const width = Math.max(...SETTING_SPECS.map((spec) => displayWidth(messages[spec.label])));
1034
+ const lines = [activeMessages().settingsHeading, ""];
1035
+ for (const spec of SETTING_SPECS) {
1036
+ const value = settings[spec.key];
1037
+ // A marker beats printing the default beside every row: only the changed
1038
+ // ones need attention.
1039
+ const marker = value === spec.default ? " " : "*";
1040
+ lines.push(`${marker} ${padEndDisplay(messages[spec.label], width + 2)}${padEndDisplay(formatSettingValue(spec, value), 12)}--${spec.flag}`);
1041
+ }
1042
+ lines.push("", SETTINGS_PATH);
1043
+ return lines.join("\n");
1044
+ }
339
1045
  function print(value) {
340
1046
  process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
341
1047
  }
1048
+ /** Carriage return plus "erase to end of line", written as an escape so no raw control byte sits in source. */
1049
+ const CLEAR_LINE = "\r\u001b[K";
1050
+ /**
1051
+ * Polls a run to completion, rewriting one progress line in place.
1052
+ *
1053
+ * A backtest can take minutes, so it is queued rather than held open on a single
1054
+ * request; this is the interactive counterpart to that design.
1055
+ */
1056
+ async function followRun(client, runId, quiet) {
1057
+ const interactive = !quiet && process.stdout.isTTY;
1058
+ for (;;) {
1059
+ const response = await client.call("strategy_get_run", { runId });
1060
+ const run = response.data;
1061
+ if (run.status === "completed" || run.status === "failed" || run.status === "cancelled") {
1062
+ if (interactive)
1063
+ process.stdout.write(CLEAR_LINE);
1064
+ return run;
1065
+ }
1066
+ if (interactive) {
1067
+ const eta = run.etaMs === null ? "" : ` eta ${Math.max(1, Math.round(run.etaMs / 1000))}s`;
1068
+ process.stdout.write(`${CLEAR_LINE} ${runId} ${run.status} ${run.progressPct.toFixed(1)}%${eta}`);
1069
+ }
1070
+ await new Promise((resolve) => setTimeout(resolve, 500));
1071
+ }
1072
+ }
342
1073
  //# sourceMappingURL=index.js.map