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.
- tradear/__init__.py +34 -0
- tradear/backtest/__init__.py +3 -0
- tradear/backtest/_build.py +454 -0
- tradear/backtest/_runner.py +628 -0
- tradear/backtest/_validation.py +35 -0
- tradear/backtest/engine.py +747 -0
- tradear/backtest/pool.py +1186 -0
- tradear/backtest/pool_adapter.py +83 -0
- tradear/connectors/__init__.py +6 -0
- tradear/connectors/_disclaimer.py +97 -0
- tradear/connectors/binance.py +301 -0
- tradear/connectors/bybit.py +1045 -0
- tradear/connectors/ccxt.py +1076 -0
- tradear/connectors/mt5.py +1125 -0
- tradear/core/__init__.py +36 -0
- tradear/core/broker.py +2013 -0
- tradear/core/constants.py +297 -0
- tradear/core/data_types.py +1703 -0
- tradear/data/__init__.py +34 -0
- tradear/data/_book_replay.py +222 -0
- tradear/data/_klines.py +450 -0
- tradear/data/_proxy.py +732 -0
- tradear/data/_ring.py +649 -0
- tradear/data/_store.py +120 -0
- tradear/data/_views.py +509 -0
- tradear/data/data.py +37 -0
- tradear/datasets/__init__.py +354 -0
- tradear/datasets/_data/AAPL_1d.csv +301 -0
- tradear/datasets/_data/ADAUSDT_1m.hdf5 +0 -0
- tradear/datasets/_data/ADAUSDT_book.hdf5 +0 -0
- tradear/datasets/_data/ADAUSDT_ticks.hdf5 +0 -0
- tradear/datasets/_data/BTCUSDT_1m.hdf5 +0 -0
- tradear/datasets/_data/GOOG_1d.csv +301 -0
- tradear/datasets/_data/MESU26_ticks.hdf5 +0 -0
- tradear/datasets/_data/MNQU26_ticks.hdf5 +0 -0
- tradear/exceptions.py +113 -0
- tradear/io/__init__.py +17 -0
- tradear/io/io.py +1399 -0
- tradear/live/__init__.py +5 -0
- tradear/live/_builder.py +738 -0
- tradear/live/_feed.py +712 -0
- tradear/live/_loop.py +337 -0
- tradear/live/_warmup.py +231 -0
- tradear/live/engine.py +574 -0
- tradear/logger.py +573 -0
- tradear/models/__init__.py +4 -0
- tradear/models/_warmup_policy.py +228 -0
- tradear/models/decorators.py +124 -0
- tradear/models/footprint/__init__.py +13 -0
- tradear/models/footprint/_compute.py +105 -0
- tradear/models/footprint/_config.py +62 -0
- tradear/models/footprint/_construction.py +186 -0
- tradear/models/footprint/_proxy.py +138 -0
- tradear/models/footprint/_ring.py +128 -0
- tradear/models/footprint/_store.py +70 -0
- tradear/models/footprint/_types.py +58 -0
- tradear/models/strategy.py +1525 -0
- tradear/optimize/__init__.py +13 -0
- tradear/optimize/grid_search.py +48 -0
- tradear/optimize/optimizer.py +82 -0
- tradear/optimize/random_search.py +36 -0
- tradear/optimize/reporter.py +68 -0
- tradear/optimize/result.py +175 -0
- tradear/optimize/space.py +141 -0
- tradear/optimize/task.py +156 -0
- tradear/playback/__init__.py +15 -0
- tradear/playback/base.py +1174 -0
- tradear/plotting/__init__.py +3 -0
- tradear/plotting/_fig_factory.py +111 -0
- tradear/plotting/_layout.py +830 -0
- tradear/plotting/_util.py +851 -0
- tradear/plotting/chart.py +16 -0
- tradear/plotting/config.py +253 -0
- tradear/plotting/js/__init__.py +0 -0
- tradear/plotting/js/autoscale_equity.js +19 -0
- tradear/plotting/js/autoscale_indicator.js +24 -0
- tradear/plotting/js/autoscale_ohlc.js +59 -0
- tradear/plotting/js/autoscale_ohlc_live.js +64 -0
- tradear/plotting/js/autoscale_primitives.js +60 -0
- tradear/plotting/js/autoscale_volume.js +15 -0
- tradear/plotting/js/fp_lazy.js +24 -0
- tradear/plotting/js/fp_lazy_live.js +24 -0
- tradear/plotting/js/fp_yaxis_populate.js +11 -0
- tradear/plotting/js/lazy_labels.js +21 -0
- tradear/plotting/js/lazy_labels_group.js +15 -0
- tradear/plotting/js/legend_toggle.js +8 -0
- tradear/plotting/js/ylock_reset.js +7 -0
- tradear/plotting/js/ylock_watch.js +12 -0
- tradear/plotting/plotting.py +616 -0
- tradear/plotting/render/__init__.py +12 -0
- tradear/plotting/render/_annotations.py +11 -0
- tradear/plotting/render/_equity.py +153 -0
- tradear/plotting/render/_indicators.py +230 -0
- tradear/plotting/render/_ohlc_volume.py +167 -0
- tradear/plotting/render/_panels.py +225 -0
- tradear/plotting/render/_tools.py +407 -0
- tradear/plotting/render/_trades.py +237 -0
- tradear/plotting/sources.py +610 -0
- tradear/plotting/theme/__init__.py +31 -0
- tradear/plotting/theme/dark.py +50 -0
- tradear/plotting/theme/light.py +50 -0
- tradear/plotting/theme/registry.py +41 -0
- tradear/session/__init__.py +18 -0
- tradear/session/base.py +1238 -0
- tradear/session/fake_live_sesh.py +745 -0
- tradear/signal.py +566 -0
- tradear/stats/__init__.py +2 -0
- tradear/stats/stats.py +1382 -0
- tradear/streaming/__init__.py +65 -0
- tradear/streaming/_protocol.py +292 -0
- tradear/streaming/base.py +420 -0
- tradear/streaming/ccxt_pro.py +397 -0
- tradear/streaming/fake.py +73 -0
- tradear/streaming/replay_feed.py +157 -0
- tradear/ta/__init__.py +63 -0
- tradear/ta/_volume_profile.py +1204 -0
- tradear/ta/annotations.py +1857 -0
- tradear/ta/base.py +479 -0
- tradear/ta/bill_williams.py +439 -0
- tradear/ta/draw.py +137 -0
- tradear/ta/momentum.py +2133 -0
- tradear/ta/order_flow/__init__.py +79 -0
- tradear/ta/order_flow/_core.py +2891 -0
- tradear/ta/order_flow/large_trades.py +398 -0
- tradear/ta/pattern/__init__.py +87 -0
- tradear/ta/pattern/pivot_mixin.py +87 -0
- tradear/ta/structure/__init__.py +10 -0
- tradear/ta/structure/_utils.py +33 -0
- tradear/ta/structure/hhll.py +173 -0
- tradear/ta/structure/nbs.py +264 -0
- tradear/ta/structure/pivots.py +334 -0
- tradear/ta/structure/swings.py +454 -0
- tradear/ta/structure/zigzag.py +145 -0
- tradear/ta/tool/__init__.py +56 -0
- tradear/ta/tool/base.py +136 -0
- tradear/ta/tool/draw.py +82 -0
- tradear/ta/tool/fibonacci.py +209 -0
- tradear/ta/tool/volume_profile.py +368 -0
- tradear/ta/trend.py +542 -0
- tradear/ta/volatility.py +237 -0
- tradear/ta/volume.py +703 -0
- tradear-0.1.0.dist-info/METADATA +160 -0
- tradear-0.1.0.dist-info/RECORD +144 -0
- 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,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)
|