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,150 @@
1
+ """One-minute to higher-timeframe aggregation.
2
+
3
+ This mirrors the TypeScript `TimeframeAggregator` exactly. Both are verified
4
+ against the same fixtures so a backtest and the terminal's own views can never
5
+ disagree about a bucket boundary.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass
11
+
12
+ ONE_MINUTE_MS = 60_000
13
+
14
+ INTERVAL_MS: dict[str, int] = {
15
+ "1m": ONE_MINUTE_MS,
16
+ "3m": 3 * ONE_MINUTE_MS,
17
+ "5m": 5 * ONE_MINUTE_MS,
18
+ "15m": 15 * ONE_MINUTE_MS,
19
+ "30m": 30 * ONE_MINUTE_MS,
20
+ "1H": 60 * ONE_MINUTE_MS,
21
+ "2H": 120 * ONE_MINUTE_MS,
22
+ "4H": 240 * ONE_MINUTE_MS,
23
+ "6H": 360 * ONE_MINUTE_MS,
24
+ "12H": 720 * ONE_MINUTE_MS,
25
+ "1D": 1_440 * ONE_MINUTE_MS,
26
+ }
27
+
28
+
29
+ class DataContractError(Exception):
30
+ """Raised when input violates the one-minute data contract."""
31
+
32
+
33
+ @dataclass(frozen=True)
34
+ class Bar:
35
+ """An immutable OHLCV bar covering ``[open_time_ms, close_time_ms)``."""
36
+
37
+ open_time_ms: int
38
+ close_time_ms: int
39
+ open: float
40
+ high: float
41
+ low: float
42
+ close: float
43
+ volume: float
44
+ confirmed: bool = True
45
+
46
+ # Protocol field names are camel-case; expose both spellings so strategy
47
+ # code can use the documented names without a conversion step.
48
+ @property
49
+ def openTimeMs(self) -> int: # noqa: N802 - protocol field name
50
+ return self.open_time_ms
51
+
52
+ @property
53
+ def closeTimeMs(self) -> int: # noqa: N802 - protocol field name
54
+ return self.close_time_ms
55
+
56
+
57
+ def interval_ms(interval: str) -> int:
58
+ if interval not in INTERVAL_MS:
59
+ supported = ", ".join(INTERVAL_MS)
60
+ raise DataContractError(f"Unsupported interval '{interval}'. Supported: {supported}")
61
+ return INTERVAL_MS[interval]
62
+
63
+
64
+ class TimeframeAggregator:
65
+ """Incrementally folds a contiguous confirmed one-minute feed into one timeframe.
66
+
67
+ Two rules keep the output safe to hand to a strategy:
68
+
69
+ 1. A leading partial bucket is discarded rather than published as a
70
+ completed candle, which would invent a bar that never existed.
71
+ 2. Only the final item may be unconfirmed, and its OHLCV contains solely
72
+ the minutes already known at the current cutoff.
73
+ """
74
+
75
+ __slots__ = ("_completed", "_current", "_interval_ms", "_last_input_open", "_maximum", "_started")
76
+
77
+ def __init__(self, interval: str, maximum_bars: int = 20_000) -> None:
78
+ if maximum_bars <= 0:
79
+ raise DataContractError("maximum_bars must be greater than zero")
80
+ self._interval_ms = interval_ms(interval)
81
+ self._maximum = maximum_bars
82
+ self._completed: list[Bar] = []
83
+ self._current: Bar | None = None
84
+ self._started = False
85
+ self._last_input_open: int | None = None
86
+
87
+ def push(self, bar: Bar) -> None:
88
+ if bar.close_time_ms - bar.open_time_ms != ONE_MINUTE_MS:
89
+ raise DataContractError("Timeframe aggregation requires closed one-minute bars")
90
+ if self._last_input_open is not None:
91
+ if bar.open_time_ms == self._last_input_open:
92
+ raise DataContractError("Timeframe aggregation received a duplicate one-minute bar")
93
+ if bar.open_time_ms < self._last_input_open:
94
+ raise DataContractError("Timeframe aggregation received an out-of-order one-minute bar")
95
+ if bar.open_time_ms != self._last_input_open + ONE_MINUTE_MS:
96
+ raise DataContractError("Timeframe aggregation detected a missing one-minute bar")
97
+ self._last_input_open = bar.open_time_ms
98
+
99
+ step = self._interval_ms
100
+ bucket_open = (bar.open_time_ms // step) * step
101
+ bucket_close = bucket_open + step
102
+
103
+ if not self._started:
104
+ if bar.open_time_ms != bucket_open:
105
+ return
106
+ self._started = True
107
+
108
+ current = self._current
109
+ if current is not None and current.open_time_ms == bucket_open:
110
+ self._current = Bar(
111
+ open_time_ms=current.open_time_ms,
112
+ close_time_ms=current.close_time_ms,
113
+ open=current.open,
114
+ high=max(current.high, bar.high),
115
+ low=min(current.low, bar.low),
116
+ close=bar.close,
117
+ volume=current.volume + bar.volume,
118
+ confirmed=bar.close_time_ms == current.close_time_ms,
119
+ )
120
+ return
121
+
122
+ if current is not None:
123
+ self._completed.append(current)
124
+ while len(self._completed) > self._maximum:
125
+ self._completed.pop(0)
126
+ self._current = Bar(
127
+ open_time_ms=bucket_open,
128
+ close_time_ms=bucket_close,
129
+ open=bar.open,
130
+ high=bar.high,
131
+ low=bar.low,
132
+ close=bar.close,
133
+ volume=bar.volume,
134
+ confirmed=bar.close_time_ms == bucket_close,
135
+ )
136
+
137
+ def snapshot(self) -> tuple[Bar, ...]:
138
+ if self._current is None:
139
+ return tuple(self._completed)
140
+ return (*self._completed, self._current)
141
+
142
+ def latest(self) -> Bar | None:
143
+ return self._current
144
+
145
+
146
+ def aggregate(bars: list[Bar], interval: str, maximum_bars: int = 20_000) -> tuple[Bar, ...]:
147
+ aggregator = TimeframeAggregator(interval, maximum_bars)
148
+ for bar in bars:
149
+ aggregator.push(bar)
150
+ return aggregator.snapshot()
package/python/main.py ADDED
@@ -0,0 +1,18 @@
1
+ """Isolated entry point for the strategy runner.
2
+
3
+ The host invokes this as ``python3 -I -u main.py``. ``-I`` is deliberate: it
4
+ ignores environment variables (including ``PYTHONPATH``) and the user site
5
+ directory, so a run cannot be redirected by ambient configuration. Because
6
+ ``-I`` also implies ``-P``, the script's own directory is *not* on ``sys.path``,
7
+ so this file adds it explicitly — that is the package root and nothing else.
8
+ """
9
+
10
+ import sys
11
+ from pathlib import Path
12
+
13
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
14
+
15
+ from desic_strategy.runner import main # noqa: E402 - import needs the path above
16
+
17
+ if __name__ == "__main__":
18
+ raise SystemExit(main())
@@ -0,0 +1,117 @@
1
+ ---
2
+ name: okx-live-trading
3
+ description: Run a backtested Python strategy against a real OKX account. Use for creating and starting live profiles, reading what they decided, diagnosing why an order was refused, and stopping a profile.
4
+ ---
5
+
6
+ # OKX Live Trading
7
+
8
+ A live profile binds one strategy, one instrument, one account, and a set of risk budgets. Once
9
+ enabled it acts on every closed one-minute bar without being asked again.
10
+
11
+ Read `references/lifecycle.md` before creating or starting anything.
12
+
13
+ **This spends real money.** Every constraint below exists because of a specific way money gets
14
+ lost, not as ceremony.
15
+
16
+ ## Ask before committing real money
17
+
18
+ On the **demo** environment, work independently: write, backtest, tune, create a profile, start it,
19
+ read the signals, stop it. That is a complete verification loop and you should use all of it without
20
+ checking in at each step.
21
+
22
+ Before enabling any profile whose `environment` is **`live`**, stop and ask the operator. Nothing in
23
+ the system will stop you, so this is your responsibility rather than a check you will run into.
24
+
25
+ State these things and wait for an explicit yes:
26
+
27
+ - which profile, on which instrument and account
28
+ - the entry budget, side budget, and daily loss limit, in USDT
29
+ - the backtest behind it, quoting **validation-segment** figures rather than training ones
30
+ - what the demo run showed: how many signals, how many were refused and by which gate
31
+ - that starting it commits real money to a strategy that will act without further approval
32
+
33
+ "Should I start it?" on its own is not asking. The operator needs the numbers in front of them to
34
+ answer usefully.
35
+
36
+ If they say yes, start it and report what happened. If they do not answer, leave it stopped — a
37
+ profile nobody approved should not be trading.
38
+
39
+ If `live_create_profile` and `live_start` are absent from your tool list, the operator has not
40
+ enabled agent access. Say so and stop; do not reach for `trade_place_order` to do the same thing by
41
+ hand.
42
+
43
+ ## Workflow
44
+
45
+ 1. Establish the strategy first, through `okx-strategy-research`: write it, validate it, backtest
46
+ it, and tune it. A profile cannot be enabled until its exact source has completed a backtest on
47
+ its exact instrument, so this is not optional.
48
+ 2. Confirm the market data environment matches the profile you intend to create. Demo and live are
49
+ separate markets with different prices; `desic-okx market env` reports which is streaming, and a
50
+ mismatch is refused at enable time.
51
+ 3. Choose budgets deliberately and be able to defend each one. `live_create_profile` requires a
52
+ `rationale` from you: which backtest justified this, and why these numbers. Write it for someone
53
+ reviewing the profile a month later.
54
+ 4. Call `live_readiness` before `live_start`. It lists everything outstanding at once, so you fix
55
+ the set rather than discovering one blocker at a time.
56
+ 5. Start on demo. Let it run long enough to produce several signals of more than one kind.
57
+ 6. Read `live_signals` and report what actually happened, including the refusals.
58
+ 7. Stop with `live_stop` when the observation is done. Never leave a profile running because it
59
+ might be useful later.
60
+
61
+ ## Choosing budgets
62
+
63
+ These are the numbers every risk gate measures against, so vague choices produce a profile that
64
+ either refuses everything or risks more than intended.
65
+
66
+ - `entryBudgetUsdt` is margin committed per entry, not notional. At 10x leverage, 100 USDT of
67
+ margin is roughly 1,000 USDT of exposure. Pick an amount the operator could lose entirely
68
+ without it mattering.
69
+ - `sideBudgetUsdt` must be at least `entryBudgetUsdt`, or no entry can ever be placed. Two to four
70
+ times is a reasonable starting point.
71
+ - `dailyLossLimitUsdt` is the only stop-like constraint in the system. Set it small on a first
72
+ run — one or two times the entry budget — so it is easy to trigger and you find out whether it
73
+ works before relying on it.
74
+ - `cooldownSeconds` bounds how often a misbehaving strategy can trade. Zero is appropriate only
75
+ once you have seen the signal history and know the frequency is intended.
76
+
77
+ Restricting to one direction with `allowShort: false` removes half the ways a first run can
78
+ surprise someone. Prefer it unless the strategy genuinely needs both.
79
+
80
+ `leverage` is not only a local number. Starting a profile writes it to the exchange for that
81
+ instrument and margin mode, and refuses to start if the write fails — the entry budget is sized
82
+ against it, so a mismatch means the margin actually committed differs from the budget by that
83
+ ratio. Mention this when the operator's account trades that instrument by hand as well: the setting
84
+ is shared, so their manual positions will be on the profile's leverage afterwards.
85
+
86
+ ## Reading results
87
+
88
+ State facts before interpretation: which profile, which minutes, and the counts by status.
89
+
90
+ **Report refusals, not only fills.** A `blocked` signal is risk control working, and the gate that
91
+ produced it is recorded. A day with no orders and a loop that silently stopped evaluating look
92
+ identical unless you say which one happened.
93
+
94
+ Never describe a live result as expected future return. A handful of minutes is not evidence of
95
+ anything, and neither is a profitable afternoon.
96
+
97
+ ## When something is wrong
98
+
99
+ - **`ambiguous`** means an order may or may not exist at the exchange. Nothing is retried
100
+ automatically, and you must not retry it either. Read the order with `account_get_order` and the
101
+ position with `account_get_positions`, report what you found, and let a person decide.
102
+ - **A profile that stopped itself** hit three consecutive systemic failures. Read `lastError`,
103
+ explain the cause, and fix that before suggesting it be restarted.
104
+ - **Repeated `blocked` on one gate** is usually a configuration mismatch rather than a fault —
105
+ a side budget below the entry budget, or a daily loss limit already reached. Say which.
106
+
107
+ ## Constraints
108
+
109
+ - One profile per account, environment, and instrument.
110
+ - A profile trades the strategy source captured when it was created. Editing the file changes
111
+ nothing; a new version needs a new backtest and a new profile.
112
+ - Closing actions are sized from the open position and skip the budget, loss, and cooldown gates.
113
+ A limit that prevented closing a losing position would make the loss worse.
114
+ - Reversal is not supported. Close before opening the opposite side.
115
+ - `set_protection` and `cancel_protection` do not exist here. Attach take-profit and stop-loss at
116
+ entry or not at all.
117
+ - Stopping is always permitted and never checked. When in doubt, stop.
@@ -0,0 +1,9 @@
1
+ interface:
2
+ display_name: "OKX Live Trading"
3
+ short_description: "Run a backtested strategy against a real account, with risk gates and signal history."
4
+ default_prompt: "Use $okx-live-trading to review what my live profiles decided today."
5
+ dependencies:
6
+ tools:
7
+ - type: "mcp"
8
+ value: "desic-okx"
9
+ description: "Desic OKX Agent MCP server"
@@ -0,0 +1,128 @@
1
+ # Live profile lifecycle
2
+
3
+ ## Tools
4
+
5
+ | Tool | Use |
6
+ | --- | --- |
7
+ | `live_list_profiles` | Every profile with its state and error streak. |
8
+ | `live_get_profile` | One profile, including its rationale and the backtest behind it. |
9
+ | `live_readiness` | Everything outstanding before it can start, all at once. |
10
+ | `live_signals` | What it decided per minute, including every refusal. |
11
+ | `live_stop` | Disable. Always permitted. |
12
+ | `live_create_profile` | Create one. Present only when the operator enabled agent access. |
13
+ | `live_start` | Enable one. Same condition. Ask first when the environment is `live`. |
14
+ | `live_delete_profile` | Remove a stopped profile and its signal history. Refused while enabled. |
15
+
16
+ Creating a profile never starts it. Enabling is the only step that can lead to an order.
17
+
18
+ Deleting removes the signal history along with the profile, which is the record of what it did.
19
+ Prefer stopping and leaving it in place; delete only when asked, and never as cleanup after an
20
+ experiment worth reviewing.
21
+
22
+ ## Creating
23
+
24
+ ```
25
+ live_create_profile {
26
+ "name": "ema-btc-demo",
27
+ "source": "...",
28
+ "instId": "BTC-USDT-SWAP",
29
+ "account": "demo",
30
+ "environment": "demo",
31
+ "leverage": 10,
32
+ "entryBudgetUsdt": 100,
33
+ "sideBudgetUsdt": 400,
34
+ "dailyLossLimitUsdt": 200,
35
+ "cooldownSeconds": 300,
36
+ "allowShort": false,
37
+ "origin": "agent",
38
+ "rationale": "bt_a1b2 validation segment +8.2% with 4.1% max drawdown over 30 days; entry budget is 0.5% of account equity; short disabled because the strategy only defines a long entry"
39
+ }
40
+ ```
41
+
42
+ Set `origin: "agent"` and supply a `rationale` whenever you create a profile. It is required, and
43
+ it is what makes the profile reviewable later — write which run justified it and why each budget
44
+ was chosen, not a restatement of the parameters.
45
+
46
+ ## Preconditions
47
+
48
+ `live_readiness` returns every unmet one together. There are six.
49
+
50
+ | Code | Meaning |
51
+ | --- | --- |
52
+ | no&#95;backtest | The exact source has not completed a backtest on this instrument. |
53
+ | python&#95;missing | The local Python environment is not ready. |
54
+ | account&#95;unverified | OKX did not accept the credentials. |
55
+ | no&#95;trade&#95;permission | The API key cannot trade. |
56
+ | contract&#95;unreadable | No usable contract specification, so no size can be derived. |
57
+ | environment&#95;mismatch | The profile's venue differs from the streaming market data. |
58
+
59
+ The backtest is matched by **source hash**, so changing one character means it no longer counts.
60
+ A losing backtest satisfies this: the requirement is that one ran and someone saw the behaviour,
61
+ not that it made money. Do not tune a strategy further in order to satisfy this check — that
62
+ inverts what it is for.
63
+
64
+ Every precondition is a verifiable fact about the system, so readiness returning empty means the
65
+ profile *can* start — not that it *should*. Whether to commit real money is the operator's decision,
66
+ and on a `live` profile you ask first. See the asking rules in `SKILL.md`.
67
+
68
+ ## The risk gates
69
+
70
+ Checked in order on every closed minute, cheapest first. **Every gate refuses; none adjusts.** An
71
+ intent needing more than the budget allows is rejected and recorded, never quietly resized — so a
72
+ fill is never an order whose size was rewritten.
73
+
74
+ | # | Gate | Refuses when |
75
+ | --- | --- | --- |
76
+ | 1 | disabled | The profile is no longer enabled. |
77
+ | 2 | generation | It was stopped while the strategy was deciding. |
78
+ | 3 | direction | The profile forbids opening that side. |
79
+ | 4 | entry&#95;budget | No margin available for one entry. |
80
+ | 5 | side&#95;budget | That direction is already fully committed. |
81
+ | 6 | daily&#95;loss | The UTC day's realized loss reached the limit. |
82
+ | 7 | cooldown | Not enough time since the last order was actually sent. |
83
+ | 8 | minimum&#95;size | The budget cannot buy one legal contract lot. |
84
+
85
+ Closing actions are sized from the open position and skip gates 4 through 7. A loss limit that
86
+ blocked closing a losing position would deepen the loss, and a budget-sized partial close would
87
+ leave exposure the strategy believed it had removed.
88
+
89
+ Position, committed margin, and the day's realized loss all come from the exchange, never from a
90
+ local tally — a manual close or a partial fill would leave a local figure wrong.
91
+
92
+ ## Signal states
93
+
94
+ | Status | Meaning |
95
+ | --- | --- |
96
+ | no&#95;action | The strategy asked for nothing. |
97
+ | submitted | An order reached the exchange. |
98
+ | blocked | A gate refused; the gate is recorded. |
99
+ | error | Evaluation failed. |
100
+ | ambiguous | The outcome is unknown. Needs a person. |
101
+ | reserved | Claimed but never decided — a crash remnant. |
102
+
103
+ One signal per closed minute, guaranteed by a unique constraint. Redelivered bars, coalesced
104
+ bursts, and restarts all converge on at most one order per minute.
105
+
106
+ ## Recovery
107
+
108
+ At startup, profiles a dead worker left running are stopped and their unresolved signals are
109
+ checked against the exchange. Nothing is ever resubmitted: re-sending an order that may already
110
+ exist is worse than missing one.
111
+
112
+ "The exchange says there is no such order" closes a signal. "The exchange could not be reached"
113
+ does not — that becomes `ambiguous`, because treating it as absence would discard an order that
114
+ may have filled. A still-open order also becomes `ambiguous` rather than being closed
115
+ automatically, since that would decide for someone what to do with exposure they have not seen.
116
+
117
+ ## Automatic shutdown
118
+
119
+ Three consecutive **systemic** failures stop a profile. Systemic means it will not fix itself: a
120
+ missing interpreter, invalid source, revoked permission, a strategy raising, an ambiguous write, or
121
+ anything unrecognised. A dropped connection or a rate limit is transient — recorded, but it does
122
+ not count, and twenty in a row leave the profile running.
123
+
124
+ A success clears the streak. A transient failure between two systemic ones does not, because the
125
+ fault is still there.
126
+
127
+ When you find a stopped profile, read `lastError` and explain the cause rather than suggesting a
128
+ restart. Restarting into the same fault produces the same three failures.
@@ -0,0 +1,113 @@
1
+ ---
2
+ name: okx-strategy-research
3
+ description: Write, validate, and backtest Python trading strategies against local OKX one-minute history. Use for strategy authoring, backtest execution, reading backtest results, and diagnosing local data coverage or gaps.
4
+ ---
5
+
6
+ # OKX Strategy Research
7
+
8
+ Strategies are Python modules that define `on_bar(ctx)`. The host owns the clock,
9
+ the market window, matching, sizing, and risk. A strategy returns one decision
10
+ per bar and never receives an exchange client, credentials, or an order API.
11
+
12
+ Read `references/python-api.md` before writing code. Use the documented field
13
+ names directly; do not probe for spellings.
14
+
15
+ ## Workflow
16
+
17
+ 1. Confirm the environment once with `strategy_environment`. If it is not ready,
18
+ tell the user to run `desic-okx strategy env --setup` rather than attempting
19
+ installation yourself.
20
+ 2. Write the strategy. Return exactly one decision per bar. Do not pass a
21
+ contract count — the host derives size from its own budget.
22
+ 3. Call `strategy_validate_source` before every backtest. It returns violations
23
+ with line numbers in milliseconds, so it is always cheaper than discovering a
24
+ forbidden import after queuing.
25
+ 4. Check coverage with `data_coverage` for the instrument and range you intend to
26
+ test. Repair gaps with `data_download` first: a backtest over a window with a
27
+ hole fails closed rather than skipping the minute.
28
+ 5. Queue the run with `strategy_run_backtest`. It returns a `runId` immediately.
29
+ Poll `strategy_get_run` until `status` is `completed`, `failed`, or
30
+ `cancelled`. Never assume a run finished because the call returned.
31
+ 6. Read details only as needed, through `strategy_get_run_equity`,
32
+ `strategy_get_run_trades`, and `strategy_get_run_actions`. Request pages; a
33
+ full equity curve is tens of thousands of points.
34
+ 7. To tune parameters, use `strategy_run_optimize` rather than running backtests
35
+ in a loop and picking the best. It splits the window and ranks on a segment the
36
+ candidates never saw; choosing by hand across whole-window results selects for
37
+ overfitting with no way to detect it. Read `references/tools-and-data.md`
38
+ before reporting a search.
39
+ 8. To weigh two runs against each other, use `strategy_compare_runs` rather than
40
+ quoting two reports in sequence. Read its `warnings` first and repeat them: two
41
+ runs over different data, instruments, windows, or costs are not two answers to
42
+ one question, and that is invisible in the metrics alone.
43
+
44
+ ## Where this leads
45
+
46
+ Research is one segment of a longer chain: market analysis produces an idea, this
47
+ skill turns it into a strategy with evidence, and `okx-live-trading` binds it to an
48
+ account. A backtest here is the hard precondition for enabling a live profile, so
49
+ what you do at this stage is what a live profile will later be justified by.
50
+
51
+ Two things follow from that:
52
+
53
+ - A backtest is matched to a profile by **source hash**, not filename. Editing the
54
+ strategy after a good run invalidates that evidence, and the profile will refuse
55
+ to enable until it is rerun.
56
+ - Demo and live are separate markets with different prices. A run on demo bars does
57
+ not justify a live profile; that window has to be rerun against live data.
58
+
59
+ If `live_*` tools are absent from your list, the operator has not enabled agent
60
+ access to live trading. Finish the research and say what the next step would be
61
+ rather than reaching for `trade_place_order` to trade the strategy manually.
62
+
63
+ ## Reporting results
64
+
65
+ State the facts before any interpretation: instrument, evaluation range, bar
66
+ count, `dataSnapshotId`, trade count, and the cost assumptions used.
67
+
68
+ Then report `returnPct`, `maxDrawdownPct`, `calmar`, `winRatePct`,
69
+ `profitFactor`, `totalFeesUsdt`, and `exposurePct`. Win rate and profit factor
70
+ are net of fees.
71
+
72
+ Always say explicitly that a backtest is a simulation. Name the specific
73
+ assumptions that make it one: fees, slippage, fills at the next bar's open, and
74
+ a conservative K-line estimate for limit orders with no order-book queue. Never
75
+ describe a backtest result as expected future return.
76
+
77
+ When `marginExhausted` is true, say the run hit its simulated collateral limit
78
+ and that this is a research risk boundary, not an OKX liquidation estimate.
79
+
80
+ ## Judging a result
81
+
82
+ A high trade count with a profit factor near or below 1 usually means fees
83
+ dominate the edge; check `totalFeesUsdt` against the equity change before
84
+ concluding the logic is wrong.
85
+
86
+ Treat a single strong result on one range with suspicion. Prefer parameters that
87
+ stay reasonable across neighbouring values over a single sharp optimum, and say
88
+ so when a result looks like it was selected rather than discovered.
89
+
90
+ After a parameter search, quote the validation metrics and never the training
91
+ ones. Report both columns so the gap is visible, and call a candidate overfitted
92
+ when the search does. Add that the winner's validation figure is still optimistic,
93
+ because it was selected by that very score.
94
+
95
+ A verdict of `unexplained` — losing on training, winning on validation — is not a
96
+ better `holds`. Nothing crossed the split, so the validation profit describes those
97
+ particular bars rather than the parameters. When most of the table reads
98
+ `unexplained`, say that the window is the problem: the validation segment favoured
99
+ this kind of strategy, and the search cannot distinguish between the candidates.
100
+ Propose a window containing rises, falls, and chop rather than reporting a winner.
101
+
102
+ Report a negative result plainly. A strategy that loses money is a valid finding.
103
+
104
+ ## Constraints
105
+
106
+ - Imports are limited to `collections`, `dataclasses`, `math`, `numpy`,
107
+ `pandas`, `statistics`, and `typing`.
108
+ - `getattr`, `setattr`, `eval`, `exec`, `open`, `__import__`, dunder access,
109
+ async handlers, and generators are rejected. Read documented fields directly.
110
+ - Only `1m` bars are stored. Every higher timeframe is derived from them.
111
+ - Indicators accept only `1m`, because a higher-timeframe bucket can still be
112
+ revised before it confirms.
113
+ - A strategy cannot see past its cutoff. Do not write code that tries.
@@ -0,0 +1,9 @@
1
+ interface:
2
+ display_name: "OKX Strategy Research"
3
+ short_description: "Write, validate, and backtest Python strategies on local OKX history."
4
+ default_prompt: "Use $okx-strategy-research to backtest an EMA trend strategy on BTC-USDT-SWAP."
5
+ dependencies:
6
+ tools:
7
+ - type: "mcp"
8
+ value: "desic-okx"
9
+ description: "Desic OKX Agent MCP server"
@@ -0,0 +1,107 @@
1
+ # Execution semantics
2
+
3
+ Understanding when a decision becomes a fill is the difference between a backtest
4
+ that means something and one that quietly reports impossible performance.
5
+
6
+ ## The clock
7
+
8
+ `on_bar` runs after a one-minute bar has **closed and confirmed**. The decision
9
+ is made at that close. A market decision fills at the **following bar's open**,
10
+ never at the close the decision was made on.
11
+
12
+ ```
13
+ bar[k] closes ──▶ on_bar sees bar[k] ──▶ decision ──▶ fills at bar[k+1].open
14
+ ```
15
+
16
+ This one-bar delay is why `preloadBars` must be at least 2: an action needs a
17
+ following open to fill at.
18
+
19
+ ## Limit orders
20
+
21
+ A limit decision becomes a resting order at the next bar's open, then waits.
22
+
23
+ Matching is deliberately conservative:
24
+
25
+ - A buy requires a later bar to trade **strictly below** its limit. Touching the
26
+ limit exactly is not a fill.
27
+ - A sell requires a bar to trade **strictly above** its limit.
28
+ - Any single bar can absorb at most **10% of that bar's volume**.
29
+
30
+ There is no historical order book, so there is no queue position, no latency
31
+ model, and no guarantee a real venue would have filled the same order. Treat a
32
+ limit fill as an estimate, never a promise.
33
+
34
+ Resting orders appear in `ctx.portfolio.open_orders`, can fill partially, and can
35
+ be cancelled with `ctx.cancel_order(order_id, reason)`.
36
+
37
+ ## Protection
38
+
39
+ `protection` on an opening decision is a request carrying absolute prices. The
40
+ simulator checks it against each following bar's OHLC.
41
+
42
+ When a single bar spans both the stop and the target, the **stop is taken**.
43
+ Intrabar ordering is unknowable from OHLC alone, so the adverse outcome is
44
+ assumed rather than the favourable one.
45
+
46
+ A full close removes attached protection automatically.
47
+
48
+ ## Sizing and margin
49
+
50
+ The strategy never specifies size. The host computes a contract count from the
51
+ entry budget, leverage, and the instrument's own contract specification, then
52
+ aligns it down to a legal lot.
53
+
54
+ The specification is read from OKX per instrument: `ctVal` and `ctMult` give the
55
+ base-currency units one contract represents, and `lotSz` / `minSz` decide which
56
+ quantities are legal. These differ between instruments — BTC-USDT-SWAP is 0.01
57
+ BTC per contract with a 0.1 lot, while ETH-USDT-SWAP is 0.1 ETH with a 0.01 lot
58
+ — so the same budget buys different contract counts. A run whose specification
59
+ cannot be read fails rather than assuming one, because a guessed contract size
60
+ produces confident metrics for positions that could never have been taken.
61
+
62
+ Opening reserves `notional / leverage * marginSafetyMultiplier` from equity. If an
63
+ adverse move exhausts that collateral, the position is closed at the conservative
64
+ available price and `marginExhausted` is set.
65
+
66
+ That is a **research risk boundary, not an OKX liquidation model**. Real
67
+ liquidation depends on maintenance margin tiers, funding, and mark price
68
+ mechanics that this simulation does not reproduce.
69
+
70
+ ## Costs
71
+
72
+ Fees and slippage apply to every fill. Defaults are 5 bps taker, 2 bps maker, and
73
+ 1 bp slippage on entry and exit.
74
+
75
+ Fees compound quickly on a high-turnover strategy. A profit factor near 1 with
76
+ hundreds of trades usually means fees consumed the edge — compare
77
+ `totalFeesUsdt` against the change in equity before concluding the signal is
78
+ wrong.
79
+
80
+ ## Multi-timeframe
81
+
82
+ `on_bar` fires on every confirmed minute regardless of which timeframes the
83
+ strategy reads. Reading `30m` bars changes the data, not the callback clock.
84
+
85
+ To act only on a 30-minute close:
86
+
87
+ ```python
88
+ buckets = ctx.market.bars(ctx.instrument_id, "30m", lookback=2)
89
+ if not buckets[-1].confirmed:
90
+ return ctx.no_action("waiting for the 30m close")
91
+ ```
92
+
93
+ The resulting decision still fills at the next one-minute open.
94
+
95
+ Do not inspect a higher-timeframe bar's high or low and emit a delayed close to
96
+ imitate a protective fill. Use `protection` so the simulator monitors it against
97
+ each minute.
98
+
99
+ ## What cannot happen
100
+
101
+ `ctx.market.bars(...)` returns only bars at or before `ctx.as_of_ms`. The window
102
+ ends exactly at the cutoff, so there is no way to read a later bar — attempting
103
+ it simply yields the same visible window.
104
+
105
+ A window with a missing minute fails the run outright rather than skipping it,
106
+ because performance measured over a timeline that never existed is worse than no
107
+ answer.