tradear 0.1.0__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 (144) hide show
  1. tradear/__init__.py +34 -0
  2. tradear/backtest/__init__.py +3 -0
  3. tradear/backtest/_build.py +454 -0
  4. tradear/backtest/_runner.py +628 -0
  5. tradear/backtest/_validation.py +35 -0
  6. tradear/backtest/engine.py +747 -0
  7. tradear/backtest/pool.py +1186 -0
  8. tradear/backtest/pool_adapter.py +83 -0
  9. tradear/connectors/__init__.py +6 -0
  10. tradear/connectors/_disclaimer.py +97 -0
  11. tradear/connectors/binance.py +301 -0
  12. tradear/connectors/bybit.py +1045 -0
  13. tradear/connectors/ccxt.py +1076 -0
  14. tradear/connectors/mt5.py +1125 -0
  15. tradear/core/__init__.py +36 -0
  16. tradear/core/broker.py +2013 -0
  17. tradear/core/constants.py +297 -0
  18. tradear/core/data_types.py +1703 -0
  19. tradear/data/__init__.py +34 -0
  20. tradear/data/_book_replay.py +222 -0
  21. tradear/data/_klines.py +450 -0
  22. tradear/data/_proxy.py +732 -0
  23. tradear/data/_ring.py +649 -0
  24. tradear/data/_store.py +120 -0
  25. tradear/data/_views.py +509 -0
  26. tradear/data/data.py +37 -0
  27. tradear/datasets/__init__.py +354 -0
  28. tradear/datasets/_data/AAPL_1d.csv +301 -0
  29. tradear/datasets/_data/ADAUSDT_1m.hdf5 +0 -0
  30. tradear/datasets/_data/ADAUSDT_book.hdf5 +0 -0
  31. tradear/datasets/_data/ADAUSDT_ticks.hdf5 +0 -0
  32. tradear/datasets/_data/BTCUSDT_1m.hdf5 +0 -0
  33. tradear/datasets/_data/GOOG_1d.csv +301 -0
  34. tradear/datasets/_data/MESU26_ticks.hdf5 +0 -0
  35. tradear/datasets/_data/MNQU26_ticks.hdf5 +0 -0
  36. tradear/exceptions.py +113 -0
  37. tradear/io/__init__.py +17 -0
  38. tradear/io/io.py +1399 -0
  39. tradear/live/__init__.py +5 -0
  40. tradear/live/_builder.py +738 -0
  41. tradear/live/_feed.py +712 -0
  42. tradear/live/_loop.py +337 -0
  43. tradear/live/_warmup.py +231 -0
  44. tradear/live/engine.py +574 -0
  45. tradear/logger.py +573 -0
  46. tradear/models/__init__.py +4 -0
  47. tradear/models/_warmup_policy.py +228 -0
  48. tradear/models/decorators.py +124 -0
  49. tradear/models/footprint/__init__.py +13 -0
  50. tradear/models/footprint/_compute.py +105 -0
  51. tradear/models/footprint/_config.py +62 -0
  52. tradear/models/footprint/_construction.py +186 -0
  53. tradear/models/footprint/_proxy.py +138 -0
  54. tradear/models/footprint/_ring.py +128 -0
  55. tradear/models/footprint/_store.py +70 -0
  56. tradear/models/footprint/_types.py +58 -0
  57. tradear/models/strategy.py +1525 -0
  58. tradear/optimize/__init__.py +13 -0
  59. tradear/optimize/grid_search.py +48 -0
  60. tradear/optimize/optimizer.py +82 -0
  61. tradear/optimize/random_search.py +36 -0
  62. tradear/optimize/reporter.py +68 -0
  63. tradear/optimize/result.py +175 -0
  64. tradear/optimize/space.py +141 -0
  65. tradear/optimize/task.py +156 -0
  66. tradear/playback/__init__.py +15 -0
  67. tradear/playback/base.py +1174 -0
  68. tradear/plotting/__init__.py +3 -0
  69. tradear/plotting/_fig_factory.py +111 -0
  70. tradear/plotting/_layout.py +830 -0
  71. tradear/plotting/_util.py +851 -0
  72. tradear/plotting/chart.py +16 -0
  73. tradear/plotting/config.py +253 -0
  74. tradear/plotting/js/__init__.py +0 -0
  75. tradear/plotting/js/autoscale_equity.js +19 -0
  76. tradear/plotting/js/autoscale_indicator.js +24 -0
  77. tradear/plotting/js/autoscale_ohlc.js +59 -0
  78. tradear/plotting/js/autoscale_ohlc_live.js +64 -0
  79. tradear/plotting/js/autoscale_primitives.js +60 -0
  80. tradear/plotting/js/autoscale_volume.js +15 -0
  81. tradear/plotting/js/fp_lazy.js +24 -0
  82. tradear/plotting/js/fp_lazy_live.js +24 -0
  83. tradear/plotting/js/fp_yaxis_populate.js +11 -0
  84. tradear/plotting/js/lazy_labels.js +21 -0
  85. tradear/plotting/js/lazy_labels_group.js +15 -0
  86. tradear/plotting/js/legend_toggle.js +8 -0
  87. tradear/plotting/js/ylock_reset.js +7 -0
  88. tradear/plotting/js/ylock_watch.js +12 -0
  89. tradear/plotting/plotting.py +616 -0
  90. tradear/plotting/render/__init__.py +12 -0
  91. tradear/plotting/render/_annotations.py +11 -0
  92. tradear/plotting/render/_equity.py +153 -0
  93. tradear/plotting/render/_indicators.py +230 -0
  94. tradear/plotting/render/_ohlc_volume.py +167 -0
  95. tradear/plotting/render/_panels.py +225 -0
  96. tradear/plotting/render/_tools.py +407 -0
  97. tradear/plotting/render/_trades.py +237 -0
  98. tradear/plotting/sources.py +610 -0
  99. tradear/plotting/theme/__init__.py +31 -0
  100. tradear/plotting/theme/dark.py +50 -0
  101. tradear/plotting/theme/light.py +50 -0
  102. tradear/plotting/theme/registry.py +41 -0
  103. tradear/session/__init__.py +18 -0
  104. tradear/session/base.py +1238 -0
  105. tradear/session/fake_live_sesh.py +745 -0
  106. tradear/signal.py +566 -0
  107. tradear/stats/__init__.py +2 -0
  108. tradear/stats/stats.py +1382 -0
  109. tradear/streaming/__init__.py +65 -0
  110. tradear/streaming/_protocol.py +292 -0
  111. tradear/streaming/base.py +420 -0
  112. tradear/streaming/ccxt_pro.py +397 -0
  113. tradear/streaming/fake.py +73 -0
  114. tradear/streaming/replay_feed.py +157 -0
  115. tradear/ta/__init__.py +63 -0
  116. tradear/ta/_volume_profile.py +1204 -0
  117. tradear/ta/annotations.py +1857 -0
  118. tradear/ta/base.py +479 -0
  119. tradear/ta/bill_williams.py +439 -0
  120. tradear/ta/draw.py +137 -0
  121. tradear/ta/momentum.py +2133 -0
  122. tradear/ta/order_flow/__init__.py +79 -0
  123. tradear/ta/order_flow/_core.py +2891 -0
  124. tradear/ta/order_flow/large_trades.py +398 -0
  125. tradear/ta/pattern/__init__.py +87 -0
  126. tradear/ta/pattern/pivot_mixin.py +87 -0
  127. tradear/ta/structure/__init__.py +10 -0
  128. tradear/ta/structure/_utils.py +33 -0
  129. tradear/ta/structure/hhll.py +173 -0
  130. tradear/ta/structure/nbs.py +264 -0
  131. tradear/ta/structure/pivots.py +334 -0
  132. tradear/ta/structure/swings.py +454 -0
  133. tradear/ta/structure/zigzag.py +145 -0
  134. tradear/ta/tool/__init__.py +56 -0
  135. tradear/ta/tool/base.py +136 -0
  136. tradear/ta/tool/draw.py +82 -0
  137. tradear/ta/tool/fibonacci.py +209 -0
  138. tradear/ta/tool/volume_profile.py +368 -0
  139. tradear/ta/trend.py +542 -0
  140. tradear/ta/volatility.py +237 -0
  141. tradear/ta/volume.py +703 -0
  142. tradear-0.1.0.dist-info/METADATA +160 -0
  143. tradear-0.1.0.dist-info/RECORD +144 -0
  144. tradear-0.1.0.dist-info/WHEEL +4 -0
tradear/__init__.py ADDED
@@ -0,0 +1,34 @@
1
+ from tradear import exceptions
2
+ from tradear.backtest import BacktestEngine, PoolBacktestEngine
3
+ from tradear.live import LiveEngine
4
+ from tradear.connectors._disclaimer import LIVE_DISCLAIMER
5
+ from tradear.models import Strategy, FootprintConfig
6
+ from tradear.session import SeshSimulatorBase
7
+ from tradear.core import TickData, KlineData, parse_timeframe, TIMEFRAME_PRESETS
8
+ from tradear.stats import Stats, compute_stats
9
+ from tradear.plotting import plot, PlotConfig
10
+ from tradear.ta import (
11
+ Indicator,
12
+ IndicatorPlotConfig,
13
+ SMA,
14
+ EMA,
15
+ MACD,
16
+ RSI,
17
+ ATR,
18
+ BollingerBands,
19
+ VolumeProfile,
20
+ RollingVolumeProfile,
21
+ VolumeNode,
22
+ detect_volume_nodes,
23
+ ZigZag,
24
+ ConfirmedPivot,
25
+ PivotHighLow,
26
+ SwingHL,
27
+ EqualHL,
28
+ FairValueGap,
29
+ OrderBlock,
30
+ MarketSessions,
31
+ SessionLevels,
32
+ KillZones,
33
+ LargeTrades,
34
+ )
@@ -0,0 +1,3 @@
1
+ from tradear.backtest.engine import BacktestEngine
2
+ from tradear.backtest.pool import PoolBacktestEngine
3
+ from tradear.backtest.pool_adapter import PoolEvaluator
@@ -0,0 +1,454 @@
1
+ from __future__ import annotations
2
+
3
+ from collections import defaultdict
4
+
5
+ import numpy as np
6
+
7
+ from tradear.models.strategy import FeedType
8
+ from tradear.core.constants import (
9
+ TICK_COLS,
10
+ OHLC_COLS,
11
+ N_TICK_COLS,
12
+ N_OHLC_COLS,
13
+ _TICK_COL,
14
+ _OHLC_COL,
15
+ )
16
+ from tradear.data.data import (
17
+ TickProxy,
18
+ OhlcProxy,
19
+ IndicatorProxy,
20
+ ColumnRef,
21
+ WindowView,
22
+ OhlcDataStore,
23
+ TickDataStore,
24
+ OhlcIndicatorView,
25
+ MultiOhlcIndicatorView,
26
+ MultiBandProxy,
27
+ build_candles_from_ticks,
28
+ )
29
+ from tradear.ta.base import Indicator
30
+ from tradear.models.strategy import Strategy
31
+ from tradear.exceptions import ConfigError
32
+
33
+
34
+ # HELPER: BUILD CANDLES FROM KLINES
35
+
36
+ def _build_candles_from_klines(klines: np.ndarray, interval_ms: int) -> dict:
37
+ '''
38
+ Build OHLC candles from klines data.
39
+
40
+ Constructs a dict for OhlcDataStore from klines [N x >= 6].
41
+
42
+ When interval_ms matches the data interval, each kline produces
43
+ exactly one candle and its high/low are preserved.
44
+
45
+ When aggregating (e.g. 1m data -> 5m candles), OHLC values are
46
+ correctly propagated:
47
+ open = open of first kline in group
48
+ high = max(highs of group)
49
+ low = min(lows of group)
50
+ close = close of last kline in group
51
+ volume = sum(volumes of group)
52
+
53
+ Args:
54
+ klines (np.ndarray): Input data [N x >= 6]
55
+ interval_ms (int): Target interval in milliseconds
56
+
57
+ Returns:
58
+ dict: Contains closed_candles, tick_to_candle_map, accumulated_per_tick,
59
+ candle_ts_per_tick, prices
60
+
61
+ Note:
62
+ Expected columns: ts(0), open(1), high(2), low(3), close(4), vol(5).
63
+ '''
64
+ from tradear.core.constants import N_OHLC_COLS, _OHLC_COL
65
+
66
+ interval_ms = int(interval_ms)
67
+ ts_col = klines[:, 0]
68
+ open_col = klines[:, 1]
69
+ high_col = klines[:, 2]
70
+ low_col = klines[:, 3]
71
+ close_col = klines[:, 4]
72
+ volume_col = klines[:, 5]
73
+ n = len(klines)
74
+
75
+ # ── Step 1: assign each kline to its target candle ──────────────────────
76
+ candle_ts_per_kline = (ts_col // interval_ms) * interval_ms
77
+ is_new_candle = np.r_[True, candle_ts_per_kline[1:] != candle_ts_per_kline[:-1]]
78
+ start_indices = np.where(is_new_candle)[0]
79
+ end_indices = np.r_[start_indices[1:], n]
80
+ count_per_candle = np.diff(np.r_[start_indices, n])
81
+
82
+ n_total_candles = len(start_indices)
83
+ kline_to_candle_map = np.repeat(np.arange(n_total_candles), count_per_candle)
84
+
85
+ # ── Step 2: OHLC accumulators per kline (for partial candle in O(1)) ─────────
86
+ # open per kline = open of the first kline in its group
87
+ open_per_kline = np.repeat(open_col[start_indices], count_per_candle)
88
+
89
+ # high accumulated up to each kline within its group
90
+ high_acum = high_col.copy()
91
+ low_acum = low_col.copy()
92
+ for s, e in zip(start_indices, end_indices):
93
+ np.maximum.accumulate(high_acum[s:e], out=high_acum[s:e])
94
+ np.minimum.accumulate(low_acum[s:e], out=low_acum[s:e])
95
+
96
+ # volume accumulated restarting at each group
97
+ cumsum_vol = np.cumsum(volume_col)
98
+ vol_at_start = cumsum_vol[start_indices] - volume_col[start_indices]
99
+ vol_acum = cumsum_vol - np.repeat(vol_at_start, count_per_candle)
100
+
101
+ # accumulated_per_kline[i] = [open, high, low, vol] up to kline i within its candle
102
+ accumulated_per_kline = np.column_stack(
103
+ [open_per_kline, high_acum, low_acum, vol_acum]
104
+ )
105
+
106
+ # ── Step 3: closed candles (all but the last) ────────────────────────
107
+ close_indices = end_indices - 1
108
+ n_closed_candles = n_total_candles - 1
109
+
110
+ if n_closed_candles > 0:
111
+ idx_c = close_indices[:n_closed_candles]
112
+ closed_candles = np.column_stack(
113
+ [
114
+ candle_ts_per_kline[idx_c], # ts open
115
+ accumulated_per_kline[idx_c, 0], # open
116
+ accumulated_per_kline[idx_c, 1], # high
117
+ accumulated_per_kline[idx_c, 2], # low
118
+ close_col[idx_c], # close (last price of group)
119
+ accumulated_per_kline[idx_c, 3], # volume
120
+ ]
121
+ ).astype(np.float64)
122
+ else:
123
+ closed_candles = np.empty((0, N_OHLC_COLS), dtype=np.float64)
124
+
125
+ return {
126
+ "closed_candles": closed_candles,
127
+ "tick_to_candle_map": kline_to_candle_map,
128
+ "accumulated_per_tick": accumulated_per_kline.astype(np.float64),
129
+ "candle_ts_per_tick": candle_ts_per_kline,
130
+ "prices": close_col,
131
+ }
132
+
133
+
134
+ # MIXIN: BUILDING DATA STORES
135
+
136
+ class _StoreBuilderMixin:
137
+ '''Shared methods for constructing data stores across engines.'''
138
+
139
+ strategy: Strategy
140
+ _tick_stores: dict
141
+ _ohlc_stores: dict
142
+
143
+ def _build_tick_store(
144
+ self, symbol: str, tick_matrix: np.ndarray
145
+ ) -> TickDataStore:
146
+ '''
147
+ Build TickDataStore for a specific symbol.
148
+
149
+ Filters indicator definitions by exact symbol (multi-symbol fix).
150
+ Supports multi-source indicators (list[ColumnRef]) and multi-band
151
+ (n_outputs > 1) patterns.
152
+
153
+ Args:
154
+ symbol (str): Trading symbol
155
+ tick_matrix (np.ndarray): Tick data matrix
156
+
157
+ Returns:
158
+ TickDataStore: Store with computed indicators
159
+ '''
160
+ col_index: dict[str, int] = dict(zip(TICK_COLS, range(N_TICK_COLS)))
161
+ cols_ind: list[np.ndarray] = []
162
+
163
+ for defn in self.strategy._indicator_defs:
164
+ fuente = defn["source"]
165
+ if not isinstance(fuente.proxy, TickProxy):
166
+ continue
167
+ if fuente.symbol != symbol:
168
+ continue
169
+
170
+ indicator = defn["indicator"]
171
+ fuentes = defn.get("sources", [fuente])
172
+ multi_fuente = defn.get("multi_source", False)
173
+ multi_banda = defn.get("multi_band", False)
174
+
175
+ # Construir array fuente
176
+ if multi_fuente:
177
+ fuente_arr = np.column_stack([
178
+ tick_matrix[:, _TICK_COL[f._col_name]] for f in fuentes
179
+ ])
180
+ else:
181
+ col_fuente = _TICK_COL[fuente._col_name]
182
+ fuente_arr = tick_matrix[:, col_fuente]
183
+
184
+ resultado = indicator.calculate(fuente_arr)
185
+
186
+ if multi_banda:
187
+ K = indicator.n_outputs
188
+ col_names = []
189
+ for k in range(K):
190
+ band_col_name = f"{indicator.col_name(symbol, fuente._col_name)}_b{k}"
191
+ col_index[band_col_name] = N_TICK_COLS + len(cols_ind)
192
+ col_names.append(band_col_name)
193
+ cols_ind.append(resultado[k])
194
+ defn["col_names"] = col_names
195
+ defn["col_name"] = col_names[0]
196
+ defn["en_tick_store"] = True
197
+ defn["tick_symbol"] = symbol
198
+ else:
199
+ nombre_col = indicator.col_name(symbol, fuente._col_name)
200
+ col_index[nombre_col] = N_TICK_COLS + len(cols_ind)
201
+ defn["col_name"] = nombre_col
202
+ defn["en_tick_store"] = True
203
+ defn["tick_symbol"] = symbol
204
+ cols_ind.append(resultado.ravel() if resultado.ndim > 1 else resultado)
205
+
206
+ if cols_ind:
207
+ matriz = np.ascontiguousarray(
208
+ np.hstack([tick_matrix, np.column_stack(cols_ind)])
209
+ )
210
+ else:
211
+ matriz = np.ascontiguousarray(tick_matrix)
212
+
213
+ return TickDataStore(matriz, col_index)
214
+
215
+ def _build_ohlc_stores(self, data: dict, feed_type: FeedType = "tick") -> dict:
216
+ '''
217
+ Build OhlcDataStores for all OhlcProxy instances.
218
+
219
+ Args:
220
+ data (dict): Symbol -> data matrix mapping
221
+ feed_type (FeedType): 'tick' or 'kline'
222
+
223
+ Returns:
224
+ dict: id(proxy) -> OhlcDataStore mapping
225
+
226
+ Note:
227
+ In tick mode: data[symbol] are ticks [N x 7], aggregated by interval.
228
+ In kline mode: data[symbol] are klines [N x 7], interval from KlineData.
229
+ '''
230
+ ohlc_stores = {}
231
+
232
+ # Agrupar indicator_defs por proxy de origen — O(n_indicadores) una sola vez.
233
+ # Evita el loop O(proxies × indicadores) que había antes.
234
+ inds_por_proxy: dict[int, list] = defaultdict(list)
235
+ for defn in self.strategy._indicator_defs:
236
+ fuente = defn["source"]
237
+ if isinstance(fuente.proxy, OhlcProxy):
238
+ inds_por_proxy[id(fuente.proxy)].append(defn)
239
+
240
+ for ohlc_proxy in self.strategy._ohlc_proxies:
241
+ symbol = ohlc_proxy.symbol
242
+ intervalo = ohlc_proxy.interval_ms
243
+ arr = data[symbol].astype(np.float64)
244
+
245
+ if feed_type == "tick":
246
+ timestamps = arr[:, _TICK_COL["ts"]]
247
+ prices = arr[:, _TICK_COL["price"]]
248
+ volumes = arr[:, _TICK_COL["volume"]]
249
+ candle_result = build_candles_from_ticks(
250
+ timestamps, prices, volumes, intervalo
251
+ )
252
+ else:
253
+ # kline mode: treat each row as a "tick" with price=close
254
+ candle_result = _build_candles_from_klines(arr, intervalo)
255
+
256
+ closed_candles = candle_result["closed_candles"]
257
+ tick_to_candle_map = candle_result["tick_to_candle_map"]
258
+ accumulated_per_tick = candle_result["accumulated_per_tick"]
259
+ candle_ts_per_tick = candle_result["candle_ts_per_tick"]
260
+ prices_per_row = candle_result["prices"]
261
+
262
+ col_index: dict[str, int] = dict(zip(OHLC_COLS, range(N_OHLC_COLS)))
263
+ cols_ind: list[np.ndarray] = []
264
+
265
+ # Solo los indicadores de este proxy — O(n_inds_de_este_proxy)
266
+ for defn in inds_por_proxy[id(ohlc_proxy)]:
267
+ fuente = defn["source"]
268
+ fuentes = defn.get("sources", [fuente])
269
+ indicator = defn["indicator"]
270
+ multi_fuente = defn.get("multi_source", False)
271
+ multi_banda = defn.get("multi_band", False)
272
+
273
+ # Construir array fuente
274
+ if multi_fuente:
275
+ if len(closed_candles) > 0:
276
+ fuente_arr = np.column_stack([
277
+ closed_candles[:, _OHLC_COL[f._col_name]] for f in fuentes
278
+ ])
279
+ else:
280
+ fuente_arr = np.empty((0, len(fuentes)), dtype=np.float64)
281
+ else:
282
+ col_fuente = _OHLC_COL[fuente._col_name]
283
+ fuente_arr = (
284
+ closed_candles[:, col_fuente]
285
+ if len(closed_candles) > 0
286
+ else np.array([], dtype=np.float64)
287
+ )
288
+
289
+ # Single full-history pass (precomputed once for all closed
290
+ # bars). Backtest/replay parity for recursive indicators (RSI,
291
+ # MACD) is achieved by feeding the replay the same candle
292
+ # history via warmup (it reconstructs a full window_size of
293
+ # candles), not by windowing the backtest precompute. This keeps
294
+ # the backtest at its original O(N) precompute speed.
295
+ resultado = indicator.calculate(fuente_arr)
296
+
297
+ if multi_banda:
298
+ K = indicator.n_outputs
299
+ # region XXX debug
300
+ n_bandas_reales = resultado.shape[0] if resultado.ndim > 1 else 1
301
+ if K != n_bandas_reales:
302
+ raise ConfigError(
303
+ f"Indicador '{type(indicator).__name__}': n_outputs={K} pero "
304
+ f"calculate retornó {n_bandas_reales} filas. "
305
+ f"Revisa que n_outputs coincida con el shape de calculate."
306
+ )
307
+ #endregion
308
+ col_names = []
309
+ src_idxs = (
310
+ [_OHLC_COL[f._col_name] for f in fuentes]
311
+ if multi_fuente
312
+ else _OHLC_COL[fuente._col_name]
313
+ )
314
+ for k in range(K):
315
+ cname = f"{indicator.col_name(symbol, fuente._col_name)}_b{k}"
316
+ col_index[cname] = N_OHLC_COLS + len(cols_ind)
317
+ col_names.append(cname)
318
+ band_data = resultado[k] if len(resultado.shape) > 1 else resultado
319
+ cols_ind.append(band_data)
320
+ defn["col_names"] = col_names
321
+ defn["col_name"] = col_names[0]
322
+ defn["en_tick_store"] = False
323
+ defn["ohlc_proxy"] = ohlc_proxy
324
+ defn["src_col_idx"] = src_idxs
325
+ else:
326
+ nombre_col = indicator.col_name(symbol, fuente._col_name)
327
+ col_index[nombre_col] = N_OHLC_COLS + len(cols_ind)
328
+ defn["col_name"] = nombre_col
329
+ defn["en_tick_store"] = False
330
+ defn["ohlc_proxy"] = ohlc_proxy
331
+ defn["src_col_idx"] = (
332
+ [_OHLC_COL[f._col_name] for f in fuentes]
333
+ if multi_fuente
334
+ else _OHLC_COL[fuente._col_name]
335
+ )
336
+ cols_ind.append(resultado.ravel() if resultado.ndim > 1 else resultado)
337
+
338
+ if len(closed_candles) > 0 and cols_ind:
339
+ matriz_ohlc = np.ascontiguousarray(
340
+ np.hstack([closed_candles, np.column_stack(cols_ind)])
341
+ )
342
+ elif len(closed_candles) > 0:
343
+ matriz_ohlc = np.ascontiguousarray(closed_candles)
344
+ else:
345
+ n_cols = N_OHLC_COLS + len(cols_ind)
346
+ matriz_ohlc = np.empty((0, n_cols), dtype=np.float64)
347
+
348
+ ohlc_stores[id(ohlc_proxy)] = OhlcDataStore(
349
+ matrix=matriz_ohlc,
350
+ col_index=col_index,
351
+ tick_to_candle_mapping=tick_to_candle_map,
352
+ accumulated_by_tick=accumulated_per_tick,
353
+ ts_candle_by_tick=candle_ts_per_tick,
354
+ prices_per_tick=prices_per_row,
355
+ interval_ms=intervalo,
356
+ symbol=symbol,
357
+ kline_mode=(feed_type == "kline"),
358
+ )
359
+
360
+ return ohlc_stores
361
+
362
+ def _connect_proxies(self, tick_stores: dict, ohlc_stores: dict):
363
+ '''Connect each proxy to its data store.'''
364
+ for tp in self.strategy._tick_proxies:
365
+ store = tick_stores.get(tp.symbol)
366
+ if store is not None:
367
+ tp._connect_backtest(store)
368
+
369
+ for op in self.strategy._ohlc_proxies:
370
+ op._connect_backtest(ohlc_stores[id(op)])
371
+
372
+ for defn in self.strategy._indicator_defs:
373
+ proxy: "IndicatorProxy | MultiBandProxy" = defn["proxy"]
374
+ indicator: Indicator = defn["indicator"]
375
+ fuente: ColumnRef = defn["source"]
376
+ multi_banda: bool = defn.get("multi_band", False)
377
+
378
+ if defn.get("en_tick_store", True) and isinstance(fuente.proxy, TickProxy):
379
+ # ── Tick store ─────────────────────────────────────────────────
380
+ tick_store = tick_stores[defn["tick_symbol"]]
381
+ tamaño = fuente.proxy._window_size
382
+
383
+ if multi_banda:
384
+ col_names = defn["col_names"]
385
+ for k, cname in enumerate(col_names):
386
+ col_idx = tick_store.col_index[cname]
387
+ view = WindowView(col_idx=col_idx, size=tamaño, tick_store=tick_store)
388
+ proxy._connect_band(k, view)
389
+ else:
390
+ col_idx = tick_store.col_index[defn["col_name"]]
391
+ view = WindowView(col_idx=col_idx, size=tamaño, tick_store=tick_store)
392
+ proxy._connect(view)
393
+ else:
394
+ # ── OHLC store ─────────────────────────────────────────────────
395
+ ohlc_proxy: OhlcProxy = defn["ohlc_proxy"]
396
+ ohlc_store: OhlcDataStore = ohlc_stores[id(ohlc_proxy)]
397
+ src_col_idx = defn["src_col_idx"]
398
+ tamaño = ohlc_proxy._window_size
399
+
400
+ if multi_banda:
401
+ col_names = defn["col_names"]
402
+ ind_col_idxs = [ohlc_store.col_index[n] for n in col_names]
403
+ shared_cache = {}
404
+ for k in range(len(col_names)):
405
+ view = MultiOhlcIndicatorView(
406
+ ohlc_store=ohlc_store,
407
+ indicator=indicator,
408
+ ind_col_idxs=ind_col_idxs,
409
+ src_col_idx=src_col_idx,
410
+ band_idx=k,
411
+ size=tamaño,
412
+ shared_cache=shared_cache,
413
+ )
414
+ proxy._connect_band(k, view)
415
+ else:
416
+ ind_col_idx = ohlc_store.col_index[defn["col_name"]]
417
+ view = OhlcIndicatorView(
418
+ ohlc_store=ohlc_store,
419
+ indicator=indicator,
420
+ ind_col_idx=ind_col_idx,
421
+ src_col_idx=src_col_idx,
422
+ size=tamaño,
423
+ )
424
+ proxy._connect(view)
425
+
426
+ def _get_ind_def_for_proxy(self, proxy) -> "dict | None":
427
+ '''Find the indicator definition corresponding to a proxy.'''
428
+ for defn in self.strategy._indicator_defs:
429
+ if defn["proxy"] is proxy:
430
+ return defn
431
+ return None
432
+
433
+ def _build_pattern_stores(self, ohlc_stores: dict) -> None:
434
+ '''Build pattern stores from OHLC data.'''
435
+ # No patterns -> nothing to build. Guarding here also avoids importing
436
+ # the pattern DSL, which is absent from lower-tier (gated) builds.
437
+ if not getattr(self.strategy, "_pattern_matcher_defs", None):
438
+ return
439
+ from tradear.ta.pattern._builder import iter_pattern_matcher_defs
440
+ from tradear.ta.pattern.sequence import FrozenPivotSequence
441
+ from tradear.ta.pattern.store import PatternStore
442
+
443
+ for pm_def, _, ohlc_proxy, symbol, base_cols, decorator_cols, tag_decoders \
444
+ in iter_pattern_matcher_defs(self.strategy, self._get_ind_def_for_proxy):
445
+
446
+ ohlc_store = ohlc_stores[id(ohlc_proxy)]
447
+ sequence = FrozenPivotSequence.from_ohlc_store(
448
+ ohlc_store = ohlc_store,
449
+ base_col_names = base_cols,
450
+ decorator_cols = decorator_cols,
451
+ tag_decoders = tag_decoders,
452
+ )
453
+ store = PatternStore(sequence, pm_def.pattern)
454
+ pm_def.proxy._connect_backtest(store, ohlc_store=ohlc_store)