desic-okx-agent 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (237) hide show
  1. package/README.en.md +353 -0
  2. package/README.md +194 -190
  3. package/dist/account/private-websocket.js +4 -4
  4. package/dist/account/private-websocket.js.map +1 -1
  5. package/dist/account/service.d.ts +12 -1
  6. package/dist/account/service.js +18 -0
  7. package/dist/account/service.js.map +1 -1
  8. package/dist/bars/rate-limiter.d.ts +18 -0
  9. package/dist/bars/rate-limiter.js +84 -0
  10. package/dist/bars/rate-limiter.js.map +1 -0
  11. package/dist/bars/schema.d.ts +36 -0
  12. package/dist/bars/schema.js +134 -0
  13. package/dist/bars/schema.js.map +1 -0
  14. package/dist/bars/service.d.ts +60 -0
  15. package/dist/bars/service.js +120 -0
  16. package/dist/bars/service.js.map +1 -0
  17. package/dist/bars/store.d.ts +105 -0
  18. package/dist/bars/store.js +415 -0
  19. package/dist/bars/store.js.map +1 -0
  20. package/dist/bars/timeframe.d.ts +40 -0
  21. package/dist/bars/timeframe.js +146 -0
  22. package/dist/bars/timeframe.js.map +1 -0
  23. package/dist/bars/types.d.ts +68 -0
  24. package/dist/bars/types.js +13 -0
  25. package/dist/bars/types.js.map +1 -0
  26. package/dist/cli/data-render.d.ts +37 -0
  27. package/dist/cli/data-render.js +143 -0
  28. package/dist/cli/data-render.js.map +1 -0
  29. package/dist/cli/doctor.js +7 -3
  30. package/dist/cli/doctor.js.map +1 -1
  31. package/dist/cli/index.js +758 -27
  32. package/dist/cli/index.js.map +1 -1
  33. package/dist/cli/live-render.d.ts +24 -0
  34. package/dist/cli/live-render.js +85 -0
  35. package/dist/cli/live-render.js.map +1 -0
  36. package/dist/cli/range.d.ts +28 -0
  37. package/dist/cli/range.js +63 -0
  38. package/dist/cli/range.js.map +1 -0
  39. package/dist/cli/render.js +3 -0
  40. package/dist/cli/render.js.map +1 -1
  41. package/dist/cli/strategy-render.d.ts +36 -0
  42. package/dist/cli/strategy-render.js +391 -0
  43. package/dist/cli/strategy-render.js.map +1 -0
  44. package/dist/cli/width.d.ts +18 -0
  45. package/dist/cli/width.js +71 -0
  46. package/dist/cli/width.js.map +1 -0
  47. package/dist/config/loader.js +1 -1
  48. package/dist/config/schema.d.ts +7 -0
  49. package/dist/config/schema.js +24 -0
  50. package/dist/config/schema.js.map +1 -1
  51. package/dist/core/okx-client.d.ts +9 -1
  52. package/dist/core/okx-client.js +14 -5
  53. package/dist/core/okx-client.js.map +1 -1
  54. package/dist/i18n/locale.d.ts +24 -0
  55. package/dist/i18n/locale.js +65 -0
  56. package/dist/i18n/locale.js.map +1 -0
  57. package/dist/i18n/messages.d.ts +333 -0
  58. package/dist/i18n/messages.js +660 -0
  59. package/dist/i18n/messages.js.map +1 -0
  60. package/dist/live/account-snapshot.d.ts +30 -0
  61. package/dist/live/account-snapshot.js +130 -0
  62. package/dist/live/account-snapshot.js.map +1 -0
  63. package/dist/live/cutoff-queue.d.ts +42 -0
  64. package/dist/live/cutoff-queue.js +69 -0
  65. package/dist/live/cutoff-queue.js.map +1 -0
  66. package/dist/live/execution-key.d.ts +23 -0
  67. package/dist/live/execution-key.js +31 -0
  68. package/dist/live/execution-key.js.map +1 -0
  69. package/dist/live/failures.d.ts +37 -0
  70. package/dist/live/failures.js +57 -0
  71. package/dist/live/failures.js.map +1 -0
  72. package/dist/live/gates.d.ts +65 -0
  73. package/dist/live/gates.js +136 -0
  74. package/dist/live/gates.js.map +1 -0
  75. package/dist/live/loop.d.ts +56 -0
  76. package/dist/live/loop.js +197 -0
  77. package/dist/live/loop.js.map +1 -0
  78. package/dist/live/preconditions.d.ts +48 -0
  79. package/dist/live/preconditions.js +69 -0
  80. package/dist/live/preconditions.js.map +1 -0
  81. package/dist/live/reconcile.d.ts +46 -0
  82. package/dist/live/reconcile.js +104 -0
  83. package/dist/live/reconcile.js.map +1 -0
  84. package/dist/live/runner.d.ts +57 -0
  85. package/dist/live/runner.js +160 -0
  86. package/dist/live/runner.js.map +1 -0
  87. package/dist/live/schema.d.ts +18 -0
  88. package/dist/live/schema.js +91 -0
  89. package/dist/live/schema.js.map +1 -0
  90. package/dist/live/service.d.ts +144 -0
  91. package/dist/live/service.js +303 -0
  92. package/dist/live/service.js.map +1 -0
  93. package/dist/live/session.d.ts +85 -0
  94. package/dist/live/session.js +234 -0
  95. package/dist/live/session.js.map +1 -0
  96. package/dist/live/sizing.d.ts +62 -0
  97. package/dist/live/sizing.js +79 -0
  98. package/dist/live/sizing.js.map +1 -0
  99. package/dist/live/store.d.ts +123 -0
  100. package/dist/live/store.js +350 -0
  101. package/dist/live/store.js.map +1 -0
  102. package/dist/live/types.d.ts +82 -0
  103. package/dist/live/types.js +2 -0
  104. package/dist/live/types.js.map +1 -0
  105. package/dist/market/websocket.d.ts +16 -1
  106. package/dist/market/websocket.js +60 -5
  107. package/dist/market/websocket.js.map +1 -1
  108. package/dist/mcp/server.d.ts +1 -0
  109. package/dist/mcp/server.js +15 -1
  110. package/dist/mcp/server.js.map +1 -1
  111. package/dist/network/connectivity.d.ts +9 -1
  112. package/dist/network/connectivity.js +28 -1
  113. package/dist/network/connectivity.js.map +1 -1
  114. package/dist/report/chart-script.d.ts +12 -0
  115. package/dist/report/chart-script.js +146 -0
  116. package/dist/report/chart-script.js.map +1 -0
  117. package/dist/report/compare-html.d.ts +8 -0
  118. package/dist/report/compare-html.js +254 -0
  119. package/dist/report/compare-html.js.map +1 -0
  120. package/dist/report/compare-script.d.ts +12 -0
  121. package/dist/report/compare-script.js +109 -0
  122. package/dist/report/compare-script.js.map +1 -0
  123. package/dist/report/compare.d.ts +61 -0
  124. package/dist/report/compare.js +205 -0
  125. package/dist/report/compare.js.map +1 -0
  126. package/dist/report/fetch.d.ts +20 -0
  127. package/dist/report/fetch.js +56 -0
  128. package/dist/report/fetch.js.map +1 -0
  129. package/dist/report/html.d.ts +54 -0
  130. package/dist/report/html.js +641 -0
  131. package/dist/report/html.js.map +1 -0
  132. package/dist/report/open.d.ts +42 -0
  133. package/dist/report/open.js +114 -0
  134. package/dist/report/open.js.map +1 -0
  135. package/dist/runtime/server.d.ts +16 -1
  136. package/dist/runtime/server.js +112 -9
  137. package/dist/runtime/server.js.map +1 -1
  138. package/dist/setup/installer.d.ts +1 -0
  139. package/dist/setup/installer.js +8 -0
  140. package/dist/setup/installer.js.map +1 -1
  141. package/dist/setup/wizard.d.ts +3 -0
  142. package/dist/setup/wizard.js +131 -6
  143. package/dist/setup/wizard.js.map +1 -1
  144. package/dist/strategy/constants.d.ts +23 -0
  145. package/dist/strategy/constants.js +24 -0
  146. package/dist/strategy/constants.js.map +1 -0
  147. package/dist/strategy/environment.d.ts +52 -0
  148. package/dist/strategy/environment.js +187 -0
  149. package/dist/strategy/environment.js.map +1 -0
  150. package/dist/strategy/instrument.d.ts +29 -0
  151. package/dist/strategy/instrument.js +39 -0
  152. package/dist/strategy/instrument.js.map +1 -0
  153. package/dist/strategy/optimize.d.ts +73 -0
  154. package/dist/strategy/optimize.js +113 -0
  155. package/dist/strategy/optimize.js.map +1 -0
  156. package/dist/strategy/parameter-space.d.ts +59 -0
  157. package/dist/strategy/parameter-space.js +221 -0
  158. package/dist/strategy/parameter-space.js.map +1 -0
  159. package/dist/strategy/python-bridge.d.ts +24 -0
  160. package/dist/strategy/python-bridge.js +114 -0
  161. package/dist/strategy/python-bridge.js.map +1 -0
  162. package/dist/strategy/schema.d.ts +9 -0
  163. package/dist/strategy/schema.js +91 -0
  164. package/dist/strategy/schema.js.map +1 -0
  165. package/dist/strategy/service.d.ts +138 -0
  166. package/dist/strategy/service.js +745 -0
  167. package/dist/strategy/service.js.map +1 -0
  168. package/dist/strategy/settings.d.ts +162 -0
  169. package/dist/strategy/settings.js +243 -0
  170. package/dist/strategy/settings.js.map +1 -0
  171. package/dist/strategy/store.d.ts +96 -0
  172. package/dist/strategy/store.js +367 -0
  173. package/dist/strategy/store.js.map +1 -0
  174. package/dist/strategy/templates.d.ts +11 -0
  175. package/dist/strategy/templates.js +134 -0
  176. package/dist/strategy/templates.js.map +1 -0
  177. package/dist/strategy/types.d.ts +111 -0
  178. package/dist/strategy/types.js +2 -0
  179. package/dist/strategy/types.js.map +1 -0
  180. package/dist/tools/catalog.d.ts +16 -0
  181. package/dist/tools/catalog.js +118 -17
  182. package/dist/tools/catalog.js.map +1 -1
  183. package/dist/trade/service.d.ts +12 -0
  184. package/dist/trade/service.js +24 -7
  185. package/dist/trade/service.js.map +1 -1
  186. package/dist/tui/app.d.ts +23 -0
  187. package/dist/tui/app.js +322 -0
  188. package/dist/tui/app.js.map +1 -0
  189. package/dist/tui/commands.d.ts +70 -0
  190. package/dist/tui/commands.js +313 -0
  191. package/dist/tui/commands.js.map +1 -0
  192. package/dist/tui/entries.d.ts +17 -0
  193. package/dist/tui/entries.js +24 -0
  194. package/dist/tui/entries.js.map +1 -0
  195. package/dist/tui/execute.d.ts +26 -0
  196. package/dist/tui/execute.js +664 -0
  197. package/dist/tui/execute.js.map +1 -0
  198. package/dist/tui/history.d.ts +17 -0
  199. package/dist/tui/history.js +48 -0
  200. package/dist/tui/history.js.map +1 -0
  201. package/dist/tui/index.d.ts +8 -0
  202. package/dist/tui/index.js +48 -0
  203. package/dist/tui/index.js.map +1 -0
  204. package/dist/tui/line-editor.d.ts +44 -0
  205. package/dist/tui/line-editor.js +98 -0
  206. package/dist/tui/line-editor.js.map +1 -0
  207. package/dist/tui/progress.d.ts +23 -0
  208. package/dist/tui/progress.js +46 -0
  209. package/dist/tui/progress.js.map +1 -0
  210. package/dist/tui/settings-editor.d.ts +18 -0
  211. package/dist/tui/settings-editor.js +115 -0
  212. package/dist/tui/settings-editor.js.map +1 -0
  213. package/docs/live-trading.md +455 -0
  214. package/docs/strategy-research.md +597 -0
  215. package/package.json +11 -1
  216. package/python/desic_strategy/__init__.py +34 -0
  217. package/python/desic_strategy/actions.py +158 -0
  218. package/python/desic_strategy/context.py +164 -0
  219. package/python/desic_strategy/engine.py +614 -0
  220. package/python/desic_strategy/indicators.py +159 -0
  221. package/python/desic_strategy/live.py +253 -0
  222. package/python/desic_strategy/policy.py +193 -0
  223. package/python/desic_strategy/portfolio.py +152 -0
  224. package/python/desic_strategy/report.py +319 -0
  225. package/python/desic_strategy/runner.py +574 -0
  226. package/python/desic_strategy/timeframe.py +150 -0
  227. package/python/main.py +18 -0
  228. package/skills/okx-live-trading/SKILL.md +117 -0
  229. package/skills/okx-live-trading/agents/openai.yaml +9 -0
  230. package/skills/okx-live-trading/references/lifecycle.md +128 -0
  231. package/skills/okx-strategy-research/SKILL.md +113 -0
  232. package/skills/okx-strategy-research/agents/openai.yaml +9 -0
  233. package/skills/okx-strategy-research/references/execution-semantics.md +107 -0
  234. package/skills/okx-strategy-research/references/field-traps.md +142 -0
  235. package/skills/okx-strategy-research/references/python-api.md +121 -0
  236. package/skills/okx-strategy-research/references/tools-and-data.md +192 -0
  237. package/skills/okx-trading/SKILL.md +11 -10
@@ -0,0 +1,136 @@
1
+ import { committedMarginUsdt, sizeEntry } from "./sizing.js";
2
+ /** Actions that open exposure. Only these are budgeted. */
3
+ const OPENING = new Set(["open_long", "open_short"]);
4
+ /** Actions that reduce exposure. */
5
+ const CLOSING = new Set(["close_long", "close_short"]);
6
+ export function sideOf(action) {
7
+ return action === "open_long" || action === "close_long" ? "long" : "short";
8
+ }
9
+ export function isOpening(action) {
10
+ return OPENING.has(action);
11
+ }
12
+ export function isClosing(action) {
13
+ return CLOSING.has(action);
14
+ }
15
+ /**
16
+ * Runs every gate in order and returns either a legal size or the refusal.
17
+ *
18
+ * Closing actions skip the budget, loss, and cooldown gates. Those exist to limit new
19
+ * exposure, and applying them to an exit would trap a position open precisely when
20
+ * risk control most wants it closed — a daily loss limit that prevented closing a
21
+ * losing position would make the loss worse.
22
+ */
23
+ export function evaluateGates(context) {
24
+ const { profile, action } = context;
25
+ // 1. Still enabled. Checked first because it is free and makes everything else
26
+ // moot.
27
+ if (!context.enabledNow) {
28
+ return refuse("disabled", `Profile ${profile.name} is not enabled`, { profileId: profile.id });
29
+ }
30
+ // 2. Generation unchanged. This is the TOCTOU guard: a stop that landed while the
31
+ // strategy was deciding must still block the order it decided on.
32
+ if (context.currentGeneration !== context.generation) {
33
+ return refuse("generation", "The profile was stopped or restarted during this evaluation", {
34
+ decidedUnder: context.generation,
35
+ currentGeneration: context.currentGeneration
36
+ });
37
+ }
38
+ const side = sideOf(action);
39
+ // 3. Direction permitted. A long-only profile must not be talked into a short by a
40
+ // strategy that emits both.
41
+ if (isOpening(action)) {
42
+ const permitted = side === "long" ? profile.allowLong : profile.allowShort;
43
+ if (!permitted) {
44
+ return refuse("direction", `This profile does not allow opening ${side} positions`, { side });
45
+ }
46
+ }
47
+ if (isClosing(action)) {
48
+ // An exit is sized from the open position, not from a budget: a partial close
49
+ // driven by a budget would leave exposure the strategy believed it had removed.
50
+ const held = holdingQuantity(context.positions, side);
51
+ if (held <= 0) {
52
+ return refuse("minimum_size", `There is no ${side} position to close`, { side });
53
+ }
54
+ return { allowed: true, contracts: held, notionalUsdt: 0, marginUsdt: 0, side };
55
+ }
56
+ // 4. Entry budget. The ceiling on one entry.
57
+ const budget = Math.min(profile.entryBudgetUsdt, context.availableMarginUsdt);
58
+ if (!(budget > 0)) {
59
+ return refuse("entry_budget", "No margin is available for a new entry", {
60
+ entryBudgetUsdt: profile.entryBudgetUsdt,
61
+ availableMarginUsdt: context.availableMarginUsdt
62
+ });
63
+ }
64
+ // 5. Side budget. Total margin already committed in this direction, read from the
65
+ // exchange rather than accumulated locally.
66
+ const committed = committedMarginUsdt(context.positions, side);
67
+ const headroom = profile.sideBudgetUsdt - committed;
68
+ if (headroom <= 0) {
69
+ return refuse("side_budget", `The ${side} budget is fully committed`, {
70
+ committedUsdt: round(committed),
71
+ sideBudgetUsdt: profile.sideBudgetUsdt
72
+ });
73
+ }
74
+ // 6. Daily loss limit. Blocks new exposure only; an exit already returned above.
75
+ if (context.realizedLossUsdtToday >= profile.dailyLossLimitUsdt) {
76
+ return refuse("daily_loss", "Today's realized loss has reached the limit", {
77
+ realizedLossUsdt: round(context.realizedLossUsdtToday),
78
+ dailyLossLimitUsdt: profile.dailyLossLimitUsdt
79
+ });
80
+ }
81
+ // 7. Cooldown, measured from the last order actually submitted rather than the last
82
+ // evaluation: a stretch of blocked signals must not keep the cooldown alive.
83
+ if (profile.cooldownSeconds > 0 && context.lastSubmittedAt !== undefined) {
84
+ const elapsedSeconds = (context.cutoffAt - context.lastSubmittedAt) / 1_000;
85
+ if (elapsedSeconds < profile.cooldownSeconds) {
86
+ return refuse("cooldown", "The cooldown after the last order has not elapsed", {
87
+ elapsedSeconds: round(elapsedSeconds),
88
+ cooldownSeconds: profile.cooldownSeconds
89
+ });
90
+ }
91
+ }
92
+ // 8. A legal contract count. The budget is capped by the side headroom so an entry
93
+ // cannot straddle the limit.
94
+ const sized = sizeEntry({
95
+ budgetUsdt: Math.min(budget, headroom),
96
+ price: context.price,
97
+ leverage: profile.leverage,
98
+ marginSafetyMultiplier: 1,
99
+ contract: context.contract
100
+ });
101
+ if (sized.contracts <= 0) {
102
+ return refuse("minimum_size", "The budget is too small for one legal contract lot", {
103
+ budgetUsdt: round(Math.min(budget, headroom)),
104
+ price: context.price,
105
+ minSize: context.contract.minSize,
106
+ lotSize: context.contract.lotSize,
107
+ reason: sized.refusedBecause ?? "below_minimum_size"
108
+ });
109
+ }
110
+ return {
111
+ allowed: true,
112
+ contracts: sized.contracts,
113
+ notionalUsdt: sized.notionalUsdt,
114
+ marginUsdt: sized.marginUsdt,
115
+ side
116
+ };
117
+ }
118
+ /** Contracts held on one side, as the exchange reports them. */
119
+ export function holdingQuantity(positions, side) {
120
+ let total = 0;
121
+ for (const position of positions) {
122
+ if (String(position.side ?? "") !== side)
123
+ continue;
124
+ const quantity = Number(position.quantity ?? 0);
125
+ if (Number.isFinite(quantity) && quantity > 0)
126
+ total += quantity;
127
+ }
128
+ return total;
129
+ }
130
+ function refuse(gate, message, details) {
131
+ return { allowed: false, refusal: { gate, message, details } };
132
+ }
133
+ function round(value) {
134
+ return Number(value.toFixed(6));
135
+ }
136
+ //# sourceMappingURL=gates.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gates.js","sourceRoot":"","sources":["../../src/live/gates.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,56 @@
1
+ import type { PythonStatus } from "../strategy/environment.js";
2
+ import { type RunnerDeps } from "./runner.js";
3
+ import type { LiveService } from "./service.js";
4
+ export interface LoopDeps extends RunnerDeps {
5
+ service: LiveService;
6
+ /** Resolves the interpreter and engine root, or reports why it cannot. */
7
+ python: () => Promise<PythonStatus>;
8
+ }
9
+ /**
10
+ * Drives live profiles off the confirmed-bar feed.
11
+ *
12
+ * One session per profile, started when the profile is enabled and torn down when it stops.
13
+ * A session is expensive to build — it warms hundreds of bars of indicator state — so it is
14
+ * kept alive across minutes rather than rebuilt per bar.
15
+ *
16
+ * Bars arrive through a coalescing queue, so a burst after a reconnect collapses to the
17
+ * newest cutoff. Older minutes are not evaluated at all: by the time a decision for one
18
+ * reached the exchange the market would have moved on, and the bar itself is already stored.
19
+ */
20
+ export declare class LiveLoop {
21
+ private readonly deps;
22
+ private readonly queue;
23
+ private readonly running;
24
+ private unsubscribe?;
25
+ private stopping;
26
+ constructor(deps: LoopDeps);
27
+ /** Begins listening for closed bars. Enabled profiles are attached as their bars arrive. */
28
+ start(): void;
29
+ /** Stops every session and detaches from the feed. */
30
+ stop(): Promise<void>;
31
+ /** Profiles with a live session, for status reporting. */
32
+ activeProfileIds(): string[];
33
+ /**
34
+ * Releases a profile's session.
35
+ *
36
+ * Called when a profile is disabled, so a stopped profile is not left holding a Python
37
+ * process. Safe to call for a profile that was never running.
38
+ */
39
+ release(profileId: string): Promise<void>;
40
+ /**
41
+ * Evaluates whatever is pending for one instrument.
42
+ *
43
+ * Serialised per instrument by the queue: two concurrent evaluations could each read the
44
+ * position before the other's order landed, and both would then open one.
45
+ */
46
+ private drain;
47
+ private evaluateProfiles;
48
+ private evaluateOne;
49
+ /**
50
+ * Returns the profile's session, starting and warming one if needed.
51
+ *
52
+ * Warm-up feeds history without evaluating it, so a session joining mid-stream does not
53
+ * emit decisions for minutes that have already passed.
54
+ */
55
+ private ensureSession;
56
+ }
@@ -0,0 +1,197 @@
1
+ import { appendRuntimeLifecycle } from "../runtime/state.js";
2
+ import { loadContractSpec } from "../strategy/instrument.js";
3
+ import { CutoffQueue } from "./cutoff-queue.js";
4
+ import { evaluateCutoff, warmSession } from "./runner.js";
5
+ import { PythonLiveSession } from "./session.js";
6
+ /**
7
+ * Drives live profiles off the confirmed-bar feed.
8
+ *
9
+ * One session per profile, started when the profile is enabled and torn down when it stops.
10
+ * A session is expensive to build — it warms hundreds of bars of indicator state — so it is
11
+ * kept alive across minutes rather than rebuilt per bar.
12
+ *
13
+ * Bars arrive through a coalescing queue, so a burst after a reconnect collapses to the
14
+ * newest cutoff. Older minutes are not evaluated at all: by the time a decision for one
15
+ * reached the exchange the market would have moved on, and the bar itself is already stored.
16
+ */
17
+ export class LiveLoop {
18
+ deps;
19
+ queue = new CutoffQueue();
20
+ running = new Map();
21
+ unsubscribe;
22
+ stopping = false;
23
+ constructor(deps) {
24
+ this.deps = deps;
25
+ }
26
+ /** Begins listening for closed bars. Enabled profiles are attached as their bars arrive. */
27
+ start() {
28
+ this.stopping = false;
29
+ this.unsubscribe = this.deps.bars.store.onConfirmed((instId, bar) => {
30
+ this.queue.offer(instId, bar);
31
+ // Dispatch is deliberately not awaited: this runs inside the websocket's write path,
32
+ // and blocking it would delay every other instrument's bar.
33
+ void this.drain(instId);
34
+ });
35
+ }
36
+ /** Stops every session and detaches from the feed. */
37
+ async stop() {
38
+ this.stopping = true;
39
+ this.unsubscribe?.();
40
+ this.unsubscribe = undefined;
41
+ const sessions = [...this.running.values()];
42
+ this.running.clear();
43
+ await Promise.all(sessions.map((entry) => entry.session.stop().catch(() => undefined)));
44
+ }
45
+ /** Profiles with a live session, for status reporting. */
46
+ activeProfileIds() {
47
+ return [...this.running.keys()];
48
+ }
49
+ /**
50
+ * Releases a profile's session.
51
+ *
52
+ * Called when a profile is disabled, so a stopped profile is not left holding a Python
53
+ * process. Safe to call for a profile that was never running.
54
+ */
55
+ async release(profileId) {
56
+ const entry = this.running.get(profileId);
57
+ if (!entry)
58
+ return;
59
+ this.running.delete(profileId);
60
+ this.queue.forget(entry.profile.instId);
61
+ await entry.session.stop().catch(() => undefined);
62
+ }
63
+ /**
64
+ * Evaluates whatever is pending for one instrument.
65
+ *
66
+ * Serialised per instrument by the queue: two concurrent evaluations could each read the
67
+ * position before the other's order landed, and both would then open one.
68
+ */
69
+ async drain(instId) {
70
+ if (this.stopping)
71
+ return;
72
+ const bar = this.queue.claim(instId);
73
+ if (!bar)
74
+ return;
75
+ try {
76
+ await this.evaluateProfiles(instId, bar);
77
+ }
78
+ finally {
79
+ this.queue.release(instId);
80
+ }
81
+ // A bar that arrived during the evaluation is still pending, so the newest cutoff is not
82
+ // left waiting for the next minute.
83
+ if (this.queue.pendingCutoff(instId) !== undefined)
84
+ void this.drain(instId);
85
+ }
86
+ async evaluateProfiles(instId, bar) {
87
+ const profiles = this.deps.store.listProfiles({ enabledOnly: true, instId });
88
+ for (const profile of profiles) {
89
+ // Re-read rather than trusting the list: an enable or stop may have landed since.
90
+ const current = this.deps.store.profile(profile.id);
91
+ if (!current?.enabled) {
92
+ await this.release(profile.id);
93
+ continue;
94
+ }
95
+ await this.evaluateOne(current, bar);
96
+ }
97
+ // A profile that was disabled while holding a session — by an operator or by the error
98
+ // streak — releases its process here rather than lingering until the runtime stops.
99
+ for (const profileId of this.activeProfileIds()) {
100
+ if (this.deps.store.profile(profileId)?.enabled !== true)
101
+ await this.release(profileId);
102
+ }
103
+ }
104
+ async evaluateOne(profile, bar) {
105
+ let result;
106
+ try {
107
+ const entry = await this.ensureSession(profile, bar);
108
+ // A generation change means the profile was restarted; its session holds state warmed
109
+ // under the previous run, so it is rebuilt rather than reused.
110
+ if (entry.generation !== profile.generation) {
111
+ await this.release(profile.id);
112
+ return;
113
+ }
114
+ result = await evaluateCutoff(this.deps, profile, entry.session, bar, entry.generation);
115
+ // Only a completed evaluation clears the streak. A blocked signal is a working risk
116
+ // gate, not a fault, so it counts as success here.
117
+ this.deps.service.recordSuccess(profile.id);
118
+ }
119
+ catch (error) {
120
+ const outcome = this.deps.service.recordFailure(profile.id, error);
121
+ appendRuntimeLifecycle("live-evaluation-failed", {
122
+ profileId: profile.id,
123
+ cutoffAt: bar.closeTimeMs,
124
+ kind: outcome.data.kind,
125
+ streak: outcome.data.streak,
126
+ disabled: outcome.data.disabled,
127
+ error: error instanceof Error ? error.message : String(error)
128
+ });
129
+ // A session whose process died, or a profile that just stopped itself, must not be
130
+ // reused on the next bar.
131
+ if (outcome.data.disabled || !this.running.get(profile.id)?.session.running) {
132
+ await this.release(profile.id);
133
+ }
134
+ return;
135
+ }
136
+ if (result.kind === "submitted") {
137
+ appendRuntimeLifecycle("live-order-submitted", {
138
+ profileId: profile.id,
139
+ cutoffAt: bar.closeTimeMs,
140
+ orderId: result.orderId,
141
+ contracts: result.contracts
142
+ });
143
+ }
144
+ }
145
+ /**
146
+ * Returns the profile's session, starting and warming one if needed.
147
+ *
148
+ * Warm-up feeds history without evaluating it, so a session joining mid-stream does not
149
+ * emit decisions for minutes that have already passed.
150
+ */
151
+ async ensureSession(profile, bar) {
152
+ const existing = this.running.get(profile.id);
153
+ if (existing?.session.running && existing.warmed)
154
+ return existing;
155
+ if (existing)
156
+ await this.release(profile.id);
157
+ const python = await this.deps.python();
158
+ if (!python.ready || !python.interpreter) {
159
+ throw new RuntimeErrorLike(`The Python environment is not ready: ${python.problems.join("; ") || "unknown reason"}`);
160
+ }
161
+ const source = this.deps.store.profileSource(profile.id);
162
+ if (!source)
163
+ throw new RuntimeErrorLike(`Profile ${profile.id} has no stored strategy source`);
164
+ // The exchange's real specification, not a placeholder: strategy code reads
165
+ // `contractValue` off its own position rows, so a stand-in would have it reasoning about
166
+ // a contract that does not exist.
167
+ const contract = await loadContractSpec(this.deps.market, profile.instId);
168
+ const session = new PythonLiveSession({
169
+ interpreter: python.interpreter,
170
+ engineRoot: python.engineRoot,
171
+ source,
172
+ instrument: {
173
+ instId: contract.instId,
174
+ contractValue: contract.contractValue,
175
+ contractMultiplier: contract.contractMultiplier,
176
+ lotSize: contract.lotSize,
177
+ minSize: contract.minSize,
178
+ tickSize: contract.tickSize
179
+ },
180
+ params: profile.params,
181
+ intervals: ["1m"]
182
+ });
183
+ await session.start();
184
+ const entry = { profile, session, generation: profile.generation, warmed: false };
185
+ this.running.set(profile.id, entry);
186
+ // Warm up to the bar *before* this one: the triggering minute is the first thing
187
+ // evaluated, not part of the history.
188
+ await warmSession(this.deps, profile, session, bar.openTimeMs - 60_000);
189
+ entry.warmed = true;
190
+ appendRuntimeLifecycle("live-session-started", { profileId: profile.id, instId: profile.instId });
191
+ return entry;
192
+ }
193
+ }
194
+ /** Local error type, so the loop does not depend on the error module's shape. */
195
+ class RuntimeErrorLike extends Error {
196
+ }
197
+ //# sourceMappingURL=loop.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loop.js","sourceRoot":"","sources":["../../src/live/loop.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,48 @@
1
+ import type { LiveEnvironment, LiveProfile } from "./types.js";
2
+ /**
3
+ * What must be true before a profile is allowed to trade.
4
+ *
5
+ * These are checked once, at enable time, and every one of them is a fact about the system
6
+ * that can be verified: an environment that is ready, credentials that work, a contract the
7
+ * exchange will describe, a venue that matches.
8
+ *
9
+ * One is deliberate friction rather than a technical check. A profile cannot be enabled until
10
+ * its exact source has completed a backtest on its exact instrument, which blocks "wrote it,
11
+ * shipped it" — using real money for the first test. The bar is that a backtest *ran*, not
12
+ * that it did well: a losing run still proves the strategy executes and that someone has seen
13
+ * how it behaves.
14
+ *
15
+ * Deciding *whether* to commit real money is not on this list, because it is not a fact about
16
+ * the system — it is the operator's call. An agent is required to ask before enabling a
17
+ * real-money profile, and that belongs in the agent's instructions where it can be stated
18
+ * plainly, not in a schema check that only rewards guessing a string.
19
+ */
20
+ export type PreconditionCode = "no_backtest" | "python_missing" | "account_unverified" | "no_trade_permission" | "contract_unreadable" | "environment_mismatch";
21
+ export interface PreconditionFailure {
22
+ code: PreconditionCode;
23
+ message: string;
24
+ details?: Record<string, unknown>;
25
+ }
26
+ export interface PreconditionInput {
27
+ profile: LiveProfile;
28
+ /** A completed backtest of this source on this instrument, if one exists. */
29
+ backtestRunId: string | null;
30
+ pythonReady: boolean;
31
+ pythonProblems: string[];
32
+ /** Whether the account's credentials were accepted by the exchange. */
33
+ accountVerified: boolean;
34
+ /** Permission string as OKX reports it, or null when it did not say. */
35
+ accountPermissions: string | null;
36
+ contractReadable: boolean;
37
+ /** Which venue market data is streaming from. */
38
+ marketEnvironment: LiveEnvironment;
39
+ }
40
+ /**
41
+ * Returns every unmet precondition, in the order they are worth fixing.
42
+ *
43
+ * All of them are collected rather than stopping at the first: someone about to trade
44
+ * real money should see the full list once, not discover a new blocker on each attempt.
45
+ */
46
+ export declare function checkPreconditions(input: PreconditionInput): PreconditionFailure[];
47
+ /** Throws with every unmet precondition, or returns cleanly. */
48
+ export declare function assertPreconditions(input: PreconditionInput): void;
@@ -0,0 +1,69 @@
1
+ import { RuntimeError } from "../core/errors.js";
2
+ /**
3
+ * Returns every unmet precondition, in the order they are worth fixing.
4
+ *
5
+ * All of them are collected rather than stopping at the first: someone about to trade
6
+ * real money should see the full list once, not discover a new blocker on each attempt.
7
+ */
8
+ export function checkPreconditions(input) {
9
+ const failures = [];
10
+ const { profile } = input;
11
+ if (!input.backtestRunId) {
12
+ failures.push({
13
+ code: "no_backtest",
14
+ message: `No completed backtest of this strategy on ${profile.instId}. Run one first: there is otherwise no evidence the strategy works, or even that it runs.`,
15
+ details: { instId: profile.instId, strategyHash: profile.strategyHash }
16
+ });
17
+ }
18
+ if (!input.pythonReady) {
19
+ failures.push({
20
+ code: "python_missing",
21
+ message: "The local Python environment is not ready, so the first closed minute would fail. Run 'desic-okx strategy env --setup'.",
22
+ details: { problems: input.pythonProblems }
23
+ });
24
+ }
25
+ if (!input.accountVerified) {
26
+ failures.push({
27
+ code: "account_unverified",
28
+ message: `OKX did not accept the credentials for account '${profile.account}'.`,
29
+ details: { account: profile.account }
30
+ });
31
+ }
32
+ else if (input.accountPermissions !== null && !input.accountPermissions.toLowerCase().includes("trade")) {
33
+ // Checked only once the credentials work, since an unverified account has no
34
+ // permission string worth reporting.
35
+ failures.push({
36
+ code: "no_trade_permission",
37
+ message: `The API key for '${profile.account}' has no trade permission, so every order would be rejected.`,
38
+ details: { account: profile.account, permissions: input.accountPermissions }
39
+ });
40
+ }
41
+ if (!input.contractReadable) {
42
+ failures.push({
43
+ code: "contract_unreadable",
44
+ message: `OKX did not report a usable contract specification for ${profile.instId}, so no order size can be derived.`,
45
+ details: { instId: profile.instId }
46
+ });
47
+ }
48
+ // A profile trading on one venue while bars arrive from the other would decide on
49
+ // prices that never applied to its own market. The environment split makes the two
50
+ // genuinely different, so this has to be refused rather than warned about.
51
+ if (input.marketEnvironment !== profile.environment) {
52
+ failures.push({
53
+ code: "environment_mismatch",
54
+ message: `This profile trades on ${profile.environment} while market data is streaming from ${input.marketEnvironment}. Switch with 'desic-okx market env ${profile.environment}' and restart.`,
55
+ details: { profileEnvironment: profile.environment, marketEnvironment: input.marketEnvironment }
56
+ });
57
+ }
58
+ return failures;
59
+ }
60
+ /** Throws with every unmet precondition, or returns cleanly. */
61
+ export function assertPreconditions(input) {
62
+ const failures = checkPreconditions(input);
63
+ if (failures.length === 0)
64
+ return;
65
+ throw new RuntimeError("VALIDATION", `${failures.length} precondition(s) block enabling '${input.profile.name}': ${failures
66
+ .map((failure) => failure.message)
67
+ .join(" ")}`, false, { failures });
68
+ }
69
+ //# sourceMappingURL=preconditions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"preconditions.js","sourceRoot":"","sources":["../../src/live/preconditions.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,46 @@
1
+ import type { AccountService } from "../account/service.js";
2
+ import type { OkxClient } from "../core/okx-client.js";
3
+ import type { LiveStore } from "./store.js";
4
+ /**
5
+ * Resolves signals whose outcome was unknown when the process died.
6
+ *
7
+ * Two states are unresolved, and neither may be assumed:
8
+ *
9
+ * `submitted` means an order was sent but its terminal result was never recorded. It may
10
+ * have filled.
11
+ *
12
+ * `reserved` means a minute was claimed and the worker vanished before deciding. Usually
13
+ * nothing was sent — but "usually" is not a basis for trading, because the crash could
14
+ * equally have landed between the write and the response.
15
+ *
16
+ * So both are checked against the exchange rather than guessed at. Nothing is ever
17
+ * resubmitted: re-sending an order that may already exist is the one mistake worse than
18
+ * missing one. A signal that cannot be resolved is marked `ambiguous` and waits for a
19
+ * person.
20
+ */
21
+ export interface ReconcileOutcome {
22
+ /** Signals whose remote state was found and recorded. */
23
+ resolved: number;
24
+ /** Signals confirmed to have no order at the exchange. */
25
+ absent: number;
26
+ /** Signals left for a person to inspect. */
27
+ ambiguous: number;
28
+ details: Array<{
29
+ signalId: string;
30
+ profileId: string;
31
+ cutoffAt: number;
32
+ result: string;
33
+ }>;
34
+ }
35
+ export interface Reconciler {
36
+ client: Pick<OkxClient, "privateGet">;
37
+ accounts: Pick<AccountService, "account">;
38
+ store: LiveStore;
39
+ }
40
+ /**
41
+ * Checks every unresolved signal against the exchange.
42
+ *
43
+ * Called at startup, after the executions table has had its own chance to recover: the
44
+ * two live in separate tables and a signal's execution record may still be settling.
45
+ */
46
+ export declare function reconcileSignals(deps: Reconciler): Promise<ReconcileOutcome>;