1818"""
1919
2020import pandas as pd
21-
2221from quanttradeai .utils .metrics import sharpe_ratio , max_drawdown
2322from quanttradeai .trading .risk import apply_stop_loss_take_profit
2423from quanttradeai .trading .portfolio import PortfolioManager
@@ -28,24 +27,125 @@ def _simulate_single(
2827 df : pd .DataFrame ,
2928 stop_loss_pct : float | None = None ,
3029 take_profit_pct : float | None = None ,
31- transaction_cost : float = 0.0 ,
32- slippage : float = 0.0 ,
30+ execution : dict | None = None ,
3331) -> pd .DataFrame :
34- """Simulate trades for a single symbol."""
32+ """Simulate trades for a single symbol with execution effects ."""
3533 data = df .copy ()
3634 if stop_loss_pct is not None or take_profit_pct is not None :
3735 data = apply_stop_loss_take_profit (data , stop_loss_pct , take_profit_pct )
38- data ["price_return" ] = data ["Close" ].pct_change ()
39- data ["strategy_return" ] = data ["price_return" ].shift (- 1 ) * data ["label" ]
40- data ["strategy_return" ] = data ["strategy_return" ].fillna (0.0 )
41-
42- trade_cost = transaction_cost + slippage
43- if trade_cost > 0 :
44- trades = data ["label" ].diff ().abs ()
45- trades .iloc [0 ] = abs (data ["label" ].iloc [0 ])
46- data ["strategy_return" ] -= trades * trade_cost
4736
37+ exec_cfg = execution or {}
38+ tc = exec_cfg .get ("transaction_costs" , {})
39+ sl = exec_cfg .get ("slippage" , {})
40+ liq = exec_cfg .get ("liquidity" , {})
41+
42+ ref_col = "Close"
43+ if sl .get ("reference_price" , "close" ) == "mid" and "Mid" in data .columns :
44+ ref_col = "Mid"
45+ prices = data [ref_col ].astype (float )
46+ volumes = data .get ("Volume" , pd .Series (float ("inf" ), index = data .index ))
47+
48+ position = 0.0
49+ carry = 0.0
50+ entry_price : float | None = None
51+ gross_returns : list [float ] = [0.0 ] * len (data )
52+ net_returns : list [float ] = [0.0 ] * len (data )
53+ ledger : list [dict ] = []
54+
55+ for i in range (len (data )):
56+ price = prices .iloc [i ]
57+ volume = volumes .iloc [i ] if i < len (volumes ) else float ("inf" )
58+ desired = data ["label" ].iloc [i ] + carry
59+
60+ diff = desired - position
61+ side = 1 if diff > 0 else - 1 if diff < 0 else 0
62+ qty = abs (diff )
63+ total_cost = 0.0
64+
65+ if side != 0 and qty > 0 :
66+ if liq .get ("enabled" , False ):
67+ max_part = liq .get ("max_participation" , 0.0 )
68+ max_qty = max_part * float (volume )
69+ exec_qty = min (qty , max_qty )
70+ carry = qty - exec_qty
71+ else :
72+ exec_qty = qty
73+ carry = 0.0
74+
75+ if exec_qty > 0 :
76+ slip_amt = 0.0
77+ slip_bps = 0.0
78+ if sl .get ("enabled" , False ) and sl .get ("value" , 0 ) > 0 :
79+ if sl .get ("mode" , "bps" ) == "bps" :
80+ slip_bps = sl ["value" ]
81+ slip_amt = price * slip_bps / 10000
82+ else :
83+ slip_amt = sl ["value" ]
84+ slip_bps = slip_amt / price * 10000
85+ fill_price = price + slip_amt if side > 0 else price - slip_amt
86+
87+ t_cost = 0.0
88+ if tc .get ("enabled" , False ) and tc .get ("value" , 0 ) > 0 :
89+ if tc .get ("mode" , "bps" ) == "bps" :
90+ t_cost = price * exec_qty * tc ["value" ] / 10000
91+ else :
92+ if tc .get ("apply_on" , "notional" ) == "shares" :
93+ t_cost = tc ["value" ] * exec_qty
94+ else :
95+ t_cost = tc ["value" ]
96+
97+ sl_cost = abs (slip_amt ) * exec_qty
98+ total_cost = t_cost + sl_cost
99+
100+ gross_pnl = 0.0
101+ if position != 0 and side != (1 if position > 0 else - 1 ):
102+ close_qty = min (abs (position ), exec_qty )
103+ if position > 0 :
104+ gross_pnl = (fill_price - entry_price ) * close_qty
105+ else :
106+ gross_pnl = (entry_price - fill_price ) * close_qty
107+ if exec_qty > close_qty :
108+ entry_price = fill_price
109+ elif position == 0 :
110+ entry_price = fill_price
111+ elif side == (1 if position > 0 else - 1 ):
112+ entry_price = (
113+ entry_price * abs (position ) + fill_price * exec_qty
114+ ) / (abs (position ) + exec_qty )
115+
116+ position += side * exec_qty
117+ if position == 0 :
118+ entry_price = None
119+
120+ ledger .append (
121+ {
122+ "timestamp" : data .index [i ],
123+ "side" : "buy" if side > 0 else "sell" ,
124+ "qty" : exec_qty ,
125+ "reference_price" : price ,
126+ "fill_price" : fill_price ,
127+ "gross_pnl_contrib" : gross_pnl ,
128+ "transaction_cost" : t_cost ,
129+ "slippage_cost" : sl_cost ,
130+ "costs" : total_cost ,
131+ "slippage_bps_applied" : slip_bps ,
132+ "net_pnl_contrib" : gross_pnl - total_cost ,
133+ }
134+ )
135+
136+ if i < len (data ) - 1 :
137+ price_next = prices .iloc [i + 1 ]
138+ gross_ret = (price_next - price ) / price * position
139+ cost_return = total_cost / price if price else 0.0
140+ gross_returns [i ] = gross_ret
141+ net_returns [i ] = gross_ret - cost_return
142+ gross_returns [- 1 ] = 0.0
143+ net_returns [- 1 ] = 0.0
144+ data ["gross_return" ] = pd .Series (gross_returns , index = data .index )
145+ data ["strategy_return" ] = pd .Series (net_returns , index = data .index )
146+ data ["gross_equity_curve" ] = (1 + data ["gross_return" ]).cumprod ()
48147 data ["equity_curve" ] = (1 + data ["strategy_return" ]).cumprod ()
148+ data .attrs ["ledger" ] = pd .DataFrame (ledger )
49149 return data
50150
51151
@@ -55,6 +155,7 @@ def simulate_trades(
55155 take_profit_pct : float | None = None ,
56156 transaction_cost : float = 0.0 ,
57157 slippage : float = 0.0 ,
158+ execution : dict | None = None ,
58159 portfolio : PortfolioManager | None = None ,
59160) -> pd .DataFrame | dict [str , pd .DataFrame ]:
60161 """Simulate trades using label signals.
@@ -72,9 +173,14 @@ def simulate_trades(
72173 Take profit percentage applied to each trade. ``None`` disables take
73174 profits.
74175 transaction_cost : float, optional
75- Fixed cost applied every time a position is opened or closed.
176+ Legacy cost per trade (as fraction of notional). Converted to execution
177+ config if provided.
76178 slippage : float, optional
77- Additional cost applied on each trade to model slippage.
179+ Legacy slippage per trade (as fraction of notional). Converted to
180+ execution config if provided.
181+ execution : dict, optional
182+ Execution configuration controlling transaction costs, slippage and
183+ liquidity limits.
78184 portfolio : PortfolioManager or None, optional
79185 Portfolio manager used to allocate capital when backtesting multiple
80186 symbols. Required if ``df`` is a dictionary.
@@ -87,6 +193,18 @@ def simulate_trades(
87193 dictionary, returns a dictionary with per-symbol results as well as an
88194 aggregated ``"portfolio"`` entry containing the combined equity curve.
89195 """
196+ exec_cfg = execution .copy () if execution else {}
197+ if transaction_cost :
198+ exec_cfg .setdefault ("transaction_costs" , {})
199+ exec_cfg ["transaction_costs" ].update (
200+ {"enabled" : True , "mode" : "bps" , "value" : transaction_cost * 10000 }
201+ )
202+ if slippage :
203+ exec_cfg .setdefault ("slippage" , {})
204+ exec_cfg ["slippage" ].update (
205+ {"enabled" : True , "mode" : "bps" , "value" : slippage * 10000 }
206+ )
207+
90208 if isinstance (df , dict ):
91209 if portfolio is None :
92210 raise ValueError ("portfolio manager required for multiple symbols" )
@@ -97,8 +215,7 @@ def simulate_trades(
97215 data ,
98216 stop_loss_pct = stop_loss_pct ,
99217 take_profit_pct = take_profit_pct ,
100- transaction_cost = transaction_cost ,
101- slippage = slippage ,
218+ execution = exec_cfg ,
102219 )
103220 results [symbol ] = res
104221 qty = portfolio .open_position (symbol , data ["Close" ].iloc [0 ], stop_loss_pct )
@@ -120,35 +237,12 @@ def simulate_trades(
120237 df ,
121238 stop_loss_pct = stop_loss_pct ,
122239 take_profit_pct = take_profit_pct ,
123- transaction_cost = transaction_cost ,
124- slippage = slippage ,
240+ execution = exec_cfg ,
125241 )
126242
127243
128244def compute_metrics (data : pd .DataFrame , risk_free_rate : float = 0.0 ) -> dict :
129- """Calculate basic performance metrics for a strategy.
245+ """Return gross and net performance summary."""
246+ from quanttradeai .utils .metrics import compute_performance
130247
131- Parameters
132- ----------
133- data : pd.DataFrame
134- Output from :func:`simulate_trades` containing ``strategy_return`` and
135- ``equity_curve`` columns.
136- risk_free_rate : float, optional
137- Annual risk free rate used in Sharpe ratio, by default 0.0.
138-
139- Returns
140- -------
141- dict
142- Dictionary with ``cumulative_return``, ``sharpe_ratio`` and
143- ``max_drawdown`` keys.
144- """
145- returns = data ["strategy_return" ]
146- equity = data ["equity_curve" ]
147- cumulative_return = equity .iloc [- 1 ] - 1
148- sharpe = sharpe_ratio (returns , risk_free_rate )
149- mdd = max_drawdown (equity )
150- return {
151- "cumulative_return" : cumulative_return ,
152- "sharpe_ratio" : sharpe ,
153- "max_drawdown" : mdd ,
154- }
248+ return compute_performance (data , risk_free_rate )
0 commit comments