desic-okx-agent 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +90 -10
- package/README.md +76 -10
- package/dist/account/private-websocket.js +4 -4
- package/dist/account/private-websocket.js.map +1 -1
- package/dist/account/service.d.ts +12 -1
- package/dist/account/service.js +18 -0
- package/dist/account/service.js.map +1 -1
- package/dist/bars/rate-limiter.d.ts +18 -0
- package/dist/bars/rate-limiter.js +84 -0
- package/dist/bars/rate-limiter.js.map +1 -0
- package/dist/bars/schema.d.ts +36 -0
- package/dist/bars/schema.js +134 -0
- package/dist/bars/schema.js.map +1 -0
- package/dist/bars/service.d.ts +60 -0
- package/dist/bars/service.js +120 -0
- package/dist/bars/service.js.map +1 -0
- package/dist/bars/store.d.ts +105 -0
- package/dist/bars/store.js +415 -0
- package/dist/bars/store.js.map +1 -0
- package/dist/bars/timeframe.d.ts +40 -0
- package/dist/bars/timeframe.js +146 -0
- package/dist/bars/timeframe.js.map +1 -0
- package/dist/bars/types.d.ts +68 -0
- package/dist/bars/types.js +13 -0
- package/dist/bars/types.js.map +1 -0
- package/dist/cli/data-render.d.ts +37 -0
- package/dist/cli/data-render.js +143 -0
- package/dist/cli/data-render.js.map +1 -0
- package/dist/cli/doctor.js +7 -3
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/index.js +757 -26
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/live-render.d.ts +24 -0
- package/dist/cli/live-render.js +85 -0
- package/dist/cli/live-render.js.map +1 -0
- package/dist/cli/range.d.ts +28 -0
- package/dist/cli/range.js +63 -0
- package/dist/cli/range.js.map +1 -0
- package/dist/cli/render.js +3 -0
- package/dist/cli/render.js.map +1 -1
- package/dist/cli/strategy-render.d.ts +36 -0
- package/dist/cli/strategy-render.js +391 -0
- package/dist/cli/strategy-render.js.map +1 -0
- package/dist/cli/width.d.ts +18 -0
- package/dist/cli/width.js +71 -0
- package/dist/cli/width.js.map +1 -0
- package/dist/config/loader.js +1 -1
- package/dist/config/schema.d.ts +7 -0
- package/dist/config/schema.js +24 -0
- package/dist/config/schema.js.map +1 -1
- package/dist/core/okx-client.d.ts +9 -1
- package/dist/core/okx-client.js +14 -5
- package/dist/core/okx-client.js.map +1 -1
- package/dist/i18n/locale.d.ts +24 -0
- package/dist/i18n/locale.js +65 -0
- package/dist/i18n/locale.js.map +1 -0
- package/dist/i18n/messages.d.ts +333 -0
- package/dist/i18n/messages.js +660 -0
- package/dist/i18n/messages.js.map +1 -0
- package/dist/live/account-snapshot.d.ts +30 -0
- package/dist/live/account-snapshot.js +130 -0
- package/dist/live/account-snapshot.js.map +1 -0
- package/dist/live/cutoff-queue.d.ts +42 -0
- package/dist/live/cutoff-queue.js +69 -0
- package/dist/live/cutoff-queue.js.map +1 -0
- package/dist/live/execution-key.d.ts +23 -0
- package/dist/live/execution-key.js +31 -0
- package/dist/live/execution-key.js.map +1 -0
- package/dist/live/failures.d.ts +37 -0
- package/dist/live/failures.js +57 -0
- package/dist/live/failures.js.map +1 -0
- package/dist/live/gates.d.ts +65 -0
- package/dist/live/gates.js +136 -0
- package/dist/live/gates.js.map +1 -0
- package/dist/live/loop.d.ts +56 -0
- package/dist/live/loop.js +197 -0
- package/dist/live/loop.js.map +1 -0
- package/dist/live/preconditions.d.ts +48 -0
- package/dist/live/preconditions.js +69 -0
- package/dist/live/preconditions.js.map +1 -0
- package/dist/live/reconcile.d.ts +46 -0
- package/dist/live/reconcile.js +104 -0
- package/dist/live/reconcile.js.map +1 -0
- package/dist/live/runner.d.ts +57 -0
- package/dist/live/runner.js +160 -0
- package/dist/live/runner.js.map +1 -0
- package/dist/live/schema.d.ts +18 -0
- package/dist/live/schema.js +91 -0
- package/dist/live/schema.js.map +1 -0
- package/dist/live/service.d.ts +144 -0
- package/dist/live/service.js +303 -0
- package/dist/live/service.js.map +1 -0
- package/dist/live/session.d.ts +85 -0
- package/dist/live/session.js +234 -0
- package/dist/live/session.js.map +1 -0
- package/dist/live/sizing.d.ts +62 -0
- package/dist/live/sizing.js +79 -0
- package/dist/live/sizing.js.map +1 -0
- package/dist/live/store.d.ts +123 -0
- package/dist/live/store.js +350 -0
- package/dist/live/store.js.map +1 -0
- package/dist/live/types.d.ts +82 -0
- package/dist/live/types.js +2 -0
- package/dist/live/types.js.map +1 -0
- package/dist/market/websocket.d.ts +16 -1
- package/dist/market/websocket.js +60 -5
- package/dist/market/websocket.js.map +1 -1
- package/dist/mcp/server.d.ts +1 -0
- package/dist/mcp/server.js +15 -1
- package/dist/mcp/server.js.map +1 -1
- package/dist/network/connectivity.d.ts +9 -1
- package/dist/network/connectivity.js +28 -1
- package/dist/network/connectivity.js.map +1 -1
- package/dist/report/chart-script.d.ts +12 -0
- package/dist/report/chart-script.js +146 -0
- package/dist/report/chart-script.js.map +1 -0
- package/dist/report/compare-html.d.ts +8 -0
- package/dist/report/compare-html.js +254 -0
- package/dist/report/compare-html.js.map +1 -0
- package/dist/report/compare-script.d.ts +12 -0
- package/dist/report/compare-script.js +109 -0
- package/dist/report/compare-script.js.map +1 -0
- package/dist/report/compare.d.ts +61 -0
- package/dist/report/compare.js +205 -0
- package/dist/report/compare.js.map +1 -0
- package/dist/report/fetch.d.ts +20 -0
- package/dist/report/fetch.js +56 -0
- package/dist/report/fetch.js.map +1 -0
- package/dist/report/html.d.ts +54 -0
- package/dist/report/html.js +641 -0
- package/dist/report/html.js.map +1 -0
- package/dist/report/open.d.ts +42 -0
- package/dist/report/open.js +114 -0
- package/dist/report/open.js.map +1 -0
- package/dist/runtime/server.d.ts +16 -1
- package/dist/runtime/server.js +112 -9
- package/dist/runtime/server.js.map +1 -1
- package/dist/setup/installer.d.ts +1 -0
- package/dist/setup/installer.js +8 -0
- package/dist/setup/installer.js.map +1 -1
- package/dist/setup/wizard.js +19 -22
- package/dist/setup/wizard.js.map +1 -1
- package/dist/strategy/constants.d.ts +23 -0
- package/dist/strategy/constants.js +24 -0
- package/dist/strategy/constants.js.map +1 -0
- package/dist/strategy/environment.d.ts +52 -0
- package/dist/strategy/environment.js +187 -0
- package/dist/strategy/environment.js.map +1 -0
- package/dist/strategy/instrument.d.ts +29 -0
- package/dist/strategy/instrument.js +39 -0
- package/dist/strategy/instrument.js.map +1 -0
- package/dist/strategy/optimize.d.ts +73 -0
- package/dist/strategy/optimize.js +113 -0
- package/dist/strategy/optimize.js.map +1 -0
- package/dist/strategy/parameter-space.d.ts +59 -0
- package/dist/strategy/parameter-space.js +221 -0
- package/dist/strategy/parameter-space.js.map +1 -0
- package/dist/strategy/python-bridge.d.ts +24 -0
- package/dist/strategy/python-bridge.js +114 -0
- package/dist/strategy/python-bridge.js.map +1 -0
- package/dist/strategy/schema.d.ts +9 -0
- package/dist/strategy/schema.js +91 -0
- package/dist/strategy/schema.js.map +1 -0
- package/dist/strategy/service.d.ts +138 -0
- package/dist/strategy/service.js +745 -0
- package/dist/strategy/service.js.map +1 -0
- package/dist/strategy/settings.d.ts +162 -0
- package/dist/strategy/settings.js +243 -0
- package/dist/strategy/settings.js.map +1 -0
- package/dist/strategy/store.d.ts +96 -0
- package/dist/strategy/store.js +367 -0
- package/dist/strategy/store.js.map +1 -0
- package/dist/strategy/templates.d.ts +11 -0
- package/dist/strategy/templates.js +134 -0
- package/dist/strategy/templates.js.map +1 -0
- package/dist/strategy/types.d.ts +111 -0
- package/dist/strategy/types.js +2 -0
- package/dist/strategy/types.js.map +1 -0
- package/dist/tools/catalog.d.ts +16 -0
- package/dist/tools/catalog.js +118 -17
- package/dist/tools/catalog.js.map +1 -1
- package/dist/trade/service.d.ts +12 -0
- package/dist/trade/service.js +24 -7
- package/dist/trade/service.js.map +1 -1
- package/dist/tui/app.d.ts +23 -0
- package/dist/tui/app.js +322 -0
- package/dist/tui/app.js.map +1 -0
- package/dist/tui/commands.d.ts +70 -0
- package/dist/tui/commands.js +313 -0
- package/dist/tui/commands.js.map +1 -0
- package/dist/tui/entries.d.ts +17 -0
- package/dist/tui/entries.js +24 -0
- package/dist/tui/entries.js.map +1 -0
- package/dist/tui/execute.d.ts +26 -0
- package/dist/tui/execute.js +664 -0
- package/dist/tui/execute.js.map +1 -0
- package/dist/tui/history.d.ts +17 -0
- package/dist/tui/history.js +48 -0
- package/dist/tui/history.js.map +1 -0
- package/dist/tui/index.d.ts +8 -0
- package/dist/tui/index.js +48 -0
- package/dist/tui/index.js.map +1 -0
- package/dist/tui/line-editor.d.ts +44 -0
- package/dist/tui/line-editor.js +98 -0
- package/dist/tui/line-editor.js.map +1 -0
- package/dist/tui/progress.d.ts +23 -0
- package/dist/tui/progress.js +46 -0
- package/dist/tui/progress.js.map +1 -0
- package/dist/tui/settings-editor.d.ts +18 -0
- package/dist/tui/settings-editor.js +115 -0
- package/dist/tui/settings-editor.js.map +1 -0
- package/docs/live-trading.md +455 -0
- package/docs/strategy-research.md +597 -0
- package/package.json +10 -1
- package/python/desic_strategy/__init__.py +34 -0
- package/python/desic_strategy/actions.py +158 -0
- package/python/desic_strategy/context.py +164 -0
- package/python/desic_strategy/engine.py +614 -0
- package/python/desic_strategy/indicators.py +159 -0
- package/python/desic_strategy/live.py +253 -0
- package/python/desic_strategy/policy.py +193 -0
- package/python/desic_strategy/portfolio.py +152 -0
- package/python/desic_strategy/report.py +319 -0
- package/python/desic_strategy/runner.py +574 -0
- package/python/desic_strategy/timeframe.py +150 -0
- package/python/main.py +18 -0
- package/skills/okx-live-trading/SKILL.md +117 -0
- package/skills/okx-live-trading/agents/openai.yaml +9 -0
- package/skills/okx-live-trading/references/lifecycle.md +128 -0
- package/skills/okx-strategy-research/SKILL.md +113 -0
- package/skills/okx-strategy-research/agents/openai.yaml +9 -0
- package/skills/okx-strategy-research/references/execution-semantics.md +107 -0
- package/skills/okx-strategy-research/references/field-traps.md +142 -0
- package/skills/okx-strategy-research/references/python-api.md +121 -0
- package/skills/okx-strategy-research/references/tools-and-data.md +192 -0
- package/skills/okx-trading/SKILL.md +11 -10
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
|
|
192
|
-
const candidate =
|
|
193
|
-
candidate.accounts[name] = {
|
|
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:
|
|
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("
|
|
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
|
|
923
|
+
const credentials = { name, apiKey: current.apiKey, secretKey: current.secretKey, passphrase: current.passphrase };
|
|
274
924
|
if (replace) {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
if (!
|
|
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] =
|
|
283
|
-
|
|
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:
|
|
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
|
|
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
|
|
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
|