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
File without changes
@@ -0,0 +1,257 @@
1
+ """
2
+ OHLCV timeframe aggregation — converts lower timeframe data to higher timeframes.
3
+
4
+ Uses Resampler.get_bar_time() for correct bar boundary alignment across all
5
+ timeframe types including weekly and monthly. Multi-period targets (nD/nW/nM,
6
+ n > 1) live on the year-reset scheduled grid (see ``resampler`` module docs);
7
+ 'observed' symbols (exchange-listed) count the actual trading days seen in the
8
+ source stream, which reproduces TradingView's holiday-aware grid.
9
+ """
10
+ from datetime import timezone as dt_timezone
11
+ from pathlib import Path
12
+ from zoneinfo import ZoneInfo
13
+
14
+ from .ohlcv_file import OHLCVReader, OHLCVWriter
15
+ from .resampler import (
16
+ Resampler, ObservedDayCounter, grid_mode, overnight_opens, trading_day,
17
+ )
18
+ from ..lib.timeframe import in_seconds, _process_tf
19
+ from ..types.ohlcv import OHLCV
20
+
21
+
22
+ def validate_aggregation(source_tf: str, target_tf: str) -> None:
23
+ """
24
+ Validate that aggregation from source to target timeframe is possible.
25
+
26
+ :param source_tf: Source timeframe string (e.g., '5', '1D')
27
+ :param target_tf: Target timeframe string (e.g., '60', '1W')
28
+ :raises ValueError: If timeframes are incompatible
29
+ """
30
+ source_sec = in_seconds(source_tf)
31
+ target_sec = in_seconds(target_tf)
32
+
33
+ if target_sec <= source_sec:
34
+ raise ValueError(
35
+ f"Target timeframe ({target_tf}) must be larger than "
36
+ f"source timeframe ({source_tf})"
37
+ )
38
+
39
+ if target_sec % source_sec != 0:
40
+ raise ValueError(
41
+ f"Target timeframe ({target_tf}) must be evenly divisible by "
42
+ f"source timeframe ({source_tf})"
43
+ )
44
+
45
+
46
+ def _merge_candles(candles: list[OHLCV], bar_time: int) -> OHLCV:
47
+ """
48
+ Merge a window of candles into a single aggregated candle.
49
+
50
+ :param candles: Non-empty list of OHLCV candles belonging to the same bar
51
+ :param bar_time: Aligned bar opening timestamp in seconds
52
+ :return: Aggregated OHLCV candle
53
+ """
54
+ return OHLCV(
55
+ timestamp=bar_time,
56
+ open=candles[0].open,
57
+ high=max(c.high for c in candles),
58
+ low=min(c.low for c in candles),
59
+ close=candles[-1].close,
60
+ volume=sum(c.volume for c in candles),
61
+ )
62
+
63
+
64
+ def aggregate_ohlcv(
65
+ source_path: Path,
66
+ target_path: Path,
67
+ target_tf: str,
68
+ tz: ZoneInfo | dt_timezone | None = None,
69
+ session_starts: list | None = None,
70
+ opening_hours: list | None = None,
71
+ sym_type: str | None = None,
72
+ source_tf: str | None = None,
73
+ ) -> tuple[int, int]:
74
+ """
75
+ Aggregate OHLCV data from a lower timeframe file to a higher timeframe file.
76
+
77
+ :param source_path: Path to source .ohlcv file
78
+ :param target_path: Path to target .ohlcv file (will be overwritten)
79
+ :param target_tf: Target timeframe string (e.g., '60', '1W')
80
+ :param tz: Timezone for day/week/month boundary alignment.
81
+ Should match the data's timezone (from TOML metadata).
82
+ :param session_starts: Per-trading-day primary opens for intraday session
83
+ anchoring and multi-period grids. When given, intraday bars align
84
+ to the session open (TradingView behaviour) instead of the UTC
85
+ clock; ``None`` keeps the pure clock-floor.
86
+ See :meth:`Resampler.get_bar_time`.
87
+ :param opening_hours: ``SymInfo.opening_hours`` — trading-day roll source
88
+ for multi-period (nD/nW/nM) grids.
89
+ :param sym_type: ``SymInfo.type`` for :func:`grid_mode` classification of
90
+ multi-period grids. Exchange-listed symbols ('observed' mode)
91
+ count the actual trading days present in the source data; for an
92
+ exact TradingView-matching grid their source data should reach
93
+ back to the year's first trading day (the in-year counter is
94
+ otherwise approximated from the weekday grid).
95
+ :param source_tf: Source timeframe string. On multi-period (nD/nW/nM)
96
+ targets an intraday source bar belongs to the trading day its
97
+ *last* instant falls into — the bar containing a session open
98
+ starts the new day even when its timestamp precedes the open
99
+ (see the ``resampler`` module docs).
100
+ :return: Tuple of (source_candles_read, target_candles_written)
101
+ """
102
+ # noinspection PyProtectedMember
103
+ modifier, multiplier = _process_tf(target_tf)
104
+ mode = grid_mode(sym_type, opening_hours)
105
+
106
+ src_off = 0
107
+ fold = False
108
+ if modifier in ('D', 'W', 'M') and multiplier > 1 and source_tf:
109
+ # noinspection PyProtectedMember
110
+ src_mod, _ = _process_tf(source_tf)
111
+ if src_mod in ('', 'S'):
112
+ src_off = in_seconds(source_tf) - 1
113
+ # An intraday source carries the per-bar end instants the observed
114
+ # holiday half-day fold needs (a daily source is already folded).
115
+ fold = True
116
+
117
+ if modifier in ('D', 'W', 'M') and multiplier > 1 and mode == 'observed':
118
+ return _aggregate_observed(
119
+ source_path, target_path, modifier, multiplier, tz,
120
+ session_starts, opening_hours, src_off, fold)
121
+
122
+ resampler = Resampler.get_resampler(target_tf)
123
+
124
+ source_count = 0
125
+ target_count = 0
126
+
127
+ with OHLCVReader(source_path) as reader:
128
+ with OHLCVWriter(target_path, truncate=True) as writer:
129
+ window: list[OHLCV] = []
130
+ current_bar_time: int | None = None
131
+
132
+ start_ts = reader.start_timestamp
133
+ if start_ts is None:
134
+ if reader.size == 1:
135
+ # A single-record source has no derivable interval (the reader
136
+ # needs two timestamps to infer one), so ``start_timestamp`` is
137
+ # None and ``read_from`` yields nothing — yet that lone bar IS a
138
+ # whole target period and must be emitted. Floor its timestamp
139
+ # onto the target grid (exactly as the loop below does) so HTF
140
+ # confirmation (the ``bar_opens`` clamp) and
141
+ # ``request.security(.., time)`` see the period boundary, not the
142
+ # raw sub-bar instant.
143
+ only = reader.read(0)
144
+ only_bar_time = resampler.get_bar_time(
145
+ (only.timestamp + src_off) * 1000, tz=tz,
146
+ session_starts=session_starts,
147
+ opening_hours=opening_hours, mode=mode) // 1000
148
+ writer.write(_merge_candles([only], only_bar_time))
149
+ return 1, 1
150
+ return 0, 0
151
+
152
+ for candle in reader.read_from(start_ts):
153
+ source_count += 1
154
+
155
+ # Resampler works in ms, OHLCV timestamps are in seconds.
156
+ # src_off resolves multi-period bars by their last instant.
157
+ bar_time_ms = resampler.get_bar_time(
158
+ (candle.timestamp + src_off) * 1000, tz=tz,
159
+ session_starts=session_starts,
160
+ opening_hours=opening_hours, mode=mode)
161
+ bar_time = bar_time_ms // 1000
162
+
163
+ if current_bar_time is not None and bar_time != current_bar_time:
164
+ # New bar boundary — flush the window
165
+ writer.write(_merge_candles(window, current_bar_time))
166
+ target_count += 1
167
+ window = []
168
+
169
+ current_bar_time = bar_time
170
+ window.append(candle)
171
+
172
+ # Flush last window
173
+ if window and current_bar_time is not None:
174
+ writer.write(_merge_candles(window, current_bar_time))
175
+ target_count += 1
176
+
177
+ return source_count, target_count
178
+
179
+
180
+ def _aggregate_observed(
181
+ source_path: Path,
182
+ target_path: Path,
183
+ modifier: str,
184
+ multiplier: int,
185
+ tz: ZoneInfo | dt_timezone | None,
186
+ session_starts: list | None,
187
+ opening_hours: list | None,
188
+ src_off: int = 0,
189
+ fold: bool = False,
190
+ ) -> tuple[int, int]:
191
+ """
192
+ Multi-period aggregation for 'observed' symbols (exchange-listed).
193
+
194
+ TradingView's grid on these symbols counts its holiday calendar's scheduled
195
+ trading days; the actual daily data realizes that calendar, so counting the
196
+ trading days present in the source stream reproduces the grid (year-reset
197
+ counter, verified 100% on CME 2022+). Periods are stamped with their first
198
+ source candle — TradingView's stamp on these symbols is the period's first
199
+ actual session.
200
+
201
+ The first (partial) year's counter is seeded from the weekday grid: the
202
+ trading days between Jan 1 and the data start are not observable, so the
203
+ phase there is approximate. Data reaching back to a year start is exact
204
+ from that year on.
205
+
206
+ :param source_path: Path to source .ohlcv file
207
+ :param target_path: Path to target .ohlcv file (will be overwritten)
208
+ :param modifier: 'D', 'W' or 'M' (from ``_process_tf``)
209
+ :param multiplier: Period multiplier (> 1)
210
+ :param tz: Exchange timezone
211
+ :param session_starts: ``SymInfo.session_starts`` template
212
+ :param opening_hours: ``SymInfo.opening_hours`` template
213
+ :param src_off: Source bar open -> last instant offset in seconds; a bar
214
+ belongs to the trading day its last instant falls into
215
+ :param fold: Fold holiday half-days into the early-close day (intraday
216
+ source only; see :class:`ObservedDayCounter`)
217
+ :return: Tuple of (source_candles_read, target_candles_written)
218
+ """
219
+ on = overnight_opens(opening_hours, session_starts)
220
+
221
+ source_count = 0
222
+ target_count = 0
223
+
224
+ with OHLCVReader(source_path) as reader:
225
+ with OHLCVWriter(target_path, truncate=True) as writer:
226
+ window: list[OHLCV] = []
227
+ window_start: int | None = None
228
+ group_key: tuple | None = None
229
+ counter = ObservedDayCounter(tz, opening_hours, fold=fold)
230
+
231
+ start_ts = reader.start_timestamp
232
+ if start_ts is None:
233
+ return 0, 0
234
+
235
+ for candle in reader.read_from(start_ts):
236
+ source_count += 1
237
+ td = trading_day(candle.timestamp + src_off, tz, on)
238
+ bar_end = candle.timestamp + src_off + 1 if fold else None
239
+ counter.ordinal(td, bar_end)
240
+ key = counter.key(modifier, multiplier)
241
+
242
+ if group_key is not None and key != group_key:
243
+ writer.write(_merge_candles(window, window_start))
244
+ target_count += 1
245
+ window = []
246
+ window_start = None
247
+
248
+ group_key = key
249
+ if window_start is None:
250
+ window_start = candle.timestamp
251
+ window.append(candle)
252
+
253
+ if window and window_start is not None:
254
+ writer.write(_merge_candles(window, window_start))
255
+ target_count += 1
256
+
257
+ return source_count, target_count
@@ -0,0 +1,168 @@
1
+ """
2
+ Bar Magnifier — groups lower-timeframe OHLCV candles into chart-timeframe windows.
3
+
4
+ Used by ScriptRunner when use_bar_magnifier=true: the script sees aggregated chart-TF
5
+ bars, while the broker emulator processes orders against each sub-bar for accurate fills.
6
+
7
+ Multi-period chart timeframes (nD/nW/nM, n > 1) live on the year-reset scheduled
8
+ grid (see the ``resampler`` module docs); 'observed' symbols (exchange-listed)
9
+ count the actual trading days seen in the sub-bar stream, which reproduces
10
+ TradingView's holiday-aware grid.
11
+ """
12
+ from dataclasses import dataclass
13
+ from datetime import timezone as dt_timezone
14
+ from typing import Iterable, Iterator
15
+ from zoneinfo import ZoneInfo
16
+
17
+ from .aggregator import _merge_candles
18
+ from .resampler import (
19
+ Resampler, ObservedDayCounter, grid_mode, overnight_opens, trading_day,
20
+ )
21
+ from ..lib.timeframe import in_seconds, _process_tf
22
+ from ..types.ohlcv import OHLCV
23
+
24
+ __all__ = ['BarMagnifier', 'MagnifiedWindow']
25
+
26
+
27
+ @dataclass(slots=True)
28
+ class MagnifiedWindow:
29
+ """A single chart-timeframe bar with its constituent sub-bars."""
30
+ sub_bars: list[OHLCV]
31
+ aggregated: OHLCV
32
+ is_last_window: bool
33
+
34
+
35
+ class BarMagnifier:
36
+ """
37
+ Groups sub-timeframe OHLCV candles into chart-timeframe windows.
38
+
39
+ Uses Resampler for bar boundary alignment (same logic as aggregator.py);
40
+ multi-period (nD/nW/nM) chart timeframes on 'observed' symbols count the
41
+ actual trading days in the stream instead. Yields MagnifiedWindow objects
42
+ with peek-ahead for last-window detection.
43
+ """
44
+
45
+ def __init__(
46
+ self,
47
+ ohlcv_iter: Iterable[OHLCV],
48
+ chart_tf: str,
49
+ tz: ZoneInfo | dt_timezone | None = None,
50
+ session_starts: 'list | None' = None,
51
+ opening_hours: 'list | None' = None,
52
+ sym_type: str | None = None,
53
+ source_tf: str | None = None,
54
+ ):
55
+ """
56
+ :param ohlcv_iter: Iterator of sub-timeframe OHLCV candles
57
+ :param chart_tf: Chart timeframe string (e.g., '60', '1D')
58
+ :param tz: Timezone for day/week/month boundary alignment
59
+ :param session_starts: Per-trading-day primary opens for intraday session
60
+ anchoring and multi-period grids. ``None`` keeps the pure clock-floor
61
+ (see :meth:`Resampler.get_bar_time`).
62
+ :param opening_hours: ``SymInfo.opening_hours`` — trading-day roll source
63
+ for multi-period (nD/nW/nM) grids.
64
+ :param sym_type: ``SymInfo.type`` for :func:`grid_mode` classification of
65
+ multi-period grids.
66
+ :param source_tf: Sub-bar timeframe string. On multi-period chart
67
+ timeframes an intraday sub-bar belongs to the trading day its *last*
68
+ instant falls into — the bar containing a session open starts the
69
+ new day even when its timestamp precedes the open.
70
+ """
71
+ self._ohlcv_iter = ohlcv_iter
72
+ self._resampler = Resampler.get_resampler(chart_tf)
73
+ self._tz = tz
74
+ self._session_starts = session_starts
75
+ self._opening_hours = opening_hours
76
+
77
+ # noinspection PyProtectedMember
78
+ modifier, multiplier = _process_tf(chart_tf)
79
+ self._modifier = modifier
80
+ self._multiplier = multiplier
81
+ multi = modifier in ('D', 'W', 'M') and multiplier > 1
82
+ self._mode = grid_mode(sym_type, opening_hours) if multi else None
83
+
84
+ self._src_off = 0
85
+ self._fold = False
86
+ if multi and source_tf:
87
+ # noinspection PyProtectedMember
88
+ src_mod, _ = _process_tf(source_tf)
89
+ if src_mod in ('', 'S'):
90
+ self._src_off = in_seconds(source_tf) - 1
91
+ # Intraday sub-bars carry the end instants the holiday half-day
92
+ # fold needs (a daily source stream is already folded).
93
+ self._fold = True
94
+
95
+ if multi and self._mode == 'observed':
96
+ self._overnight = overnight_opens(opening_hours, session_starts)
97
+ self._counter: ObservedDayCounter | None = ObservedDayCounter(
98
+ tz, opening_hours, fold=self._fold)
99
+ else:
100
+ self._overnight = {}
101
+ self._counter = None
102
+
103
+ def _key_and_stamp(self, candle: OHLCV) -> tuple[object, int]:
104
+ """
105
+ Grouping key and window-opening timestamp (seconds) for a sub-bar.
106
+
107
+ For 'observed' multi-period grids the key counts the actual trading
108
+ days and the window is stamped by its first sub-bar; everything else
109
+ uses the scheduled-grid bar time as both.
110
+ """
111
+ if self._counter is not None:
112
+ td = trading_day(candle.timestamp + self._src_off, self._tz, self._overnight)
113
+ bar_end = candle.timestamp + self._src_off + 1 if self._fold else None
114
+ self._counter.ordinal(td, bar_end)
115
+ key = self._counter.key(self._modifier, self._multiplier)
116
+ return key, candle.timestamp
117
+
118
+ bar_time_ms = self._resampler.get_bar_time(
119
+ (candle.timestamp + self._src_off) * 1000, tz=self._tz,
120
+ session_starts=self._session_starts,
121
+ opening_hours=self._opening_hours, mode=self._mode)
122
+ bar_time = bar_time_ms // 1000
123
+ return bar_time, bar_time
124
+
125
+ def __iter__(self) -> Iterator[MagnifiedWindow]:
126
+ window: list[OHLCV] = []
127
+ current_key: object | None = None
128
+ window_stamp: int | None = None
129
+ next_window: MagnifiedWindow | None = None
130
+
131
+ for candle in self._ohlcv_iter:
132
+ key, stamp = self._key_and_stamp(candle)
133
+
134
+ if current_key is not None and key != current_key:
135
+ # New bar boundary — flush current window
136
+ new_window = MagnifiedWindow(
137
+ sub_bars=window,
138
+ aggregated=_merge_candles(window, window_stamp),
139
+ is_last_window=False,
140
+ )
141
+
142
+ # Peek-ahead: yield the previous window (now we know it's not the last)
143
+ if next_window is not None:
144
+ yield next_window
145
+ next_window = new_window
146
+ window = []
147
+ window_stamp = None
148
+
149
+ current_key = key
150
+ if window_stamp is None:
151
+ window_stamp = stamp
152
+ window.append(candle)
153
+
154
+ # Flush last window
155
+ if window and window_stamp is not None:
156
+ last_window = MagnifiedWindow(
157
+ sub_bars=window,
158
+ aggregated=_merge_candles(window, window_stamp),
159
+ is_last_window=True,
160
+ )
161
+
162
+ if next_window is not None:
163
+ yield next_window
164
+ yield last_window
165
+ elif next_window is not None:
166
+ # Edge case: no trailing candles, previous window is the last
167
+ next_window.is_last_window = True
168
+ yield next_window
@@ -0,0 +1,64 @@
1
+ """
2
+ Broker plugin runtime support.
3
+
4
+ - :mod:`pynecore.core.broker.models` — intent, event, exchange-state,
5
+ capability and requirement dataclasses.
6
+ - :mod:`pynecore.core.broker.exceptions` — broker error hierarchy.
7
+ - :mod:`pynecore.core.broker.position` — :class:`BrokerPosition` live
8
+ position tracker (no simulation).
9
+ """
10
+ from pynecore.core.broker.exceptions import (
11
+ AuthenticationError,
12
+ BrokerError,
13
+ ExchangeCapabilityError,
14
+ ExchangeConnectionError,
15
+ ExchangeOrderRejectedError,
16
+ ExchangeRateLimitError,
17
+ InsufficientMarginError,
18
+ OrderSyncError,
19
+ UnexpectedCancelError,
20
+ )
21
+ from pynecore.core.broker.models import (
22
+ OrderStatus,
23
+ OrderType,
24
+ LegType,
25
+ CapabilityLevel,
26
+ ExchangeOrder,
27
+ OrderEvent,
28
+ ExchangePosition,
29
+ ExchangeCapabilities,
30
+ EntryIntent,
31
+ ExitIntent,
32
+ CloseIntent,
33
+ CancelIntent,
34
+ ScriptRequirements,
35
+ InterceptorResult,
36
+ )
37
+ from pynecore.core.broker.position import BrokerPosition
38
+
39
+ __all__ = [
40
+ 'AuthenticationError',
41
+ 'BrokerError',
42
+ 'ExchangeCapabilityError',
43
+ 'ExchangeConnectionError',
44
+ 'ExchangeOrderRejectedError',
45
+ 'ExchangeRateLimitError',
46
+ 'InsufficientMarginError',
47
+ 'OrderSyncError',
48
+ 'UnexpectedCancelError',
49
+ 'OrderStatus',
50
+ 'OrderType',
51
+ 'LegType',
52
+ 'CapabilityLevel',
53
+ 'ExchangeOrder',
54
+ 'OrderEvent',
55
+ 'ExchangePosition',
56
+ 'ExchangeCapabilities',
57
+ 'EntryIntent',
58
+ 'ExitIntent',
59
+ 'CloseIntent',
60
+ 'CancelIntent',
61
+ 'ScriptRequirements',
62
+ 'InterceptorResult',
63
+ 'BrokerPosition',
64
+ ]
@@ -0,0 +1,113 @@
1
+ """
2
+ Cross-broker runtime defaults loaded from ``workdir/config/brokers.toml``.
3
+
4
+ Holds policies that are broker-agnostic by design — the four
5
+ ``on_unexpected_cancel`` modes, for instance, share identical semantics
6
+ regardless of which exchange the plugin talks to. Living here rather than
7
+ in each plugin's own config keeps the user-facing knob in a single place
8
+ and prevents every new broker plugin from copy-pasting the same field.
9
+
10
+ The CLI (``pyne run --broker``) loads :class:`BrokerDefaults` once and
11
+ injects the resolved values onto the plugin instance just before the
12
+ script runner starts. Plugin code reads them through the
13
+ :class:`~pynecore.core.plugin.broker.BrokerPlugin` class attributes that
14
+ they shadow.
15
+ """
16
+ from dataclasses import dataclass
17
+ from pathlib import Path
18
+
19
+ from pynecore.core.config import ensure_config
20
+
21
+ __all__ = [
22
+ 'BrokerDefaults',
23
+ 'VALID_UNEXPECTED_CANCEL_POLICIES',
24
+ 'VALID_INVENTORY_CONFLICT_POLICIES',
25
+ 'load_broker_defaults',
26
+ ]
27
+
28
+
29
+ VALID_UNEXPECTED_CANCEL_POLICIES = frozenset({
30
+ "stop",
31
+ "stop_and_cancel",
32
+ "re_place",
33
+ "ignore",
34
+ "halt",
35
+ })
36
+
37
+ VALID_INVENTORY_CONFLICT_POLICIES = frozenset({
38
+ "quarantine",
39
+ "halt",
40
+ })
41
+
42
+
43
+ @dataclass
44
+ class BrokerDefaults:
45
+ """Cross-broker runtime defaults.
46
+
47
+ Loaded from ``workdir/config/brokers.toml`` via
48
+ :func:`load_broker_defaults`. The file is self-healing — fields at
49
+ their default are emitted as commented-out lines, user-edited values
50
+ are preserved across regenerations.
51
+ """
52
+
53
+ on_unexpected_cancel: str = "stop"
54
+ """Policy when a bot-owned order disappears without the bot cancelling it.
55
+
56
+ ``"stop"`` (default) — quarantine: trading stops (no new or
57
+ exposure-increasing dispatch) but the process stays alive — event
58
+ ingestion, cancels and closes keep working and observability can
59
+ page. Resumed by an operator restart.
60
+ ``"stop_and_cancel"`` — quarantine plus a best-effort cancel pass
61
+ over the remaining bot-owned orders.
62
+ ``"re_place"`` — no-op on the cancel; the sync engine re-dispatches
63
+ the protective order on the next diff cycle.
64
+ ``"ignore"`` — silently continue. Only safe when manual external
65
+ cancellations are an expected part of the operational workflow.
66
+ ``"halt"`` — exit the process via the graceful manual-intervention
67
+ path, leaving any remaining orders unsupervised until restart.
68
+ """
69
+
70
+ on_inventory_conflict: str = "quarantine"
71
+ """Policy for a confirmed spot balance-invariant conflict.
72
+
73
+ Only used by plugins that opt into the core spot inventory layer.
74
+ ``"quarantine"`` (default) — trading stops, the process stays alive
75
+ as an observer; recovery is an operator rebaseline plus restart.
76
+ ``"halt"`` — exit via the graceful manual-intervention path.
77
+ The set is deliberately narrower than ``on_unexpected_cancel``:
78
+ an attribution conflict has no safe ``re_place`` or ``ignore``
79
+ analogue (the former would buy back an operator's withdrawal, the
80
+ latter would trade on corrupt books).
81
+ """
82
+
83
+
84
+ def load_broker_defaults(config_dir: Path) -> BrokerDefaults:
85
+ """Load :class:`BrokerDefaults` from ``<config_dir>/brokers.toml``.
86
+
87
+ Delegates to :func:`pynecore.core.config.ensure_config`, which
88
+ auto-creates the file with commented defaults on first run, preserves
89
+ user-edited values across regenerations, and caches the result on the
90
+ dataclass. Validation runs after loading — invalid values raise
91
+ :class:`ValueError` with the list of accepted policies so the
92
+ misconfiguration surfaces immediately at startup, not at the first
93
+ reconcile cycle.
94
+
95
+ :param config_dir: The ``workdir/config`` directory.
96
+ :return: A populated :class:`BrokerDefaults` instance.
97
+ :raises ValueError: If a loaded value falls outside its allowed set.
98
+ """
99
+ instance = ensure_config(BrokerDefaults, config_dir / 'brokers.toml')
100
+ assert isinstance(instance, BrokerDefaults)
101
+ if instance.on_unexpected_cancel not in VALID_UNEXPECTED_CANCEL_POLICIES:
102
+ raise ValueError(
103
+ f"brokers.toml: on_unexpected_cancel must be one of "
104
+ f"{sorted(VALID_UNEXPECTED_CANCEL_POLICIES)}, got "
105
+ f"{instance.on_unexpected_cancel!r}",
106
+ )
107
+ if instance.on_inventory_conflict not in VALID_INVENTORY_CONFLICT_POLICIES:
108
+ raise ValueError(
109
+ f"brokers.toml: on_inventory_conflict must be one of "
110
+ f"{sorted(VALID_INVENTORY_CONFLICT_POLICIES)}, got "
111
+ f"{instance.on_inventory_conflict!r}",
112
+ )
113
+ return instance