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