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,442 @@
1
+ """
2
+ Live state machine for ``request.security_lower_tf``.
3
+
4
+ Drives one chart period's lower-timeframe (LTF) intrabar executions and
5
+ accumulates their expression values into a :class:`LiveLtfWindow`. This is the
6
+ orchestration layer extracted from the security subprocess so the hard parts —
7
+ monotonic ``bar_index``, snapshot save/restore around the developing intrabar,
8
+ finalize-on-close, provisional rollover under a reordered feed, and
9
+ per-chart-period window reset — can be exercised in-process, with no subprocess,
10
+ by injecting the side effects as callbacks. The collector only *sequences* those
11
+ callbacks; it performs no I/O and touches no shared memory.
12
+
13
+ Model (TradingView, verified by a realtime probe on BINANCE:BTCUSDT 30m/3m):
14
+ while a chart bar develops, the LTF array is the closed intrabars of
15
+ ``[period_start, period_end)`` plus the currently-developing intrabar as the
16
+ live *last* element; at chart close it converges to the full closed period. The
17
+ ``bar_index`` is the continuous LTF series index — it never rewinds at a
18
+ chart-period boundary (only the window resets), so expression history such as a
19
+ ``ta.sma`` inside the LTF expression stays intact across periods.
20
+
21
+ Reordered feed (provisional rollover): a live feed can deliver the forming tick
22
+ of intrabar ``N+1`` ahead of the late close of intrabar ``N`` (e.g. cTrader
23
+ closes a slot on the next slot's open, or plain stream reordering). The array
24
+ must still grow when ``N+1`` starts, so ``N`` is *promoted* to a provisional
25
+ element holding its last developing value at its original ``bar_index``; when
26
+ ``N``'s close finally arrives it *replaces* that value at the same index (it is
27
+ never appended at a new index). Because the logical evaluation order is
28
+ timestamp order regardless of arrival order, a late close that shifts the
29
+ confirmed baseline triggers a *replay* of the still-pending provisional chain
30
+ and the developing tail on the corrected baseline — so a ``var``/``ta.*`` LTF
31
+ expression is never poisoned by a provisional intrabar whose close later
32
+ differs.
33
+
34
+ Baseline invariant: the saved baseline is the var state *after the latest
35
+ confirmed (closed) intrabar and before any provisional/developing execution*. It
36
+ is saved after every confirmed intrabar run and restored before every re-run at
37
+ an already-used ``bar_index`` — a developing re-tick, a provisional-chain
38
+ replay, or a developing/provisional intrabar's own confirmed close. A brand-new
39
+ intrabar at a fresh ``bar_index`` runs against the current baseline and never
40
+ saves afterwards (until it is confirmed). ``restore`` also resets the per-bar
41
+ add/set tracking so a ``Series.add`` at the reused index degrades to a ``set``
42
+ (overwrite), matching the proven live-HTF developing mechanism; a single restore
43
+ then lets a sequence of intrabars at distinct indices each append exactly once.
44
+ """
45
+
46
+ from collections.abc import Callable, Sequence
47
+ from dataclasses import dataclass
48
+ from typing import Any, Protocol
49
+
50
+ from .live_ltf_window import LiveLtfWindow
51
+
52
+ __all__ = ["LiveLtfCollector", "IntrabarLike"]
53
+
54
+
55
+ class IntrabarLike(Protocol):
56
+ """Minimal shape the collector needs from an intrabar (an ``OHLCV``).
57
+
58
+ Only the ``timestamp`` (seconds) is read here; the value an intrabar
59
+ contributes to the array comes from ``run_intrabar``, not from the OHLCV.
60
+ ``OHLCV`` is an immutable ``NamedTuple``, so a reference kept for replay
61
+ cannot be mutated out from under the collector.
62
+ """
63
+
64
+ timestamp: int
65
+
66
+
67
+ @dataclass
68
+ class _Intrabar:
69
+ """A replayable in-period intrabar slot.
70
+
71
+ Carries the last-known OHLCV alongside the assigned ``bar_index`` so a
72
+ provisional intrabar can be re-run on a corrected baseline after a late close
73
+ (replay needs the original input, not just the timestamp/index).
74
+
75
+ ``is_new`` is the pending ``barstate.isnew`` flag: it is True only until the
76
+ slot has executed once. A bar promoted from the developing tail already fired
77
+ ``is_new=True`` while it was developing, so its slot starts False; a bar that
78
+ is deferred straight into the chain without ever being a developing tail (a
79
+ newer closed bar held behind an unresolved gap) starts True so its very first
80
+ replay still reports a new bar, exactly like the normal closed-bar path. The
81
+ flag is consumed (cleared) by the first run, so later replays report False.
82
+ """
83
+
84
+ ts_ms: int
85
+ bar_index: int
86
+ ohlcv: Any
87
+ is_new: bool = False
88
+
89
+
90
+ class LiveLtfCollector:
91
+ """Sequences live LTF intrabar runs into a per-chart-period array.
92
+
93
+ :param run_intrabar: ``(ohlcv, bar_index, confirmed, is_new, islast) ->
94
+ value`` — runs the LTF script for one intrabar (sets lib properties +
95
+ barstate, executes ``main``) and returns the expression value it wrote.
96
+ ``islast`` is the live last element flag (``barstate.islast``): true
97
+ only for the single developing tail, false for every closed and
98
+ provisional (rolled-over) intrabar — a provisional is no longer the
99
+ last element once a newer intrabar has started, even though it is still
100
+ unconfirmed.
101
+ :param save_baseline: Persist the current var state as the rollback
102
+ baseline (no-op acceptable when the script has no ``var`` slots).
103
+ :param restore_baseline: Restore the var state to the saved baseline and
104
+ reset per-bar add/set tracking, in preparation for a re-run at an
105
+ already-used ``bar_index``.
106
+ :param start_bar_index: The first ``bar_index`` to assign (default 0).
107
+ """
108
+
109
+ def __init__(
110
+ self,
111
+ run_intrabar: Callable[[Any, int, bool, bool, bool], Any],
112
+ save_baseline: Callable[[], None],
113
+ restore_baseline: Callable[[], None],
114
+ *,
115
+ start_bar_index: int = 0,
116
+ ) -> None:
117
+ self._run_intrabar = run_intrabar
118
+ self._save_baseline = save_baseline
119
+ self._restore_baseline = restore_baseline
120
+
121
+ self._window = LiveLtfWindow()
122
+ self._bar_index = start_bar_index
123
+ self._seen_any = False
124
+ self._has_baseline = False
125
+
126
+ self._period_start: int | None = None
127
+ self._developing: _Intrabar | None = None
128
+ self._pending: list[_Intrabar] = []
129
+ self._late_closes: dict[int, IntrabarLike] = {}
130
+
131
+ @property
132
+ def bar_index(self) -> int:
133
+ """The most recently assigned LTF series index (monotonic)."""
134
+ return self._bar_index
135
+
136
+ @property
137
+ def window(self) -> LiveLtfWindow:
138
+ """The underlying per-chart-period accumulator (for inspection)."""
139
+ return self._window
140
+
141
+ def _advance_index(self) -> int:
142
+ """Assign the next ``bar_index``; the very first bar reuses the start."""
143
+ if self._seen_any:
144
+ self._bar_index += 1
145
+ self._seen_any = True
146
+ return self._bar_index
147
+
148
+ def _ensure_baseline(self) -> None:
149
+ """Save an initial baseline if none exists (process opened on a dev bar)."""
150
+ if not self._has_baseline:
151
+ self._save_baseline()
152
+ self._has_baseline = True
153
+
154
+ def _is_pending_ts(self, ts_ms: int) -> bool:
155
+ """Whether ``ts_ms`` belongs to a rolled-over provisional awaiting close."""
156
+ return any(p.ts_ms == ts_ms for p in self._pending)
157
+
158
+ def _run_pending(self, slot: _Intrabar, confirmed: bool) -> Any:
159
+ """Run one pending provisional slot, consuming its first-run ``is_new``.
160
+
161
+ A deferred-into-the-chain slot must report ``barstate.isnew`` on its very
162
+ first execution (the normal closed-bar path does); the flag is cleared
163
+ here so every later replay of the same slot reports ``is_new=False``.
164
+ Pending slots are never the live last element, so ``islast`` is False.
165
+ """
166
+ is_new = slot.is_new
167
+ slot.is_new = False
168
+ return self._run_intrabar(slot.ohlcv, slot.bar_index, confirmed, is_new, False)
169
+
170
+ def _drain_pending_closes(self) -> bool:
171
+ """Finalize buffered late closes for the provisional chain, oldest first.
172
+
173
+ A provisional is finalized only once its own close is buffered AND it is
174
+ at the front of the chain (closes apply front-to-back so the confirmed
175
+ baseline advances contiguously). Each finalize restores the baseline,
176
+ re-runs the intrabar as confirmed at its ORIGINAL ``bar_index``, replaces
177
+ the provisional value with the confirmed one, and saves the new baseline.
178
+
179
+ :return: Whether at least one provisional was finalized.
180
+ """
181
+ drained = False
182
+ while self._pending and self._pending[0].ts_ms in self._late_closes:
183
+ slot = self._pending.pop(0)
184
+ bar = self._late_closes.pop(slot.ts_ms)
185
+ slot.ohlcv = bar
186
+ self._restore_baseline()
187
+ value = self._run_pending(slot, True)
188
+ self._window.finalize_first_provisional(value)
189
+ self._save_baseline()
190
+ self._has_baseline = True
191
+ drained = True
192
+ return drained
193
+
194
+ def _rebuild_tail(self, *, run_developing: bool) -> None:
195
+ """Replay the pending provisional chain (and optionally the developing
196
+ tail) on the current confirmed baseline.
197
+
198
+ Restores once to the post-confirmed baseline, then re-runs every pending
199
+ provisional in timestamp order (each at its own index, so each
200
+ ``Series.add`` appends exactly once) and writes its value back in place;
201
+ finally re-runs the developing tail on top when requested. None of these
202
+ runs save a baseline — they are all unconfirmed.
203
+ """
204
+ self._restore_baseline()
205
+ for i, slot in enumerate(self._pending):
206
+ value = self._run_pending(slot, False)
207
+ self._window.replace_provisional(i, value)
208
+ if run_developing and self._developing is not None:
209
+ value = self._run_intrabar(
210
+ self._developing.ohlcv, self._developing.bar_index, False, False, True
211
+ )
212
+ self._window.set_developing(value)
213
+
214
+ def process_round(
215
+ self,
216
+ period_start: int,
217
+ period_end_exclusive: int,
218
+ closed_bars: Sequence[IntrabarLike],
219
+ developing_bar: IntrabarLike | None,
220
+ chart_confirmed: bool,
221
+ ) -> list[Any]:
222
+ """Advance one chart tick and return the published LTF array (a copy).
223
+
224
+ ``closed_bars`` are the newly available closed intrabars (ordered, and
225
+ already bounded above by ``period_end_exclusive`` by the caller — a bar
226
+ belonging to a future chart period must be withheld until that period is
227
+ current). Every closed intrabar is *run* to maintain expression history;
228
+ only those at or after ``period_start`` join the array. ``developing_bar``
229
+ is the currently-forming intrabar (``None`` when none is available, e.g.
230
+ right at period open); it is processed only while the chart bar itself is
231
+ still developing (``chart_confirmed`` is False).
232
+
233
+ :param period_start: The chart bar's open instant (ms).
234
+ :param period_end_exclusive: The chart bar's period end (ms, exclusive).
235
+ :param closed_bars: Newly available closed intrabars, time-ordered.
236
+ :param developing_bar: The currently-developing intrabar, or ``None``.
237
+ :param chart_confirmed: Whether the chart bar has closed (period complete).
238
+ :return: A fresh list — closed + provisional values plus the developing
239
+ tail, if any.
240
+ """
241
+ # ── Chart-period roll ──
242
+ # The closing period's confirmed round always arrives (with its old
243
+ # ``period_start``) before the first round of the next period, so the
244
+ # window is already finalized here; the reset just clears it. Stale
245
+ # rollover tracking is dropped defensively.
246
+ if period_start != self._period_start:
247
+ self._window.reset()
248
+ self._period_start = period_start
249
+ self._developing = None
250
+ self._pending = []
251
+ self._late_closes = {}
252
+
253
+ # ── Closed intrabars (run all for history; window membership in-period) ──
254
+ # Tracks whether the confirmed baseline advanced inside this loop (an
255
+ # in-loop drain finalized a provisional, or a brand-new closed bar saved).
256
+ # A still-tracked developing tail computed against the OLD baseline is then
257
+ # stale and must be rebuilt below even if the post-loop drain finalizes
258
+ # nothing (it already drained in the loop).
259
+ baseline_advanced = False
260
+ for bar in closed_bars:
261
+ ts_ms = int(bar.timestamp * 1000)
262
+ if (self._developing is not None and ts_ms == self._developing.ts_ms
263
+ and not self._pending):
264
+ # In-order close of the current developing intrabar (no reorder):
265
+ # re-run as confirmed at the SAME index (its dev ticks restored
266
+ # away) and finalize the tail. The confirmed close may differ
267
+ # from the last developing tick.
268
+ self._restore_baseline()
269
+ value = self._run_intrabar(bar, self._developing.bar_index, True, False, False)
270
+ self._window.finalize_developing(value)
271
+ self._save_baseline()
272
+ self._has_baseline = True
273
+ self._developing = None
274
+ elif self._developing is not None and ts_ms == self._developing.ts_ms:
275
+ # Late close of the current developing intrabar arriving while a
276
+ # provisional chain still leads it (its close was batched behind
277
+ # the heads' closes). Promote it to the back of the chain — it has
278
+ # the largest timestamp — and buffer its close, so the drain below
279
+ # finalizes the whole chain front-to-back in timestamp order, each
280
+ # at its ORIGINAL bar_index, instead of appending a new index.
281
+ self._window.promote_developing_to_provisional()
282
+ self._pending.append(self._developing)
283
+ self._developing = None
284
+ self._late_closes[ts_ms] = bar
285
+ elif self._is_pending_ts(ts_ms):
286
+ # Late close of a rolled-over provisional: buffer it and finalize
287
+ # the provisional chain front-to-back below, so the close
288
+ # replaces the provisional value at its ORIGINAL bar_index rather
289
+ # than appending at a new one.
290
+ self._late_closes[ts_ms] = bar
291
+ else:
292
+ # A brand-new closed intrabar. The streamer can batch a pending
293
+ # provisional's late close ahead of (or behind) this newer bar in
294
+ # the same round; finalize every drainable provisional FIRST so the
295
+ # baseline advances and the chain becomes closed in timestamp order
296
+ # before this newer bar runs and appends. Without this drain the
297
+ # newer bar would run on the stale pre-close baseline, save it, and
298
+ # land in the closed segment ahead of the still-provisional chain —
299
+ # corrupting both the published order and the persisted var state.
300
+ if self._drain_pending_closes():
301
+ baseline_advanced = True
302
+ # If an older developing intrabar is still unconfirmed AND gets no
303
+ # fresh forming tick this round (its close was skipped — a gap),
304
+ # promote it to the BACK of the provisional chain so this newer bar
305
+ # is deferred behind it below instead of falling through to the
306
+ # closed segment. The developing tail always carries the largest
307
+ # timestamp of the tracked intrabars (it started after every pending
308
+ # provisional), so the back of the chain keeps timestamp order even
309
+ # when an earlier provisional gap left ``_pending`` non-empty.
310
+ # Without this the newer bar would publish ahead of the older
311
+ # developing tail (out of timestamp order) and advance the saved
312
+ # baseline past the gap. A developing that IS still forming this round
313
+ # (its tick is in ``developing_bar``) stays the live tail and the
314
+ # newer closed bar legitimately appends ahead of it.
315
+ if (self._developing is not None
316
+ and self._developing.ts_ms < ts_ms
317
+ and not (developing_bar is not None
318
+ and int(developing_bar.timestamp * 1000)
319
+ == self._developing.ts_ms)):
320
+ self._window.promote_developing_to_provisional()
321
+ self._pending.append(self._developing)
322
+ self._developing = None
323
+ idx = self._advance_index()
324
+ in_period = period_start <= ts_ms < period_end_exclusive
325
+ if self._pending and in_period:
326
+ # A provider gap left an older provisional's close missing, so
327
+ # the drain could not empty the chain. This newer bar's value
328
+ # cannot join the closed segment (it sits ahead of the still-
329
+ # pending chain in ``publish()``) nor advance the saved baseline
330
+ # past the unresolved earlier intrabar. Defer it into the
331
+ # provisional chain behind that gap: hold it at its own index,
332
+ # buffer its (already-arrived) close so it finalizes the moment
333
+ # the chain ahead drains, and replay the whole chain in place.
334
+ # It enters the chain as a brand-new bar, so its first replay
335
+ # must still report ``barstate.isnew`` like the normal closed
336
+ # path below — unlike a slot promoted from the developing tail.
337
+ self._pending.append(
338
+ _Intrabar(ts_ms=ts_ms, bar_index=idx, ohlcv=bar, is_new=True))
339
+ self._late_closes[ts_ms] = bar
340
+ self._window.append_provisional(None)
341
+ self._rebuild_tail(run_developing=True)
342
+ else:
343
+ value = self._run_intrabar(bar, idx, True, True, False)
344
+ if in_period:
345
+ self._window.append_closed(value)
346
+ self._save_baseline()
347
+ self._has_baseline = True
348
+ baseline_advanced = True
349
+
350
+ # ── Finalize buffered late closes; replay the rest of the chain ──
351
+ # After the baseline advances, the still-pending provisionals — and a
352
+ # developing tail computed against the pre-advance baseline — are stale.
353
+ # Rebuild them now unless the developing block below will rebuild the
354
+ # whole tail this round (a fresh developing tick is present), which avoids
355
+ # replaying the provisional chain twice. The post-loop drain alone is not
356
+ # enough to detect staleness: if the only baseline advance happened INSIDE
357
+ # the closed-bar loop (an in-loop drain, or a brand-new bar's save) this
358
+ # final drain finalizes nothing and returns False, yet a tail left from a
359
+ # prior round still sits on the old baseline — so ``baseline_advanced``
360
+ # forces the rebuild too. Skip entirely when nothing remains to replay (the
361
+ # whole chain closed and no developing tail) — a bare ``_rebuild_tail``
362
+ # would only restore the baseline, and that restore also resets
363
+ # function-instance state without a re-run to rebuild it, poisoning the
364
+ # next intrabar's child (``ta.*``-in-function) history.
365
+ if ((self._drain_pending_closes() or baseline_advanced) and developing_bar is None
366
+ and (self._pending or self._developing is not None)):
367
+ self._rebuild_tail(run_developing=True)
368
+
369
+ # ── Confirmed-round convergence ──
370
+ # A confirmed chart bar's array is exactly its closed intrabars. Any
371
+ # unconfirmed tail left from the last forming tick — a developing tail or
372
+ # a rolled-over provisional whose close never arrived (a provider gap or
373
+ # grace timeout; the caller publishes the round anyway rather than
374
+ # stalling the bot) — must not appear in a confirmed array. The discarded
375
+ # runs also mutated the script's var state without saving a baseline, so
376
+ # restore the post-confirmed baseline before dropping — otherwise the next
377
+ # period's intrabar would compound from the abandoned ticks. Then drop the
378
+ # tails and clear the rollover tracking so a late close in a later period
379
+ # is treated as its own intrabar, not a re-finalization at a stale index.
380
+ if chart_confirmed and (self._window.has_developing or self._window.has_provisional):
381
+ if self._has_baseline:
382
+ self._restore_baseline()
383
+ self._window.drop_developing()
384
+ self._window.drop_provisionals()
385
+ self._developing = None
386
+ self._pending = []
387
+ self._late_closes = {}
388
+
389
+ # ── Developing intrabar (live last element) ──
390
+ if not chart_confirmed and developing_bar is not None:
391
+ ts_ms = int(developing_bar.timestamp * 1000)
392
+ if period_start <= ts_ms < period_end_exclusive:
393
+ cur = self._developing
394
+ if cur is not None and ts_ms == cur.ts_ms:
395
+ # Another tick of the same developing intrabar: rewind to the
396
+ # baseline and re-run in place (replaying any provisional
397
+ # chain ahead of it so it accumulates from the confirmed
398
+ # prefix, not an abandoned tick).
399
+ cur.ohlcv = developing_bar
400
+ self._rebuild_tail(run_developing=True)
401
+ elif self._is_pending_ts(ts_ms):
402
+ # A stale forming update for an intrabar that already rolled
403
+ # over into the provisional chain (the feed reordered N's late
404
+ # tick behind N+1's open). It must NOT allocate a new index —
405
+ # refresh the provisional's stored OHLCV so its eventual close
406
+ # / replay uses the latest forming value, then rebuild the
407
+ # chain (and developing tail) in place at the same indices.
408
+ for slot in self._pending:
409
+ if slot.ts_ms == ts_ms:
410
+ slot.ohlcv = developing_bar
411
+ break
412
+ self._rebuild_tail(run_developing=True)
413
+ else:
414
+ # A fresh developing intrabar at a new timestamp.
415
+ if cur is not None:
416
+ # RACE: the previous developing intrabar never closed in
417
+ # order; promote it to a provisional element (preserve its
418
+ # slot + index) before advancing, so its late close can
419
+ # replace it in place rather than append at a new index.
420
+ self._window.promote_developing_to_provisional()
421
+ self._pending.append(cur)
422
+ self._ensure_baseline()
423
+ idx = self._advance_index()
424
+ self._developing = _Intrabar(ts_ms=ts_ms, bar_index=idx,
425
+ ohlcv=developing_bar)
426
+ if self._pending:
427
+ # Rebuild the provisional chain on the post-confirmed
428
+ # baseline, then run the NEW developing on top (first run
429
+ # at a fresh index -> is_new=True), without saving.
430
+ self._restore_baseline()
431
+ for i, slot in enumerate(self._pending):
432
+ v = self._run_pending(slot, False)
433
+ self._window.replace_provisional(i, v)
434
+ value = self._run_intrabar(developing_bar, idx, False, True, True)
435
+ self._window.set_developing(value)
436
+ else:
437
+ # No reorder in flight: run once at a new index against
438
+ # the post-confirmed baseline; do NOT save after.
439
+ value = self._run_intrabar(developing_bar, idx, False, True, True)
440
+ self._window.set_developing(value)
441
+
442
+ return self._window.publish()
@@ -0,0 +1,189 @@
1
+ """
2
+ Per-chart-period accumulator for live ``request.security_lower_tf``.
3
+
4
+ In a live run the chart bar is *developing*: its lower-timeframe (LTF) array
5
+ grows as intrabars arrive and only becomes the full period ``[T, T+tf)`` once
6
+ the chart bar closes. TradingView includes the currently-developing LTF bar as
7
+ the live *last* element of the array (verified against TV: array size is
8
+ ``floor(elapsed / ltf_span) + 1`` — the closed intrabars plus the one
9
+ developing intrabar, whose value tracks the live price tick-by-tick).
10
+
11
+ This container holds exactly that shape in three ordered segments:
12
+
13
+ * ``closed`` — finalized (confirmed-close) intrabar values;
14
+ * ``provisional`` — intrabars that have rolled over (a newer developing intrabar
15
+ started) but whose confirmed close has not arrived yet, so their value is the
16
+ last developing tick and is *replaced* in place when the late close lands;
17
+ * ``developing`` — the single currently-forming intrabar as the live tail.
18
+
19
+ The provisional segment exists only because a live feed can deliver the forming
20
+ tick of intrabar ``N+1`` ahead of the late close of intrabar ``N`` (e.g.
21
+ cTrader closes a slot on the next slot's open, or plain stream reordering). The
22
+ array must still grow by one when ``N+1`` starts — ``N`` is held provisionally
23
+ at its last developing value until its close replaces it at the same position.
24
+
25
+ It is pure value bookkeeping — it does NOT run the script or touch shared
26
+ memory; the security subprocess decides which intrabars to feed it, re-runs the
27
+ developing intrabar per tick, and replays the provisional chain when a late
28
+ close shifts the baseline. Keeping the accumulation logic here (rather than as
29
+ raw list surgery on the flush buffer) keeps the "closed + provisional +
30
+ developing" invariant in one place.
31
+
32
+ The child must still *execute* every LTF bar it needs to maintain expression
33
+ history (e.g. a ``ta.sma`` inside the LTF expression); only array *membership*
34
+ is bounded by the chart period, so the child appends to this accumulator only
35
+ for intrabars inside ``[period_start, period_end)``.
36
+ """
37
+
38
+ from typing import Any
39
+
40
+ __all__ = ["LiveLtfWindow"]
41
+
42
+
43
+ class LiveLtfWindow:
44
+ """Accumulates one chart period's LTF intrabar values for the live path.
45
+
46
+ The array published to the script is ``closed + provisional + [developing]``.
47
+ A developing chart bar reports ``len(closed) + len(provisional) + 1`` elements
48
+ while a tail is forming and exactly ``len(closed)`` once every tail has been
49
+ finalized into the closed list at the chart period's end. The provisional
50
+ segment carries intrabars that have rolled over but whose confirmed close has
51
+ not yet arrived; their values are revised in place by a replay and become
52
+ closed (front-to-back) as their late closes land.
53
+ """
54
+
55
+ def __init__(self) -> None:
56
+ self._closed: list[Any] = []
57
+ self._provisional: list[Any] = []
58
+ self._developing: Any = None
59
+ self._has_developing: bool = False
60
+
61
+ def reset(self) -> None:
62
+ """Drop all state for the start of a new chart period."""
63
+ self._closed.clear()
64
+ self._provisional.clear()
65
+ self._developing = None
66
+ self._has_developing = False
67
+
68
+ def append_closed(self, value: Any) -> None:
69
+ """Append a finalized (confirmed) intrabar value.
70
+
71
+ Used for closed intrabars that are neither the finalization of the
72
+ current developing tail (:meth:`finalize_developing`) nor of a pending
73
+ provisional (:meth:`finalize_first_provisional`).
74
+ """
75
+ self._closed.append(value)
76
+
77
+ def set_developing(self, value: Any) -> None:
78
+ """Set or replace the developing tail value.
79
+
80
+ Called on every tick of the currently-developing LTF intrabar; the
81
+ previous tick's value is discarded (the developing bar occupies a
82
+ single array slot whose value is revised in place).
83
+ """
84
+ self._developing = value
85
+ self._has_developing = True
86
+
87
+ def finalize_developing(self, value: Any) -> None:
88
+ """Finalize the developing tail into the closed list.
89
+
90
+ Called when the developing LTF intrabar closes in order (a confirmed bar
91
+ arrives for its timestamp with no provisional chain ahead of it); the
92
+ confirmed ``value`` may differ from the last developing tick, so it is
93
+ supplied explicitly rather than reusing the in-flight tail. The array
94
+ length is unchanged — the tail slot simply becomes a closed element.
95
+ """
96
+ self._closed.append(value)
97
+ self._developing = None
98
+ self._has_developing = False
99
+
100
+ def promote_developing_to_provisional(self) -> None:
101
+ """Roll the developing tail into the provisional segment.
102
+
103
+ Called when a newer developing intrabar starts before the current one's
104
+ close has arrived: the current tail keeps its (last developing) value
105
+ and position, becoming provisional, so the array still grows by one when
106
+ the new developing tail is set. A no-op if there is no developing tail.
107
+ """
108
+ if self._has_developing:
109
+ self._provisional.append(self._developing)
110
+ self._developing = None
111
+ self._has_developing = False
112
+
113
+ def append_provisional(self, value: Any) -> None:
114
+ """Append a new provisional entry behind the existing provisional chain.
115
+
116
+ Called when a brand-new closed intrabar arrives while an older provisional
117
+ is still unresolved (a provider gap left the chain head open). Its
118
+ confirmed value cannot join the closed segment yet — that sits ahead of
119
+ the provisional chain, so appending there would place the newer bar before
120
+ the older, still-pending one. It is held provisionally at the back instead,
121
+ in timestamp order, and finalized once the chain ahead of it drains.
122
+ """
123
+ self._provisional.append(value)
124
+
125
+ def finalize_first_provisional(self, value: Any) -> None:
126
+ """Promote the earliest provisional entry to a confirmed closed value.
127
+
128
+ Called when the late close of the oldest pending provisional arrives.
129
+ The provisional slot at the front becomes a closed element at the SAME
130
+ array position (closed entries sit ahead of provisional ones); the
131
+ confirmed ``value`` replaces the last-developing value it was holding.
132
+ """
133
+ del self._provisional[0]
134
+ self._closed.append(value)
135
+
136
+ def replace_provisional(self, index: int, value: Any) -> None:
137
+ """Revise a still-pending provisional value in place (replay).
138
+
139
+ After a late close shifts the baseline, the remaining provisional
140
+ intrabars are re-run on the corrected state; their values are written
141
+ back at their existing positions without changing the array length.
142
+ """
143
+ self._provisional[index] = value
144
+
145
+ def drop_developing(self) -> None:
146
+ """Discard the developing tail without finalizing it into the closed list.
147
+
148
+ Used when a chart bar confirms but the developing intrabar's close never
149
+ arrived (a provider gap): the unconfirmed tail must not remain in the
150
+ confirmed period array, and unlike :meth:`finalize_developing` there is no
151
+ confirmed value to keep — the array length shrinks by one.
152
+ """
153
+ self._developing = None
154
+ self._has_developing = False
155
+
156
+ def drop_provisionals(self) -> None:
157
+ """Discard every still-pending provisional entry.
158
+
159
+ Used at chart confirm when one or more rolled-over intrabars never
160
+ received their close (a provider gap or grace timeout): like an
161
+ unconfirmed developing tail, an unconfirmed provisional must not remain
162
+ in the confirmed period array. Already-finalized provisionals live in the
163
+ closed segment and are unaffected.
164
+ """
165
+ self._provisional.clear()
166
+
167
+ @property
168
+ def has_developing(self) -> bool:
169
+ """Whether a developing tail is currently present."""
170
+ return self._has_developing
171
+
172
+ @property
173
+ def has_provisional(self) -> bool:
174
+ """Whether any pending (rolled-over, not-yet-closed) provisional remains."""
175
+ return bool(self._provisional)
176
+
177
+ def publish(self) -> list[Any]:
178
+ """Return a copy of the current array (closed + provisional + developing).
179
+
180
+ A fresh list is returned each call so the consumer may hand it across
181
+ the result block without the accumulator's later mutations leaking in.
182
+ """
183
+ out = [*self._closed, *self._provisional]
184
+ if self._has_developing:
185
+ out.append(self._developing)
186
+ return out
187
+
188
+ def __len__(self) -> int:
189
+ return len(self._closed) + len(self._provisional) + (1 if self._has_developing else 0)