fix: backtest force-closes open positions at end + uses real market prices for exits
This commit is contained in:
@@ -212,7 +212,7 @@ class BacktestReplay:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Simulate simple exit logic: close positions held > 5 days
|
# Simulate simple exit logic: close positions held > 5 days
|
||||||
# (simplified — real engine uses stop-loss/take-profit)
|
# or use actual market price if available
|
||||||
tickers_to_close = []
|
tickers_to_close = []
|
||||||
for ticker, pos_info in simulated_positions.items():
|
for ticker, pos_info in simulated_positions.items():
|
||||||
hold_days = (current_date - pos_info["entry_date"]).days
|
hold_days = (current_date - pos_info["entry_date"]).days
|
||||||
@@ -221,8 +221,8 @@ class BacktestReplay:
|
|||||||
|
|
||||||
for ticker in tickers_to_close:
|
for ticker in tickers_to_close:
|
||||||
pos_info = simulated_positions.pop(ticker)
|
pos_info = simulated_positions.pop(ticker)
|
||||||
# Simulate a small random-ish exit based on entry price
|
# Use actual market price if available, otherwise estimate
|
||||||
exit_price = pos_info["entry_price"] * 1.01 # simplified
|
exit_price = company_prices.get(ticker, pos_info["entry_price"] * 1.01)
|
||||||
qty = pos_info["quantity"]
|
qty = pos_info["quantity"]
|
||||||
pnl = (exit_price - pos_info["entry_price"]) * qty
|
pnl = (exit_price - pos_info["entry_price"]) * qty
|
||||||
pnl_pct = (
|
pnl_pct = (
|
||||||
@@ -253,10 +253,10 @@ class BacktestReplay:
|
|||||||
0, portfolio_state.open_position_count - 1
|
0, portfolio_state.open_position_count - 1
|
||||||
)
|
)
|
||||||
|
|
||||||
# Compute daily portfolio value
|
# Compute daily portfolio value using latest market prices
|
||||||
positions_value = sum(
|
positions_value = sum(
|
||||||
p["entry_price"] * p["quantity"]
|
company_prices.get(t, p["entry_price"]) * p["quantity"]
|
||||||
for p in simulated_positions.values()
|
for t, p in simulated_positions.items()
|
||||||
)
|
)
|
||||||
current_value = portfolio_state.active_pool + positions_value
|
current_value = portfolio_state.active_pool + positions_value
|
||||||
portfolio_state.total_value = current_value
|
portfolio_state.total_value = current_value
|
||||||
@@ -277,6 +277,42 @@ class BacktestReplay:
|
|||||||
|
|
||||||
current_date += timedelta(days=1)
|
current_date += timedelta(days=1)
|
||||||
|
|
||||||
|
# Force-close any remaining open positions at end of backtest
|
||||||
|
# using the latest available market prices
|
||||||
|
for ticker in list(simulated_positions.keys()):
|
||||||
|
pos_info = simulated_positions.pop(ticker)
|
||||||
|
exit_price = company_prices.get(ticker, pos_info["entry_price"])
|
||||||
|
qty = pos_info["quantity"]
|
||||||
|
pnl = (exit_price - pos_info["entry_price"]) * qty
|
||||||
|
pnl_pct = (
|
||||||
|
(exit_price - pos_info["entry_price"]) / pos_info["entry_price"]
|
||||||
|
if pos_info["entry_price"] > 0
|
||||||
|
else 0.0
|
||||||
|
)
|
||||||
|
hold_duration = timedelta(
|
||||||
|
days=(config.end_date - pos_info["entry_date"]).days
|
||||||
|
)
|
||||||
|
|
||||||
|
trade = ClosedTrade(
|
||||||
|
ticker=ticker,
|
||||||
|
entry_price=pos_info["entry_price"],
|
||||||
|
exit_price=exit_price,
|
||||||
|
quantity=qty,
|
||||||
|
pnl=pnl,
|
||||||
|
pnl_pct=pnl_pct,
|
||||||
|
hold_duration=hold_duration,
|
||||||
|
recommendation_id=pos_info.get("recommendation_id"),
|
||||||
|
)
|
||||||
|
closed_trades.append(trade)
|
||||||
|
trade_log.append(self._perf.compute_trade_metrics(trade))
|
||||||
|
portfolio_state.active_pool += exit_price * qty
|
||||||
|
|
||||||
|
if closed_trades:
|
||||||
|
logger.info(
|
||||||
|
"Backtest %s completed: %d trades, final value=$%.2f",
|
||||||
|
backtest_id, len(closed_trades), portfolio_state.active_pool,
|
||||||
|
)
|
||||||
|
|
||||||
# Compute final metrics
|
# Compute final metrics
|
||||||
metrics = self._perf.compute_metrics(
|
metrics = self._perf.compute_metrics(
|
||||||
closed_trades=closed_trades,
|
closed_trades=closed_trades,
|
||||||
|
|||||||
Reference in New Issue
Block a user