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,456 @@
1
+ """
2
+ Shared memory layer for request.security() inter-process communication.
3
+
4
+ Two types of shared memory blocks:
5
+
6
+ 1. **SyncBlock** — single fixed-size block containing metadata for all security slots
7
+ (timestamp, version, result_size, flags per slot).
8
+
9
+ 2. **ResultBlock** — one per security ID, holds the pickled result value.
10
+ Reallocated with doubled size when the pickle data outgrows the current block.
11
+ """
12
+ import pickle
13
+ import struct
14
+ from multiprocessing.shared_memory import SharedMemory
15
+
16
+ # Per-slot layout in the sync block:
17
+ # offset 0: int64 last_timestamp (8 bytes) — last bar timestamp from security process
18
+ # offset 8: uint32 version (4 bytes) — result block version (incremented on realloc)
19
+ # offset 12: uint32 result_size (4 bytes) — current pickle data size in bytes
20
+ # offset 16: int64 target_time (8 bytes) — target time the process should advance to
21
+ # offset 24: uint8 flags (1 byte) — state flags
22
+ # offset 25: 7 bytes pad (alignment to 32)
23
+ # offset 32: float64 dev_open (8 bytes) — developing HTF bar OHLCV (lookahead_on live)
24
+ # offset 40: float64 dev_high (8 bytes)
25
+ # offset 48: float64 dev_low (8 bytes)
26
+ # offset 56: float64 dev_close (8 bytes)
27
+ # offset 64: float64 dev_volume (8 bytes)
28
+ # offset 72: int64 dev_time (8 bytes) — developing HTF bar timestamp (ms);
29
+ # reused as period_end_exclusive (ms) by the live LTF-window
30
+ # path, which carries no pushed OHLCV (see FLAG_LTF_WINDOW)
31
+ # offset 80: uint32 write_seq (4 bytes) — bumped on every result write
32
+ # (value or na); cache key for the security-child
33
+ # cross-context read cache (version alone only tracks
34
+ # reallocation, not writes)
35
+ # offset 84: 4 bytes pad (alignment to 88)
36
+ # offset 88: int64 ltf_period_start (8 bytes) — file-backed LTF rounds: the
37
+ # chart bar's period START (ms). The child runs every
38
+ # feed bar up to target_time for expression state, but
39
+ # only values written at/after this instant belong in
40
+ # the flushed intrabar array (TradingView arrays hold
41
+ # only the bar's OWN period).
42
+ #
43
+ # Total per slot: 96 bytes
44
+ SLOT_FORMAT = '<qIIqB'
45
+ SLOT_SIZE = 96
46
+ SLOT_DATA_SIZE = struct.calcsize(SLOT_FORMAT) # 25 bytes — original fields only
47
+ _WRITE_SEQ_OFFSET = 80
48
+ _LTF_PERIOD_START_OFFSET = 88
49
+
50
+ # Offset of the developing-bar block within a slot.
51
+ _DEV_OHLCV_OFFSET = 32
52
+ _DEV_OHLCV_FORMAT = '<dddddq' # open, high, low, close, volume, time(ms)
53
+
54
+ # Flag bits
55
+ FLAG_HAS_DATA = 0x01 # result block contains valid data
56
+ FLAG_SAME_CONTEXT = 0x02 # same symbol + TF as chart (no process needed)
57
+ FLAG_IS_DEVELOPING = 0x04 # current target_time refers to a developing (open) HTF bar
58
+ FLAG_CLOSED_OVERRIDE = 0x08 # closed-bar OHLCV is supplied via SyncBlock (live mode);
59
+ # subprocess must use SyncBlock OHLCV, not the .ohlcv file
60
+ FLAG_LTF_WINDOW = 0x10 # live request.security_lower_tf window round: subprocess pulls
61
+ # intrabars from its own LTF streamer (not pushed OHLCV);
62
+ # target_time = period_start, dev_time = period_end_exclusive
63
+ FLAG_LTF_CHART_DEVELOPING = 0x20 # within an LTF-window round, the chart bar is still
64
+ # developing (keep the developing tail); clear means
65
+ # the chart bar has closed (finalize, publish full period)
66
+ FLAG_LTF_LIVE_PHASE = 0x40 # within an LTF-window round, the chart has crossed the
67
+ # warmup->live transition (barstate realtime, not history)
68
+
69
+
70
+ def is_ltf_window(flags: int) -> bool:
71
+ """Whether a slot is in a live LTF-window round (see :data:`FLAG_LTF_WINDOW`)."""
72
+ return bool(flags & FLAG_LTF_WINDOW)
73
+
74
+
75
+ def is_ltf_chart_developing(flags: int) -> bool:
76
+ """Whether the chart bar of an LTF-window round is still developing."""
77
+ return bool(flags & FLAG_LTF_CHART_DEVELOPING)
78
+
79
+
80
+ def is_ltf_live_phase(flags: int) -> bool:
81
+ """Whether an LTF-window round is past the warmup->live transition."""
82
+ return bool(flags & FLAG_LTF_LIVE_PHASE)
83
+
84
+
85
+ # Initial result block size
86
+ INITIAL_RESULT_SIZE = 4096
87
+
88
+
89
+ def _result_block_name(sec_id: str, version: int) -> str:
90
+ """Generate SharedMemory name for a result block."""
91
+ safe_id = sec_id.replace('\xb7', '_')
92
+ return f"pyne_{safe_id}_v{version}"
93
+
94
+
95
+ class SyncBlock:
96
+ """
97
+ Fixed-size shared memory block containing sync metadata for all security slots.
98
+
99
+ Layout: N consecutive slots of SLOT_SIZE bytes each.
100
+ """
101
+
102
+ def __init__(self, sec_ids: list[str], *, create: bool = True, name: str | None = None):
103
+ self._sec_ids = list(sec_ids)
104
+ self._index = {sid: i for i, sid in enumerate(sec_ids)}
105
+ total_size = max(SLOT_SIZE * len(sec_ids), 1)
106
+
107
+ if create:
108
+ self._shm = SharedMemory(
109
+ name=name, create=True, size=total_size
110
+ )
111
+ else:
112
+ self._shm = SharedMemory(name=name, create=False)
113
+
114
+ buf = self._shm.buf
115
+ assert buf is not None
116
+ self._buf: memoryview = buf
117
+
118
+ if create:
119
+ self._buf[:total_size] = b'\x00' * total_size
120
+
121
+ @property
122
+ def name(self) -> str:
123
+ return self._shm.name
124
+
125
+ def _offset(self, sec_id: str) -> int:
126
+ return self._index[sec_id] * SLOT_SIZE
127
+
128
+ def get_slot(self, sec_id: str) -> tuple[int, int, int, int, int]:
129
+ """
130
+ Read a slot's fields.
131
+
132
+ :return: (last_timestamp, version, result_size, target_time, flags)
133
+ """
134
+ off = self._offset(sec_id)
135
+ return struct.unpack_from(SLOT_FORMAT, self._buf, off)
136
+
137
+ def set_target_time(self, sec_id: str, target_time: int):
138
+ """Set the target_time field for a slot."""
139
+ off = self._offset(sec_id)
140
+ struct.pack_into('<q', self._buf, off + 16, target_time)
141
+
142
+ def get_target_time(self, sec_id: str) -> int:
143
+ """Read the target_time field for a slot."""
144
+ off = self._offset(sec_id)
145
+ return struct.unpack_from('<q', self._buf, off + 16)[0]
146
+
147
+ def set_ltf_period_end(self, sec_id: str, period_end_exclusive: int):
148
+ """Set the LTF chart-period end (ms, exclusive) for a slot.
149
+
150
+ Reuses the ``dev_time`` field (offset 72): the live LTF-window path
151
+ pushes no developing OHLCV, so this slot is free to carry the chart
152
+ bar's period end alongside ``target_time`` (the period start).
153
+ """
154
+ off = self._offset(sec_id)
155
+ struct.pack_into('<q', self._buf, off + 72, period_end_exclusive)
156
+
157
+ def get_ltf_period_end(self, sec_id: str) -> int:
158
+ """Read the LTF chart-period end (ms, exclusive) for a slot."""
159
+ off = self._offset(sec_id)
160
+ return struct.unpack_from('<q', self._buf, off + 72)[0]
161
+
162
+ def set_timestamp(self, sec_id: str, timestamp: int):
163
+ """Set the last_timestamp field."""
164
+ off = self._offset(sec_id)
165
+ struct.pack_into('<q', self._buf, off, timestamp)
166
+
167
+ def get_timestamp(self, sec_id: str) -> int:
168
+ """Read the last_timestamp field."""
169
+ off = self._offset(sec_id)
170
+ return struct.unpack_from('<q', self._buf, off)[0]
171
+
172
+ def set_result_meta(self, sec_id: str, version: int, result_size: int):
173
+ """Set version and result_size fields."""
174
+ off = self._offset(sec_id)
175
+ struct.pack_into('<II', self._buf, off + 8, version, result_size)
176
+
177
+ def get_result_meta(self, sec_id: str) -> tuple[int, int]:
178
+ """
179
+ Read version and result_size.
180
+
181
+ :return: (version, result_size)
182
+ """
183
+ off = self._offset(sec_id)
184
+ return struct.unpack_from('<II', self._buf, off + 8)
185
+
186
+ def set_ltf_period_start(self, sec_id: str, period_start_ms: int) -> None:
187
+ """Set the file-backed LTF round's chart-bar period start (ms)."""
188
+ off = self._offset(sec_id) + _LTF_PERIOD_START_OFFSET
189
+ struct.pack_into('<q', self._buf, off, period_start_ms)
190
+
191
+ def get_ltf_period_start(self, sec_id: str) -> int:
192
+ """Read the file-backed LTF round's chart-bar period start (ms)."""
193
+ off = self._offset(sec_id) + _LTF_PERIOD_START_OFFSET
194
+ return struct.unpack_from('<q', self._buf, off)[0]
195
+
196
+ def bump_write_seq(self, sec_id: str) -> None:
197
+ """Increment the slot's write sequence (wraps at 2^32)."""
198
+ off = self._offset(sec_id) + _WRITE_SEQ_OFFSET
199
+ seq = struct.unpack_from('<I', self._buf, off)[0]
200
+ struct.pack_into('<I', self._buf, off, (seq + 1) & 0xFFFFFFFF)
201
+
202
+ def get_write_seq(self, sec_id: str) -> int:
203
+ """Read the slot's write sequence."""
204
+ off = self._offset(sec_id) + _WRITE_SEQ_OFFSET
205
+ return struct.unpack_from('<I', self._buf, off)[0]
206
+
207
+ def set_flags(self, sec_id: str, flags: int):
208
+ """Set the flags byte."""
209
+ off = self._offset(sec_id)
210
+ struct.pack_into('<B', self._buf, off + 24, flags)
211
+
212
+ def get_flags(self, sec_id: str) -> int:
213
+ """Read the flags byte."""
214
+ off = self._offset(sec_id)
215
+ return struct.unpack_from('<B', self._buf, off + 24)[0]
216
+
217
+ def set_developing_bar(
218
+ self, sec_id: str,
219
+ dev_open: float, dev_high: float, dev_low: float,
220
+ dev_close: float, dev_volume: float, dev_time: int,
221
+ ):
222
+ """Write developing HTF bar OHLCV+time for live ``lookahead_on``."""
223
+ off = self._offset(sec_id) + _DEV_OHLCV_OFFSET
224
+ struct.pack_into(
225
+ _DEV_OHLCV_FORMAT, self._buf, off,
226
+ dev_open, dev_high, dev_low, dev_close, dev_volume, dev_time,
227
+ )
228
+
229
+ def get_developing_bar(
230
+ self, sec_id: str,
231
+ ) -> tuple[float, float, float, float, float, int]:
232
+ """Read developing HTF bar OHLCV+time.
233
+
234
+ :return: (open, high, low, close, volume, time_ms)
235
+ """
236
+ off = self._offset(sec_id) + _DEV_OHLCV_OFFSET
237
+ return struct.unpack_from(_DEV_OHLCV_FORMAT, self._buf, off)
238
+
239
+ def close(self):
240
+ """Close the shared memory (does not unlink)."""
241
+ self._shm.close()
242
+
243
+ def unlink(self):
244
+ """Unlink (destroy) the shared memory."""
245
+ try:
246
+ self._shm.unlink()
247
+ except FileNotFoundError:
248
+ pass
249
+
250
+
251
+ class ResultBlock:
252
+ """
253
+ Per-security-ID shared memory block holding the pickled result value.
254
+
255
+ Supports automatic reallocation when data outgrows the block.
256
+ The version number is embedded in the SharedMemory name to allow
257
+ readers to detect and re-attach after reallocation.
258
+ """
259
+
260
+ def __init__(self, sec_id: str, *, create: bool = True,
261
+ version: int = 0, size: int = INITIAL_RESULT_SIZE):
262
+ self._sec_id = sec_id
263
+ self._version = version
264
+ name = _result_block_name(sec_id, version)
265
+
266
+ if create:
267
+ try:
268
+ self._shm = SharedMemory(name=name, create=True, size=size)
269
+ except FileExistsError:
270
+ stale = SharedMemory(name=name, create=False)
271
+ stale.close()
272
+ stale.unlink()
273
+ self._shm = SharedMemory(name=name, create=True, size=size)
274
+ else:
275
+ self._shm = SharedMemory(name=name, create=False)
276
+
277
+ buf = self._shm.buf
278
+ assert buf is not None
279
+ self._buf: memoryview = buf
280
+
281
+ @property
282
+ def sec_id(self) -> str:
283
+ return self._sec_id
284
+
285
+ @property
286
+ def version(self) -> int:
287
+ return self._version
288
+
289
+ @property
290
+ def size(self) -> int:
291
+ return self._shm.size
292
+
293
+ def write(self, data: bytes, sync_block: SyncBlock) -> int:
294
+ """
295
+ Write pickle data to the result block. Reallocates if needed.
296
+
297
+ :param data: Pickled bytes to write
298
+ :param sync_block: SyncBlock to update version/size metadata
299
+ :return: New version number
300
+ """
301
+ if len(data) > self._shm.size:
302
+ new_size = max(len(data) * 2, INITIAL_RESULT_SIZE)
303
+ new_version = self._version + 1
304
+ new_name = _result_block_name(self._sec_id, new_version)
305
+
306
+ try:
307
+ new_shm = SharedMemory(name=new_name, create=True, size=new_size)
308
+ except FileExistsError:
309
+ # A segment of this exact name leaked from an earlier run that
310
+ # was killed before cleanup (the name is deterministic per
311
+ # sec_id+version). Drop the stale block and recreate, mirroring
312
+ # the initial-allocation path above.
313
+ stale = SharedMemory(name=new_name, create=False)
314
+ stale.close()
315
+ stale.unlink()
316
+ new_shm = SharedMemory(name=new_name, create=True, size=new_size)
317
+ new_buf = new_shm.buf
318
+ assert new_buf is not None
319
+ new_buf[:len(data)] = data
320
+
321
+ old_shm = self._shm
322
+ self._shm = new_shm
323
+ self._buf = new_buf
324
+ self._version = new_version
325
+
326
+ old_shm.close()
327
+ try:
328
+ old_shm.unlink()
329
+ except FileNotFoundError:
330
+ pass
331
+ else:
332
+ self._buf[:len(data)] = data
333
+
334
+ sync_block.set_result_meta(self._sec_id, self._version, len(data))
335
+ sync_block.bump_write_seq(self._sec_id)
336
+ return self._version
337
+
338
+ def read(self, size: int) -> bytes:
339
+ """
340
+ Read raw bytes from the result block.
341
+
342
+ :param size: Number of bytes to read
343
+ :return: Raw bytes
344
+ """
345
+ return bytes(self._buf[:size])
346
+
347
+ def close(self):
348
+ """Close the shared memory (does not unlink)."""
349
+ self._shm.close()
350
+
351
+ def unlink(self):
352
+ """Unlink (destroy) the shared memory."""
353
+ try:
354
+ self._shm.unlink()
355
+ except FileNotFoundError:
356
+ pass
357
+
358
+
359
+ class ResultReader:
360
+ """
361
+ Reader-side handle for a security result block.
362
+
363
+ Tracks the current version and re-attaches when the writer reallocates.
364
+ """
365
+
366
+ def __init__(self, sec_id: str):
367
+ self._sec_id = sec_id
368
+ self._shm: SharedMemory | None = None
369
+ self._version: int = -1
370
+ self._cached_seq: int = -1
371
+ self._cached_value = None
372
+
373
+ def read(self, sync_block: SyncBlock, default=None):
374
+ """
375
+ Read the latest result value from shared memory.
376
+
377
+ :param sync_block: SyncBlock to check version/size
378
+ :param default: Value to return if no data available
379
+ :return: Unpickled result value, or default
380
+ """
381
+ version, result_size = sync_block.get_result_meta(self._sec_id)
382
+
383
+ if result_size == 0:
384
+ return default
385
+
386
+ # Re-attach if version changed (writer reallocated)
387
+ if version != self._version:
388
+ if self._shm is not None:
389
+ self._shm.close()
390
+ name = _result_block_name(self._sec_id, version)
391
+ self._shm = SharedMemory(name=name, create=False)
392
+ self._version = version
393
+
394
+ shm = self._shm
395
+ assert shm is not None
396
+ buf = shm.buf
397
+ assert buf is not None
398
+ return pickle.loads(buf[:result_size])
399
+
400
+ def read_cached(self, sync_block: SyncBlock, default=None):
401
+ """Like :meth:`read`, but unpickle only when the slot's write sequence
402
+ changed since the last call — repeated reads of an unchanged result
403
+ return the SAME cached object.
404
+
405
+ For the security-child cross-context path only: a child re-runs the
406
+ whole script per bar (per intrabar for LTF), so it re-reads every peer
407
+ context's latest value on each run. Without the cache a peer's large
408
+ LTF array (tens of thousands of elements after a deep first round) is
409
+ re-unpickled per intrabar — quadratic work that turns a 30-second run
410
+ into hours. The trade-off is aliasing: a script mutating the returned
411
+ container would poison subsequent reads, so the chart-side protocol
412
+ keeps the fresh-unpickle :meth:`read`.
413
+ """
414
+ seq = sync_block.get_write_seq(self._sec_id)
415
+ if seq == self._cached_seq:
416
+ return self._cached_value if self._cached_value is not None else default
417
+ value = self.read(sync_block, default)
418
+ self._cached_seq = seq
419
+ self._cached_value = None if value is default else value
420
+ return value
421
+
422
+ def close(self):
423
+ """Close the reader's shared memory handle."""
424
+ if self._shm is not None:
425
+ self._shm.close()
426
+ self._shm = None
427
+
428
+
429
+ def write_result(result_block: ResultBlock, sync_block: SyncBlock, value) -> int:
430
+ """
431
+ Pickle a value and write it to the result block.
432
+
433
+ :param result_block: The result block to write to
434
+ :param sync_block: The sync block to update metadata
435
+ :param value: The value to pickle and store
436
+ :return: New version number
437
+ """
438
+ data = pickle.dumps(value, protocol=pickle.HIGHEST_PROTOCOL)
439
+ return result_block.write(data, sync_block)
440
+
441
+
442
+ def write_na(result_block: ResultBlock, sync_block: SyncBlock) -> int:
443
+ """
444
+ Write an empty result (na) — sets result_size to 0.
445
+
446
+ :param result_block: The result block (not modified)
447
+ :param sync_block: The sync block to update metadata
448
+ :return: Current version number
449
+ """
450
+ sync_block.set_result_meta(
451
+ result_block.sec_id,
452
+ result_block.version,
453
+ 0 # zero size = no data = na
454
+ )
455
+ sync_block.bump_write_seq(result_block.sec_id)
456
+ return result_block.version