desic-okx-agent 0.2.1 → 0.3.1

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 (239) 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 +11 -0
  162. package/dist/strategy/schema.js +94 -0
  163. package/dist/strategy/schema.js.map +1 -0
  164. package/dist/strategy/series-codec.d.ts +50 -0
  165. package/dist/strategy/series-codec.js +103 -0
  166. package/dist/strategy/series-codec.js.map +1 -0
  167. package/dist/strategy/service.d.ts +138 -0
  168. package/dist/strategy/service.js +745 -0
  169. package/dist/strategy/service.js.map +1 -0
  170. package/dist/strategy/settings.d.ts +162 -0
  171. package/dist/strategy/settings.js +243 -0
  172. package/dist/strategy/settings.js.map +1 -0
  173. package/dist/strategy/store.d.ts +96 -0
  174. package/dist/strategy/store.js +369 -0
  175. package/dist/strategy/store.js.map +1 -0
  176. package/dist/strategy/templates.d.ts +11 -0
  177. package/dist/strategy/templates.js +134 -0
  178. package/dist/strategy/templates.js.map +1 -0
  179. package/dist/strategy/types.d.ts +111 -0
  180. package/dist/strategy/types.js +2 -0
  181. package/dist/strategy/types.js.map +1 -0
  182. package/dist/tools/catalog.d.ts +16 -0
  183. package/dist/tools/catalog.js +118 -17
  184. package/dist/tools/catalog.js.map +1 -1
  185. package/dist/trade/service.d.ts +12 -0
  186. package/dist/trade/service.js +24 -7
  187. package/dist/trade/service.js.map +1 -1
  188. package/dist/tui/app.d.ts +23 -0
  189. package/dist/tui/app.js +322 -0
  190. package/dist/tui/app.js.map +1 -0
  191. package/dist/tui/commands.d.ts +70 -0
  192. package/dist/tui/commands.js +313 -0
  193. package/dist/tui/commands.js.map +1 -0
  194. package/dist/tui/entries.d.ts +17 -0
  195. package/dist/tui/entries.js +24 -0
  196. package/dist/tui/entries.js.map +1 -0
  197. package/dist/tui/execute.d.ts +26 -0
  198. package/dist/tui/execute.js +664 -0
  199. package/dist/tui/execute.js.map +1 -0
  200. package/dist/tui/history.d.ts +17 -0
  201. package/dist/tui/history.js +48 -0
  202. package/dist/tui/history.js.map +1 -0
  203. package/dist/tui/index.d.ts +8 -0
  204. package/dist/tui/index.js +48 -0
  205. package/dist/tui/index.js.map +1 -0
  206. package/dist/tui/line-editor.d.ts +44 -0
  207. package/dist/tui/line-editor.js +98 -0
  208. package/dist/tui/line-editor.js.map +1 -0
  209. package/dist/tui/progress.d.ts +23 -0
  210. package/dist/tui/progress.js +46 -0
  211. package/dist/tui/progress.js.map +1 -0
  212. package/dist/tui/settings-editor.d.ts +18 -0
  213. package/dist/tui/settings-editor.js +115 -0
  214. package/dist/tui/settings-editor.js.map +1 -0
  215. package/docs/live-trading.md +455 -0
  216. package/docs/strategy-research.md +608 -0
  217. package/package.json +10 -1
  218. package/python/desic_strategy/__init__.py +34 -0
  219. package/python/desic_strategy/actions.py +158 -0
  220. package/python/desic_strategy/context.py +171 -0
  221. package/python/desic_strategy/engine.py +614 -0
  222. package/python/desic_strategy/indicators.py +159 -0
  223. package/python/desic_strategy/live.py +253 -0
  224. package/python/desic_strategy/policy.py +193 -0
  225. package/python/desic_strategy/portfolio.py +152 -0
  226. package/python/desic_strategy/report.py +319 -0
  227. package/python/desic_strategy/runner.py +574 -0
  228. package/python/desic_strategy/timeframe.py +150 -0
  229. package/python/main.py +18 -0
  230. package/skills/okx-live-trading/SKILL.md +117 -0
  231. package/skills/okx-live-trading/agents/openai.yaml +9 -0
  232. package/skills/okx-live-trading/references/lifecycle.md +128 -0
  233. package/skills/okx-strategy-research/SKILL.md +113 -0
  234. package/skills/okx-strategy-research/agents/openai.yaml +9 -0
  235. package/skills/okx-strategy-research/references/execution-semantics.md +107 -0
  236. package/skills/okx-strategy-research/references/field-traps.md +142 -0
  237. package/skills/okx-strategy-research/references/python-api.md +125 -0
  238. package/skills/okx-strategy-research/references/tools-and-data.md +192 -0
  239. package/skills/okx-trading/SKILL.md +11 -10
@@ -0,0 +1,158 @@
1
+ """Strategy decisions.
2
+
3
+ A decision is an *intent*, never an order. It carries no contract count: the
4
+ host derives a legal size from its own budget and the instrument's rules, so a
5
+ strategy cannot size a position past the risk boundary.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass, field
11
+
12
+ NO_ACTION = "no_action"
13
+ OPEN_LONG = "open_long"
14
+ OPEN_SHORT = "open_short"
15
+ CLOSE_LONG = "close_long"
16
+ CLOSE_SHORT = "close_short"
17
+ CANCEL_ORDER = "cancel_order"
18
+
19
+ OPENING_ACTIONS = frozenset({OPEN_LONG, OPEN_SHORT})
20
+ CLOSING_ACTIONS = frozenset({CLOSE_LONG, CLOSE_SHORT})
21
+ ALL_ACTIONS = frozenset({NO_ACTION, *OPENING_ACTIONS, *CLOSING_ACTIONS, CANCEL_ORDER})
22
+
23
+ MARKET = "market"
24
+ LIMIT = "limit"
25
+
26
+
27
+ class StrategyError(Exception):
28
+ """Raised when strategy output violates the decision contract."""
29
+
30
+
31
+ @dataclass(frozen=True)
32
+ class Execution:
33
+ order_type: str
34
+ limit_price: float | None = None
35
+
36
+ def as_dict(self) -> dict[str, object]:
37
+ if self.order_type == MARKET:
38
+ return {"orderType": MARKET}
39
+ return {"orderType": LIMIT, "limitPrice": self.limit_price}
40
+
41
+
42
+ @dataclass(frozen=True)
43
+ class Protection:
44
+ """An optional *request* attached to an opening action, not an order."""
45
+
46
+ stop_loss_price: float | None = None
47
+ take_profit_price: float | None = None
48
+
49
+ def as_dict(self) -> dict[str, object]:
50
+ payload: dict[str, object] = {}
51
+ if self.stop_loss_price is not None:
52
+ payload["stopLossPrice"] = self.stop_loss_price
53
+ if self.take_profit_price is not None:
54
+ payload["takeProfitPrice"] = self.take_profit_price
55
+ return payload
56
+
57
+
58
+ @dataclass(frozen=True)
59
+ class Decision:
60
+ action: str
61
+ reason: str
62
+ execution: Execution = field(default_factory=lambda: Execution(MARKET))
63
+ protection: Protection | None = None
64
+ order_id: str | None = None
65
+
66
+ def as_dict(self) -> dict[str, object]:
67
+ payload: dict[str, object] = {"action": self.action, "reason": self.reason}
68
+ if self.action in OPENING_ACTIONS or self.action in CLOSING_ACTIONS:
69
+ payload["execution"] = self.execution.as_dict()
70
+ if self.protection is not None:
71
+ protection = self.protection.as_dict()
72
+ if protection:
73
+ payload["protection"] = protection
74
+ if self.order_id is not None:
75
+ payload["orderId"] = self.order_id
76
+ return payload
77
+
78
+
79
+ def market_order() -> Execution:
80
+ return Execution(MARKET)
81
+
82
+
83
+ def limit_order(limit_price: float) -> Execution:
84
+ price = _positive_price(limit_price, "limit_price")
85
+ return Execution(LIMIT, price)
86
+
87
+
88
+ def _positive_price(value: object, label: str) -> float:
89
+ try:
90
+ price = float(value) # type: ignore[arg-type]
91
+ except (TypeError, ValueError) as error:
92
+ raise StrategyError(f"{label} must be a number") from error
93
+ if price != price or price in (float("inf"), float("-inf")):
94
+ raise StrategyError(f"{label} must be finite")
95
+ if price <= 0:
96
+ raise StrategyError(f"{label} must be greater than zero")
97
+ return price
98
+
99
+
100
+ def _reason(value: object) -> str:
101
+ if not isinstance(value, str) or not value.strip():
102
+ raise StrategyError("Every decision requires a non-empty audit reason")
103
+ if len(value) > 500:
104
+ raise StrategyError("A decision reason must be at most 500 characters")
105
+ return value.strip()
106
+
107
+
108
+ def _protection(value: object) -> Protection | None:
109
+ """Accepts the documented mapping form: ``{"stopLossPrice": .., "takeProfitPrice": ..}``."""
110
+ if value is None:
111
+ return None
112
+ if not isinstance(value, dict):
113
+ raise StrategyError("protection must be a mapping with stopLossPrice and/or takeProfitPrice")
114
+ unknown = set(value) - {"stopLossPrice", "takeProfitPrice"}
115
+ if unknown:
116
+ raise StrategyError(f"Unknown protection field(s): {', '.join(sorted(unknown))}")
117
+ stop = value.get("stopLossPrice")
118
+ take = value.get("takeProfitPrice")
119
+ if stop is None and take is None:
120
+ raise StrategyError("protection requires stopLossPrice and/or takeProfitPrice")
121
+ return Protection(
122
+ stop_loss_price=None if stop is None else _positive_price(stop, "stopLossPrice"),
123
+ take_profit_price=None if take is None else _positive_price(take, "takeProfitPrice"),
124
+ )
125
+
126
+
127
+ def _execution(value: object) -> Execution:
128
+ if value is None:
129
+ return Execution(MARKET)
130
+ if isinstance(value, Execution):
131
+ return value
132
+ raise StrategyError("execution must be ctx.market_order() or ctx.limit_order(price)")
133
+
134
+
135
+ def no_action(reason: object = "no action") -> Decision:
136
+ return Decision(NO_ACTION, _reason(reason))
137
+
138
+
139
+ def open_long(reason: object, protection: object = None, execution: object = None) -> Decision:
140
+ return Decision(OPEN_LONG, _reason(reason), _execution(execution), _protection(protection))
141
+
142
+
143
+ def open_short(reason: object, protection: object = None, execution: object = None) -> Decision:
144
+ return Decision(OPEN_SHORT, _reason(reason), _execution(execution), _protection(protection))
145
+
146
+
147
+ def close_long(reason: object, execution: object = None) -> Decision:
148
+ return Decision(CLOSE_LONG, _reason(reason), _execution(execution))
149
+
150
+
151
+ def close_short(reason: object, execution: object = None) -> Decision:
152
+ return Decision(CLOSE_SHORT, _reason(reason), _execution(execution))
153
+
154
+
155
+ def cancel_order(order_id: object, reason: object) -> Decision:
156
+ if not isinstance(order_id, str) or not order_id.strip():
157
+ raise StrategyError("cancel_order requires the id of a current open order")
158
+ return Decision(CANCEL_ORDER, _reason(reason), order_id=order_id.strip())
@@ -0,0 +1,171 @@
1
+ """The immutable context handed to a strategy handler.
2
+
3
+ Every field is read-only and bounded by ``as_of_ms``. The market view holds the
4
+ full bar array but exposes only the visible prefix, so look-ahead is prevented
5
+ structurally rather than by convention.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from types import MappingProxyType
11
+ from typing import Mapping
12
+
13
+ from . import actions
14
+ from .actions import Decision, Execution
15
+ from .indicators import IndicatorCache, IndicatorView
16
+ from .portfolio import PortfolioView
17
+ from .timeframe import Bar, TimeframeAggregator, interval_ms
18
+
19
+
20
+ class ContextError(Exception):
21
+ """Raised when a strategy requests data outside its contract."""
22
+
23
+
24
+ class MarketView:
25
+ """Point-in-time K-line access across derived timeframes."""
26
+
27
+ __slots__ = ("_aggregates", "_instrument_id", "_minutes", "_visible")
28
+
29
+ def __init__(
30
+ self,
31
+ instrument_id: str,
32
+ minutes: list[Bar],
33
+ visible: int,
34
+ aggregates: Mapping[str, tuple[Bar, ...]],
35
+ ) -> None:
36
+ self._instrument_id = instrument_id
37
+ self._minutes = minutes
38
+ self._visible = visible
39
+ self._aggregates = aggregates
40
+
41
+ def bars(self, instrument_id: str, interval: str = "1m", lookback: int | None = None) -> tuple[Bar, ...]:
42
+ if instrument_id != self._instrument_id:
43
+ raise ContextError(
44
+ f"Market data is available for {self._instrument_id}, not {instrument_id}"
45
+ )
46
+ interval_ms(interval)
47
+ if lookback is not None and (
48
+ not isinstance(lookback, int) or isinstance(lookback, bool) or lookback <= 0
49
+ ):
50
+ raise ValueError("lookback must be a positive integer")
51
+ if interval == "1m":
52
+ # Sliced before the copy, not after. `_minutes` holds the whole run, so
53
+ # copying the visible prefix and then discarding all but the tail costs one
54
+ # copy of everything seen so far, on every bar — quadratic in the window
55
+ # length. Measured on a 260k-bar run: a strategy asking for 17 bars here
56
+ # took 576 us/bar, against 16 us/bar for one that never called this method.
57
+ stop = self._visible
58
+ start = 0 if lookback is None else max(0, stop - lookback)
59
+ return tuple(self._minutes[start:stop])
60
+ found = self._aggregates.get(interval)
61
+ if found is None:
62
+ raise ContextError(f"Series '{interval}' is not available in this run")
63
+ # Aggregates are already immutable tuples ending at the cutoff, so slicing one
64
+ # copies only what was asked for.
65
+ return found if lookback is None else found[-lookback:]
66
+
67
+
68
+ class StrategyContext:
69
+ """Read-only decision context for one cutoff."""
70
+
71
+ __slots__ = (
72
+ "_frozen",
73
+ "as_of_ms",
74
+ "bar",
75
+ "indicators",
76
+ "instrument_id",
77
+ "interval",
78
+ "kind",
79
+ "market",
80
+ "params",
81
+ "portfolio",
82
+ )
83
+
84
+ def __init__(
85
+ self,
86
+ *,
87
+ kind: str,
88
+ as_of_ms: int,
89
+ instrument_id: str,
90
+ interval: str,
91
+ bar: Bar | None,
92
+ market: MarketView,
93
+ portfolio: PortfolioView,
94
+ params: Mapping[str, object],
95
+ indicators: IndicatorView,
96
+ ) -> None:
97
+ object.__setattr__(self, "kind", kind)
98
+ object.__setattr__(self, "as_of_ms", as_of_ms)
99
+ object.__setattr__(self, "instrument_id", instrument_id)
100
+ object.__setattr__(self, "interval", interval)
101
+ object.__setattr__(self, "bar", bar)
102
+ object.__setattr__(self, "market", market)
103
+ object.__setattr__(self, "portfolio", portfolio)
104
+ object.__setattr__(self, "params", params)
105
+ object.__setattr__(self, "indicators", indicators)
106
+ object.__setattr__(self, "_frozen", True)
107
+
108
+ def __setattr__(self, name: str, value: object) -> None:
109
+ raise ContextError("The strategy context is read-only")
110
+
111
+ def __delattr__(self, name: str) -> None:
112
+ raise ContextError("The strategy context is read-only")
113
+
114
+ # Position lookup shortcut, matching `portfolio.position`.
115
+ def position(self, instrument_id: str, side: str): # noqa: ANN201 - returns Position | None
116
+ return self.portfolio.position(instrument_id, side)
117
+
118
+ def market_order(self) -> Execution:
119
+ return actions.market_order()
120
+
121
+ def limit_order(self, limit_price: float) -> Execution:
122
+ return actions.limit_order(limit_price)
123
+
124
+ def no_action(self, reason: object = "no action") -> Decision:
125
+ return actions.no_action(reason)
126
+
127
+ def open_long(self, reason: object, protection: object = None, execution: object = None) -> Decision:
128
+ return actions.open_long(reason, protection, execution)
129
+
130
+ def open_short(self, reason: object, protection: object = None, execution: object = None) -> Decision:
131
+ return actions.open_short(reason, protection, execution)
132
+
133
+ def close_long(self, reason: object, execution: object = None) -> Decision:
134
+ return actions.close_long(reason, execution)
135
+
136
+ def close_short(self, reason: object, execution: object = None) -> Decision:
137
+ return actions.close_short(reason, execution)
138
+
139
+ def cancel_order(self, order_id: object, reason: object) -> Decision:
140
+ return actions.cancel_order(order_id, reason)
141
+
142
+
143
+ def immutable_params(params: Mapping[str, object] | None) -> Mapping[str, object]:
144
+ return MappingProxyType(dict(params or {}))
145
+
146
+
147
+ class SeriesSet:
148
+ """Maintains one aggregator per requested higher timeframe."""
149
+
150
+ __slots__ = ("_aggregators", "_cache", "_instrument_id")
151
+
152
+ def __init__(self, instrument_id: str, intervals: list[str]) -> None:
153
+ self._instrument_id = instrument_id
154
+ self._aggregators = {
155
+ interval: TimeframeAggregator(interval)
156
+ for interval in intervals
157
+ if interval != "1m"
158
+ }
159
+ self._cache = IndicatorCache()
160
+
161
+ @property
162
+ def indicators(self) -> IndicatorCache:
163
+ return self._cache
164
+
165
+ def push(self, bar: Bar) -> None:
166
+ self._cache.append(bar)
167
+ for aggregator in self._aggregators.values():
168
+ aggregator.push(bar)
169
+
170
+ def snapshot(self) -> dict[str, tuple[Bar, ...]]:
171
+ return {interval: agg.snapshot() for interval, agg in self._aggregators.items()}