desic-okx-agent 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (236) hide show
  1. package/README.en.md +90 -10
  2. package/README.md +76 -10
  3. package/dist/account/private-websocket.js +4 -4
  4. package/dist/account/private-websocket.js.map +1 -1
  5. package/dist/account/service.d.ts +12 -1
  6. package/dist/account/service.js +18 -0
  7. package/dist/account/service.js.map +1 -1
  8. package/dist/bars/rate-limiter.d.ts +18 -0
  9. package/dist/bars/rate-limiter.js +84 -0
  10. package/dist/bars/rate-limiter.js.map +1 -0
  11. package/dist/bars/schema.d.ts +36 -0
  12. package/dist/bars/schema.js +134 -0
  13. package/dist/bars/schema.js.map +1 -0
  14. package/dist/bars/service.d.ts +60 -0
  15. package/dist/bars/service.js +120 -0
  16. package/dist/bars/service.js.map +1 -0
  17. package/dist/bars/store.d.ts +105 -0
  18. package/dist/bars/store.js +415 -0
  19. package/dist/bars/store.js.map +1 -0
  20. package/dist/bars/timeframe.d.ts +40 -0
  21. package/dist/bars/timeframe.js +146 -0
  22. package/dist/bars/timeframe.js.map +1 -0
  23. package/dist/bars/types.d.ts +68 -0
  24. package/dist/bars/types.js +13 -0
  25. package/dist/bars/types.js.map +1 -0
  26. package/dist/cli/data-render.d.ts +37 -0
  27. package/dist/cli/data-render.js +143 -0
  28. package/dist/cli/data-render.js.map +1 -0
  29. package/dist/cli/doctor.js +7 -3
  30. package/dist/cli/doctor.js.map +1 -1
  31. package/dist/cli/index.js +757 -26
  32. package/dist/cli/index.js.map +1 -1
  33. package/dist/cli/live-render.d.ts +24 -0
  34. package/dist/cli/live-render.js +85 -0
  35. package/dist/cli/live-render.js.map +1 -0
  36. package/dist/cli/range.d.ts +28 -0
  37. package/dist/cli/range.js +63 -0
  38. package/dist/cli/range.js.map +1 -0
  39. package/dist/cli/render.js +3 -0
  40. package/dist/cli/render.js.map +1 -1
  41. package/dist/cli/strategy-render.d.ts +36 -0
  42. package/dist/cli/strategy-render.js +391 -0
  43. package/dist/cli/strategy-render.js.map +1 -0
  44. package/dist/cli/width.d.ts +18 -0
  45. package/dist/cli/width.js +71 -0
  46. package/dist/cli/width.js.map +1 -0
  47. package/dist/config/loader.js +1 -1
  48. package/dist/config/schema.d.ts +7 -0
  49. package/dist/config/schema.js +24 -0
  50. package/dist/config/schema.js.map +1 -1
  51. package/dist/core/okx-client.d.ts +9 -1
  52. package/dist/core/okx-client.js +14 -5
  53. package/dist/core/okx-client.js.map +1 -1
  54. package/dist/i18n/locale.d.ts +24 -0
  55. package/dist/i18n/locale.js +65 -0
  56. package/dist/i18n/locale.js.map +1 -0
  57. package/dist/i18n/messages.d.ts +333 -0
  58. package/dist/i18n/messages.js +660 -0
  59. package/dist/i18n/messages.js.map +1 -0
  60. package/dist/live/account-snapshot.d.ts +30 -0
  61. package/dist/live/account-snapshot.js +130 -0
  62. package/dist/live/account-snapshot.js.map +1 -0
  63. package/dist/live/cutoff-queue.d.ts +42 -0
  64. package/dist/live/cutoff-queue.js +69 -0
  65. package/dist/live/cutoff-queue.js.map +1 -0
  66. package/dist/live/execution-key.d.ts +23 -0
  67. package/dist/live/execution-key.js +31 -0
  68. package/dist/live/execution-key.js.map +1 -0
  69. package/dist/live/failures.d.ts +37 -0
  70. package/dist/live/failures.js +57 -0
  71. package/dist/live/failures.js.map +1 -0
  72. package/dist/live/gates.d.ts +65 -0
  73. package/dist/live/gates.js +136 -0
  74. package/dist/live/gates.js.map +1 -0
  75. package/dist/live/loop.d.ts +56 -0
  76. package/dist/live/loop.js +197 -0
  77. package/dist/live/loop.js.map +1 -0
  78. package/dist/live/preconditions.d.ts +48 -0
  79. package/dist/live/preconditions.js +69 -0
  80. package/dist/live/preconditions.js.map +1 -0
  81. package/dist/live/reconcile.d.ts +46 -0
  82. package/dist/live/reconcile.js +104 -0
  83. package/dist/live/reconcile.js.map +1 -0
  84. package/dist/live/runner.d.ts +57 -0
  85. package/dist/live/runner.js +160 -0
  86. package/dist/live/runner.js.map +1 -0
  87. package/dist/live/schema.d.ts +18 -0
  88. package/dist/live/schema.js +91 -0
  89. package/dist/live/schema.js.map +1 -0
  90. package/dist/live/service.d.ts +144 -0
  91. package/dist/live/service.js +303 -0
  92. package/dist/live/service.js.map +1 -0
  93. package/dist/live/session.d.ts +85 -0
  94. package/dist/live/session.js +234 -0
  95. package/dist/live/session.js.map +1 -0
  96. package/dist/live/sizing.d.ts +62 -0
  97. package/dist/live/sizing.js +79 -0
  98. package/dist/live/sizing.js.map +1 -0
  99. package/dist/live/store.d.ts +123 -0
  100. package/dist/live/store.js +350 -0
  101. package/dist/live/store.js.map +1 -0
  102. package/dist/live/types.d.ts +82 -0
  103. package/dist/live/types.js +2 -0
  104. package/dist/live/types.js.map +1 -0
  105. package/dist/market/websocket.d.ts +16 -1
  106. package/dist/market/websocket.js +60 -5
  107. package/dist/market/websocket.js.map +1 -1
  108. package/dist/mcp/server.d.ts +1 -0
  109. package/dist/mcp/server.js +15 -1
  110. package/dist/mcp/server.js.map +1 -1
  111. package/dist/network/connectivity.d.ts +9 -1
  112. package/dist/network/connectivity.js +28 -1
  113. package/dist/network/connectivity.js.map +1 -1
  114. package/dist/report/chart-script.d.ts +12 -0
  115. package/dist/report/chart-script.js +146 -0
  116. package/dist/report/chart-script.js.map +1 -0
  117. package/dist/report/compare-html.d.ts +8 -0
  118. package/dist/report/compare-html.js +254 -0
  119. package/dist/report/compare-html.js.map +1 -0
  120. package/dist/report/compare-script.d.ts +12 -0
  121. package/dist/report/compare-script.js +109 -0
  122. package/dist/report/compare-script.js.map +1 -0
  123. package/dist/report/compare.d.ts +61 -0
  124. package/dist/report/compare.js +205 -0
  125. package/dist/report/compare.js.map +1 -0
  126. package/dist/report/fetch.d.ts +20 -0
  127. package/dist/report/fetch.js +56 -0
  128. package/dist/report/fetch.js.map +1 -0
  129. package/dist/report/html.d.ts +54 -0
  130. package/dist/report/html.js +641 -0
  131. package/dist/report/html.js.map +1 -0
  132. package/dist/report/open.d.ts +42 -0
  133. package/dist/report/open.js +114 -0
  134. package/dist/report/open.js.map +1 -0
  135. package/dist/runtime/server.d.ts +16 -1
  136. package/dist/runtime/server.js +112 -9
  137. package/dist/runtime/server.js.map +1 -1
  138. package/dist/setup/installer.d.ts +1 -0
  139. package/dist/setup/installer.js +8 -0
  140. package/dist/setup/installer.js.map +1 -1
  141. package/dist/setup/wizard.js +19 -22
  142. package/dist/setup/wizard.js.map +1 -1
  143. package/dist/strategy/constants.d.ts +23 -0
  144. package/dist/strategy/constants.js +24 -0
  145. package/dist/strategy/constants.js.map +1 -0
  146. package/dist/strategy/environment.d.ts +52 -0
  147. package/dist/strategy/environment.js +187 -0
  148. package/dist/strategy/environment.js.map +1 -0
  149. package/dist/strategy/instrument.d.ts +29 -0
  150. package/dist/strategy/instrument.js +39 -0
  151. package/dist/strategy/instrument.js.map +1 -0
  152. package/dist/strategy/optimize.d.ts +73 -0
  153. package/dist/strategy/optimize.js +113 -0
  154. package/dist/strategy/optimize.js.map +1 -0
  155. package/dist/strategy/parameter-space.d.ts +59 -0
  156. package/dist/strategy/parameter-space.js +221 -0
  157. package/dist/strategy/parameter-space.js.map +1 -0
  158. package/dist/strategy/python-bridge.d.ts +24 -0
  159. package/dist/strategy/python-bridge.js +114 -0
  160. package/dist/strategy/python-bridge.js.map +1 -0
  161. package/dist/strategy/schema.d.ts +9 -0
  162. package/dist/strategy/schema.js +91 -0
  163. package/dist/strategy/schema.js.map +1 -0
  164. package/dist/strategy/service.d.ts +138 -0
  165. package/dist/strategy/service.js +745 -0
  166. package/dist/strategy/service.js.map +1 -0
  167. package/dist/strategy/settings.d.ts +162 -0
  168. package/dist/strategy/settings.js +243 -0
  169. package/dist/strategy/settings.js.map +1 -0
  170. package/dist/strategy/store.d.ts +96 -0
  171. package/dist/strategy/store.js +367 -0
  172. package/dist/strategy/store.js.map +1 -0
  173. package/dist/strategy/templates.d.ts +11 -0
  174. package/dist/strategy/templates.js +134 -0
  175. package/dist/strategy/templates.js.map +1 -0
  176. package/dist/strategy/types.d.ts +111 -0
  177. package/dist/strategy/types.js +2 -0
  178. package/dist/strategy/types.js.map +1 -0
  179. package/dist/tools/catalog.d.ts +16 -0
  180. package/dist/tools/catalog.js +118 -17
  181. package/dist/tools/catalog.js.map +1 -1
  182. package/dist/trade/service.d.ts +12 -0
  183. package/dist/trade/service.js +24 -7
  184. package/dist/trade/service.js.map +1 -1
  185. package/dist/tui/app.d.ts +23 -0
  186. package/dist/tui/app.js +322 -0
  187. package/dist/tui/app.js.map +1 -0
  188. package/dist/tui/commands.d.ts +70 -0
  189. package/dist/tui/commands.js +313 -0
  190. package/dist/tui/commands.js.map +1 -0
  191. package/dist/tui/entries.d.ts +17 -0
  192. package/dist/tui/entries.js +24 -0
  193. package/dist/tui/entries.js.map +1 -0
  194. package/dist/tui/execute.d.ts +26 -0
  195. package/dist/tui/execute.js +664 -0
  196. package/dist/tui/execute.js.map +1 -0
  197. package/dist/tui/history.d.ts +17 -0
  198. package/dist/tui/history.js +48 -0
  199. package/dist/tui/history.js.map +1 -0
  200. package/dist/tui/index.d.ts +8 -0
  201. package/dist/tui/index.js +48 -0
  202. package/dist/tui/index.js.map +1 -0
  203. package/dist/tui/line-editor.d.ts +44 -0
  204. package/dist/tui/line-editor.js +98 -0
  205. package/dist/tui/line-editor.js.map +1 -0
  206. package/dist/tui/progress.d.ts +23 -0
  207. package/dist/tui/progress.js +46 -0
  208. package/dist/tui/progress.js.map +1 -0
  209. package/dist/tui/settings-editor.d.ts +18 -0
  210. package/dist/tui/settings-editor.js +115 -0
  211. package/dist/tui/settings-editor.js.map +1 -0
  212. package/docs/live-trading.md +455 -0
  213. package/docs/strategy-research.md +597 -0
  214. package/package.json +10 -1
  215. package/python/desic_strategy/__init__.py +34 -0
  216. package/python/desic_strategy/actions.py +158 -0
  217. package/python/desic_strategy/context.py +164 -0
  218. package/python/desic_strategy/engine.py +614 -0
  219. package/python/desic_strategy/indicators.py +159 -0
  220. package/python/desic_strategy/live.py +253 -0
  221. package/python/desic_strategy/policy.py +193 -0
  222. package/python/desic_strategy/portfolio.py +152 -0
  223. package/python/desic_strategy/report.py +319 -0
  224. package/python/desic_strategy/runner.py +574 -0
  225. package/python/desic_strategy/timeframe.py +150 -0
  226. package/python/main.py +18 -0
  227. package/skills/okx-live-trading/SKILL.md +117 -0
  228. package/skills/okx-live-trading/agents/openai.yaml +9 -0
  229. package/skills/okx-live-trading/references/lifecycle.md +128 -0
  230. package/skills/okx-strategy-research/SKILL.md +113 -0
  231. package/skills/okx-strategy-research/agents/openai.yaml +9 -0
  232. package/skills/okx-strategy-research/references/execution-semantics.md +107 -0
  233. package/skills/okx-strategy-research/references/field-traps.md +142 -0
  234. package/skills/okx-strategy-research/references/python-api.md +121 -0
  235. package/skills/okx-strategy-research/references/tools-and-data.md +192 -0
  236. package/skills/okx-trading/SKILL.md +11 -10
@@ -0,0 +1,104 @@
1
+ import { liveExecutionKey, liveClientOrderId } from "./execution-key.js";
2
+ /**
3
+ * Checks every unresolved signal against the exchange.
4
+ *
5
+ * Called at startup, after the executions table has had its own chance to recover: the
6
+ * two live in separate tables and a signal's execution record may still be settling.
7
+ */
8
+ export async function reconcileSignals(deps) {
9
+ const outcome = { resolved: 0, absent: 0, ambiguous: 0, details: [] };
10
+ const unresolved = deps.store.unresolvedSignals();
11
+ if (unresolved.length === 0)
12
+ return outcome;
13
+ // Profiles are read once each: a stalled worker usually leaves a run of consecutive
14
+ // minutes from the same profile.
15
+ const profiles = new Map();
16
+ for (const signal of unresolved) {
17
+ if (!profiles.has(signal.profileId))
18
+ profiles.set(signal.profileId, deps.store.profile(signal.profileId));
19
+ const profile = profiles.get(signal.profileId);
20
+ if (!profile) {
21
+ // The profile is gone, so there is nothing left to trade against and no account to
22
+ // ask. Recording it as ambiguous keeps the row honest rather than silently closing
23
+ // a question nobody can answer.
24
+ deps.store.settleAmbiguous(signal.id, "The profile no longer exists, so this signal cannot be reconciled");
25
+ outcome.ambiguous += 1;
26
+ outcome.details.push({ ...locator(signal), result: "profile_missing" });
27
+ continue;
28
+ }
29
+ const record = await lookup(deps, profile, signal);
30
+ if (record === "unavailable") {
31
+ deps.store.settleAmbiguous(signal.id, "The exchange could not be reached to determine this order's fate");
32
+ outcome.ambiguous += 1;
33
+ outcome.details.push({ ...locator(signal), result: "unreachable" });
34
+ continue;
35
+ }
36
+ if (record === undefined) {
37
+ // The exchange has no such order. For a `reserved` signal that is the expected
38
+ // answer: the worker died before sending anything.
39
+ deps.store.settleNoAction(signal.id, "No order reached the exchange before the runtime stopped");
40
+ outcome.absent += 1;
41
+ outcome.details.push({ ...locator(signal), result: "no_order" });
42
+ continue;
43
+ }
44
+ const state = String(record.state ?? "");
45
+ const orderId = record.ordId === undefined || record.ordId === null ? null : String(record.ordId);
46
+ if (TERMINAL_STATES.has(state)) {
47
+ deps.store.settleSubmitted(signal.id, {
48
+ action: signal.action === "reserved" ? "recovered" : signal.action,
49
+ reason: signal.reason === "reserved" ? "recovered at startup" : signal.reason,
50
+ quantity: Number(record.sz ?? signal.quantity ?? 0),
51
+ orderId,
52
+ executionKey: signal.executionKey ?? liveExecutionKey(profile.id, signal.cutoffAt),
53
+ details: { reconciled: true, state, filledSize: record.accFillSz ?? null, averagePrice: record.avgPx ?? null }
54
+ });
55
+ outcome.resolved += 1;
56
+ outcome.details.push({ ...locator(signal), result: state });
57
+ continue;
58
+ }
59
+ // A live or partially filled order is a real position the strategy does not know
60
+ // about. It is not resubmitted and not closed automatically — deciding for someone
61
+ // what to do with an open position they did not see is worse than telling them.
62
+ deps.store.settleAmbiguous(signal.id, `An order from before the restart is still ${state || "open"} at the exchange (${orderId ?? "unknown id"}); inspect it before enabling this profile again`);
63
+ outcome.ambiguous += 1;
64
+ outcome.details.push({ ...locator(signal), result: state || "open" });
65
+ }
66
+ return outcome;
67
+ }
68
+ /** Order states that need no further action. */
69
+ const TERMINAL_STATES = new Set(["filled", "canceled", "cancelled"]);
70
+ /**
71
+ * Asks the exchange about one signal's order.
72
+ *
73
+ * Returns the order, `undefined` when the exchange says there is none, or
74
+ * `"unavailable"` when the question could not be asked at all. The difference matters:
75
+ * "no order exists" is an answer, while "could not reach OKX" is not, and treating the
76
+ * second as the first would quietly discard a signal that may have filled.
77
+ */
78
+ async function lookup(deps, profile, signal) {
79
+ const executionKey = signal.executionKey ?? liveExecutionKey(profile.id, signal.cutoffAt);
80
+ try {
81
+ const account = deps.accounts.account(profile.account);
82
+ const orders = await deps.client.privateGet("/api/v5/trade/order", account, {
83
+ instId: profile.instId,
84
+ clOrdId: liveClientOrderId(executionKey)
85
+ });
86
+ return orders[0];
87
+ }
88
+ catch (error) {
89
+ // OKX answers "order does not exist" as an error, which is a definitive answer
90
+ // rather than a failure to ask.
91
+ if (isOrderNotFound(error))
92
+ return undefined;
93
+ return "unavailable";
94
+ }
95
+ }
96
+ /** OKX reports an unknown `clOrdId` as error 51603. */
97
+ function isOrderNotFound(error) {
98
+ const message = error instanceof Error ? error.message : String(error);
99
+ return message.includes("51603") || /order does not exist/i.test(message);
100
+ }
101
+ function locator(signal) {
102
+ return { signalId: signal.id, profileId: signal.profileId, cutoffAt: signal.cutoffAt };
103
+ }
104
+ //# sourceMappingURL=reconcile.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reconcile.js","sourceRoot":"","sources":["../../src/live/reconcile.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,57 @@
1
+ import type { AccountService } from "../account/service.js";
2
+ import type { BarService } from "../bars/service.js";
3
+ import type { MarketService } from "../market/service.js";
4
+ import type { TradeService } from "../trade/service.js";
5
+ import type { Bar } from "../bars/types.js";
6
+ import { type ContractSpec } from "../strategy/instrument.js";
7
+ import type { PythonLiveSession } from "./session.js";
8
+ import type { LiveStore } from "./store.js";
9
+ import type { LiveProfile } from "./types.js";
10
+ /** Bars of history fed to a session before its first evaluation. */
11
+ export declare const WARMUP_BARS = 300;
12
+ export interface RunnerDeps {
13
+ store: LiveStore;
14
+ bars: BarService;
15
+ market: MarketService;
16
+ accounts: AccountService;
17
+ trade: TradeService;
18
+ }
19
+ export type EvaluationResult = {
20
+ kind: "skipped";
21
+ reason: string;
22
+ } | {
23
+ kind: "no_action";
24
+ reason: string;
25
+ } | {
26
+ kind: "blocked";
27
+ gate: string;
28
+ reason: string;
29
+ } | {
30
+ kind: "submitted";
31
+ orderId: string | null;
32
+ contracts: number;
33
+ };
34
+ /**
35
+ * Turns one closed minute into an order, or into a recorded reason there was none.
36
+ *
37
+ * The order of operations is the safety property, not an implementation detail:
38
+ *
39
+ * 1. Claim the minute first. The unique constraint can only prevent a duplicate order if
40
+ * the row exists before the order is sent.
41
+ * 2. Verify the window is complete. A gap means deciding against price action that never
42
+ * happened, so the minute is skipped rather than evaluated.
43
+ * 3. Read the account from the exchange, not from a local tally.
44
+ * 4. Ask the strategy for an intent, which carries no size.
45
+ * 5. Size it and run the gates.
46
+ * 6. Re-read the generation immediately before submitting, so a stop that landed during
47
+ * the evaluation still blocks the order.
48
+ */
49
+ export declare function evaluateCutoff(deps: RunnerDeps, profile: LiveProfile, session: PythonLiveSession, bar: Bar, generation: number): Promise<EvaluationResult>;
50
+ /**
51
+ * Prepares a session for live evaluation.
52
+ *
53
+ * The warm-up is history only and produces no decisions: running the strategy over past
54
+ * minutes would emit orders for minutes that have already gone.
55
+ */
56
+ export declare function warmSession(deps: RunnerDeps, profile: LiveProfile, session: PythonLiveSession, latestOpen: number): Promise<number>;
57
+ export type { ContractSpec };
@@ -0,0 +1,160 @@
1
+ import { RuntimeError } from "../core/errors.js";
2
+ import { ONE_MINUTE_MS } from "../bars/types.js";
3
+ import { loadContractSpec } from "../strategy/instrument.js";
4
+ import { readAccountSnapshot } from "./account-snapshot.js";
5
+ import { liveExecutionKey } from "./execution-key.js";
6
+ import { evaluateGates, isClosing, sideOf } from "./gates.js";
7
+ /** Bars of history fed to a session before its first evaluation. */
8
+ export const WARMUP_BARS = 300;
9
+ /**
10
+ * Turns one closed minute into an order, or into a recorded reason there was none.
11
+ *
12
+ * The order of operations is the safety property, not an implementation detail:
13
+ *
14
+ * 1. Claim the minute first. The unique constraint can only prevent a duplicate order if
15
+ * the row exists before the order is sent.
16
+ * 2. Verify the window is complete. A gap means deciding against price action that never
17
+ * happened, so the minute is skipped rather than evaluated.
18
+ * 3. Read the account from the exchange, not from a local tally.
19
+ * 4. Ask the strategy for an intent, which carries no size.
20
+ * 5. Size it and run the gates.
21
+ * 6. Re-read the generation immediately before submitting, so a stop that landed during
22
+ * the evaluation still blocks the order.
23
+ */
24
+ export async function evaluateCutoff(deps, profile, session, bar, generation) {
25
+ const cutoffAt = bar.closeTimeMs;
26
+ // 1. Claim before anything else can act.
27
+ const signalId = deps.store.reserveSignal(profile.id, cutoffAt);
28
+ if (!signalId) {
29
+ return { kind: "skipped", reason: `Minute ${cutoffAt} was already evaluated for this profile` };
30
+ }
31
+ try {
32
+ // 2. The window has to be intact up to and including this bar.
33
+ const window = deps.bars.store.loadWindow(profile.instId, bar.openTimeMs - ONE_MINUTE_MS, bar.openTimeMs);
34
+ const latest = window.at(-1);
35
+ if (!latest || latest.openTimeMs !== bar.openTimeMs) {
36
+ throw new RuntimeError("STALE_DATA", `Local history does not reach ${bar.openTimeMs}`, true);
37
+ }
38
+ const contract = await loadContractSpec(deps.market, profile.instId);
39
+ const markPrice = Number(bar.close);
40
+ // 3. Account state as the exchange reports it.
41
+ const snapshot = await readAccountSnapshot(deps.accounts, profile.account, profile.instId, contract, markPrice, cutoffAt);
42
+ // 4. The strategy's intent, with no size attached.
43
+ const decision = await session.evaluate(bar, snapshot.portfolio);
44
+ if (decision.action === "no_action") {
45
+ deps.store.settleNoAction(signalId, decision.reason);
46
+ return { kind: "no_action", reason: decision.reason };
47
+ }
48
+ // 5. Size and gate. Every refusal is recorded with the gate that produced it.
49
+ const outcome = evaluateGates({
50
+ profile,
51
+ generation,
52
+ currentGeneration: deps.store.requireProfile(profile.id).generation,
53
+ enabledNow: deps.store.requireProfile(profile.id).enabled,
54
+ action: decision.action,
55
+ contract,
56
+ price: markPrice,
57
+ positions: snapshot.positions,
58
+ availableMarginUsdt: snapshot.portfolio.availableMarginUsdt,
59
+ realizedLossUsdtToday: snapshot.realizedLossUsdtToday,
60
+ cutoffAt,
61
+ ...(deps.store.lastSubmittedAt(profile.id) === undefined
62
+ ? {}
63
+ : { lastSubmittedAt: deps.store.lastSubmittedAt(profile.id) })
64
+ });
65
+ if (!outcome.allowed) {
66
+ deps.store.settleBlocked(signalId, {
67
+ action: decision.action,
68
+ reason: decision.reason,
69
+ blockedBy: outcome.refusal.gate,
70
+ details: outcome.refusal.details
71
+ });
72
+ return { kind: "blocked", gate: outcome.refusal.gate, reason: outcome.refusal.message };
73
+ }
74
+ // 6. Last look at the generation. Between the gate check and here the strategy has not
75
+ // run, but an operator may have pressed stop, and this is the final chance to honour
76
+ // it before the order leaves.
77
+ const current = deps.store.requireProfile(profile.id);
78
+ if (!current.enabled || current.generation !== generation) {
79
+ deps.store.settleBlocked(signalId, {
80
+ action: decision.action,
81
+ reason: decision.reason,
82
+ blockedBy: "generation",
83
+ details: { decidedUnder: generation, currentGeneration: current.generation, enabled: current.enabled }
84
+ });
85
+ return { kind: "blocked", gate: "generation", reason: "The profile was stopped during this evaluation" };
86
+ }
87
+ const executionKey = liveExecutionKey(profile.id, cutoffAt);
88
+ const placed = await deps.trade.placeOrder({
89
+ account: profile.account,
90
+ executionKey,
91
+ instId: profile.instId,
92
+ tdMode: profile.marginMode,
93
+ side: orderSide(decision.action),
94
+ posSide: sideOf(decision.action),
95
+ ordType: "market",
96
+ size: String(outcome.contracts),
97
+ // The snapshot's eight components cannot agree within a second on a live feed —
98
+ // funding rate moves every eight hours — and that skew says nothing about whether this
99
+ // order can fill. Every executability check still applies, and the skew is recorded as
100
+ // a warning rather than silently dropped.
101
+ executabilityOnly: true,
102
+ // A closing order is reduce-only, so a late-arriving fill can never open the opposite
103
+ // side instead of flattening.
104
+ ...(isClosing(decision.action) ? { reduceOnly: true } : {})
105
+ });
106
+ const orderId = readOrderId(placed.data);
107
+ deps.store.settleSubmitted(signalId, {
108
+ action: decision.action,
109
+ reason: decision.reason,
110
+ quantity: outcome.contracts,
111
+ orderId,
112
+ executionKey,
113
+ details: {
114
+ notionalUsdt: outcome.notionalUsdt,
115
+ marginUsdt: outcome.marginUsdt,
116
+ markPrice,
117
+ ...(decision.protection ? { protection: decision.protection } : {})
118
+ }
119
+ });
120
+ return { kind: "submitted", orderId, contracts: outcome.contracts };
121
+ }
122
+ catch (error) {
123
+ const message = error instanceof Error ? error.message : String(error);
124
+ // An ambiguous write is recorded as ambiguous rather than an error: the order may exist,
125
+ // and that distinction is what recovery reads.
126
+ if (error instanceof RuntimeError && error.code === "AMBIGUOUS_WRITE") {
127
+ deps.store.settleAmbiguous(signalId, message);
128
+ }
129
+ else {
130
+ deps.store.settleError(signalId, message);
131
+ }
132
+ throw error;
133
+ }
134
+ }
135
+ /**
136
+ * Prepares a session for live evaluation.
137
+ *
138
+ * The warm-up is history only and produces no decisions: running the strategy over past
139
+ * minutes would emit orders for minutes that have already gone.
140
+ */
141
+ export async function warmSession(deps, profile, session, latestOpen) {
142
+ const from = latestOpen - WARMUP_BARS * ONE_MINUTE_MS;
143
+ const history = deps.bars.store.loadWindow(profile.instId, from, latestOpen);
144
+ return session.warm(history);
145
+ }
146
+ /** Which side of the book an action trades on. */
147
+ function orderSide(action) {
148
+ // Opening long and closing short both buy; the other two sell.
149
+ return action === "open_long" || action === "close_short" ? "buy" : "sell";
150
+ }
151
+ /** The exchange's order id, when it reported one. */
152
+ function readOrderId(response) {
153
+ const orders = response.orders;
154
+ if (!Array.isArray(orders) || orders.length === 0)
155
+ return null;
156
+ const first = orders[0];
157
+ const value = first.ordId;
158
+ return value === undefined || value === null || value === "" ? null : String(value);
159
+ }
160
+ //# sourceMappingURL=runner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runner.js","sourceRoot":"","sources":["../../src/live/runner.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Live execution storage.
3
+ *
4
+ * Two tables, and both carry a constraint that exists to make a specific failure
5
+ * impossible rather than merely unlikely.
6
+ *
7
+ * `live_profiles` stores the strategy *source*, not a path to it. Files get edited,
8
+ * and a profile has to keep trading the version that was reviewed and enabled —
9
+ * otherwise saving a file would silently change what a running profile does, with
10
+ * no record that anything happened.
11
+ *
12
+ * `live_signals` is unique on `(profile_id, cutoff_at)`. That is the root of
13
+ * idempotency: no matter how many times a bar is delivered, how many times the
14
+ * runtime restarts, or how a burst is coalesced, one closed minute can produce at
15
+ * most one signal. Every other guard in the live path can be re-run safely because
16
+ * this one cannot be.
17
+ */
18
+ export declare const LIVE_SCHEMA_SQL = "\nCREATE TABLE IF NOT EXISTS live_profiles (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL,\n -- A snapshot, taken when the profile was created. Never a file path.\n strategy_source TEXT NOT NULL,\n strategy_hash TEXT NOT NULL,\n params_json TEXT NOT NULL DEFAULT '{}',\n inst_id TEXT NOT NULL,\n account TEXT NOT NULL,\n environment TEXT NOT NULL,\n enabled INTEGER NOT NULL DEFAULT 0,\n status TEXT NOT NULL DEFAULT 'stopped',\n -- Bumped on every enable and disable. An action decided before a stop carries an\n -- older generation and is refused at submit time, which closes the window\n -- between \"evaluation started\" and \"order sent\".\n generation INTEGER NOT NULL DEFAULT 0,\n leverage REAL NOT NULL,\n margin_mode TEXT NOT NULL DEFAULT 'cross',\n entry_budget_usdt REAL NOT NULL,\n side_budget_usdt REAL NOT NULL,\n daily_loss_limit_usdt REAL NOT NULL,\n cooldown_seconds INTEGER NOT NULL DEFAULT 0,\n allow_long INTEGER NOT NULL DEFAULT 1,\n allow_short INTEGER NOT NULL DEFAULT 1,\n error_streak INTEGER NOT NULL DEFAULT 0,\n last_error TEXT,\n -- Evidence the strategy was backtested on this instrument before being enabled.\n backtest_run_id TEXT,\n -- Who created this profile: operator or agent. Recorded because an automated decision\n -- and a human one warrant different scrutiny when reading back why a strategy is trading.\n origin TEXT NOT NULL DEFAULT 'operator',\n -- Why these budgets and this instrument. Required of an agent: a profile nobody can explain\n -- is one nobody can review.\n rationale TEXT,\n created_at INTEGER NOT NULL,\n updated_at INTEGER NOT NULL,\n -- One profile per account, environment, and instrument. Two profiles trading the\n -- same contract on the same account would each size against a position the other\n -- also opened.\n UNIQUE(account, environment, inst_id)\n);\n\nCREATE INDEX IF NOT EXISTS idx_live_profiles_enabled\n ON live_profiles(enabled, inst_id);\n\nCREATE TABLE IF NOT EXISTS live_signals (\n id TEXT PRIMARY KEY,\n profile_id TEXT NOT NULL,\n cutoff_at INTEGER NOT NULL,\n action TEXT NOT NULL,\n reason TEXT NOT NULL,\n -- reserved | no_action | submitted | blocked | error | ambiguous\n status TEXT NOT NULL,\n -- Why a risk gate refused. Present on every blocked row: these are the evidence\n -- that risk control ran, and without them a quiet day and a broken loop look\n -- identical.\n blocked_by TEXT,\n quantity REAL,\n order_id TEXT,\n execution_key TEXT,\n error TEXT,\n details_json TEXT NOT NULL DEFAULT '{}',\n created_at INTEGER NOT NULL,\n updated_at INTEGER NOT NULL,\n UNIQUE(profile_id, cutoff_at)\n);\n\nCREATE INDEX IF NOT EXISTS idx_live_signals_profile\n ON live_signals(profile_id, cutoff_at DESC);\nCREATE INDEX IF NOT EXISTS idx_live_signals_status\n ON live_signals(status, cutoff_at DESC);\n";
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Live execution storage.
3
+ *
4
+ * Two tables, and both carry a constraint that exists to make a specific failure
5
+ * impossible rather than merely unlikely.
6
+ *
7
+ * `live_profiles` stores the strategy *source*, not a path to it. Files get edited,
8
+ * and a profile has to keep trading the version that was reviewed and enabled —
9
+ * otherwise saving a file would silently change what a running profile does, with
10
+ * no record that anything happened.
11
+ *
12
+ * `live_signals` is unique on `(profile_id, cutoff_at)`. That is the root of
13
+ * idempotency: no matter how many times a bar is delivered, how many times the
14
+ * runtime restarts, or how a burst is coalesced, one closed minute can produce at
15
+ * most one signal. Every other guard in the live path can be re-run safely because
16
+ * this one cannot be.
17
+ */
18
+ export const LIVE_SCHEMA_SQL = `
19
+ CREATE TABLE IF NOT EXISTS live_profiles (
20
+ id TEXT PRIMARY KEY,
21
+ name TEXT NOT NULL,
22
+ -- A snapshot, taken when the profile was created. Never a file path.
23
+ strategy_source TEXT NOT NULL,
24
+ strategy_hash TEXT NOT NULL,
25
+ params_json TEXT NOT NULL DEFAULT '{}',
26
+ inst_id TEXT NOT NULL,
27
+ account TEXT NOT NULL,
28
+ environment TEXT NOT NULL,
29
+ enabled INTEGER NOT NULL DEFAULT 0,
30
+ status TEXT NOT NULL DEFAULT 'stopped',
31
+ -- Bumped on every enable and disable. An action decided before a stop carries an
32
+ -- older generation and is refused at submit time, which closes the window
33
+ -- between "evaluation started" and "order sent".
34
+ generation INTEGER NOT NULL DEFAULT 0,
35
+ leverage REAL NOT NULL,
36
+ margin_mode TEXT NOT NULL DEFAULT 'cross',
37
+ entry_budget_usdt REAL NOT NULL,
38
+ side_budget_usdt REAL NOT NULL,
39
+ daily_loss_limit_usdt REAL NOT NULL,
40
+ cooldown_seconds INTEGER NOT NULL DEFAULT 0,
41
+ allow_long INTEGER NOT NULL DEFAULT 1,
42
+ allow_short INTEGER NOT NULL DEFAULT 1,
43
+ error_streak INTEGER NOT NULL DEFAULT 0,
44
+ last_error TEXT,
45
+ -- Evidence the strategy was backtested on this instrument before being enabled.
46
+ backtest_run_id TEXT,
47
+ -- Who created this profile: operator or agent. Recorded because an automated decision
48
+ -- and a human one warrant different scrutiny when reading back why a strategy is trading.
49
+ origin TEXT NOT NULL DEFAULT 'operator',
50
+ -- Why these budgets and this instrument. Required of an agent: a profile nobody can explain
51
+ -- is one nobody can review.
52
+ rationale TEXT,
53
+ created_at INTEGER NOT NULL,
54
+ updated_at INTEGER NOT NULL,
55
+ -- One profile per account, environment, and instrument. Two profiles trading the
56
+ -- same contract on the same account would each size against a position the other
57
+ -- also opened.
58
+ UNIQUE(account, environment, inst_id)
59
+ );
60
+
61
+ CREATE INDEX IF NOT EXISTS idx_live_profiles_enabled
62
+ ON live_profiles(enabled, inst_id);
63
+
64
+ CREATE TABLE IF NOT EXISTS live_signals (
65
+ id TEXT PRIMARY KEY,
66
+ profile_id TEXT NOT NULL,
67
+ cutoff_at INTEGER NOT NULL,
68
+ action TEXT NOT NULL,
69
+ reason TEXT NOT NULL,
70
+ -- reserved | no_action | submitted | blocked | error | ambiguous
71
+ status TEXT NOT NULL,
72
+ -- Why a risk gate refused. Present on every blocked row: these are the evidence
73
+ -- that risk control ran, and without them a quiet day and a broken loop look
74
+ -- identical.
75
+ blocked_by TEXT,
76
+ quantity REAL,
77
+ order_id TEXT,
78
+ execution_key TEXT,
79
+ error TEXT,
80
+ details_json TEXT NOT NULL DEFAULT '{}',
81
+ created_at INTEGER NOT NULL,
82
+ updated_at INTEGER NOT NULL,
83
+ UNIQUE(profile_id, cutoff_at)
84
+ );
85
+
86
+ CREATE INDEX IF NOT EXISTS idx_live_signals_profile
87
+ ON live_signals(profile_id, cutoff_at DESC);
88
+ CREATE INDEX IF NOT EXISTS idx_live_signals_status
89
+ ON live_signals(status, cutoff_at DESC);
90
+ `;
91
+ //# sourceMappingURL=schema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/live/schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAwE9B,CAAC"}
@@ -0,0 +1,144 @@
1
+ import { type ToolResult } from "../core/types.js";
2
+ import type { AccountService } from "../account/service.js";
3
+ import type { OkxClient } from "../core/okx-client.js";
4
+ import type { BarService } from "../bars/service.js";
5
+ import type { MarketService } from "../market/service.js";
6
+ import type { StrategyStore } from "../strategy/store.js";
7
+ import type { TradeService } from "../trade/service.js";
8
+ import { type PreconditionFailure } from "./preconditions.js";
9
+ import { type ReconcileOutcome } from "./reconcile.js";
10
+ import { type FailureKind } from "./failures.js";
11
+ import { LiveStore } from "./store.js";
12
+ import type { CreateProfileInput, LiveProfile, LiveSignal } from "./types.js";
13
+ /** Consecutive failures after which a profile stops itself. */
14
+ export declare const ERROR_STREAK_LIMIT = 3;
15
+ /** What enabling did to the instrument's leverage on the exchange. */
16
+ export interface LeverageApplication {
17
+ requested: number;
18
+ applied: boolean;
19
+ }
20
+ export interface EnableResult {
21
+ profile: LiveProfile;
22
+ /** The backtest that justified enabling. */
23
+ backtestRunId: string | null;
24
+ /** The leverage written to the exchange before the profile was enabled. */
25
+ leverage: LeverageApplication;
26
+ }
27
+ export interface ReadinessReport {
28
+ profile: LiveProfile;
29
+ ready: boolean;
30
+ failures: PreconditionFailure[];
31
+ /** The backtest found for this exact source and instrument, if any. */
32
+ backtestRunId: string | null;
33
+ }
34
+ /**
35
+ * Owns live profiles: their lifecycle, and what has to be true before one trades.
36
+ *
37
+ * Creating a profile never starts it. Enabling is the only step that can lead to an
38
+ * order, and it is the step that checks preconditions — so a profile can be written,
39
+ * inspected, and left alone safely.
40
+ */
41
+ export declare class LiveService {
42
+ private readonly store;
43
+ private readonly strategyStore;
44
+ private readonly bars;
45
+ private readonly market;
46
+ private readonly accounts;
47
+ private readonly client;
48
+ /** Used to apply the profile's leverage at enable time. See `applyLeverage`. */
49
+ private readonly trade;
50
+ /** Consecutive systemic failures after which a profile stops itself. */
51
+ private readonly errorStreakLimit;
52
+ constructor(store: LiveStore, strategyStore: StrategyStore, bars: BarService, market: MarketService, accounts: AccountService, client: Pick<OkxClient, "privateGet">,
53
+ /** Used to apply the profile's leverage at enable time. See `applyLeverage`. */
54
+ trade: Pick<TradeService, "setLeverage">,
55
+ /** Consecutive systemic failures after which a profile stops itself. */
56
+ errorStreakLimit?: number);
57
+ private onDisabled?;
58
+ /**
59
+ * Marks profiles a dead worker left running.
60
+ *
61
+ * Called at startup. Their generation advances, so an order decided by the previous
62
+ * process cannot be submitted if it somehow still arrives.
63
+ */
64
+ recoverInterrupted(): number;
65
+ /**
66
+ * Resolves signals left unresolved by a previous process.
67
+ *
68
+ * Every one is checked against the exchange rather than assumed. Nothing is resubmitted:
69
+ * re-sending an order that may already exist is the one mistake worse than missing one,
70
+ * so anything that cannot be resolved is marked ambiguous and waits for a person.
71
+ */
72
+ reconcile(): Promise<ToolResult<ReconcileOutcome>>;
73
+ createProfile(input: CreateProfileInput): ToolResult<LiveProfile>;
74
+ listProfiles(): ToolResult<LiveProfile[]>;
75
+ getProfile(id: string): ToolResult<LiveProfile>;
76
+ signals(profileId: string, limit?: number): ToolResult<LiveSignal[]>;
77
+ /**
78
+ * Reports whether a profile could be enabled, without enabling it.
79
+ *
80
+ * Separate from `enable` so the list of blockers can be read before committing to
81
+ * anything — and so a caller that wants to show them does not have to trigger a
82
+ * failure to find out.
83
+ */
84
+ readiness(id: string): Promise<ToolResult<ReadinessReport>>;
85
+ /**
86
+ * Enables a profile, refusing unless every precondition holds.
87
+ *
88
+ * The backtest that justified it is recorded on the profile, so a later reader can
89
+ * see which run was the evidence rather than having to trust that one existed.
90
+ */
91
+ enable(id: string): Promise<ToolResult<EnableResult>>;
92
+ /**
93
+ * Writes the profile's leverage to the exchange, and refuses to enable if it cannot.
94
+ *
95
+ * This changes a setting the account shares across everything trading that instrument
96
+ * and margin mode, including positions opened by hand. That is the cost of the budgets
97
+ * meaning what they say, and it is why it happens at enable rather than silently on
98
+ * every bar.
99
+ *
100
+ * A failure here stops the enable. Continuing would leave the profile trading at a
101
+ * leverage nobody chose, which is the exact condition this call exists to remove.
102
+ */
103
+ private applyLeverage;
104
+ /**
105
+ * Stops a profile.
106
+ *
107
+ * Always allowed and never precondition-checked: whatever is wrong, stopping is the
108
+ * safe direction, and a stop that could fail would be worse than useless.
109
+ */
110
+ disable(id: string, reason?: string): ToolResult<LiveProfile>;
111
+ /** Registers the loop's session release, called after a profile is disabled. */
112
+ onProfileDisabled(release: (profileId: string) => Promise<void>): void;
113
+ /**
114
+ * Records a failed evaluation and stops the profile if they keep coming.
115
+ *
116
+ * One failure never stops anything: the risk gates still refuse whatever is unsafe, and
117
+ * halting on a single dropped connection would make the whole thing unusable. A run of
118
+ * failures is different — it means something systemic, and continuing only accumulates
119
+ * errors against an exchange nobody is watching.
120
+ *
121
+ * Only failures that will not fix themselves count. A profile that could not reach OKX
122
+ * for one minute is in a different position from one whose strategy throws on every bar.
123
+ * Transient failures are still recorded, so the minute is never silently dropped; they
124
+ * just do not push toward a shutdown.
125
+ */
126
+ recordFailure(id: string, error: unknown): ToolResult<{
127
+ kind: FailureKind;
128
+ streak: number;
129
+ disabled: boolean;
130
+ }>;
131
+ /** Clears the streak after an evaluation that worked. */
132
+ recordSuccess(id: string): void;
133
+ deleteProfile(id: string): ToolResult<{
134
+ deleted: boolean;
135
+ }>;
136
+ /**
137
+ * Collects everything the preconditions need.
138
+ *
139
+ * Each lookup is guarded on its own: a profile blocked by a missing backtest should
140
+ * still learn that its account is also unverified, and one failing probe must not
141
+ * hide the rest of the list.
142
+ */
143
+ private gatherFacts;
144
+ }