tradetropy 0.2.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.
- tradetropy/__init__.py +188 -0
- tradetropy/backtest/__init__.py +3 -0
- tradetropy/backtest/_build.py +495 -0
- tradetropy/backtest/_runner.py +673 -0
- tradetropy/backtest/_shm_bundle.py +131 -0
- tradetropy/backtest/_validation.py +35 -0
- tradetropy/backtest/engine.py +818 -0
- tradetropy/backtest/pool.py +1184 -0
- tradetropy/backtest/pool_adapter.py +122 -0
- tradetropy/connectors/__init__.py +6 -0
- tradetropy/connectors/_disclaimer.py +97 -0
- tradetropy/connectors/binance.py +301 -0
- tradetropy/connectors/bybit.py +1045 -0
- tradetropy/connectors/ccxt.py +1076 -0
- tradetropy/connectors/mt5.py +1125 -0
- tradetropy/core/__init__.py +36 -0
- tradetropy/core/broker.py +2195 -0
- tradetropy/core/constants.py +297 -0
- tradetropy/core/data_types.py +1724 -0
- tradetropy/data/__init__.py +34 -0
- tradetropy/data/_book_replay.py +222 -0
- tradetropy/data/_klines.py +450 -0
- tradetropy/data/_proxy.py +809 -0
- tradetropy/data/_ring.py +649 -0
- tradetropy/data/_store.py +120 -0
- tradetropy/data/_views.py +604 -0
- tradetropy/data/data.py +37 -0
- tradetropy/datasets/__init__.py +354 -0
- tradetropy/datasets/_data/AAPL_1d.csv +301 -0
- tradetropy/datasets/_data/ADAUSDT_1m.npz +0 -0
- tradetropy/datasets/_data/ADAUSDT_book.npz +0 -0
- tradetropy/datasets/_data/ADAUSDT_ticks.npz +0 -0
- tradetropy/datasets/_data/BTCUSDT_1m.npz +0 -0
- tradetropy/datasets/_data/GOOG_1d.csv +301 -0
- tradetropy/datasets/_data/MESU26_ticks.npz +0 -0
- tradetropy/datasets/_data/MNQU26_ticks.npz +0 -0
- tradetropy/exceptions.py +113 -0
- tradetropy/io/__init__.py +17 -0
- tradetropy/io/io.py +1721 -0
- tradetropy/live/__init__.py +5 -0
- tradetropy/live/_builder.py +738 -0
- tradetropy/live/_feed.py +713 -0
- tradetropy/live/_loop.py +337 -0
- tradetropy/live/_warmup.py +231 -0
- tradetropy/live/engine.py +579 -0
- tradetropy/logger.py +573 -0
- tradetropy/models/__init__.py +4 -0
- tradetropy/models/_warmup_policy.py +228 -0
- tradetropy/models/decorators.py +124 -0
- tradetropy/models/footprint/__init__.py +13 -0
- tradetropy/models/footprint/_compute.py +105 -0
- tradetropy/models/footprint/_config.py +62 -0
- tradetropy/models/footprint/_construction.py +186 -0
- tradetropy/models/footprint/_proxy.py +138 -0
- tradetropy/models/footprint/_ring.py +128 -0
- tradetropy/models/footprint/_store.py +70 -0
- tradetropy/models/footprint/_types.py +58 -0
- tradetropy/models/strategy.py +1525 -0
- tradetropy/optimize/__init__.py +13 -0
- tradetropy/optimize/grid_search.py +48 -0
- tradetropy/optimize/optimizer.py +82 -0
- tradetropy/optimize/random_search.py +36 -0
- tradetropy/optimize/reporter.py +68 -0
- tradetropy/optimize/result.py +175 -0
- tradetropy/optimize/space.py +141 -0
- tradetropy/optimize/task.py +156 -0
- tradetropy/playback/__init__.py +15 -0
- tradetropy/playback/base.py +1174 -0
- tradetropy/plotting/__init__.py +3 -0
- tradetropy/plotting/_fig_factory.py +111 -0
- tradetropy/plotting/_layout.py +829 -0
- tradetropy/plotting/_util.py +851 -0
- tradetropy/plotting/chart.py +16 -0
- tradetropy/plotting/config.py +250 -0
- tradetropy/plotting/js/__init__.py +0 -0
- tradetropy/plotting/js/autoscale_equity.js +19 -0
- tradetropy/plotting/js/autoscale_indicator.js +24 -0
- tradetropy/plotting/js/autoscale_ohlc.js +59 -0
- tradetropy/plotting/js/autoscale_ohlc_live.js +64 -0
- tradetropy/plotting/js/autoscale_primitives.js +60 -0
- tradetropy/plotting/js/autoscale_volume.js +15 -0
- tradetropy/plotting/js/fp_lazy.js +24 -0
- tradetropy/plotting/js/fp_lazy_live.js +24 -0
- tradetropy/plotting/js/fp_yaxis_populate.js +11 -0
- tradetropy/plotting/js/lazy_labels.js +21 -0
- tradetropy/plotting/js/lazy_labels_group.js +15 -0
- tradetropy/plotting/js/legend_toggle.js +8 -0
- tradetropy/plotting/js/ylock_reset.js +7 -0
- tradetropy/plotting/js/ylock_watch.js +12 -0
- tradetropy/plotting/plotting.py +616 -0
- tradetropy/plotting/render/__init__.py +12 -0
- tradetropy/plotting/render/_annotations.py +11 -0
- tradetropy/plotting/render/_equity.py +153 -0
- tradetropy/plotting/render/_indicators.py +230 -0
- tradetropy/plotting/render/_ohlc_volume.py +167 -0
- tradetropy/plotting/render/_panels.py +225 -0
- tradetropy/plotting/render/_tools.py +407 -0
- tradetropy/plotting/render/_trades.py +237 -0
- tradetropy/plotting/sources.py +610 -0
- tradetropy/plotting/theme/__init__.py +31 -0
- tradetropy/plotting/theme/dark.py +50 -0
- tradetropy/plotting/theme/light.py +50 -0
- tradetropy/plotting/theme/registry.py +41 -0
- tradetropy/session/__init__.py +18 -0
- tradetropy/session/base.py +1256 -0
- tradetropy/session/fake_live_sesh.py +746 -0
- tradetropy/signal.py +566 -0
- tradetropy/stats/__init__.py +43 -0
- tradetropy/stats/_fast.py +517 -0
- tradetropy/stats/stats.py +1382 -0
- tradetropy/streaming/__init__.py +65 -0
- tradetropy/streaming/_protocol.py +292 -0
- tradetropy/streaming/base.py +420 -0
- tradetropy/streaming/ccxt_pro.py +397 -0
- tradetropy/streaming/fake.py +73 -0
- tradetropy/streaming/replay_feed.py +157 -0
- tradetropy/ta/__init__.py +63 -0
- tradetropy/ta/_volume_profile.py +1204 -0
- tradetropy/ta/annotations.py +1857 -0
- tradetropy/ta/base.py +479 -0
- tradetropy/ta/bill_williams.py +439 -0
- tradetropy/ta/draw.py +137 -0
- tradetropy/ta/momentum.py +2130 -0
- tradetropy/ta/order_flow/__init__.py +79 -0
- tradetropy/ta/order_flow/_core.py +2891 -0
- tradetropy/ta/order_flow/large_trades.py +398 -0
- tradetropy/ta/pattern/__init__.py +87 -0
- tradetropy/ta/pattern/pivot_mixin.py +87 -0
- tradetropy/ta/structure/__init__.py +10 -0
- tradetropy/ta/structure/_utils.py +33 -0
- tradetropy/ta/structure/hhll.py +173 -0
- tradetropy/ta/structure/nbs.py +264 -0
- tradetropy/ta/structure/pivots.py +334 -0
- tradetropy/ta/structure/swings.py +454 -0
- tradetropy/ta/structure/zigzag.py +145 -0
- tradetropy/ta/tool/__init__.py +56 -0
- tradetropy/ta/tool/base.py +136 -0
- tradetropy/ta/tool/draw.py +82 -0
- tradetropy/ta/tool/fibonacci.py +209 -0
- tradetropy/ta/tool/volume_profile.py +368 -0
- tradetropy/ta/trend.py +542 -0
- tradetropy/ta/volatility.py +237 -0
- tradetropy/ta/volume.py +704 -0
- tradetropy-0.2.0.dist-info/METADATA +175 -0
- tradetropy-0.2.0.dist-info/RECORD +146 -0
- tradetropy-0.2.0.dist-info/WHEEL +4 -0
tradetropy/__init__.py
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""
|
|
2
|
+
tradetropy - professional backtesting and live trading framework.
|
|
3
|
+
|
|
4
|
+
The public API is exposed lazily (PEP 562). Importing ``tradetropy`` itself is
|
|
5
|
+
cheap: heavy subsystems (pandas via ``stats``/``io``, ``live``, ``plotting``,
|
|
6
|
+
``robustness``) are only imported the first time one of their names is
|
|
7
|
+
accessed. This keeps the import cost of a pure backtest/optimize workflow -
|
|
8
|
+
and of every spawned optimize/pool worker on Windows/macOS - close to the
|
|
9
|
+
numpy floor, instead of paying for plotting, live and robustness that a
|
|
10
|
+
worker never touches.
|
|
11
|
+
|
|
12
|
+
The exported surface is unchanged; ``from tradetropy import BacktestEngine``,
|
|
13
|
+
``tradetropy.SMA`` and ``from tradetropy import *`` all keep working exactly as
|
|
14
|
+
before.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import importlib
|
|
20
|
+
from typing import TYPE_CHECKING
|
|
21
|
+
|
|
22
|
+
# ``exceptions`` is tiny (no third-party imports) and is referenced as the
|
|
23
|
+
# submodule ``tradetropy.exceptions`` throughout the codebase, so it stays eager.
|
|
24
|
+
from tradetropy import exceptions
|
|
25
|
+
|
|
26
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
27
|
+
# Lazy public-symbol registry: name -> (submodule, attribute)
|
|
28
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
29
|
+
#
|
|
30
|
+
# Each entry maps a public name to the module that defines it. Nothing here is
|
|
31
|
+
# imported until the name is first accessed (see ``__getattr__`` below), so a
|
|
32
|
+
# process that only runs backtests never imports ``live``/``plotting``/
|
|
33
|
+
# ``robustness`` and only pays for ``pandas`` when it actually computes stats.
|
|
34
|
+
#
|
|
35
|
+
# The ``# >>> tier:N`` / ``# <<< tier:N`` marker blocks are consumed by
|
|
36
|
+
# tools/generate_tier.py to gate optional names out of lower-tier builds; keep
|
|
37
|
+
# each gated name's registry entry AND its ``__all__`` entry inside the markers.
|
|
38
|
+
|
|
39
|
+
_LAZY: dict[str, tuple[str, str]] = {
|
|
40
|
+
# -- Engines ---------------------------------------------------------------
|
|
41
|
+
"BacktestEngine": ("tradetropy.backtest", "BacktestEngine"),
|
|
42
|
+
"PoolBacktestEngine": ("tradetropy.backtest", "PoolBacktestEngine"),
|
|
43
|
+
"LiveEngine": ("tradetropy.live", "LiveEngine"),
|
|
44
|
+
|
|
45
|
+
# -- Disclaimer ------------------------------------------------------------
|
|
46
|
+
"LIVE_DISCLAIMER": ("tradetropy.connectors._disclaimer", "LIVE_DISCLAIMER"),
|
|
47
|
+
|
|
48
|
+
# -- Strategy / sessions ---------------------------------------------------
|
|
49
|
+
"Strategy": ("tradetropy.models", "Strategy"),
|
|
50
|
+
"FootprintConfig": ("tradetropy.models", "FootprintConfig"),
|
|
51
|
+
"SeshSimulatorBase": ("tradetropy.session", "SeshSimulatorBase"),
|
|
52
|
+
|
|
53
|
+
# -- Core data types -------------------------------------------------------
|
|
54
|
+
"TickData": ("tradetropy.core", "TickData"),
|
|
55
|
+
"KlineData": ("tradetropy.core", "KlineData"),
|
|
56
|
+
"parse_timeframe": ("tradetropy.core", "parse_timeframe"),
|
|
57
|
+
"TIMEFRAME_PRESETS": ("tradetropy.core", "TIMEFRAME_PRESETS"),
|
|
58
|
+
|
|
59
|
+
# -- Stats -----------------------------------------------------------------
|
|
60
|
+
"Stats": ("tradetropy.stats", "Stats"),
|
|
61
|
+
"compute_stats": ("tradetropy.stats", "compute_stats"),
|
|
62
|
+
|
|
63
|
+
# -- Robustness ------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
# -- Plotting --------------------------------------------------------------
|
|
66
|
+
"plot": ("tradetropy.plotting", "plot"),
|
|
67
|
+
"PlotConfig": ("tradetropy.plotting", "PlotConfig"),
|
|
68
|
+
|
|
69
|
+
# -- Indicators (tradetropy.ta) ----------------------------------------------
|
|
70
|
+
"Indicator": ("tradetropy.ta", "Indicator"),
|
|
71
|
+
"IndicatorPlotConfig": ("tradetropy.ta", "IndicatorPlotConfig"),
|
|
72
|
+
"SMA": ("tradetropy.ta", "SMA"),
|
|
73
|
+
"EMA": ("tradetropy.ta", "EMA"),
|
|
74
|
+
"MACD": ("tradetropy.ta", "MACD"),
|
|
75
|
+
"RSI": ("tradetropy.ta", "RSI"),
|
|
76
|
+
"ATR": ("tradetropy.ta", "ATR"),
|
|
77
|
+
"BollingerBands": ("tradetropy.ta", "BollingerBands"),
|
|
78
|
+
"VolumeProfile": ("tradetropy.ta", "VolumeProfile"),
|
|
79
|
+
"RollingVolumeProfile": ("tradetropy.ta", "RollingVolumeProfile"),
|
|
80
|
+
"VolumeNode": ("tradetropy.ta", "VolumeNode"),
|
|
81
|
+
"detect_volume_nodes": ("tradetropy.ta", "detect_volume_nodes"),
|
|
82
|
+
"ZigZag": ("tradetropy.ta", "ZigZag"),
|
|
83
|
+
"ConfirmedPivot": ("tradetropy.ta", "ConfirmedPivot"),
|
|
84
|
+
"PivotHighLow": ("tradetropy.ta", "PivotHighLow"),
|
|
85
|
+
"SwingHL": ("tradetropy.ta", "SwingHL"),
|
|
86
|
+
"EqualHL": ("tradetropy.ta", "EqualHL"),
|
|
87
|
+
"FairValueGap": ("tradetropy.ta", "FairValueGap"),
|
|
88
|
+
"OrderBlock": ("tradetropy.ta", "OrderBlock"),
|
|
89
|
+
"MarketSessions": ("tradetropy.ta", "MarketSessions"),
|
|
90
|
+
"SessionLevels": ("tradetropy.ta", "SessionLevels"),
|
|
91
|
+
"KillZones": ("tradetropy.ta", "KillZones"),
|
|
92
|
+
"LargeTrades": ("tradetropy.ta", "LargeTrades"),
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
__all__ = [
|
|
96
|
+
"exceptions",
|
|
97
|
+
"BacktestEngine",
|
|
98
|
+
"PoolBacktestEngine",
|
|
99
|
+
"LiveEngine",
|
|
100
|
+
"LIVE_DISCLAIMER",
|
|
101
|
+
"Strategy",
|
|
102
|
+
"FootprintConfig",
|
|
103
|
+
"SeshSimulatorBase",
|
|
104
|
+
"TickData",
|
|
105
|
+
"KlineData",
|
|
106
|
+
"parse_timeframe",
|
|
107
|
+
"TIMEFRAME_PRESETS",
|
|
108
|
+
"Stats",
|
|
109
|
+
"compute_stats",
|
|
110
|
+
"plot",
|
|
111
|
+
"PlotConfig",
|
|
112
|
+
"Indicator",
|
|
113
|
+
"IndicatorPlotConfig",
|
|
114
|
+
"SMA",
|
|
115
|
+
"EMA",
|
|
116
|
+
"MACD",
|
|
117
|
+
"RSI",
|
|
118
|
+
"ATR",
|
|
119
|
+
"BollingerBands",
|
|
120
|
+
"VolumeProfile",
|
|
121
|
+
"RollingVolumeProfile",
|
|
122
|
+
"VolumeNode",
|
|
123
|
+
"detect_volume_nodes",
|
|
124
|
+
"ZigZag",
|
|
125
|
+
"ConfirmedPivot",
|
|
126
|
+
"PivotHighLow",
|
|
127
|
+
"SwingHL",
|
|
128
|
+
"EqualHL",
|
|
129
|
+
"FairValueGap",
|
|
130
|
+
"OrderBlock",
|
|
131
|
+
"MarketSessions",
|
|
132
|
+
"SessionLevels",
|
|
133
|
+
"KillZones",
|
|
134
|
+
"LargeTrades",
|
|
135
|
+
]
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def __getattr__(name: str):
|
|
139
|
+
"""
|
|
140
|
+
Resolve a public name lazily (PEP 562).
|
|
141
|
+
|
|
142
|
+
Looks the name up in the lazy registry, imports the owning submodule on
|
|
143
|
+
first access, caches the resolved object in the module globals (so later
|
|
144
|
+
lookups skip this path), and returns it. Falls back to importing a
|
|
145
|
+
same-named submodule, then raises AttributeError.
|
|
146
|
+
"""
|
|
147
|
+
entry = _LAZY.get(name)
|
|
148
|
+
if entry is not None:
|
|
149
|
+
module_path, attr = entry
|
|
150
|
+
module = importlib.import_module(module_path)
|
|
151
|
+
value = getattr(module, attr)
|
|
152
|
+
globals()[name] = value
|
|
153
|
+
return value
|
|
154
|
+
|
|
155
|
+
# Fall back to a real submodule access (e.g. ``tradetropy.core``).
|
|
156
|
+
try:
|
|
157
|
+
module = importlib.import_module(f"{__name__}.{name}")
|
|
158
|
+
globals()[name] = module
|
|
159
|
+
return module
|
|
160
|
+
except ImportError:
|
|
161
|
+
pass
|
|
162
|
+
|
|
163
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def __dir__() -> list[str]:
|
|
167
|
+
"""Expose the full public surface to ``dir()`` and tab completion."""
|
|
168
|
+
return sorted(set(globals()) | set(_LAZY))
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
# Give static analysers / IDEs the real symbols without any runtime import cost.
|
|
172
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
173
|
+
from tradetropy.backtest import BacktestEngine, PoolBacktestEngine
|
|
174
|
+
from tradetropy.live import LiveEngine, LivePool, Recorder
|
|
175
|
+
from tradetropy.connectors._disclaimer import LIVE_DISCLAIMER
|
|
176
|
+
from tradetropy.models import Strategy, FootprintConfig
|
|
177
|
+
from tradetropy.session import SeshSimulatorBase
|
|
178
|
+
from tradetropy.core import TickData, KlineData, parse_timeframe, TIMEFRAME_PRESETS
|
|
179
|
+
from tradetropy.stats import Stats, compute_stats
|
|
180
|
+
from tradetropy.robustness import MonteCarlo, MonteCarloConfig, MonteCarloResult
|
|
181
|
+
from tradetropy.plotting import plot, PlotConfig
|
|
182
|
+
from tradetropy.ta import (
|
|
183
|
+
Indicator, IndicatorPlotConfig, SMA, EMA, MACD, RSI, ATR,
|
|
184
|
+
BollingerBands, VolumeProfile, TickVolumeProfile, RollingVolumeProfile,
|
|
185
|
+
VolumeNode, detect_volume_nodes, ZigZag, ConfirmedPivot, PivotHighLow,
|
|
186
|
+
SwingHL, EqualHL, FairValueGap, OrderBlock, MarketSessions,
|
|
187
|
+
SessionLevels, KillZones, LargeTrades,
|
|
188
|
+
)
|
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections import defaultdict
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
from tradetropy.models.strategy import FeedType
|
|
8
|
+
from tradetropy.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 tradetropy.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 tradetropy.ta.base import Indicator
|
|
30
|
+
from tradetropy.models.strategy import Strategy
|
|
31
|
+
from tradetropy.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 tradetropy.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
|
+
source = defn["source"]
|
|
165
|
+
if not isinstance(source.proxy, TickProxy):
|
|
166
|
+
continue
|
|
167
|
+
if source.symbol != symbol:
|
|
168
|
+
continue
|
|
169
|
+
|
|
170
|
+
indicator = defn["indicator"]
|
|
171
|
+
sources = defn.get("sources", [source])
|
|
172
|
+
multi_source = defn.get("multi_source", False)
|
|
173
|
+
multi_band = defn.get("multi_band", False)
|
|
174
|
+
|
|
175
|
+
# Build source array
|
|
176
|
+
if multi_source:
|
|
177
|
+
source_arr = np.column_stack([
|
|
178
|
+
tick_matrix[:, _TICK_COL[f._col_name]] for f in sources
|
|
179
|
+
])
|
|
180
|
+
else:
|
|
181
|
+
src_col = _TICK_COL[source._col_name]
|
|
182
|
+
source_arr = tick_matrix[:, src_col]
|
|
183
|
+
|
|
184
|
+
result = indicator.calculate(source_arr)
|
|
185
|
+
|
|
186
|
+
if multi_band:
|
|
187
|
+
K = indicator.n_outputs
|
|
188
|
+
col_names = []
|
|
189
|
+
for k in range(K):
|
|
190
|
+
band_col_name = f"{indicator.col_name(symbol, source._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(result[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
|
+
col_name = indicator.col_name(symbol, source._col_name)
|
|
200
|
+
col_index[col_name] = N_TICK_COLS + len(cols_ind)
|
|
201
|
+
defn["col_name"] = col_name
|
|
202
|
+
defn["en_tick_store"] = True
|
|
203
|
+
defn["tick_symbol"] = symbol
|
|
204
|
+
cols_ind.append(result.ravel() if result.ndim > 1 else result)
|
|
205
|
+
|
|
206
|
+
if cols_ind:
|
|
207
|
+
matrix = np.ascontiguousarray(
|
|
208
|
+
np.hstack([tick_matrix, np.column_stack(cols_ind)])
|
|
209
|
+
)
|
|
210
|
+
else:
|
|
211
|
+
matrix = np.ascontiguousarray(tick_matrix)
|
|
212
|
+
|
|
213
|
+
return TickDataStore(matrix, 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
|
+
# Group indicator_defs by source proxy -- O(n_indicators) once.
|
|
233
|
+
# Avoids the O(proxies * indicators) loop that existed before.
|
|
234
|
+
inds_by_proxy: dict[int, list] = defaultdict(list)
|
|
235
|
+
for defn in self.strategy._indicator_defs:
|
|
236
|
+
source = defn["source"]
|
|
237
|
+
if isinstance(source.proxy, OhlcProxy):
|
|
238
|
+
inds_by_proxy[id(source.proxy)].append(defn)
|
|
239
|
+
|
|
240
|
+
for ohlc_proxy in self.strategy._ohlc_proxies:
|
|
241
|
+
symbol = ohlc_proxy.symbol
|
|
242
|
+
interval = 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, interval
|
|
251
|
+
)
|
|
252
|
+
else:
|
|
253
|
+
# kline mode: treat each row as a "tick" with price=close
|
|
254
|
+
candle_result = _build_candles_from_klines(arr, interval)
|
|
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
|
+
# Only indicators from this proxy -- O(n_inds_of_this_proxy).
|
|
266
|
+
# Dedup identical indicators (same class+params on the same source
|
|
267
|
+
# column): compute once and let every duplicate share the column,
|
|
268
|
+
# instead of recomputing and appending an identical column. Keyed by
|
|
269
|
+
# (col_name, source cols, multi_band); col_name encodes
|
|
270
|
+
# class+length+symbol and the source cols disambiguate same-name
|
|
271
|
+
# indicators fed different inputs.
|
|
272
|
+
_seen_ohlc: dict = {}
|
|
273
|
+
for defn in inds_by_proxy[id(ohlc_proxy)]:
|
|
274
|
+
source = defn["source"]
|
|
275
|
+
sources = defn.get("sources", [source])
|
|
276
|
+
indicator = defn["indicator"]
|
|
277
|
+
multi_source = defn.get("multi_source", False)
|
|
278
|
+
multi_band = defn.get("multi_band", False)
|
|
279
|
+
|
|
280
|
+
_base_name = indicator.col_name(symbol, source._col_name)
|
|
281
|
+
_src_key = (
|
|
282
|
+
tuple(_OHLC_COL[f._col_name] for f in sources)
|
|
283
|
+
if multi_source
|
|
284
|
+
else (_OHLC_COL[source._col_name],)
|
|
285
|
+
)
|
|
286
|
+
_dedup_key = (_base_name, _src_key, multi_band)
|
|
287
|
+
_prev = _seen_ohlc.get(_dedup_key)
|
|
288
|
+
if _prev is not None:
|
|
289
|
+
defn["col_name"] = _prev["col_name"]
|
|
290
|
+
defn["en_tick_store"] = False
|
|
291
|
+
defn["ohlc_proxy"] = ohlc_proxy
|
|
292
|
+
defn["src_col_idx"] = _prev["src_col_idx"]
|
|
293
|
+
if multi_band:
|
|
294
|
+
defn["col_names"] = _prev["col_names"]
|
|
295
|
+
continue
|
|
296
|
+
|
|
297
|
+
# Build source array
|
|
298
|
+
if multi_source:
|
|
299
|
+
if len(closed_candles) > 0:
|
|
300
|
+
source_arr = np.column_stack([
|
|
301
|
+
closed_candles[:, _OHLC_COL[f._col_name]] for f in sources
|
|
302
|
+
])
|
|
303
|
+
else:
|
|
304
|
+
source_arr = np.empty((0, len(sources)), dtype=np.float64)
|
|
305
|
+
else:
|
|
306
|
+
src_col = _OHLC_COL[source._col_name]
|
|
307
|
+
source_arr = (
|
|
308
|
+
closed_candles[:, src_col]
|
|
309
|
+
if len(closed_candles) > 0
|
|
310
|
+
else np.array([], dtype=np.float64)
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
# Single full-history pass (precomputed once for all closed
|
|
314
|
+
# bars). Backtest/replay parity for recursive indicators (RSI,
|
|
315
|
+
# MACD) is achieved by feeding the replay the same candle
|
|
316
|
+
# history via warmup (it reconstructs a full window_size of
|
|
317
|
+
# candles), not by windowing the backtest precompute. This keeps
|
|
318
|
+
# the backtest at its original O(N) precompute speed.
|
|
319
|
+
result = indicator.calculate(source_arr)
|
|
320
|
+
|
|
321
|
+
if multi_band:
|
|
322
|
+
K = indicator.n_outputs
|
|
323
|
+
# region XXX debug
|
|
324
|
+
n_real_bands = result.shape[0] if result.ndim > 1 else 1
|
|
325
|
+
if K != n_real_bands:
|
|
326
|
+
raise ConfigError(
|
|
327
|
+
f"Indicator '{type(indicator).__name__}': n_outputs={K} but "
|
|
328
|
+
f"calculate returned {n_real_bands} rows. "
|
|
329
|
+
f"Check that n_outputs matches the calculate output shape."
|
|
330
|
+
)
|
|
331
|
+
#endregion
|
|
332
|
+
col_names = []
|
|
333
|
+
src_idxs = (
|
|
334
|
+
[_OHLC_COL[f._col_name] for f in sources]
|
|
335
|
+
if multi_source
|
|
336
|
+
else _OHLC_COL[source._col_name]
|
|
337
|
+
)
|
|
338
|
+
for k in range(K):
|
|
339
|
+
cname = f"{indicator.col_name(symbol, source._col_name)}_b{k}"
|
|
340
|
+
col_index[cname] = N_OHLC_COLS + len(cols_ind)
|
|
341
|
+
col_names.append(cname)
|
|
342
|
+
band_data = result[k] if len(result.shape) > 1 else result
|
|
343
|
+
cols_ind.append(band_data)
|
|
344
|
+
defn["col_names"] = col_names
|
|
345
|
+
defn["col_name"] = col_names[0]
|
|
346
|
+
defn["en_tick_store"] = False
|
|
347
|
+
defn["ohlc_proxy"] = ohlc_proxy
|
|
348
|
+
defn["src_col_idx"] = src_idxs
|
|
349
|
+
_seen_ohlc[_dedup_key] = {
|
|
350
|
+
"col_name": col_names[0],
|
|
351
|
+
"col_names": col_names,
|
|
352
|
+
"src_col_idx": src_idxs,
|
|
353
|
+
}
|
|
354
|
+
else:
|
|
355
|
+
col_name = indicator.col_name(symbol, source._col_name)
|
|
356
|
+
col_index[col_name] = N_OHLC_COLS + len(cols_ind)
|
|
357
|
+
defn["col_name"] = col_name
|
|
358
|
+
defn["en_tick_store"] = False
|
|
359
|
+
defn["ohlc_proxy"] = ohlc_proxy
|
|
360
|
+
defn["src_col_idx"] = (
|
|
361
|
+
[_OHLC_COL[f._col_name] for f in sources]
|
|
362
|
+
if multi_source
|
|
363
|
+
else _OHLC_COL[source._col_name]
|
|
364
|
+
)
|
|
365
|
+
cols_ind.append(result.ravel() if result.ndim > 1 else result)
|
|
366
|
+
_seen_ohlc[_dedup_key] = {
|
|
367
|
+
"col_name": col_name,
|
|
368
|
+
"src_col_idx": defn["src_col_idx"],
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if len(closed_candles) > 0 and cols_ind:
|
|
372
|
+
ohlc_matrix = np.ascontiguousarray(
|
|
373
|
+
np.hstack([closed_candles, np.column_stack(cols_ind)])
|
|
374
|
+
)
|
|
375
|
+
elif len(closed_candles) > 0:
|
|
376
|
+
ohlc_matrix = np.ascontiguousarray(closed_candles)
|
|
377
|
+
else:
|
|
378
|
+
n_cols = N_OHLC_COLS + len(cols_ind)
|
|
379
|
+
ohlc_matrix = np.empty((0, n_cols), dtype=np.float64)
|
|
380
|
+
|
|
381
|
+
ohlc_stores[id(ohlc_proxy)] = OhlcDataStore(
|
|
382
|
+
matrix=ohlc_matrix,
|
|
383
|
+
col_index=col_index,
|
|
384
|
+
tick_to_candle_mapping=tick_to_candle_map,
|
|
385
|
+
accumulated_by_tick=accumulated_per_tick,
|
|
386
|
+
ts_candle_by_tick=candle_ts_per_tick,
|
|
387
|
+
prices_per_tick=prices_per_row,
|
|
388
|
+
interval_ms=interval,
|
|
389
|
+
symbol=symbol,
|
|
390
|
+
kline_mode=(feed_type == "kline"),
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
return ohlc_stores
|
|
394
|
+
|
|
395
|
+
def _connect_proxies(self, tick_stores: dict, ohlc_stores: dict):
|
|
396
|
+
'''Connect each proxy to its data store.'''
|
|
397
|
+
for tp in self.strategy._tick_proxies:
|
|
398
|
+
store = tick_stores.get(tp.symbol)
|
|
399
|
+
if store is not None:
|
|
400
|
+
tp._connect_backtest(store)
|
|
401
|
+
|
|
402
|
+
for op in self.strategy._ohlc_proxies:
|
|
403
|
+
op._connect_backtest(ohlc_stores[id(op)])
|
|
404
|
+
|
|
405
|
+
for defn in self.strategy._indicator_defs:
|
|
406
|
+
proxy: "IndicatorProxy | MultiBandProxy" = defn["proxy"]
|
|
407
|
+
indicator: Indicator = defn["indicator"]
|
|
408
|
+
source: ColumnRef = defn["source"]
|
|
409
|
+
multi_band: bool = defn.get("multi_band", False)
|
|
410
|
+
|
|
411
|
+
if defn.get("en_tick_store", True) and isinstance(source.proxy, TickProxy):
|
|
412
|
+
# -- Tick store ------------------------------------------------
|
|
413
|
+
tick_store = tick_stores[defn["tick_symbol"]]
|
|
414
|
+
size = source.proxy._window_size
|
|
415
|
+
|
|
416
|
+
if multi_band:
|
|
417
|
+
col_names = defn["col_names"]
|
|
418
|
+
for k, cname in enumerate(col_names):
|
|
419
|
+
col_idx = tick_store.col_index[cname]
|
|
420
|
+
view = WindowView(col_idx=col_idx, size=size, tick_store=tick_store)
|
|
421
|
+
proxy._connect_band(k, view)
|
|
422
|
+
else:
|
|
423
|
+
col_idx = tick_store.col_index[defn["col_name"]]
|
|
424
|
+
view = WindowView(col_idx=col_idx, size=size, tick_store=tick_store)
|
|
425
|
+
proxy._connect(view)
|
|
426
|
+
else:
|
|
427
|
+
# -- OHLC store ------------------------------------------------
|
|
428
|
+
ohlc_proxy: OhlcProxy = defn["ohlc_proxy"]
|
|
429
|
+
ohlc_store: OhlcDataStore = ohlc_stores[id(ohlc_proxy)]
|
|
430
|
+
src_col_idx = defn["src_col_idx"]
|
|
431
|
+
size = ohlc_proxy._window_size
|
|
432
|
+
|
|
433
|
+
if multi_band:
|
|
434
|
+
col_names = defn["col_names"]
|
|
435
|
+
ind_col_idxs = [ohlc_store.col_index[n] for n in col_names]
|
|
436
|
+
shared_cache = {}
|
|
437
|
+
for k in range(len(col_names)):
|
|
438
|
+
view = MultiOhlcIndicatorView(
|
|
439
|
+
ohlc_store=ohlc_store,
|
|
440
|
+
indicator=indicator,
|
|
441
|
+
ind_col_idxs=ind_col_idxs,
|
|
442
|
+
src_col_idx=src_col_idx,
|
|
443
|
+
band_idx=k,
|
|
444
|
+
size=size,
|
|
445
|
+
shared_cache=shared_cache,
|
|
446
|
+
)
|
|
447
|
+
proxy._connect_band(k, view)
|
|
448
|
+
else:
|
|
449
|
+
ind_col_idx = ohlc_store.col_index[defn["col_name"]]
|
|
450
|
+
view = OhlcIndicatorView(
|
|
451
|
+
ohlc_store=ohlc_store,
|
|
452
|
+
indicator=indicator,
|
|
453
|
+
ind_col_idx=ind_col_idx,
|
|
454
|
+
src_col_idx=src_col_idx,
|
|
455
|
+
size=size,
|
|
456
|
+
)
|
|
457
|
+
proxy._connect(view)
|
|
458
|
+
# Wire the single-frame scalar read fast path for the common
|
|
459
|
+
# kline-mode read (self.ind[-1] / [-k]); non-kline (tick)
|
|
460
|
+
# backtests keep the view path. See IndicatorProxy.
|
|
461
|
+
if ohlc_store.kline_mode and isinstance(src_col_idx, int):
|
|
462
|
+
proxy._enable_flat_kline(
|
|
463
|
+
ohlc_store, ind_col_idx, size,
|
|
464
|
+
getattr(indicator, "warmup_factor", 1) == 1,
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
def _get_ind_def_for_proxy(self, proxy) -> "dict | None":
|
|
468
|
+
'''Find the indicator definition corresponding to a proxy.'''
|
|
469
|
+
for defn in self.strategy._indicator_defs:
|
|
470
|
+
if defn["proxy"] is proxy:
|
|
471
|
+
return defn
|
|
472
|
+
return None
|
|
473
|
+
|
|
474
|
+
def _build_pattern_stores(self, ohlc_stores: dict) -> None:
|
|
475
|
+
'''Build pattern stores from OHLC data.'''
|
|
476
|
+
# No patterns -> nothing to build. Guarding here also avoids importing
|
|
477
|
+
# the pattern DSL, which is absent from lower-tier (gated) builds.
|
|
478
|
+
if not getattr(self.strategy, "_pattern_matcher_defs", None):
|
|
479
|
+
return
|
|
480
|
+
from tradetropy.ta.pattern._builder import iter_pattern_matcher_defs
|
|
481
|
+
from tradetropy.ta.pattern.sequence import FrozenPivotSequence
|
|
482
|
+
from tradetropy.ta.pattern.store import PatternStore
|
|
483
|
+
|
|
484
|
+
for pm_def, _, ohlc_proxy, symbol, base_cols, decorator_cols, tag_decoders \
|
|
485
|
+
in iter_pattern_matcher_defs(self.strategy, self._get_ind_def_for_proxy):
|
|
486
|
+
|
|
487
|
+
ohlc_store = ohlc_stores[id(ohlc_proxy)]
|
|
488
|
+
sequence = FrozenPivotSequence.from_ohlc_store(
|
|
489
|
+
ohlc_store = ohlc_store,
|
|
490
|
+
base_col_names = base_cols,
|
|
491
|
+
decorator_cols = decorator_cols,
|
|
492
|
+
tag_decoders = tag_decoders,
|
|
493
|
+
)
|
|
494
|
+
store = PatternStore(sequence, pm_def.pattern)
|
|
495
|
+
pm_def.proxy._connect_backtest(store, ohlc_store=ohlc_store)
|