desic-okx-agent 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +353 -0
- package/README.md +194 -190
- package/dist/account/private-websocket.js +4 -4
- package/dist/account/private-websocket.js.map +1 -1
- package/dist/account/service.d.ts +12 -1
- package/dist/account/service.js +18 -0
- package/dist/account/service.js.map +1 -1
- package/dist/bars/rate-limiter.d.ts +18 -0
- package/dist/bars/rate-limiter.js +84 -0
- package/dist/bars/rate-limiter.js.map +1 -0
- package/dist/bars/schema.d.ts +36 -0
- package/dist/bars/schema.js +134 -0
- package/dist/bars/schema.js.map +1 -0
- package/dist/bars/service.d.ts +60 -0
- package/dist/bars/service.js +120 -0
- package/dist/bars/service.js.map +1 -0
- package/dist/bars/store.d.ts +105 -0
- package/dist/bars/store.js +415 -0
- package/dist/bars/store.js.map +1 -0
- package/dist/bars/timeframe.d.ts +40 -0
- package/dist/bars/timeframe.js +146 -0
- package/dist/bars/timeframe.js.map +1 -0
- package/dist/bars/types.d.ts +68 -0
- package/dist/bars/types.js +13 -0
- package/dist/bars/types.js.map +1 -0
- package/dist/cli/data-render.d.ts +37 -0
- package/dist/cli/data-render.js +143 -0
- package/dist/cli/data-render.js.map +1 -0
- package/dist/cli/doctor.js +7 -3
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/index.js +758 -27
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/live-render.d.ts +24 -0
- package/dist/cli/live-render.js +85 -0
- package/dist/cli/live-render.js.map +1 -0
- package/dist/cli/range.d.ts +28 -0
- package/dist/cli/range.js +63 -0
- package/dist/cli/range.js.map +1 -0
- package/dist/cli/render.js +3 -0
- package/dist/cli/render.js.map +1 -1
- package/dist/cli/strategy-render.d.ts +36 -0
- package/dist/cli/strategy-render.js +391 -0
- package/dist/cli/strategy-render.js.map +1 -0
- package/dist/cli/width.d.ts +18 -0
- package/dist/cli/width.js +71 -0
- package/dist/cli/width.js.map +1 -0
- package/dist/config/loader.js +1 -1
- package/dist/config/schema.d.ts +7 -0
- package/dist/config/schema.js +24 -0
- package/dist/config/schema.js.map +1 -1
- package/dist/core/okx-client.d.ts +9 -1
- package/dist/core/okx-client.js +14 -5
- package/dist/core/okx-client.js.map +1 -1
- package/dist/i18n/locale.d.ts +24 -0
- package/dist/i18n/locale.js +65 -0
- package/dist/i18n/locale.js.map +1 -0
- package/dist/i18n/messages.d.ts +333 -0
- package/dist/i18n/messages.js +660 -0
- package/dist/i18n/messages.js.map +1 -0
- package/dist/live/account-snapshot.d.ts +30 -0
- package/dist/live/account-snapshot.js +130 -0
- package/dist/live/account-snapshot.js.map +1 -0
- package/dist/live/cutoff-queue.d.ts +42 -0
- package/dist/live/cutoff-queue.js +69 -0
- package/dist/live/cutoff-queue.js.map +1 -0
- package/dist/live/execution-key.d.ts +23 -0
- package/dist/live/execution-key.js +31 -0
- package/dist/live/execution-key.js.map +1 -0
- package/dist/live/failures.d.ts +37 -0
- package/dist/live/failures.js +57 -0
- package/dist/live/failures.js.map +1 -0
- package/dist/live/gates.d.ts +65 -0
- package/dist/live/gates.js +136 -0
- package/dist/live/gates.js.map +1 -0
- package/dist/live/loop.d.ts +56 -0
- package/dist/live/loop.js +197 -0
- package/dist/live/loop.js.map +1 -0
- package/dist/live/preconditions.d.ts +48 -0
- package/dist/live/preconditions.js +69 -0
- package/dist/live/preconditions.js.map +1 -0
- package/dist/live/reconcile.d.ts +46 -0
- package/dist/live/reconcile.js +104 -0
- package/dist/live/reconcile.js.map +1 -0
- package/dist/live/runner.d.ts +57 -0
- package/dist/live/runner.js +160 -0
- package/dist/live/runner.js.map +1 -0
- package/dist/live/schema.d.ts +18 -0
- package/dist/live/schema.js +91 -0
- package/dist/live/schema.js.map +1 -0
- package/dist/live/service.d.ts +144 -0
- package/dist/live/service.js +303 -0
- package/dist/live/service.js.map +1 -0
- package/dist/live/session.d.ts +85 -0
- package/dist/live/session.js +234 -0
- package/dist/live/session.js.map +1 -0
- package/dist/live/sizing.d.ts +62 -0
- package/dist/live/sizing.js +79 -0
- package/dist/live/sizing.js.map +1 -0
- package/dist/live/store.d.ts +123 -0
- package/dist/live/store.js +350 -0
- package/dist/live/store.js.map +1 -0
- package/dist/live/types.d.ts +82 -0
- package/dist/live/types.js +2 -0
- package/dist/live/types.js.map +1 -0
- package/dist/market/websocket.d.ts +16 -1
- package/dist/market/websocket.js +60 -5
- package/dist/market/websocket.js.map +1 -1
- package/dist/mcp/server.d.ts +1 -0
- package/dist/mcp/server.js +15 -1
- package/dist/mcp/server.js.map +1 -1
- package/dist/network/connectivity.d.ts +9 -1
- package/dist/network/connectivity.js +28 -1
- package/dist/network/connectivity.js.map +1 -1
- package/dist/report/chart-script.d.ts +12 -0
- package/dist/report/chart-script.js +146 -0
- package/dist/report/chart-script.js.map +1 -0
- package/dist/report/compare-html.d.ts +8 -0
- package/dist/report/compare-html.js +254 -0
- package/dist/report/compare-html.js.map +1 -0
- package/dist/report/compare-script.d.ts +12 -0
- package/dist/report/compare-script.js +109 -0
- package/dist/report/compare-script.js.map +1 -0
- package/dist/report/compare.d.ts +61 -0
- package/dist/report/compare.js +205 -0
- package/dist/report/compare.js.map +1 -0
- package/dist/report/fetch.d.ts +20 -0
- package/dist/report/fetch.js +56 -0
- package/dist/report/fetch.js.map +1 -0
- package/dist/report/html.d.ts +54 -0
- package/dist/report/html.js +641 -0
- package/dist/report/html.js.map +1 -0
- package/dist/report/open.d.ts +42 -0
- package/dist/report/open.js +114 -0
- package/dist/report/open.js.map +1 -0
- package/dist/runtime/server.d.ts +16 -1
- package/dist/runtime/server.js +112 -9
- package/dist/runtime/server.js.map +1 -1
- package/dist/setup/installer.d.ts +1 -0
- package/dist/setup/installer.js +8 -0
- package/dist/setup/installer.js.map +1 -1
- package/dist/setup/wizard.d.ts +3 -0
- package/dist/setup/wizard.js +131 -6
- package/dist/setup/wizard.js.map +1 -1
- package/dist/strategy/constants.d.ts +23 -0
- package/dist/strategy/constants.js +24 -0
- package/dist/strategy/constants.js.map +1 -0
- package/dist/strategy/environment.d.ts +52 -0
- package/dist/strategy/environment.js +187 -0
- package/dist/strategy/environment.js.map +1 -0
- package/dist/strategy/instrument.d.ts +29 -0
- package/dist/strategy/instrument.js +39 -0
- package/dist/strategy/instrument.js.map +1 -0
- package/dist/strategy/optimize.d.ts +73 -0
- package/dist/strategy/optimize.js +113 -0
- package/dist/strategy/optimize.js.map +1 -0
- package/dist/strategy/parameter-space.d.ts +59 -0
- package/dist/strategy/parameter-space.js +221 -0
- package/dist/strategy/parameter-space.js.map +1 -0
- package/dist/strategy/python-bridge.d.ts +24 -0
- package/dist/strategy/python-bridge.js +114 -0
- package/dist/strategy/python-bridge.js.map +1 -0
- package/dist/strategy/schema.d.ts +9 -0
- package/dist/strategy/schema.js +91 -0
- package/dist/strategy/schema.js.map +1 -0
- package/dist/strategy/service.d.ts +138 -0
- package/dist/strategy/service.js +745 -0
- package/dist/strategy/service.js.map +1 -0
- package/dist/strategy/settings.d.ts +162 -0
- package/dist/strategy/settings.js +243 -0
- package/dist/strategy/settings.js.map +1 -0
- package/dist/strategy/store.d.ts +96 -0
- package/dist/strategy/store.js +367 -0
- package/dist/strategy/store.js.map +1 -0
- package/dist/strategy/templates.d.ts +11 -0
- package/dist/strategy/templates.js +134 -0
- package/dist/strategy/templates.js.map +1 -0
- package/dist/strategy/types.d.ts +111 -0
- package/dist/strategy/types.js +2 -0
- package/dist/strategy/types.js.map +1 -0
- package/dist/tools/catalog.d.ts +16 -0
- package/dist/tools/catalog.js +118 -17
- package/dist/tools/catalog.js.map +1 -1
- package/dist/trade/service.d.ts +12 -0
- package/dist/trade/service.js +24 -7
- package/dist/trade/service.js.map +1 -1
- package/dist/tui/app.d.ts +23 -0
- package/dist/tui/app.js +322 -0
- package/dist/tui/app.js.map +1 -0
- package/dist/tui/commands.d.ts +70 -0
- package/dist/tui/commands.js +313 -0
- package/dist/tui/commands.js.map +1 -0
- package/dist/tui/entries.d.ts +17 -0
- package/dist/tui/entries.js +24 -0
- package/dist/tui/entries.js.map +1 -0
- package/dist/tui/execute.d.ts +26 -0
- package/dist/tui/execute.js +664 -0
- package/dist/tui/execute.js.map +1 -0
- package/dist/tui/history.d.ts +17 -0
- package/dist/tui/history.js +48 -0
- package/dist/tui/history.js.map +1 -0
- package/dist/tui/index.d.ts +8 -0
- package/dist/tui/index.js +48 -0
- package/dist/tui/index.js.map +1 -0
- package/dist/tui/line-editor.d.ts +44 -0
- package/dist/tui/line-editor.js +98 -0
- package/dist/tui/line-editor.js.map +1 -0
- package/dist/tui/progress.d.ts +23 -0
- package/dist/tui/progress.js +46 -0
- package/dist/tui/progress.js.map +1 -0
- package/dist/tui/settings-editor.d.ts +18 -0
- package/dist/tui/settings-editor.js +115 -0
- package/dist/tui/settings-editor.js.map +1 -0
- package/docs/live-trading.md +455 -0
- package/docs/strategy-research.md +597 -0
- package/package.json +11 -1
- package/python/desic_strategy/__init__.py +34 -0
- package/python/desic_strategy/actions.py +158 -0
- package/python/desic_strategy/context.py +164 -0
- package/python/desic_strategy/engine.py +614 -0
- package/python/desic_strategy/indicators.py +159 -0
- package/python/desic_strategy/live.py +253 -0
- package/python/desic_strategy/policy.py +193 -0
- package/python/desic_strategy/portfolio.py +152 -0
- package/python/desic_strategy/report.py +319 -0
- package/python/desic_strategy/runner.py +574 -0
- package/python/desic_strategy/timeframe.py +150 -0
- package/python/main.py +18 -0
- package/skills/okx-live-trading/SKILL.md +117 -0
- package/skills/okx-live-trading/agents/openai.yaml +9 -0
- package/skills/okx-live-trading/references/lifecycle.md +128 -0
- package/skills/okx-strategy-research/SKILL.md +113 -0
- package/skills/okx-strategy-research/agents/openai.yaml +9 -0
- package/skills/okx-strategy-research/references/execution-semantics.md +107 -0
- package/skills/okx-strategy-research/references/field-traps.md +142 -0
- package/skills/okx-strategy-research/references/python-api.md +121 -0
- package/skills/okx-strategy-research/references/tools-and-data.md +192 -0
- package/skills/okx-trading/SKILL.md +11 -10
|
@@ -0,0 +1,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_backtest | The exact source has not completed a backtest on this instrument. |
|
|
53
|
+
| python_missing | The local Python environment is not ready. |
|
|
54
|
+
| account_unverified | OKX did not accept the credentials. |
|
|
55
|
+
| no_trade_permission | The API key cannot trade. |
|
|
56
|
+
| contract_unreadable | No usable contract specification, so no size can be derived. |
|
|
57
|
+
| environment_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_budget | No margin available for one entry. |
|
|
80
|
+
| 5 | side_budget | That direction is already fully committed. |
|
|
81
|
+
| 6 | daily_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_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_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.
|