11import traceback
2- from typing import Optional
2+ from typing import Optional , Any
33import pandas as pd
44from concurrent .futures import ProcessPoolExecutor , as_completed
55import os
1414from core .domain .cost .cost_engine import TradeCostEngine , InstrumentCtx
1515from core .backtesting .trade_factory import TradeFactory
1616from core .domain .risk .sizing import position_size
17+ from core .strategy .plan_builder import PlanBuildContext
1718
1819
1920class Backtester :
2021 def __init__ (self ,
21- slippage : float = 0.0 ,
22+ strategy ,
2223 execution_policy : Optional [ExecutionPolicy ] = None ,
2324 cost_engine : Optional [TradeCostEngine ] = None
2425 ):
25- self .slippage = slippage
26+ self .strategy = strategy
2627 self .execution_policy = execution_policy or ExecutionPolicy ()
2728 self .cost_engine = cost_engine or TradeCostEngine (self .execution_policy )
2829
@@ -66,65 +67,70 @@ def _instrument_ctx(symbol: str) -> InstrumentCtx:
6667 def _backtest_single_symbol (self , df : pd .DataFrame , symbol : str ) -> pd .DataFrame :
6768 trades = []
6869
69- df = df .copy ()
70- df ["time" ] = df ["time" ].dt .tz_localize (None )
71-
70+ time_arr = df ["time" ].dt .tz_localize (None ).values
7271 high_arr = df ["high" ].values
7372 low_arr = df ["low" ].values
7473 close_arr = df ["close" ].values
75- time_arr = df ["time" ].values
7674
77- signal_arr = df ["signal_entry" ].values
78- levels_arr = df ["levels" ].values
75+ # instrument ctx do slippage itp.
76+ ctx_inst = self ._instrument_ctx (symbol )
77+
78+ # 1) Build vector-friendly plans once (shared logic)
79+ ctx = PlanBuildContext (
80+ symbol = symbol ,
81+ strategy_name = type (self .strategy ).__name__ ,
82+ strategy_config = self .strategy .strategy_config ,
83+ )
84+ plans = self .strategy .build_trade_plans_backtest (df = df , ctx = ctx , allow_managed_in_backtest = False )
85+
86+ # 2) Precompute arrays used in the hot loop
87+ plan_valid = plans ["plan_valid" ].values
88+ plan_dir = plans ["plan_direction" ].values
89+ plan_tag = plans ["plan_entry_tag" ].values
90+ plan_sl = plans ["plan_sl" ].values .astype (float )
91+ plan_tp1 = plans ["plan_tp1" ].values .astype (float )
92+ plan_tp2 = plans ["plan_tp2" ].values .astype (float )
7993
80- ctx = self . _instrument_ctx ( symbol )
81- point_size = ctx . point_size
82- pip_value = ctx . pip_value
94+ plan_sl_tag = plans [ "plan_sl_tag" ]. values . astype ( str )
95+ plan_tp1_tag = plans [ "plan_tp1_tag" ]. values . astype ( str )
96+ plan_tp2_tag = plans [ "plan_tp2_tag" ]. values . astype ( str )
8397
8498 n = len (df )
8599
86100 for direction in ("long" , "short" ):
87101 dir_flag = 1 if direction == "long" else - 1
88- last_exit_by_tag = {}
102+ last_exit_by_tag : dict [ str , Any ] = {}
89103
90104 for entry_pos in range (n ):
91- sig = signal_arr [entry_pos ]
92- if not isinstance (sig , dict ) or sig .get ("direction" ) != direction :
105+ if not plan_valid [entry_pos ]:
106+ continue
107+ if plan_dir [entry_pos ] != direction :
93108 continue
94109
95- entry_tag = sig [ "tag" ]
110+ entry_tag = str ( plan_tag [ entry_pos ])
96111 entry_time = time_arr [entry_pos ]
97112
98113 last_exit = last_exit_by_tag .get (entry_tag )
99114 if last_exit is not None and last_exit > entry_time :
100115 continue
101116
102- levels = levels_arr [entry_pos ]
103- if not isinstance (levels , dict ):
104- continue
105-
106- sl = (levels .get ("SL" ) or levels .get (0 ))["level" ]
107- tp1 = (levels .get ("TP1" ) or levels .get (1 ))["level" ]
108- tp2 = (levels .get ("TP2" ) or levels .get (2 ))["level" ]
117+ sl = float (plan_sl [entry_pos ])
118+ tp1 = float (plan_tp1 [entry_pos ])
119+ tp2 = float (plan_tp2 [entry_pos ])
109120
110- level_tags = {
111- "SL" : (levels .get ("SL" ) or levels .get (0 ))["tag" ],
112- "TP1" : (levels .get ("TP1" ) or levels .get (1 ))["tag" ],
113- "TP2" : (levels .get ("TP2" ) or levels .get (2 ))["tag" ],
114- }
121+ level_tags = {"SL" : plan_sl_tag [entry_pos ], "TP1" : plan_tp1_tag [entry_pos ], "TP2" : plan_tp2_tag [entry_pos ]}
115122
116123 entry_price = float (close_arr [entry_pos ])
117124
118- # legacy behavior: entry slippage on exec price (kept as-is)
119- entry_price += ctx .slippage_abs if direction == "long" else - ctx .slippage_abs
125+ entry_price += ctx_inst .slippage_abs if direction == "long" else - ctx_inst .slippage_abs
120126
121127 size = position_size (
122128 entry_price = entry_price ,
123129 stop_price = sl ,
124130 max_risk = MAX_RISK_PER_TRADE ,
125- account_size = INITIAL_BALANCE ,
126- point_size = point_size ,
127- pip_value = pip_value ,
131+ account_size = INITIAL_BALANCE , # lub INITIAL_BALANCE
132+ point_size = ctx_inst . point_size ,
133+ pip_value = ctx_inst . pip_value ,
128134 )
129135
130136 (
@@ -145,7 +151,7 @@ def _backtest_single_symbol(self, df: pd.DataFrame, symbol: str) -> pd.DataFrame
145151 low_arr ,
146152 close_arr ,
147153 time_arr ,
148- ctx .slippage_abs ,
154+ ctx_inst .slippage_abs ,
149155 )
150156
151157 exit_result = ExitProcessor .process (
@@ -161,10 +167,12 @@ def _backtest_single_symbol(self, df: pd.DataFrame, symbol: str) -> pd.DataFrame
161167 tp1 = tp1 ,
162168 tp2 = tp2 ,
163169 position_size = size ,
164- point_size = point_size ,
165- pip_value = pip_value ,
170+ point_size = ctx_inst . point_size ,
171+ pip_value = ctx_inst . pip_value ,
166172 )
167173
174+
175+
168176 trade_dict = TradeFactory .create_trade (
169177 symbol = symbol ,
170178 direction = direction ,
@@ -175,22 +183,18 @@ def _backtest_single_symbol(self, df: pd.DataFrame, symbol: str) -> pd.DataFrame
175183 sl = sl ,
176184 tp1 = tp1 ,
177185 tp2 = tp2 ,
178- point_size = point_size ,
179- pip_value = pip_value ,
186+ point_size = ctx_inst . point_size ,
187+ pip_value = ctx_inst . pip_value ,
180188 exit_result = exit_result ,
181189 level_tags = level_tags ,
182190 )
183191
184- self .cost_engine .enrich (trade_dict , df = df , ctx = ctx )
192+ self .cost_engine .enrich (trade_dict , df = df , ctx = ctx_inst )
185193
186194 trades .append (trade_dict )
187195 last_exit_by_tag [entry_tag ] = exit_time
188196
189197 print (f"✅ Finished backtest for { symbol } , { len (trades )} trades." )
190-
191- df = pd .DataFrame (trades )
192-
193- print (df .loc [df ['financing_usd_total' ] > 0 ])
194198 return pd .DataFrame (trades )
195199
196200 # -----------------------------
0 commit comments