opencode-pyneruntime 6.6.4__py3-none-any.whl

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 (261) hide show
  1. opencode_pyneruntime-6.6.4.dist-info/METADATA +281 -0
  2. opencode_pyneruntime-6.6.4.dist-info/RECORD +261 -0
  3. opencode_pyneruntime-6.6.4.dist-info/WHEEL +5 -0
  4. opencode_pyneruntime-6.6.4.dist-info/entry_points.txt +6 -0
  5. opencode_pyneruntime-6.6.4.dist-info/licenses/LICENSE +201 -0
  6. opencode_pyneruntime-6.6.4.dist-info/licenses/NOTICE +21 -0
  7. opencode_pyneruntime-6.6.4.dist-info/top_level.txt +1 -0
  8. pynecore/__init__.py +6 -0
  9. pynecore/cli/__init__.py +2 -0
  10. pynecore/cli/app.py +238 -0
  11. pynecore/cli/commands/__init__.py +343 -0
  12. pynecore/cli/commands/benchmark.py +186 -0
  13. pynecore/cli/commands/compile.py +198 -0
  14. pynecore/cli/commands/data.py +857 -0
  15. pynecore/cli/commands/debug.py +63 -0
  16. pynecore/cli/commands/optimize.py +956 -0
  17. pynecore/cli/commands/plugin.py +242 -0
  18. pynecore/cli/commands/run.py +2006 -0
  19. pynecore/cli/pluggable.py +132 -0
  20. pynecore/cli/utils/__init__.py +0 -0
  21. pynecore/cli/utils/api_error_handler.py +168 -0
  22. pynecore/cli/utils/broker_picker.py +330 -0
  23. pynecore/cli/utils/error_hook.py +28 -0
  24. pynecore/cli/utils/keyreader.py +178 -0
  25. pynecore/cli/utils/provider_picker.py +19 -0
  26. pynecore/cli/utils/symbol_browser.py +1149 -0
  27. pynecore/core/__init__.py +0 -0
  28. pynecore/core/aggregator.py +257 -0
  29. pynecore/core/bar_magnifier.py +168 -0
  30. pynecore/core/broker/__init__.py +64 -0
  31. pynecore/core/broker/defaults.py +113 -0
  32. pynecore/core/broker/disappearance.py +927 -0
  33. pynecore/core/broker/emulator.py +345 -0
  34. pynecore/core/broker/exceptions.py +346 -0
  35. pynecore/core/broker/idempotency.py +401 -0
  36. pynecore/core/broker/intent_builder.py +334 -0
  37. pynecore/core/broker/journal.py +1785 -0
  38. pynecore/core/broker/models.py +1600 -0
  39. pynecore/core/broker/native_failsafe_manager.py +1436 -0
  40. pynecore/core/broker/one_way_emulator.py +1128 -0
  41. pynecore/core/broker/position.py +787 -0
  42. pynecore/core/broker/run_identity.py +126 -0
  43. pynecore/core/broker/software_entry_stop_engine.py +351 -0
  44. pynecore/core/broker/software_partial_bracket_engine.py +1379 -0
  45. pynecore/core/broker/spot_inventory.py +1327 -0
  46. pynecore/core/broker/storage.py +2655 -0
  47. pynecore/core/broker/store_helpers.py +2161 -0
  48. pynecore/core/broker/sync_engine.py +16070 -0
  49. pynecore/core/broker/validation.py +382 -0
  50. pynecore/core/class_property.py +7 -0
  51. pynecore/core/config.py +392 -0
  52. pynecore/core/csv_file.py +547 -0
  53. pynecore/core/currency.py +262 -0
  54. pynecore/core/data_converter.py +1002 -0
  55. pynecore/core/datetime.py +296 -0
  56. pynecore/core/download_info.py +71 -0
  57. pynecore/core/download_runner.py +274 -0
  58. pynecore/core/htf_aggregator.py +181 -0
  59. pynecore/core/import_hook.py +358 -0
  60. pynecore/core/instance_state.py +494 -0
  61. pynecore/core/live_ltf_collector.py +442 -0
  62. pynecore/core/live_ltf_window.py +189 -0
  63. pynecore/core/live_runner.py +1347 -0
  64. pynecore/core/module_property.py +26 -0
  65. pynecore/core/ohlcv_file.py +1888 -0
  66. pynecore/core/overload.py +371 -0
  67. pynecore/core/pine_cast.py +113 -0
  68. pynecore/core/pine_export.py +95 -0
  69. pynecore/core/pine_method.py +244 -0
  70. pynecore/core/pine_range.py +86 -0
  71. pynecore/core/pine_udt.py +69 -0
  72. pynecore/core/plugin/__init__.py +394 -0
  73. pynecore/core/plugin/broker.py +781 -0
  74. pynecore/core/plugin/cli.py +96 -0
  75. pynecore/core/plugin/live_provider.py +208 -0
  76. pynecore/core/plugin/provider.py +331 -0
  77. pynecore/core/provider_string.py +148 -0
  78. pynecore/core/random.py +40 -0
  79. pynecore/core/resampler.py +686 -0
  80. pynecore/core/safe_convert.py +64 -0
  81. pynecore/core/script.py +1011 -0
  82. pynecore/core/script_runner.py +3202 -0
  83. pynecore/core/security.py +1749 -0
  84. pynecore/core/security_process.py +1253 -0
  85. pynecore/core/security_shm.py +456 -0
  86. pynecore/core/series.py +417 -0
  87. pynecore/core/strategy_stats.py +669 -0
  88. pynecore/core/symbol_map.py +134 -0
  89. pynecore/core/syminfo.py +505 -0
  90. pynecore/core/viz.py +591 -0
  91. pynecore/lib/__init__.py +1771 -0
  92. pynecore/lib/_fixnan.py +32 -0
  93. pynecore/lib/_math_stateful.py +202 -0
  94. pynecore/lib/_timeframe_change.py +101 -0
  95. pynecore/lib/adjustment.py +6 -0
  96. pynecore/lib/alert.py +39 -0
  97. pynecore/lib/alert.pyi +14 -0
  98. pynecore/lib/array.py +1051 -0
  99. pynecore/lib/barmerge.py +60 -0
  100. pynecore/lib/barstate.py +30 -0
  101. pynecore/lib/box.py +415 -0
  102. pynecore/lib/chart.py +128 -0
  103. pynecore/lib/color.py +152 -0
  104. pynecore/lib/color.pyi +50 -0
  105. pynecore/lib/currency.py +62 -0
  106. pynecore/lib/dayofweek.py +36 -0
  107. pynecore/lib/dayofweek.pyi +18 -0
  108. pynecore/lib/display.py +8 -0
  109. pynecore/lib/dividends.py +9 -0
  110. pynecore/lib/earnings.py +11 -0
  111. pynecore/lib/extend.py +6 -0
  112. pynecore/lib/font.py +5 -0
  113. pynecore/lib/footprint.py +79 -0
  114. pynecore/lib/format.py +11 -0
  115. pynecore/lib/hline.py +67 -0
  116. pynecore/lib/hline.pyi +24 -0
  117. pynecore/lib/label.py +409 -0
  118. pynecore/lib/line.py +433 -0
  119. pynecore/lib/linefill.py +93 -0
  120. pynecore/lib/location.py +11 -0
  121. pynecore/lib/log.py +362 -0
  122. pynecore/lib/map.py +150 -0
  123. pynecore/lib/math.py +385 -0
  124. pynecore/lib/matrix.py +708 -0
  125. pynecore/lib/order.py +8 -0
  126. pynecore/lib/pivotpointtype.py +8 -0
  127. pynecore/lib/plot.py +95 -0
  128. pynecore/lib/plot.pyi +33 -0
  129. pynecore/lib/polyline.py +91 -0
  130. pynecore/lib/position.py +15 -0
  131. pynecore/lib/request.py +281 -0
  132. pynecore/lib/runtime.py +5 -0
  133. pynecore/lib/scale.py +9 -0
  134. pynecore/lib/session.py +267 -0
  135. pynecore/lib/session.pyi +12 -0
  136. pynecore/lib/shape.py +18 -0
  137. pynecore/lib/size.py +12 -0
  138. pynecore/lib/splits.py +4 -0
  139. pynecore/lib/strategy/__init__.py +4778 -0
  140. pynecore/lib/strategy/closedtrades.py +347 -0
  141. pynecore/lib/strategy/closedtrades.pyi +53 -0
  142. pynecore/lib/strategy/commission.py +9 -0
  143. pynecore/lib/strategy/direction.py +9 -0
  144. pynecore/lib/strategy/oca.py +13 -0
  145. pynecore/lib/strategy/opentrades.py +281 -0
  146. pynecore/lib/strategy/opentrades.pyi +49 -0
  147. pynecore/lib/strategy/risk.py +109 -0
  148. pynecore/lib/string.py +649 -0
  149. pynecore/lib/syminfo.py +84 -0
  150. pynecore/lib/ta.py +2230 -0
  151. pynecore/lib/table.py +290 -0
  152. pynecore/lib/text.py +17 -0
  153. pynecore/lib/ticker.py +207 -0
  154. pynecore/lib/timeframe.py +293 -0
  155. pynecore/lib/volume_row.py +67 -0
  156. pynecore/lib/xloc.py +4 -0
  157. pynecore/lib/yloc.py +5 -0
  158. pynecore/providers/__init__.py +0 -0
  159. pynecore/providers/ccxt.py +664 -0
  160. pynecore/providers/replay.py +187 -0
  161. pynecore/pynesys/__init__.py +0 -0
  162. pynecore/pynesys/api.py +498 -0
  163. pynecore/pynesys/compiler.py +112 -0
  164. pynecore/standalone.py +99 -0
  165. pynecore/testing/__init__.py +1 -0
  166. pynecore/testing/broker_lab/__init__.py +41 -0
  167. pynecore/testing/broker_lab/__main__.py +5 -0
  168. pynecore/testing/broker_lab/cli.py +87 -0
  169. pynecore/testing/broker_lab/generate.py +47 -0
  170. pynecore/testing/broker_lab/model.py +84 -0
  171. pynecore/testing/broker_lab/reference.py +645 -0
  172. pynecore/testing/broker_lab/runner.py +372 -0
  173. pynecore/testing/broker_lab/scheduler.py +50 -0
  174. pynecore/testing/broker_lab/subprocess.py +73 -0
  175. pynecore/transformers/__init__.py +0 -0
  176. pynecore/transformers/builtin_shadow.py +136 -0
  177. pynecore/transformers/closure_arguments_transformer.py +428 -0
  178. pynecore/transformers/display_rewrite.py +140 -0
  179. pynecore/transformers/dynamic_default.py +147 -0
  180. pynecore/transformers/function_isolation.py +757 -0
  181. pynecore/transformers/import_lifter.py +61 -0
  182. pynecore/transformers/import_normalizer.py +328 -0
  183. pynecore/transformers/inline_series_hoist.py +178 -0
  184. pynecore/transformers/input_transformer.py +175 -0
  185. pynecore/transformers/lib_series.py +201 -0
  186. pynecore/transformers/locations.py +70 -0
  187. pynecore/transformers/module_properties.json +3387 -0
  188. pynecore/transformers/module_property.py +221 -0
  189. pynecore/transformers/ne_guard.py +70 -0
  190. pynecore/transformers/persistent.py +320 -0
  191. pynecore/transformers/persistent_series.py +76 -0
  192. pynecore/transformers/safe_convert_transformer.py +97 -0
  193. pynecore/transformers/safe_division_transformer.py +95 -0
  194. pynecore/transformers/script_requirements.py +308 -0
  195. pynecore/transformers/security.py +752 -0
  196. pynecore/transformers/security_instantiation.py +274 -0
  197. pynecore/transformers/series.py +275 -0
  198. pynecore/transformers/slot_layout.py +381 -0
  199. pynecore/transformers/type_checking_stripper.py +25 -0
  200. pynecore/transformers/unused_series_detector.py +267 -0
  201. pynecore/types/__init__.py +21 -0
  202. pynecore/types/alert.py +5 -0
  203. pynecore/types/barmerge.py +5 -0
  204. pynecore/types/base.py +39 -0
  205. pynecore/types/box.py +37 -0
  206. pynecore/types/chart.py +17 -0
  207. pynecore/types/color.py +107 -0
  208. pynecore/types/currency.py +5 -0
  209. pynecore/types/datetime.py +6 -0
  210. pynecore/types/display.py +5 -0
  211. pynecore/types/dividends.py +5 -0
  212. pynecore/types/earnings.py +5 -0
  213. pynecore/types/extend.py +5 -0
  214. pynecore/types/font.py +5 -0
  215. pynecore/types/footprint.py +41 -0
  216. pynecore/types/format.py +5 -0
  217. pynecore/types/hline.py +24 -0
  218. pynecore/types/ib_persistent.py +8 -0
  219. pynecore/types/ib_persistent.pyi +10 -0
  220. pynecore/types/label.py +35 -0
  221. pynecore/types/line.py +32 -0
  222. pynecore/types/linefill.py +13 -0
  223. pynecore/types/location.py +5 -0
  224. pynecore/types/matrix.py +999 -0
  225. pynecore/types/na.py +237 -0
  226. pynecore/types/na.pyi +83 -0
  227. pynecore/types/ohlcv.py +12 -0
  228. pynecore/types/order.py +5 -0
  229. pynecore/types/persistent.py +8 -0
  230. pynecore/types/persistent.pyi +13 -0
  231. pynecore/types/pine_types.py +11 -0
  232. pynecore/types/pine_types.pyi +15 -0
  233. pynecore/types/pivotpointtype.py +5 -0
  234. pynecore/types/plot.py +12 -0
  235. pynecore/types/plot_meta.py +60 -0
  236. pynecore/types/polyline.py +40 -0
  237. pynecore/types/position.py +5 -0
  238. pynecore/types/scale.py +5 -0
  239. pynecore/types/script_type.py +15 -0
  240. pynecore/types/series.py +23 -0
  241. pynecore/types/series.pyi +19 -0
  242. pynecore/types/session.py +35 -0
  243. pynecore/types/shape.py +5 -0
  244. pynecore/types/size.py +5 -0
  245. pynecore/types/source.py +33 -0
  246. pynecore/types/splits.py +5 -0
  247. pynecore/types/strategy.py +45 -0
  248. pynecore/types/table.py +87 -0
  249. pynecore/types/text.py +13 -0
  250. pynecore/types/type_checker.py +7 -0
  251. pynecore/types/type_checker.pyi +48 -0
  252. pynecore/types/volume_row.py +36 -0
  253. pynecore/types/weekdays.py +11 -0
  254. pynecore/types/xloc.py +5 -0
  255. pynecore/types/yloc.py +5 -0
  256. pynecore/utils/__init__.py +0 -0
  257. pynecore/utils/file_utils.py +50 -0
  258. pynecore/utils/rich/__init__.py +0 -0
  259. pynecore/utils/rich/date_column.py +25 -0
  260. pynecore/utils/sequence_view.py +92 -0
  261. pynecore/utils/stdlib_checker.py +17 -0
@@ -0,0 +1,645 @@
1
+ """Small reference venue used by examples and core conformance scenarios."""
2
+
3
+ from collections.abc import Sequence
4
+ from dataclasses import dataclass, field, replace
5
+ from typing import Any
6
+
7
+ from pynecore.core.broker.exceptions import (
8
+ ExchangeConnectionError,
9
+ ExchangeOrderRejectedError,
10
+ OrderSkippedByPlugin,
11
+ )
12
+ from pynecore.core.broker.models import (
13
+ CapabilityLevel,
14
+ DispatchEnvelope,
15
+ ExchangeCapabilities,
16
+ ExchangeOrder,
17
+ ExchangePosition,
18
+ INTENT_KEY_SEP,
19
+ LegType,
20
+ OrderEvent,
21
+ OrderStatus,
22
+ OrderType,
23
+ )
24
+
25
+ from .model import Step
26
+
27
+
28
+ @dataclass
29
+ class VenueOrder:
30
+ """Reference venue ownership record."""
31
+
32
+ order: ExchangeOrder
33
+ run_name: str
34
+ pine_id: str
35
+ leg_type: LegType
36
+ intent_key: str
37
+ from_entry: str | None = None
38
+
39
+
40
+ @dataclass
41
+ class VenueState:
42
+ """Shared account state for one or more reference brokers."""
43
+
44
+ symbol: str = "LABUSD"
45
+ orders: dict[str, VenueOrder] = field(default_factory=dict)
46
+ position: float = 0.0
47
+ position_owners: dict[str, float] = field(default_factory=dict)
48
+ position_legs: dict[tuple[str, str], float] = field(default_factory=dict)
49
+ pending_events: list[tuple[str, OrderEvent]] = field(default_factory=list)
50
+ calls: list[tuple[str, str, str]] = field(default_factory=list)
51
+ next_id: int = 1
52
+ reject_entries: int = 0
53
+ skip_entries: int = 0
54
+ read_error: Exception | None = None
55
+ stale_position: float | None = None
56
+ terminal_snapshots: dict[str, tuple[OrderStatus, float, float]] = field(
57
+ default_factory=dict
58
+ )
59
+
60
+ def new_id(self) -> str:
61
+ value = f"lab-{self.next_id}"
62
+ self.next_id += 1
63
+ return value
64
+
65
+
66
+ class ReferenceBroker:
67
+ """BrokerPlugin-shaped adapter over :class:`VenueState`."""
68
+
69
+ client_order_id_max_len = 30
70
+ on_unexpected_cancel = "stop"
71
+ position_port = None
72
+
73
+ def __init__(self, profile: "ReferenceVenueProfile", run_name: str) -> None:
74
+ self.profile = profile
75
+ self.state = profile.state
76
+ self.run_name = run_name
77
+ self.store_ctx: Any = None
78
+
79
+ def get_capabilities(self) -> ExchangeCapabilities:
80
+ return ExchangeCapabilities(short_selling=CapabilityLevel.NATIVE)
81
+
82
+ def _place(
83
+ self, envelope: DispatchEnvelope, leg: LegType, kind: str
84
+ ) -> list[ExchangeOrder]:
85
+ self.state.calls.append((self.run_name, kind, envelope.intent.intent_key))
86
+ if leg is LegType.ENTRY and self.state.skip_entries:
87
+ self.state.skip_entries -= 1
88
+ raise OrderSkippedByPlugin(
89
+ "reference profile skipped entry",
90
+ intent_key=envelope.intent.intent_key,
91
+ reason="below_min_size",
92
+ )
93
+ if leg is LegType.ENTRY and self.state.reject_entries:
94
+ self.state.reject_entries -= 1
95
+ raise ExchangeOrderRejectedError("reference profile rejected entry")
96
+ intent = envelope.intent
97
+ order = ExchangeOrder(
98
+ id=self.state.new_id(),
99
+ symbol=self.profile.wire_symbol,
100
+ side=getattr(intent, "side", "buy"),
101
+ order_type=(
102
+ OrderType.STOP
103
+ if getattr(intent, "stop", None) is not None
104
+ else (
105
+ OrderType.LIMIT
106
+ if getattr(intent, "limit", None) is not None
107
+ else OrderType.MARKET
108
+ )
109
+ ),
110
+ qty=float(getattr(intent, "qty", 0.0)),
111
+ filled_qty=0.0,
112
+ remaining_qty=float(getattr(intent, "qty", 0.0)),
113
+ price=getattr(intent, "limit", None),
114
+ stop_price=getattr(intent, "stop", None),
115
+ average_fill_price=None,
116
+ status=OrderStatus.OPEN,
117
+ timestamp=0.0,
118
+ fee=0.0,
119
+ fee_currency="",
120
+ client_order_id=envelope.client_order_id(kind),
121
+ )
122
+ pine_id = str(getattr(intent, "pine_id", ""))
123
+ if self.store_ctx is not None:
124
+ self.store_ctx.upsert_order(
125
+ order.client_order_id,
126
+ symbol=order.symbol,
127
+ side=order.side,
128
+ qty=order.qty,
129
+ state="confirmed",
130
+ intent_key=intent.intent_key,
131
+ exchange_order_id=order.id,
132
+ from_entry=getattr(intent, "from_entry", None),
133
+ pine_entry_id=pine_id or None,
134
+ filled_qty=0.0,
135
+ )
136
+ self.state.orders[order.id] = VenueOrder(
137
+ order=order,
138
+ run_name=self.run_name,
139
+ pine_id=pine_id,
140
+ leg_type=leg,
141
+ intent_key=intent.intent_key,
142
+ from_entry=getattr(intent, "from_entry", None),
143
+ )
144
+ return [order]
145
+
146
+ async def execute_entry(self, envelope: DispatchEnvelope) -> list[ExchangeOrder]:
147
+ return self._place(envelope, LegType.ENTRY, "e")
148
+
149
+ async def execute_exit(self, envelope: DispatchEnvelope) -> list[ExchangeOrder]:
150
+ intent = envelope.intent
151
+ limit = getattr(intent, "limit", None)
152
+ stop = getattr(intent, "stop", None)
153
+ orders: list[ExchangeOrder] = []
154
+ if limit is not None:
155
+ take_profit = self._place(envelope, LegType.TAKE_PROFIT, "t")[0]
156
+ take_profit = replace(
157
+ take_profit,
158
+ order_type=OrderType.LIMIT,
159
+ price=limit,
160
+ stop_price=None,
161
+ )
162
+ self.state.orders[take_profit.id].order = take_profit
163
+ orders.append(take_profit)
164
+ if stop is not None:
165
+ stop_loss = self._place(envelope, LegType.STOP_LOSS, "s")[0]
166
+ stop_loss = replace(
167
+ stop_loss,
168
+ order_type=OrderType.STOP,
169
+ price=None,
170
+ stop_price=stop,
171
+ )
172
+ self.state.orders[stop_loss.id].order = stop_loss
173
+ orders.append(stop_loss)
174
+ return orders or self._place(envelope, LegType.TAKE_PROFIT, "t")
175
+
176
+ async def execute_close(self, envelope: DispatchEnvelope) -> ExchangeOrder:
177
+ return self._place(envelope, LegType.CLOSE, "c")[0]
178
+
179
+ async def execute_cancel(self, envelope: DispatchEnvelope) -> bool:
180
+ self.state.calls.append((self.run_name, "cancel", envelope.intent.intent_key))
181
+ for venue_order in self.state.orders.values():
182
+ if (
183
+ venue_order.run_name == self.run_name
184
+ and venue_order.intent_key == envelope.intent.intent_key
185
+ and venue_order.order.status
186
+ in (OrderStatus.OPEN, OrderStatus.PARTIALLY_FILLED)
187
+ ):
188
+ venue_order.order = replace(
189
+ venue_order.order,
190
+ status=OrderStatus.CANCELLED,
191
+ remaining_qty=0.0,
192
+ )
193
+ return True
194
+
195
+ async def modify_entry(
196
+ self,
197
+ old: DispatchEnvelope,
198
+ new: DispatchEnvelope,
199
+ ) -> list[ExchangeOrder]:
200
+ await self.execute_cancel(old)
201
+ return await self.execute_entry(new)
202
+
203
+ async def modify_exit(
204
+ self,
205
+ old: DispatchEnvelope,
206
+ new: DispatchEnvelope,
207
+ ) -> list[ExchangeOrder]:
208
+ await self.execute_cancel(old)
209
+ return await self.execute_exit(new)
210
+
211
+ async def get_open_orders(self, symbol: str | None = None) -> list[ExchangeOrder]:
212
+ if self.state.read_error is not None:
213
+ error = self.state.read_error
214
+ self.state.read_error = None
215
+ raise error
216
+ return [
217
+ record.order
218
+ for record in self.state.orders.values()
219
+ if record.run_name == self.run_name
220
+ and record.order.status is OrderStatus.OPEN
221
+ and (
222
+ symbol is None
223
+ or symbol == self.profile.symbol
224
+ or record.order.symbol == symbol
225
+ )
226
+ ]
227
+
228
+ async def get_position(self, symbol: str) -> ExchangePosition | None:
229
+ if self.state.read_error is not None:
230
+ error = self.state.read_error
231
+ self.state.read_error = None
232
+ raise error
233
+ size = (
234
+ self.state.position
235
+ if self.state.stale_position is None
236
+ else self.state.stale_position
237
+ )
238
+ if size == 0.0:
239
+ return None
240
+ return ExchangePosition(
241
+ symbol=symbol,
242
+ side="long" if size > 0 else "short",
243
+ size=abs(size),
244
+ entry_price=100.0,
245
+ unrealized_pnl=0.0,
246
+ liquidation_price=None,
247
+ leverage=1.0,
248
+ margin_mode="cross",
249
+ )
250
+
251
+ def watch_orders(self):
252
+ raise NotImplementedError
253
+
254
+
255
+ class ReferenceVenueProfile:
256
+ """Deterministic netting profile suitable for copied plugin examples."""
257
+
258
+ plugin_name = "broker-lab-reference"
259
+ account_id = "offline-account"
260
+ symbol = "LABUSD"
261
+ wire_symbol = "LABUSD"
262
+ timeframe = "1"
263
+ quantity_step = 1.0
264
+ venue_mode = "netting"
265
+
266
+ def __init__(self) -> None:
267
+ self.state = VenueState(symbol=self.symbol)
268
+
269
+ def create_broker(self, run_name: str, store_ctx: Any) -> ReferenceBroker:
270
+ broker = ReferenceBroker(self, run_name)
271
+ broker.store_ctx = store_ctx
272
+ return broker
273
+
274
+ def handle_step(self, runner: Any, step: Step) -> bool:
275
+ if step.kind == "fill":
276
+ self._fill(runner, step)
277
+ elif step.kind in ("deliver", "delayed_deliver"):
278
+ self._deliver(runner, step)
279
+ elif step.kind == "duplicate_event":
280
+ if not self.state.pending_events:
281
+ raise ValueError("duplicate_event requires a pending event")
282
+ self.state.pending_events.append(self.state.pending_events[-1])
283
+ elif step.kind == "reorder_events":
284
+ self.state.pending_events.reverse()
285
+ elif step.kind == "drop_events":
286
+ self.state.pending_events.clear()
287
+ elif step.kind == "reject_entries":
288
+ self.state.reject_entries = int(step.values.get("count", 1))
289
+ elif step.kind == "skip_entries":
290
+ self.state.skip_entries = int(step.values.get("count", 1))
291
+ elif step.kind == "read_error":
292
+ self.state.read_error = ExchangeConnectionError("injected read failure")
293
+ elif step.kind == "stale_position":
294
+ self.state.stale_position = float(step.values.get("position", 0.0))
295
+ elif step.kind == "fresh_position":
296
+ self.state.stale_position = None
297
+ elif step.kind == "venue_cancel":
298
+ self._venue_cancel(step)
299
+ elif step.kind == "expect":
300
+ self._expect(runner, step)
301
+ else:
302
+ return False
303
+ return True
304
+
305
+ def _select_order(self, step: Step) -> VenueOrder:
306
+ order_id = step.values.get("order_id")
307
+ candidates = [
308
+ record
309
+ for record in self.state.orders.values()
310
+ if record.run_name == step.run
311
+ and (order_id is None or record.order.id == order_id)
312
+ and record.order.status in (OrderStatus.OPEN, OrderStatus.PARTIALLY_FILLED)
313
+ ]
314
+ if not candidates:
315
+ raise ValueError(f"no open venue order for run {step.run!r}")
316
+ return candidates[-1]
317
+
318
+ def _fill(self, runner: Any, step: Step) -> None:
319
+ record = self._select_order(step)
320
+ qty = float(step.values.get("qty", record.order.remaining_qty))
321
+ if qty <= 0.0 or qty > record.order.remaining_qty + 1e-12:
322
+ raise ValueError(
323
+ "fill qty must be positive and at most the remaining quantity"
324
+ )
325
+ remaining = max(0.0, record.order.remaining_qty - qty)
326
+ filled = record.order.filled_qty + qty
327
+ status = (
328
+ OrderStatus.FILLED if remaining == 0.0 else OrderStatus.PARTIALLY_FILLED
329
+ )
330
+ price = float(step.values.get("price", 100.0))
331
+ record.order = replace(
332
+ record.order,
333
+ filled_qty=filled,
334
+ remaining_qty=remaining,
335
+ average_fill_price=price,
336
+ status=status,
337
+ )
338
+ runtime = runner.runs[step.run]
339
+ runtime.store_ctx.set_filled(record.order.client_order_id, filled)
340
+ signed = qty if record.order.side == "buy" else -qty
341
+ if record.leg_type is LegType.ENTRY:
342
+ self.state.position_owners[record.run_name] = (
343
+ self.state.position_owners.get(record.run_name, 0.0) + signed
344
+ )
345
+ if self.venue_mode == "hedged":
346
+ key = (record.run_name, record.pine_id)
347
+ self.state.position_legs[key] = (
348
+ self.state.position_legs.get(key, 0.0) + signed
349
+ )
350
+ else:
351
+ current = self.state.position_owners.get(record.run_name, 0.0)
352
+ new_size = current + signed
353
+ if current != 0.0 and current * new_size < 0.0:
354
+ new_size = 0.0
355
+ self.state.position_owners[record.run_name] = new_size
356
+ if self.venue_mode == "hedged" and record.from_entry is not None:
357
+ key = (record.run_name, record.from_entry)
358
+ leg_size = self.state.position_legs.get(key, 0.0)
359
+ reduced = leg_size + signed
360
+ self.state.position_legs[key] = (
361
+ 0.0 if leg_size * reduced < 0.0 else reduced
362
+ )
363
+ self.state.position = sum(self.state.position_owners.values())
364
+ event = OrderEvent(
365
+ order=record.order,
366
+ event_type="filled" if remaining == 0.0 else "partial",
367
+ fill_price=price,
368
+ fill_qty=qty,
369
+ timestamp=runner.now_ms / 1000.0,
370
+ pine_id=record.pine_id,
371
+ leg_type=record.leg_type,
372
+ fill_id=str(step.values.get("fill_id", f"{record.order.id}:{filled}")),
373
+ )
374
+ self.state.pending_events.append((record.run_name, event))
375
+
376
+ def _deliver(self, runner: Any, step: Step) -> None:
377
+ selected = [item for item in self.state.pending_events if item[0] == step.run]
378
+ self.state.pending_events = [
379
+ item for item in self.state.pending_events if item[0] != step.run
380
+ ]
381
+
382
+ def dispatch() -> None:
383
+ for _, event in selected:
384
+ runtime = runner.runs[step.run]
385
+ runtime.engine.on_order_event(event)
386
+ if selected:
387
+ runner.runs[step.run].engine.apply_async_events()
388
+
389
+ delay_ms = int(step.values.get("delay_ms", 0))
390
+ if delay_ms:
391
+ runner.scheduler.schedule(delay_ms, dispatch)
392
+ else:
393
+ dispatch()
394
+
395
+ def _venue_cancel(self, step: Step) -> None:
396
+ record = self._select_order(step)
397
+ record.order = replace(
398
+ record.order, status=OrderStatus.CANCELLED, remaining_qty=0.0
399
+ )
400
+
401
+ def _expect(self, runner: Any, step: Step) -> None:
402
+ values = step.values
403
+ if "calls" in values and len(self.state.calls) != int(values["calls"]):
404
+ raise AssertionError(
405
+ f"expected {values['calls']} broker calls, got {len(self.state.calls)}"
406
+ )
407
+ if "position" in values:
408
+ actual = self.state.position_owners.get(step.run, 0.0)
409
+ if abs(actual - float(values["position"])) > 1e-9:
410
+ raise AssertionError(
411
+ f"expected venue position {values['position']}, got {actual}"
412
+ )
413
+ if "account_position" in values:
414
+ actual = self.state.position
415
+ if abs(actual - float(values["account_position"])) > 1e-9:
416
+ raise AssertionError(
417
+ f"expected account position {values['account_position']}, got {actual}"
418
+ )
419
+ if "engine_position" in values:
420
+ actual = runner.runs[step.run].position.size
421
+ if abs(actual - float(values["engine_position"])) > 1e-9:
422
+ raise AssertionError(
423
+ f"expected engine position {values['engine_position']}, got {actual}"
424
+ )
425
+ if "open_orders" in values:
426
+ actual = sum(
427
+ record.run_name == step.run
428
+ and record.order.status
429
+ in (OrderStatus.OPEN, OrderStatus.PARTIALLY_FILLED)
430
+ for record in self.state.orders.values()
431
+ )
432
+ if actual != int(values["open_orders"]):
433
+ raise AssertionError(
434
+ f"expected {values['open_orders']} open orders, got {actual}"
435
+ )
436
+ if "total_orders" in values:
437
+ actual = sum(
438
+ record.run_name == step.run for record in self.state.orders.values()
439
+ )
440
+ if actual != int(values["total_orders"]):
441
+ raise AssertionError(
442
+ f"expected {values['total_orders']} total orders, got {actual}"
443
+ )
444
+ if values.get("distinct_order_ids"):
445
+ ids = [
446
+ record.order.id
447
+ for record in self.state.orders.values()
448
+ if record.run_name == step.run
449
+ ]
450
+ if len(ids) != len(set(ids)):
451
+ raise AssertionError(f"venue order ids are not distinct: {ids}")
452
+ if "wire_symbols" in values:
453
+ actual_symbols = {
454
+ record.order.symbol
455
+ for record in self.state.orders.values()
456
+ if record.run_name == step.run
457
+ }
458
+ expected_symbols = set(values["wire_symbols"])
459
+ if actual_symbols != expected_symbols:
460
+ raise AssertionError(
461
+ f"expected wire symbols {expected_symbols}, got {actual_symbols}"
462
+ )
463
+
464
+ def check_invariants(self, runner: Any) -> Sequence[str]:
465
+ violations: list[str] = []
466
+ live_coids: dict[str, str] = {}
467
+ active_intents: set[tuple[Any, ...]] = set()
468
+ call_counts: dict[tuple[str, str, str], int] = {}
469
+ for call in self.state.calls:
470
+ call_counts[call] = call_counts.get(call, 0) + 1
471
+ for call, count in call_counts.items():
472
+ if count > 3:
473
+ violations.append(
474
+ f"bounded retry exceeded for {call}: {count} dispatches"
475
+ )
476
+ for record in self.state.orders.values():
477
+ order = record.order
478
+ if record.run_name not in runner.runs:
479
+ violations.append(
480
+ f"order {order.id} is owned by unknown run {record.run_name}"
481
+ )
482
+ if order.qty < 0.0 or order.filled_qty < 0.0 or order.remaining_qty < 0.0:
483
+ violations.append(f"negative venue quantity on {order.id}")
484
+ if self.quantity_step > 0.0:
485
+ units = order.qty / self.quantity_step
486
+ if abs(units - round(units)) > 1e-8:
487
+ violations.append(
488
+ f"quantity-grid residual on {order.id}: qty={order.qty} "
489
+ f"step={self.quantity_step}"
490
+ )
491
+ if (
492
+ order.status in (OrderStatus.FILLED, OrderStatus.CANCELLED)
493
+ and order.remaining_qty != 0.0
494
+ ):
495
+ violations.append(
496
+ f"terminal order {order.id} retains remaining quantity"
497
+ )
498
+ terminal = self.state.terminal_snapshots.get(order.id)
499
+ if terminal is not None:
500
+ if order.status not in (
501
+ OrderStatus.FILLED,
502
+ OrderStatus.CANCELLED,
503
+ OrderStatus.REJECTED,
504
+ ):
505
+ violations.append(
506
+ f"terminal order {order.id} resurrected as {order.status.value}"
507
+ )
508
+ elif terminal != (order.status, order.filled_qty, order.qty):
509
+ violations.append(
510
+ f"terminal order {order.id} was modified after terminalization"
511
+ )
512
+ elif order.status in (
513
+ OrderStatus.FILLED,
514
+ OrderStatus.CANCELLED,
515
+ OrderStatus.REJECTED,
516
+ ):
517
+ self.state.terminal_snapshots[order.id] = (
518
+ order.status,
519
+ order.filled_qty,
520
+ order.qty,
521
+ )
522
+ if order.client_order_id and order.status in (
523
+ OrderStatus.OPEN,
524
+ OrderStatus.PARTIALLY_FILLED,
525
+ ):
526
+ previous = live_coids.setdefault(
527
+ order.client_order_id, record.intent_key
528
+ )
529
+ if previous != record.intent_key:
530
+ violations.append(
531
+ f"client order id {order.client_order_id} aliases two intents"
532
+ )
533
+ intent_owner = (
534
+ record.run_name,
535
+ record.intent_key,
536
+ record.leg_type,
537
+ order.side,
538
+ order.qty,
539
+ order.price,
540
+ order.stop_price,
541
+ )
542
+ if intent_owner in active_intents:
543
+ violations.append(
544
+ f"economic idempotence violated for {record.run_name} "
545
+ f"intent={record.intent_key} leg={record.leg_type.value}"
546
+ )
547
+ active_intents.add(intent_owner)
548
+ if not self.state.pending_events:
549
+ owned_total = sum(self.state.position_owners.values())
550
+ if abs(owned_total - self.state.position) > 1e-9:
551
+ violations.append(
552
+ f"account position ownership mismatch: owners={owned_total} "
553
+ f"account={self.state.position}"
554
+ )
555
+ engine_total = sum(
556
+ runtime.position.size for runtime in runner.runs.values()
557
+ )
558
+ if abs(engine_total - self.state.position) > 1e-9:
559
+ violations.append(
560
+ f"economic account position mismatch: engines={engine_total} "
561
+ f"venue={self.state.position}"
562
+ )
563
+ owner_signs = {
564
+ 1 if size > 0.0 else -1
565
+ for size in self.state.position_owners.values()
566
+ if abs(size) > 1e-9
567
+ }
568
+ if self.venue_mode == "netting" and len(owner_signs) > 1:
569
+ violations.append(
570
+ "opposing run ownership on a netting account cannot be isolated"
571
+ )
572
+ for run_name, runtime in runner.runs.items():
573
+ venue_size = self.state.position_owners.get(run_name, 0.0)
574
+ if abs(runtime.position.size - venue_size) > 1e-9:
575
+ violations.append(
576
+ f"economic position mismatch for {run_name}: "
577
+ f"engine={runtime.position.size} venue={venue_size}"
578
+ )
579
+ if self.venue_mode == "hedged":
580
+ leg_total = sum(
581
+ size
582
+ for (owner, _), size in self.state.position_legs.items()
583
+ if owner == run_name
584
+ )
585
+ if abs(leg_total - venue_size) > 1e-9:
586
+ violations.append(
587
+ f"hedged leg reconstruction mismatch for {run_name}: "
588
+ f"legs={leg_total} venue={venue_size}"
589
+ )
590
+ if abs(venue_size) <= 1e-9:
591
+ continue
592
+ required: dict[tuple[str | None, str], float] = {}
593
+ dispatched_intents = {
594
+ intent_key
595
+ for owner, _, intent_key in self.state.calls
596
+ if owner == run_name
597
+ }
598
+ for (
599
+ exit_id,
600
+ from_entry,
601
+ ), logical_exit in runtime.position.exit_orders.items():
602
+ intent_key = f"{exit_id}{INTENT_KEY_SEP}{from_entry}"
603
+ if intent_key not in dispatched_intents:
604
+ continue
605
+ qty = abs(float(logical_exit.size))
606
+ if logical_exit.limit is not None:
607
+ key = (from_entry, "take-profit")
608
+ required[key] = required.get(key, 0.0) + qty
609
+ if logical_exit.stop is not None:
610
+ key = (from_entry, "stop-loss")
611
+ required[key] = required.get(key, 0.0) + qty
612
+ coverage: dict[tuple[str | None, str], float] = {}
613
+ for record in self.state.orders.values():
614
+ if record.run_name != run_name or record.order.status not in (
615
+ OrderStatus.OPEN,
616
+ OrderStatus.PARTIALLY_FILLED,
617
+ ):
618
+ continue
619
+ if record.leg_type is LegType.TAKE_PROFIT:
620
+ outcome = "take-profit"
621
+ elif record.leg_type in (LegType.STOP_LOSS, LegType.TRAILING_STOP):
622
+ outcome = "stop-loss"
623
+ else:
624
+ continue
625
+ key = (record.from_entry, outcome)
626
+ coverage[key] = coverage.get(key, 0.0) + record.order.remaining_qty
627
+ for (from_entry, outcome), required_qty in required.items():
628
+ protected = coverage.get((from_entry, outcome), 0.0)
629
+ if protected + 1e-9 < required_qty:
630
+ violations.append(
631
+ f"{outcome} protection coverage shortfall for {run_name} "
632
+ f"from_entry={from_entry!r}: protected={protected} "
633
+ f"required={required_qty}"
634
+ )
635
+ return violations
636
+
637
+ def close(self) -> None:
638
+ self.state.pending_events.clear()
639
+
640
+
641
+ class HedgedReferenceVenueProfile(ReferenceVenueProfile):
642
+ """Reference venue retaining per-entry legs behind an aggregate position view."""
643
+
644
+ plugin_name = "broker-lab-reference-hedged"
645
+ venue_mode = "hedged"