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,334 @@
1
+ """
2
+ Translate the Pine ``strategy.*`` order book into broker intent objects.
3
+
4
+ The :class:`~pynecore.lib.strategy.SimPosition` maintains two dictionaries
5
+ of pending Pine orders created by ``strategy.entry``, ``strategy.order``,
6
+ ``strategy.exit``, ``strategy.close`` and ``strategy.close_all``:
7
+
8
+ - ``position.entry_orders[pine_id]`` — entries and strategy-order adds
9
+ - ``position.exit_orders[(exit_id, from_entry)]`` — exits, closes and
10
+ close-all (composite key disambiguates multiple exits per entry)
11
+
12
+ For live trading the :class:`~pynecore.core.broker.sync_engine.OrderSyncEngine`
13
+ consumes these via :func:`build_intents`, which returns a flat list of
14
+ :class:`EntryIntent`, :class:`ExitIntent` and :class:`CloseIntent`
15
+ dataclasses the broker plugin can act on. The translation is **pure**:
16
+ no access to ``lib._script`` or ``syminfo``; the caller supplies the
17
+ symbol and keeps tick/mintick resolution inside the sync engine.
18
+ """
19
+ from typing import Iterable
20
+
21
+ from pynecore.core.broker.models import (
22
+ EntryIntent,
23
+ ExitIntent,
24
+ CloseIntent,
25
+ OcaType,
26
+ OrderType,
27
+ )
28
+ # noinspection PyProtectedMember
29
+ from pynecore.lib.strategy import (
30
+ Order,
31
+ Trade,
32
+ _order_type_normal,
33
+ )
34
+ from pynecore.types.na import NA
35
+
36
+ __all__ = [
37
+ 'build_intents',
38
+ 'build_entry_intent',
39
+ 'build_exit_intent',
40
+ 'build_close_intent',
41
+ 'CLOSE_EXIT_ID_PREFIX',
42
+ 'CLOSE_ALL_EXIT_ID',
43
+ ]
44
+
45
+ # Prefixes set by ``strategy.close`` / ``strategy.close_all`` in
46
+ # ``lib/strategy/__init__.py``. Used to distinguish a CloseIntent from an
47
+ # ExitIntent when both share ``_order_type_close``.
48
+ CLOSE_EXIT_ID_PREFIX = "Close entry(s) order "
49
+ CLOSE_ALL_EXIT_ID = "Close position order"
50
+
51
+
52
+ def _side_from_size(size: float) -> str:
53
+ """Signed Pine size → ``"buy"``/``"sell"``.
54
+
55
+ Pine uses signed sizes: positive for long, negative for short. Exit/close
56
+ orders always carry the **opposite** sign of the position, so a long close
57
+ has a negative size → ``"sell"``, matching the exchange-side semantics.
58
+ """
59
+ return "buy" if size > 0 else "sell"
60
+
61
+
62
+ def _infer_order_type(limit: float | None, stop: float | None) -> OrderType:
63
+ """Map a Pine entry's ``limit``/``stop`` prices to a broker order type.
64
+
65
+ Pine has no "stop-limit" entry. When BOTH prices are set the order is two
66
+ OCO legs: a LIMIT below the open (guaranteed-price pullback) and a STOP
67
+ above (market-on-rise). The sync engine realises the both-set case as a
68
+ native resting LIMIT plus a software price-watch on the stop side, so the
69
+ inferred type is LIMIT — both-set and limit-only share the same resting
70
+ leg. The ``EntryIntent`` still carries the ``stop`` price for the engine
71
+ to arm its watch.
72
+ """
73
+ if limit is not None:
74
+ return OrderType.LIMIT
75
+ if stop is not None:
76
+ return OrderType.STOP
77
+ return OrderType.MARKET
78
+
79
+
80
+ def _na_to_none(value):
81
+ """Strip Pine :class:`NA` markers; leave concrete values alone."""
82
+ return None if isinstance(value, NA) else value
83
+
84
+
85
+ def _coerce_oca(order: Order) -> tuple[str | None, str | None]:
86
+ """Return a protocol-friendly ``(oca_name, oca_type)`` pair.
87
+
88
+ ``Order`` always stores a non-None ``oca_type`` (defaults to :data:`oca.none`)
89
+ even when no OCA participation is requested. The intent layer uses
90
+ ``None`` to mean "not in an OCA group" — so only emit ``oca_type`` when
91
+ the order actually names a group.
92
+
93
+ Unknown ``oca_type`` strings are rejected here rather than silently passed
94
+ through to the sync engine, where a typo would disable cascade cancel
95
+ without any diagnostic. The accepted values are exactly the members of
96
+ :class:`OcaType`.
97
+ """
98
+ if order.oca_name is None:
99
+ return None, None
100
+ if order.oca_type is None:
101
+ return order.oca_name, None
102
+ oca_type_str = str(order.oca_type)
103
+ try:
104
+ OcaType(oca_type_str)
105
+ except ValueError as exc:
106
+ raise ValueError(
107
+ f"unknown oca_type {oca_type_str!r}; expected one of "
108
+ f"{[m.value for m in OcaType]}",
109
+ ) from exc
110
+ return order.oca_name, oca_type_str
111
+
112
+
113
+ def build_entry_intent(order: Order, symbol: str) -> EntryIntent:
114
+ """Translate a ``strategy.entry`` / ``strategy.order`` Pine order.
115
+
116
+ The intent carries the RAW script quantity — deliberately. Pine keeps
117
+ a consumed market entry's ``Order`` in ``entry_orders`` (it is never
118
+ popped), so ``build_intents`` re-emits the same intent every bar and
119
+ the sync engine's diff relies on it being byte-stable to recognise an
120
+ already-dispatched entry. The MARKET-reversal stop-and-reverse fold
121
+ (TV: combined = qty + |opposite position|) therefore happens at
122
+ DISPATCH time in :meth:`OrderSyncEngine._dispatch_new`, which runs
123
+ exactly once per fresh intent — folding here would make a stale
124
+ retained order's qty flap with the net position and re-trigger the
125
+ diff (measured on the Capital.com demo E2E, 2026-07-10).
126
+ """
127
+ oca_name, oca_type_str = _coerce_oca(order)
128
+ return EntryIntent(
129
+ pine_id=order.order_id or "",
130
+ symbol=symbol,
131
+ side=_side_from_size(float(order.size)),
132
+ qty=abs(order.size),
133
+ order_type=_infer_order_type(order.limit, order.stop),
134
+ limit=order.limit,
135
+ stop=order.stop,
136
+ oca_name=oca_name,
137
+ oca_type=oca_type_str,
138
+ comment=_na_to_none(order.comment),
139
+ alert_message=_na_to_none(order.alert_message),
140
+ is_strategy_order=(order.order_type == _order_type_normal),
141
+ )
142
+
143
+
144
+ def build_exit_intent(
145
+ order: Order,
146
+ symbol: str,
147
+ *,
148
+ parent_total_qty: float = 0.0,
149
+ ) -> ExitIntent:
150
+ """Translate a ``strategy.exit`` Pine order.
151
+
152
+ Tick-based exits (``profit=``/``loss=``/``trail_points=``) carry unresolved
153
+ distances: the plugin cannot place absolute TP/SL prices until the entry
154
+ fill price is known. The intent preserves the tick values
155
+ **alongside** empty ``tp_price``/``sl_price`` fields; the sync engine
156
+ resolves them on the corresponding entry fill event.
157
+
158
+ :param order: The Pine ``strategy.exit`` :class:`Order` to translate.
159
+ :param symbol: Exchange symbol the resulting :class:`ExitIntent` targets.
160
+ :param parent_total_qty: total open qty currently associated with the
161
+ ``from_entry`` Pine id — used to set
162
+ :attr:`ExitIntent.is_partial_qty_bracket` when the script asks for a
163
+ bracket on a fraction of the parent. ``0.0`` (the default) leaves
164
+ the flag ``False``; ``build_intents`` supplies the live value from
165
+ ``position.open_trades`` so the order sync engine can dispatch on
166
+ ``caps.partial_qty_bracket_exit``.
167
+ """
168
+ oca_name, oca_type_str = _coerce_oca(order)
169
+ # Tick values take priority over explicit prices — mirrors Pine's
170
+ # exit() fill-time logic where profit_ticks overwrites order.limit.
171
+ tp_price = order.limit if order.profit_ticks is None else None
172
+ sl_price = order.stop if order.loss_ticks is None else None
173
+ trail_price = order.trail_price if order.trail_points_ticks is None else None
174
+ has_trail = (
175
+ order.trail_price is not None or order.trail_points_ticks is not None
176
+ )
177
+ trail_offset = order.trail_offset if has_trail else None
178
+ has_bracket_leg = (
179
+ tp_price is not None
180
+ or sl_price is not None
181
+ or trail_price is not None
182
+ or trail_offset is not None
183
+ or order.profit_ticks is not None
184
+ or order.loss_ticks is not None
185
+ or order.trail_points_ticks is not None
186
+ )
187
+ exit_qty = abs(order.size)
188
+ # A bracket on the whole row stays on the full-row dispatch path even
189
+ # when the capability advertises a partial-aware mechanism — Pine
190
+ # semantics for ``qty == total`` are identical to the simple bracket.
191
+ is_partial = (
192
+ has_bracket_leg
193
+ and parent_total_qty > 0.0
194
+ and exit_qty + 1e-12 < parent_total_qty
195
+ )
196
+ return ExitIntent(
197
+ pine_id=order.exit_id or "",
198
+ from_entry=order.order_id or "",
199
+ symbol=symbol,
200
+ side=_side_from_size(float(order.size)),
201
+ qty=exit_qty,
202
+ tp_price=tp_price,
203
+ sl_price=sl_price,
204
+ trail_price=trail_price,
205
+ trail_offset=trail_offset,
206
+ profit_ticks=order.profit_ticks,
207
+ loss_ticks=order.loss_ticks,
208
+ trail_points_ticks=order.trail_points_ticks,
209
+ oca_name=oca_name,
210
+ oca_type=oca_type_str,
211
+ comment=_na_to_none(order.comment),
212
+ comment_profit=_na_to_none(order.comment_profit),
213
+ comment_loss=_na_to_none(order.comment_loss),
214
+ comment_trailing=_na_to_none(order.comment_trailing),
215
+ alert_message=_na_to_none(order.alert_message),
216
+ is_partial_qty_bracket=is_partial,
217
+ )
218
+
219
+
220
+ def build_close_intent(order: Order, symbol: str, *, is_close_all: bool) -> CloseIntent:
221
+ """Translate ``strategy.close(id)`` or ``strategy.close_all()``.
222
+
223
+ ``strategy.close_all()`` uses an empty ``pine_id`` — the Pine order itself
224
+ has ``order_id=None`` because the close targets the whole position rather
225
+ than a specific entry identifier.
226
+ """
227
+ pine_id = "" if is_close_all else (order.order_id or "")
228
+ return CloseIntent(
229
+ pine_id=pine_id,
230
+ symbol=symbol,
231
+ side=_side_from_size(float(order.size)),
232
+ qty=abs(order.size),
233
+ immediately=False,
234
+ comment=_na_to_none(order.comment),
235
+ alert_message=_na_to_none(order.alert_message),
236
+ )
237
+
238
+
239
+ def _classify_exit_side(order: Order) -> str:
240
+ """``'close_all' | 'close' | 'exit'`` — the exit_orders dict is polymorphic."""
241
+ if order.exit_id == CLOSE_ALL_EXIT_ID:
242
+ return 'close_all'
243
+ if order.exit_id and order.exit_id.startswith(CLOSE_EXIT_ID_PREFIX):
244
+ return 'close'
245
+ return 'exit'
246
+
247
+
248
+ def build_intents(
249
+ entry_orders: dict,
250
+ exit_orders: dict,
251
+ symbol: str,
252
+ open_trades: Iterable[Trade] | None = None,
253
+ ) -> list[EntryIntent | ExitIntent | CloseIntent]:
254
+ """Flatten a position's pending orders into intent objects.
255
+
256
+ ``entry_orders`` and ``exit_orders`` are the ``dict``s that
257
+ :class:`~pynecore.lib.strategy.SimPosition` exposes. Orders already
258
+ marked ``cancelled`` (e.g. via OCA or :func:`strategy.cancel`) are
259
+ filtered out — the caller treats their absence as an implicit cancel.
260
+
261
+ ``open_trades`` is ``position.open_trades`` — used solely to compute the
262
+ total open qty per ``from_entry`` for the partial-qty bracket flag on
263
+ :class:`ExitIntent`. Optional; when omitted the flag stays ``False``
264
+ and the exit falls back to the full-row dispatch path (the safe
265
+ default for unit tests and simulator-only callers that don't carry a
266
+ live trade ledger).
267
+ """
268
+ intents: list[EntryIntent | ExitIntent | CloseIntent] = []
269
+
270
+ qty_by_entry: dict[str, float] = {}
271
+ count_by_entry: dict[str, int] = {}
272
+ if open_trades is not None:
273
+ for trade in open_trades:
274
+ entry_id = trade.entry_id
275
+ if entry_id is None:
276
+ continue
277
+ qty_by_entry[entry_id] = (
278
+ qty_by_entry.get(entry_id, 0.0) + abs(trade.size)
279
+ )
280
+ count_by_entry[entry_id] = count_by_entry.get(entry_id, 0) + 1
281
+
282
+ for order in _active(entry_orders.values()):
283
+ intents.append(build_entry_intent(order, symbol))
284
+
285
+ for order in _active(exit_orders.values()):
286
+ kind = _classify_exit_side(order)
287
+ if kind == 'close_all':
288
+ intents.append(build_close_intent(order, symbol, is_close_all=True))
289
+ elif kind == 'close':
290
+ intents.append(build_close_intent(order, symbol, is_close_all=False))
291
+ else:
292
+ from_entry = order.order_id or ""
293
+ # ``parent_total_qty`` drives ``ExitIntent.is_partial_qty_bracket``
294
+ # (the partial-vs-whole-row dispatch). It is sourced by the open
295
+ # row count under ``from_entry``. This relies on a simulator
296
+ # invariant: ``build_intents`` reads ``SimPosition.open_trades``,
297
+ # where each Pine entry event is exactly one ``Trade`` row (the
298
+ # simulator has no split fills — an order fills in full when its
299
+ # condition is met).
300
+ #
301
+ # * >1 row — pyramiding or repeated ``strategy.order`` under one
302
+ # Pine id. ``entry_orders[from_entry]`` keeps only the LATEST
303
+ # same-id row (``_add_order`` overwrites the dict slot), so the
304
+ # declared size understates the parent and would misclassify a
305
+ # partial exit as whole-row, mis-hedging the older legs. The
306
+ # summed open qty is the only faithful total here — and it is
307
+ # exactly what Pine's ``strategy.exit(qty=N, from_entry=...)``
308
+ # measures N against.
309
+ # * ==1 row — single-row: use the script-declared size. It is the
310
+ # stable "what the script asked for" signal and survives the
311
+ # fill (``record_fill`` only touches ``open_trades``, never pops
312
+ # the entry Order), so a broker overfill/partial-fill cannot flip
313
+ # the flag mid-lifecycle and trigger a spurious native<->partial
314
+ # bracket cancel/re-arm.
315
+ # * no declared entry left (cancelled / cleared) — fall back to the
316
+ # actual open qty.
317
+ declared_entry: Order | None = entry_orders.get(from_entry)
318
+ if count_by_entry.get(from_entry, 0) > 1:
319
+ parent_total_qty = qty_by_entry[from_entry]
320
+ elif declared_entry is not None:
321
+ parent_total_qty = abs(float(declared_entry.size))
322
+ else:
323
+ parent_total_qty = qty_by_entry.get(from_entry, 0.0)
324
+ intents.append(build_exit_intent(
325
+ order, symbol, parent_total_qty=parent_total_qty,
326
+ ))
327
+
328
+ return intents
329
+
330
+
331
+ def _active(orders: Iterable[Order]) -> Iterable[Order]:
332
+ for order in orders:
333
+ if not order.cancelled:
334
+ yield order