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,787 @@
1
+ """
2
+ Position tracking for live broker trading.
3
+
4
+ :class:`BrokerPosition` extends :class:`~pynecore.lib.strategy.PositionBase`
5
+ with no simulation logic — the exchange is the source of truth for fills,
6
+ prices, fees, and margin state.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from collections import deque
11
+ from typing import TYPE_CHECKING
12
+
13
+ from pynecore import lib
14
+ from pynecore.core.broker.intent_builder import CLOSE_ALL_EXIT_ID, CLOSE_EXIT_ID_PREFIX
15
+ from pynecore.core.broker.models import LegType
16
+ from pynecore.lib.log import broker_warning as _blog_warning
17
+ from pynecore.lib.strategy import PositionBase, Trade
18
+ from pynecore.types.na import na_float
19
+ from pynecore.types import NA
20
+
21
+ if TYPE_CHECKING:
22
+ from pynecore.lib.strategy import direction
23
+ from pynecore.lib.strategy import Order
24
+ from pynecore.types.strategy import QtyType
25
+ from pynecore.core.broker.models import OrderEvent
26
+
27
+ __all__ = ['BrokerPosition']
28
+
29
+
30
+ class BrokerPosition(PositionBase):
31
+ """
32
+ Position state tracker for live broker trading.
33
+
34
+ The exchange determines fills, prices, fees, and margin state;
35
+ :meth:`record_fill` consumes :class:`OrderEvent` objects emitted by a
36
+ :class:`~pynecore.core.plugin.broker.BrokerPlugin` and updates the
37
+ local view of the position accordingly.
38
+
39
+ Trades are tracked FIFO: the first entry filled is the first closed
40
+ when the position is reduced, matching TradingView default semantics.
41
+
42
+ Note: margin, liquidation price, and fee currency conversion are all
43
+ handled by the exchange. This class only records what the exchange
44
+ tells it.
45
+ """
46
+
47
+ __slots__ = (
48
+ 'size', 'sign', 'avg_price',
49
+ 'netprofit', 'openprofit', 'grossprofit', 'grossloss',
50
+ 'open_commission',
51
+ 'eventrades', 'wintrades', 'losstrades',
52
+ 'closed_trades_count',
53
+ 'max_drawdown', 'max_runup', 'max_equity',
54
+ 'open_trades', 'closed_trades', 'new_closed_trades',
55
+ 'entry_orders', 'exit_orders',
56
+ # Per-evaluation set of close keys already seen this script run, so a
57
+ # second same-key ``strategy.close()`` THIS evaluation nets onto the
58
+ # first while a next-tick re-issue (calc_on_every_tick) replaces it.
59
+ '_closes_this_eval',
60
+ # === Risk management state (mirrors SimPosition) ===
61
+ # Configuration set by ``strategy.risk.*`` setters:
62
+ 'risk_allowed_direction',
63
+ 'risk_max_drawdown_value', 'risk_max_drawdown_type', 'risk_max_drawdown_alert',
64
+ 'risk_max_intraday_loss_value', 'risk_max_intraday_loss_type',
65
+ 'risk_max_intraday_loss_alert',
66
+ 'risk_max_cons_loss_days', 'risk_max_cons_loss_days_alert',
67
+ 'risk_max_intraday_filled_orders', 'risk_max_intraday_filled_orders_alert',
68
+ 'risk_max_position_size',
69
+ # Runtime counters / day-rollover tracking:
70
+ 'risk_cons_loss_days', 'risk_last_trading_day', 'risk_last_day_equity',
71
+ 'risk_intraday_filled_orders', 'risk_intraday_start_equity',
72
+ 'risk_halt_trading',
73
+ '_current_price',
74
+ )
75
+
76
+ def __init__(self) -> None:
77
+ self.size: float = 0.0
78
+ self.sign: float = 0.0
79
+ self.avg_price = na_float
80
+
81
+ self.netprofit: float = 0.0
82
+ self.openprofit: float = 0.0
83
+ self.grossprofit: float = 0.0
84
+ self.grossloss: float = 0.0
85
+ self.open_commission: float = 0.0
86
+
87
+ self.eventrades: int = 0
88
+ self.wintrades: int = 0
89
+ self.losstrades: int = 0
90
+ self.closed_trades_count: int = 0
91
+ self.max_drawdown: float = 0.0
92
+ self.max_runup: float = 0.0
93
+ # Mark-to-market peak equity, used by ``_peak_equity`` for the
94
+ # ``max_drawdown(percent_of_equity)`` threshold. Updated on every
95
+ # :meth:`update_unrealized_pnl` and :meth:`record_fill` call.
96
+ self.max_equity: float = -float("inf")
97
+
98
+ self.open_trades: list[Trade] = []
99
+ self.closed_trades: deque[Trade] = deque(maxlen=9000)
100
+ self.new_closed_trades: list[Trade] = []
101
+
102
+ self.entry_orders: dict[str | None, 'Order'] = {}
103
+ # Composite key ``(exit_id, from_entry)`` mirrors
104
+ # :class:`~pynecore.lib.strategy.SimPosition.exit_orders`. Single-field
105
+ # keys collide on partial-TP fan-out (multiple exits for one entry)
106
+ # and on ``from_entry=na`` fan-out (one exit_id, many per-entry rows).
107
+ self.exit_orders: dict[tuple[str | None, str | None], 'Order'] = {}
108
+
109
+ # Close keys (``(exit_id, order_id)``) already issued in the current
110
+ # script evaluation; reset by :meth:`begin_evaluation`. See
111
+ # :meth:`_add_order` for the same-eval netting it drives.
112
+ self._closes_this_eval: set[tuple[str | None, str | None]] = set()
113
+
114
+ # === Risk management state ===
115
+ # Configuration (filled by the ``strategy.risk.*`` setters via
116
+ # ``__init__.py``'s shared lib-property bridge):
117
+ self.risk_allowed_direction: 'direction.Direction | None' = None
118
+ self.risk_max_drawdown_value: float | None = None
119
+ self.risk_max_drawdown_type: 'QtyType | None' = None
120
+ self.risk_max_drawdown_alert: str | None = None
121
+ self.risk_max_intraday_loss_value: float | None = None
122
+ self.risk_max_intraday_loss_type: 'QtyType | None' = None
123
+ self.risk_max_intraday_loss_alert: str | None = None
124
+ self.risk_max_cons_loss_days: int | None = None
125
+ self.risk_max_cons_loss_days_alert: str | None = None
126
+ self.risk_max_intraday_filled_orders: int | None = None
127
+ self.risk_max_intraday_filled_orders_alert: str | None = None
128
+ self.risk_max_position_size: float | None = None
129
+ # Runtime counters / day-rollover tracking:
130
+ self.risk_cons_loss_days: int = 0
131
+ self.risk_last_trading_day: int = -1
132
+ self.risk_last_day_equity: float = 0.0
133
+ self.risk_intraday_filled_orders: int = 0
134
+ self.risk_intraday_start_equity: float = 0.0
135
+ self.risk_halt_trading: bool = False
136
+
137
+ self._current_price: float = 0.0
138
+ # Inherited from PositionBase; unused on the live path (close stacking is
139
+ # backtest-only), initialized so _next_close_seq() never raises if called.
140
+ self._close_seq_counter: int = 0
141
+
142
+ # === Pine API compatibility shims ======================================
143
+ # Pine strategy.* functions read ``position.c`` / ``.o`` / ``.h`` / ``.l``
144
+ # for the simulator's creation-time margin check. In broker mode those
145
+ # attributes are served from the live OHLCV module; the exchange enforces
146
+ # margin for real, so the Pine-level check still acts as a safety net
147
+ # on script-side state without a separate simulator update path.
148
+
149
+ @property
150
+ def c(self) -> float:
151
+ try:
152
+ v = lib.close
153
+ except AttributeError:
154
+ return self._current_price or 0.0
155
+ try:
156
+ return float(v) if v is not None else self._current_price or 0.0
157
+ except (TypeError, ValueError):
158
+ return self._current_price or 0.0
159
+
160
+ @property
161
+ def o(self) -> float:
162
+ try:
163
+ return float(lib.open)
164
+ except (AttributeError, TypeError, ValueError):
165
+ return self.c
166
+
167
+ @property
168
+ def h(self) -> float:
169
+ try:
170
+ return float(lib.high)
171
+ except (AttributeError, TypeError, ValueError):
172
+ return self.c
173
+
174
+ @property
175
+ def l(self) -> float: # noqa: E743 — mirrors the Pine attribute name
176
+ try:
177
+ return float(lib.low)
178
+ except (AttributeError, TypeError, ValueError):
179
+ return self.c
180
+
181
+ # === Pine-side order book ===
182
+
183
+ def begin_evaluation(self) -> None:
184
+ """Mark the start of a fresh script evaluation (one ``main()`` run).
185
+
186
+ Called by the runner before the libraries / ``main`` execute, in broker
187
+ mode only. It clears the per-evaluation close-key set so that two
188
+ ``strategy.close()`` calls issued in the SAME evaluation net onto one
189
+ order, while the SAME close re-issued on the next ``calc_on_every_tick``
190
+ evaluation replaces the pending order instead of doubling it. There is
191
+ no other per-evaluation order-book reset in live mode (the Pine order
192
+ book is purely event-driven), so this is the netting's idempotency
193
+ anchor.
194
+ """
195
+ self._closes_this_eval.clear()
196
+
197
+ def _add_order(self, order: 'Order') -> None:
198
+ """Register an order locally (the sync engine forwards it to the exchange).
199
+
200
+ Pre-submit risk gates run before the order is enqueued — same policy
201
+ as :meth:`SimPosition.fill_order` enforces at fill time, but applied
202
+ at the submit boundary because the broker fill is asynchronous.
203
+ Rejected entry/normal orders are silently dropped (matching the sim
204
+ ``_remove_order`` behavior on cap/direction violation); the
205
+ :attr:`risk_halt_trading` flag is set out-of-band by
206
+ :meth:`_enforce_post_bar_risk`.
207
+ """
208
+ order.bar_index = int(lib.bar_index)
209
+ # noinspection PyProtectedMember
210
+ from pynecore.lib.strategy import (
211
+ _order_type_close, _order_type_entry, _order_type_normal,
212
+ )
213
+ if order.order_type in (_order_type_entry, _order_type_normal):
214
+ if self._is_intraday_filled_cap_reached():
215
+ return
216
+ adjusted = self._adjust_for_max_position_size(float(order.size), order.sign)
217
+ if adjusted is None:
218
+ return
219
+ order.size = adjusted
220
+ if self.size == 0.0 and not self._is_direction_allowed(order.sign):
221
+ return
222
+ if order.order_type == _order_type_close:
223
+ key = (order.exit_id, order.order_id)
224
+ existing = self.exit_orders.get(key)
225
+ # Netting is for market closes only (``strategy.close(id)`` /
226
+ # ``strategy.close_all()``), identified by their reserved exit-id
227
+ # patterns. A sticky ``strategy.exit`` bracket re-issued in the SAME
228
+ # evaluation must still REPLACE: summing its size would dispatch an
229
+ # oversized protective order and the first leg's stale limit/stop/
230
+ # trailing levels would survive (netting only carries metadata).
231
+ exit_id = order.exit_id
232
+ is_market_close = exit_id == CLOSE_ALL_EXIT_ID or (
233
+ exit_id is not None and exit_id.startswith(CLOSE_EXIT_ID_PREFIX)
234
+ )
235
+ if is_market_close and key in self._closes_this_eval and existing is not None:
236
+ # Second+ same-key close THIS evaluation: net the slices into
237
+ # one reduce-only market close. Both ``strategy.close`` qty
238
+ # expressions are evaluated against the same ``position.size``
239
+ # (no fill lands mid-evaluation), so the slices simply sum; the
240
+ # over-close cap is applied later by the sync engine. Metadata
241
+ # is last-wins, matching the prior overwrite behaviour for this
242
+ # collision class. A NEXT-evaluation re-issue takes the ``else``
243
+ # branch (``begin_evaluation`` cleared the key) and replaces the
244
+ # pending order, keeping calc_on_every_tick idempotent.
245
+ existing.size += order.size
246
+ existing.reserved_size = abs(existing.size)
247
+ existing.comment = order.comment
248
+ existing.alert_message = order.alert_message
249
+ else:
250
+ self._closes_this_eval.add(key)
251
+ self.exit_orders[key] = order
252
+ else:
253
+ self.entry_orders[order.order_id] = order
254
+
255
+ def _remove_order(self, order: 'Order') -> None:
256
+ """Cancel an order locally."""
257
+ order.cancelled = True
258
+ # noinspection PyProtectedMember
259
+ from pynecore.lib.strategy import _order_type_close
260
+ if order.order_type == _order_type_close:
261
+ self.exit_orders.pop((order.exit_id, order.order_id), None)
262
+ else:
263
+ self.entry_orders.pop(order.order_id, None)
264
+
265
+ def _remove_order_by_id(self, order_id: str) -> None:
266
+ # TV-verified semantics: ``strategy.cancel(id)`` matches an exit by
267
+ # its ``exit_id`` and an entry by its entry id; no cross-matching.
268
+ for exit_order in list(self.exit_orders.values()):
269
+ if exit_order.exit_id == order_id:
270
+ self._remove_order(exit_order)
271
+ entry = self.entry_orders.get(order_id)
272
+ if entry is not None:
273
+ self._remove_order(entry)
274
+
275
+ def _cancel_all_orders(self) -> None:
276
+ # No ``orderbook`` attribute — that lives on ``SimPosition`` and drives
277
+ # the simulator's price-keyed fill loop, which has no analog in live
278
+ # trading. Clearing the two Pine-side dicts is enough; the next
279
+ # ``OrderSyncEngine.sync()`` diffs against ``_active_intents`` and
280
+ # dispatches a per-id cancel for every previously tracked intent.
281
+ self.entry_orders.clear()
282
+ self.exit_orders.clear()
283
+
284
+ # === Restart-time Pine-side reconstruction ===
285
+
286
+ def reconstruct_exit_order(
287
+ self,
288
+ *,
289
+ pine_id: str,
290
+ from_entry: str,
291
+ side: str,
292
+ qty: float,
293
+ tp_price: float | None,
294
+ sl_price: float | None,
295
+ trail_price: float | None,
296
+ trail_offset: float | None,
297
+ oca_name: str | None = None,
298
+ oca_type: str | None = None,
299
+ ) -> None:
300
+ """Re-install a persistent ``strategy.exit`` bracket order after a restart.
301
+
302
+ Pine ``strategy.exit`` orders are persistent: once placed they live in
303
+ :attr:`exit_orders` across bars (the script need not re-emit them) until
304
+ they fill or are cancelled. A fresh process starts with empty order
305
+ dicts, so the bracket the previous run armed is invisible to
306
+ :func:`~pynecore.core.broker.intent_builder.build_intents` until it is
307
+ rebuilt here from the durable broker-side ledger (the one-way
308
+ bracket-ownership rows or the engine-trigger partial-leg ledger). The
309
+ :class:`~pynecore.core.broker.sync_engine.OrderSyncEngine` calls this
310
+ once at startup so the diff sees the same exit it saw before the crash
311
+ and adopts (rather than tears down) the live broker protection.
312
+
313
+ ``side`` is the CLOSE side (``"buy"``/``"sell"``); the stored
314
+ :class:`~pynecore.lib.strategy.Order` carries the opposite-of-position
315
+ signed size so ``build_intents`` re-derives the same side. Tick fields
316
+ are left ``None`` — the ledger persists resolved absolute prices, never
317
+ the original tick distances.
318
+
319
+ :param pine_id: The ``strategy.exit(id=...)`` value (the exit id).
320
+ :param from_entry: The parent entry id the exit protects.
321
+ :param side: The exit (close) side, ``"buy"`` or ``"sell"``.
322
+ :param qty: Exit quantity magnitude.
323
+ :param tp_price: Absolute take-profit price, or ``None``.
324
+ :param sl_price: Absolute stop-loss price, or ``None``.
325
+ :param trail_price: Absolute trailing-stop activation price, or ``None``.
326
+ :param trail_offset: Trailing-stop offset (price units), or ``None``.
327
+ :param oca_name: OCA group name, or ``None``.
328
+ :param oca_type: OCA type string (``"reduce"`` / ``"cancel"`` /
329
+ ``"none"``), or ``None`` for rows persisted before the OCA fields
330
+ existed. Restored so ``build_intents`` re-derives the same group the
331
+ exit was emitted under and the cross-bracket OCA-cancel cascade keeps
332
+ firing across the restart.
333
+ """
334
+ # noinspection PyProtectedMember
335
+ from pynecore.lib.strategy import Order, _order_type_close, oca as _oca
336
+ signed_size = qty if side == "buy" else -qty
337
+ order = Order(
338
+ from_entry,
339
+ signed_size,
340
+ order_type=_order_type_close,
341
+ exit_id=pine_id,
342
+ limit=tp_price,
343
+ stop=sl_price,
344
+ trail_price=trail_price,
345
+ trail_offset=trail_offset,
346
+ oca_name=oca_name,
347
+ oca_type=_oca.Oca(oca_type) if oca_type is not None else None,
348
+ )
349
+ self.exit_orders[(pine_id, from_entry)] = order
350
+
351
+ def reconstruct_entry_order(
352
+ self,
353
+ *,
354
+ pine_id: str,
355
+ side: str,
356
+ qty: float,
357
+ limit: float | None,
358
+ stop: float | None,
359
+ ) -> None:
360
+ """Re-install a persistent ``strategy.entry`` working order after a restart.
361
+
362
+ Pine ``strategy.entry`` / ``strategy.order`` working orders are
363
+ persistent: once placed a pending LIMIT / STOP entry lives in
364
+ :attr:`entry_orders` across bars (the script need not re-emit it) until
365
+ it fills or is cancelled. A fresh process starts with empty order dicts,
366
+ so a first-bar ``strategy.cancel`` after a restart would operate on an
367
+ empty book (a silent no-op) and the live broker working order would be
368
+ stranded — neither adopted nor cancelled. The
369
+ :class:`~pynecore.core.broker.sync_engine.OrderSyncEngine` rebuilds the
370
+ entry here, once, from the live broker snapshot so the first post-restart
371
+ script sees the same order it placed: a ``strategy.cancel`` removes it and
372
+ the diff retires the live order, while leaving it standing (or re-emitting
373
+ it) adopts the live order without a duplicate dispatch.
374
+
375
+ ``side`` is the entry side (``"buy"``/``"sell"``); the stored
376
+ :class:`~pynecore.lib.strategy.Order` carries the matching signed size so
377
+ :func:`~pynecore.core.broker.intent_builder.build_intents` re-derives the
378
+ same intent. Tick fields are left ``None`` — the venue reports resolved
379
+ absolute prices, never the original tick distances.
380
+
381
+ :param pine_id: The ``strategy.entry(id=...)`` value (the entry id).
382
+ :param side: The entry side, ``"buy"`` or ``"sell"``.
383
+ :param qty: Entry quantity magnitude.
384
+ :param limit: Absolute limit price, or ``None`` (STOP entry).
385
+ :param stop: Absolute stop trigger price, or ``None`` (LIMIT entry).
386
+ """
387
+ # noinspection PyProtectedMember
388
+ from pynecore.lib.strategy import Order, _order_type_entry
389
+ signed_size = qty if side == "buy" else -qty
390
+ order = Order(
391
+ pine_id,
392
+ signed_size,
393
+ order_type=_order_type_entry,
394
+ limit=limit,
395
+ stop=stop,
396
+ )
397
+ self.entry_orders[pine_id] = order
398
+
399
+ def reconstruct_parent_trade(
400
+ self,
401
+ *,
402
+ entry_id: str,
403
+ size: float,
404
+ entry_price: float,
405
+ ) -> None:
406
+ """Seed one open :class:`~pynecore.lib.strategy.Trade` for an adopted parent.
407
+
408
+ Startup adoption restores :attr:`size`/:attr:`avg_price` but leaves
409
+ :attr:`open_trades` empty. A partial-quantity bracket needs the parent's
410
+ open qty to classify
411
+ :attr:`~pynecore.core.broker.models.ExitIntent.is_partial_qty_bracket`
412
+ consistently across bars (``build_intents`` derives the parent total
413
+ from :attr:`open_trades`); without it the exit would be misclassified as
414
+ a whole-row bracket and adopted on the wrong dispatch path. The trade is
415
+ inert until a real broker fill routes through :meth:`record_fill` (the
416
+ sole :attr:`open_trades` mutator), so seeding it cannot emit an intent or
417
+ re-open anything.
418
+
419
+ :param entry_id: The parent entry id (``from_entry``).
420
+ :param size: Signed parent open size (positive long, negative short).
421
+ :param entry_price: Parent average entry price.
422
+ """
423
+ if any(t.entry_id == entry_id for t in self.open_trades):
424
+ return
425
+ # ``entry_equity`` stays 0.0: the parent opened in a prior process so
426
+ # its true entry equity is unknowable, and startup adoption runs in
427
+ # ``start_broker`` before the script is attached (``self.equity`` reads
428
+ # ``lib._script``, not yet set). The broker close path never divides by
429
+ # a trade's entry equity, so the placeholder is inert.
430
+ self.open_trades.append(Trade(
431
+ size=size,
432
+ entry_id=entry_id,
433
+ entry_bar_index=int(getattr(lib, 'bar_index', 0)),
434
+ entry_time=0,
435
+ entry_price=entry_price,
436
+ commission=0.0,
437
+ entry_equity=0.0,
438
+ ))
439
+
440
+ # === Exchange-side state updates ===
441
+
442
+ def record_fill(self, event: 'OrderEvent') -> bool:
443
+ """
444
+ Record an exchange fill.
445
+
446
+ :param event: An :class:`OrderEvent` with ``fill_qty`` and
447
+ ``fill_price`` populated, plus Pine identity fields
448
+ (``pine_id``, ``from_entry``, ``leg_type``) filled by the plugin.
449
+ :return: ``True`` if the position side changed as a result of this fill.
450
+ """
451
+ fill_qty = event.fill_qty or 0.0
452
+ fill_price = event.fill_price or 0.0
453
+ if fill_qty <= 0.0 or fill_price <= 0.0:
454
+ # A zero-or-missing qty/price means the broker plugin emitted a
455
+ # fill event without resolving the actual fill quantity / price.
456
+ # Without a warning the silent skip leaves ``position.size``
457
+ # stuck at zero — the script keeps thinking it is flat and
458
+ # re-fires the entry next bar. Surface the offending event so
459
+ # the operator can spot the broker-side data hole instead of
460
+ # debugging by guesswork.
461
+ _blog_warning(
462
+ "ignoring fill with zero qty/price (qty=%s price=%s pine=%r leg=%s) "
463
+ "— position.size NOT updated",
464
+ fill_qty, fill_price,
465
+ event.pine_id, event.leg_type,
466
+ )
467
+ return False
468
+
469
+ signed_delta = fill_qty if event.order.side == "buy" else -fill_qty
470
+ old_sign = self.sign
471
+ new_size = self.size + signed_delta
472
+
473
+ # Commission bookkeeping — realized fee becomes part of net P&L at close
474
+ fee = event.fee
475
+
476
+ # Risk management: count every filled order toward the intraday cap,
477
+ # matching ``SimPosition._fill_order``. The counter is read by the
478
+ # pre-submit gate in :meth:`_add_order` and the post-bar halt in
479
+ # :meth:`_enforce_post_bar_risk`.
480
+ self.risk_intraday_filled_orders += 1
481
+
482
+ if self.size == 0.0 or (old_sign * signed_delta) > 0.0:
483
+ # Opening or adding to an existing position (same direction)
484
+ new_abs = abs(new_size)
485
+ old_abs = abs(self.size)
486
+ if old_abs == 0.0 or isinstance(self.avg_price, NA) or self.avg_price != self.avg_price:
487
+ self.avg_price = fill_price
488
+ else:
489
+ self.avg_price = (self.avg_price * old_abs + fill_price * fill_qty) / new_abs
490
+ self.size = new_size
491
+ self.sign = 1.0 if new_size > 0.0 else (-1.0 if new_size < 0.0 else 0.0)
492
+
493
+ trade = Trade(
494
+ size=signed_delta,
495
+ entry_id=event.pine_id,
496
+ entry_bar_index=int(getattr(lib, 'bar_index', 0)),
497
+ entry_time=int(event.timestamp * 1000.0),
498
+ entry_price=fill_price,
499
+ commission=fee,
500
+ entry_comment=None,
501
+ entry_equity=self.equity,
502
+ )
503
+ self.open_trades.append(trade)
504
+ self.open_commission += fee
505
+ # The entry Order stays in ``entry_orders`` for intent stability, but
506
+ # its filled slice now lives in ``open_trades``. Record how much of it
507
+ # has filled so ``strategy.exit``'s bound-size reservation does not
508
+ # count the same quantity twice (issue BYBIT-001).
509
+ entry_order = self.entry_orders.get(event.pine_id)
510
+ if entry_order is not None:
511
+ entry_order.filled_qty += fill_qty
512
+ return False
513
+
514
+ # Reducing or flipping — FIFO close of existing trades
515
+ remaining = fill_qty
516
+ closed_profit = 0.0
517
+ closed_fee = 0.0
518
+ while remaining > 0.0 and self.open_trades:
519
+ trade = self.open_trades[0]
520
+ trade_abs = abs(trade.size)
521
+ if trade_abs <= remaining + 1e-12:
522
+ # Close this trade fully
523
+ self._close_trade(trade, fill_price, event, fee_share=fee * (trade_abs / fill_qty))
524
+ closed_profit += trade.profit
525
+ closed_fee += trade.commission
526
+ remaining -= trade_abs
527
+ else:
528
+ # Partial close: split the trade
529
+ closed_piece = Trade(
530
+ size=trade.sign * remaining,
531
+ entry_id=trade.entry_id,
532
+ entry_bar_index=trade.entry_bar_index,
533
+ entry_time=trade.entry_time,
534
+ entry_price=trade.entry_price,
535
+ commission=trade.commission * (remaining / trade_abs),
536
+ entry_comment=trade.entry_comment,
537
+ entry_equity=trade.entry_equity,
538
+ )
539
+ self._close_trade(
540
+ closed_piece, fill_price, event,
541
+ fee_share=fee * (remaining / fill_qty),
542
+ )
543
+ closed_profit += closed_piece.profit
544
+ # Shrink the remaining open trade
545
+ trade.size -= closed_piece.size
546
+ trade.commission -= closed_piece.commission
547
+ remaining = 0.0
548
+
549
+ self.size += signed_delta
550
+ # Clamp tiny residuals to zero
551
+ if abs(self.size) < 1e-12:
552
+ self.size = 0.0
553
+ self.sign = 0.0
554
+ self.avg_price = na_float
555
+ else:
556
+ self.sign = 1.0 if self.size > 0.0 else -1.0
557
+
558
+ # If there is leftover qty after closing all open_trades → side flip.
559
+ # On a one-way account only an ENTRY leg may legitimately open the
560
+ # opposite side (stop-and-reverse). A reduce-only/exit leg (close, TP,
561
+ # SL, trailing) must NEVER flip: leftover qty means the local FIFO
562
+ # under-counted exposure (e.g. a restart adopted the net size without
563
+ # seeding open_trades). The authoritative net is already in ``self.size``
564
+ # (``self.size += signed_delta`` above), so two cases split apart:
565
+ # * the net kept its original sign → the exit only PARTIALLY reduced;
566
+ # the leftover is a stale FIFO row shortfall, NOT an over-close.
567
+ # Keep the (already-correct) residual size — clamping to flat would
568
+ # drop live broker exposure and let the script re-fire the entry.
569
+ # * the net reached or crossed zero → the exit closed at least the
570
+ # whole position; reconcile toward flat instead of fabricating an
571
+ # opposite position the diff engine could then "close" with a real
572
+ # reversing order.
573
+ if remaining > 0.0:
574
+ if event.leg_type in (
575
+ LegType.CLOSE, LegType.TAKE_PROFIT,
576
+ LegType.STOP_LOSS, LegType.TRAILING_STOP,
577
+ ):
578
+ if old_sign * self.size > 0.0:
579
+ # Partial reduce-only fill against an under-counted FIFO:
580
+ # the residual already in ``self.size`` is authoritative.
581
+ _blog_warning(
582
+ "exit fill (leg=%s qty=%s) under-counted FIFO exposure "
583
+ "by %s — keeping residual size %s (partial reduce, "
584
+ "not an over-close)",
585
+ event.leg_type, fill_qty, remaining, self.size,
586
+ )
587
+ else:
588
+ _blog_warning(
589
+ "exit fill (leg=%s qty=%s) exceeded known FIFO exposure "
590
+ "by %s — clamping to flat instead of opening an opposite "
591
+ "position",
592
+ event.leg_type, fill_qty, remaining,
593
+ )
594
+ self.size = 0.0
595
+ self.sign = 0.0
596
+ self.avg_price = na_float
597
+ else:
598
+ new_size = self.sign * remaining if self.sign != 0.0 else signed_delta
599
+ self.size = new_size
600
+ self.sign = 1.0 if new_size > 0.0 else (-1.0 if new_size < 0.0 else 0.0)
601
+ self.avg_price = fill_price
602
+ flipped = Trade(
603
+ size=new_size,
604
+ entry_id=event.pine_id,
605
+ entry_bar_index=int(getattr(lib, 'bar_index', 0)),
606
+ entry_time=int(event.timestamp * 1000.0),
607
+ entry_price=fill_price,
608
+ commission=0.0,
609
+ entry_comment=None,
610
+ entry_equity=self.equity,
611
+ )
612
+ self.open_trades.append(flipped)
613
+
614
+ # Update running stats
615
+ self.netprofit += closed_profit
616
+ if closed_profit > 0.0:
617
+ self.grossprofit += closed_profit
618
+ self.wintrades += 1
619
+ elif closed_profit < 0.0:
620
+ self.grossloss += closed_profit
621
+ self.losstrades += 1
622
+ else:
623
+ self.eventrades += 1
624
+
625
+ self.open_commission = float(sum(t.commission for t in self.open_trades))
626
+
627
+ return self.sign != old_sign
628
+
629
+ def update_unrealized_pnl(self, current_price: float) -> None:
630
+ """Mark-to-market: recompute :attr:`openprofit` at the given price.
631
+
632
+ Also rolls the :attr:`max_equity` peak forward and the
633
+ :attr:`max_drawdown` running maximum off the live price — these feed
634
+ :meth:`_peak_equity` and :meth:`_is_max_drawdown_breached` so the
635
+ broker risk gates see real-time equity, not just realized P&L.
636
+ """
637
+ self._current_price = current_price
638
+ if not self.open_trades or current_price <= 0.0:
639
+ self.openprofit = 0.0
640
+ else:
641
+ total = 0.0
642
+ for trade in self.open_trades:
643
+ total += (current_price - trade.entry_price) * trade.size
644
+ self.openprofit = total
645
+ eq = float(self.equity)
646
+ if eq > self.max_equity:
647
+ self.max_equity = eq
648
+ # Drawdown is measured from the running peak — same metric the sim
649
+ # ``max_drawdown`` field tracks, just sourced from mark-to-market.
650
+ if self.max_equity > -float("inf"):
651
+ dd = self.max_equity - eq
652
+ if dd > self.max_drawdown:
653
+ self.max_drawdown = dd
654
+
655
+ def _peak_equity(self) -> float:
656
+ """Reference equity for ``max_drawdown(percent_of_equity)``.
657
+
658
+ Falls back to initial capital before the first
659
+ :meth:`update_unrealized_pnl` (or fill) primes ``max_equity``.
660
+ """
661
+ # noinspection PyProtectedMember
662
+ initial = float(lib._script.initial_capital)
663
+ if self.max_equity == -float("inf"):
664
+ return initial
665
+ return max(initial, float(self.max_equity))
666
+
667
+ # === Risk management hooks (called by the runner once per bar) =========
668
+
669
+ def _handle_bar_open_risk(self) -> None:
670
+ """Day-rollover bookkeeping at the start of each bar.
671
+
672
+ Mirrors the rollover block in
673
+ :meth:`SimPosition._process_at_bar_open`: on a new trading day,
674
+ update ``risk_cons_loss_days`` from the prior-day equity delta,
675
+ reset the intraday anchors, and immediately halt if the
676
+ consecutive-loss-day cap is breached so queued entries cannot fill
677
+ at this bar's open. Distinct name from the sim hook because the
678
+ sim variant takes an ``ohlc`` argument and runs additional
679
+ simulator-specific work — the broker version only does
680
+ risk-related rollover.
681
+ """
682
+ if self.risk_halt_trading:
683
+ return
684
+ try:
685
+ # Statically a value (module_property), at runtime still the function
686
+ current_trading_day = int(lib.time_tradingday())
687
+ except (AttributeError, TypeError, ValueError):
688
+ return
689
+ if current_trading_day == self.risk_last_trading_day:
690
+ return
691
+ current_equity = float(self.equity)
692
+ # On the very first bar there is no prior day to compare against —
693
+ # initialise the trailing-equity anchor without touching the counter.
694
+ if self.risk_last_trading_day != -1:
695
+ if current_equity < self.risk_last_day_equity:
696
+ self.risk_cons_loss_days += 1
697
+ else:
698
+ self.risk_cons_loss_days = 0
699
+ self.risk_last_day_equity = current_equity
700
+ self.risk_intraday_start_equity = current_equity
701
+ self.risk_last_trading_day = current_trading_day
702
+ self.risk_intraday_filled_orders = 0
703
+ if self._is_max_cons_loss_days_breached():
704
+ self._trigger_risk_halt("Max consecutive loss days reached")
705
+
706
+ def _enforce_post_bar_risk(self) -> None:
707
+ """Run the post-bar ``strategy.risk.*`` checks.
708
+
709
+ Called by the runner after the script executes and before the next
710
+ :meth:`OrderSyncEngine.sync` so the queued risk-close goes out in
711
+ the same dispatch cycle. The first triggered rule wins.
712
+ """
713
+ if self.risk_halt_trading:
714
+ return
715
+ if self._is_max_drawdown_breached():
716
+ self._trigger_risk_halt("Max drawdown reached")
717
+ return
718
+ if self._is_max_intraday_loss_breached():
719
+ self._trigger_risk_halt("Max intraday loss reached")
720
+ return
721
+ if self._is_max_cons_loss_days_breached():
722
+ self._trigger_risk_halt("Max consecutive loss days reached")
723
+
724
+ def _trigger_risk_halt(self, reason: str) -> None:
725
+ """Cancel pending orders, queue a market close, set the halt flag.
726
+
727
+ Differs from :meth:`SimPosition._trigger_risk_halt` in two ways: no
728
+ synthetic fill (the exchange owns fills, not the position), and no
729
+ OHLC arguments (the close is a plain market order, the broker
730
+ decides the fill price). The next :meth:`OrderSyncEngine.sync`
731
+ observes the cleared books plus the queued close and dispatches
732
+ accordingly.
733
+ """
734
+ # noinspection PyProtectedMember
735
+ from pynecore.lib.strategy import Order, _order_type_close
736
+ self.entry_orders.clear()
737
+ self.exit_orders.clear()
738
+ if self.size != 0.0:
739
+ close_order = Order(
740
+ None, -self.size,
741
+ exit_id='Risk management close',
742
+ order_type=_order_type_close,
743
+ comment=f"Close Position ({reason})",
744
+ )
745
+ self._add_order(close_order)
746
+ self.risk_halt_trading = True
747
+
748
+ def record_liquidation(self, event: 'OrderEvent') -> None:
749
+ """Record an exchange-initiated liquidation — close all open trades."""
750
+ if not self.open_trades:
751
+ return
752
+ fill_price = event.fill_price or 0.0
753
+ for trade in list(self.open_trades):
754
+ self._close_trade(trade, fill_price, event, fee_share=event.fee / max(len(self.open_trades), 1))
755
+ self.netprofit += trade.profit
756
+ if trade.profit > 0.0:
757
+ self.grossprofit += trade.profit
758
+ self.wintrades += 1
759
+ elif trade.profit < 0.0:
760
+ self.grossloss += trade.profit
761
+ self.losstrades += 1
762
+ else:
763
+ self.eventrades += 1
764
+ self.size = 0.0
765
+ self.sign = 0.0
766
+ self.avg_price = na_float
767
+ self.openprofit = 0.0
768
+ self.open_commission = 0.0
769
+
770
+ # === Internals ===
771
+
772
+ def _close_trade(self, trade: Trade, fill_price: float,
773
+ event: 'OrderEvent', fee_share: float) -> None:
774
+ """Move a (possibly split) Trade from open_trades to closed_trades."""
775
+ trade.exit_id = event.pine_id or ""
776
+ trade.exit_bar_index = int(getattr(lib, 'bar_index', 0))
777
+ trade.exit_time = int(event.timestamp * 1000.0)
778
+ trade.exit_price = fill_price
779
+ trade.exit_comment = ''
780
+ trade.commission += fee_share
781
+ trade.profit = (fill_price - trade.entry_price) * trade.size - trade.commission
782
+ trade.exit_equity = self.equity + trade.profit
783
+ if trade in self.open_trades:
784
+ self.open_trades.remove(trade)
785
+ self.closed_trades.append(trade)
786
+ self.new_closed_trades.append(trade)
787
+ self.closed_trades_count += 1