fix: proper DCA and max_drawdown calculations in backtest engine
Three bugs fixed:
1. **Weighted average entry price for risk management**:
- Previously, entry_price was overwritten on each buy, causing stop loss
to be calculated from the latest buy price instead of average
- Added cost_basis tracking and average_entry_price property
- Stop loss now correctly uses weighted average across all buys
2. **Portfolio value accumulation in _calculate_metrics**:
- Bug: running_position = trade['quantity'] was OVERWRITING position
- Fix: running_position += trade['quantity'] to properly accumulate DCA
3. **Risk management exit reset**:
- Added cost_basis reset when position is closed
Max drawdown is now correctly bounded by stop loss percentage (~5%)
instead of showing inflated values like 59%.
This commit is contained in:
@@ -28,6 +28,7 @@ class BacktestEngine:
|
||||
self.position = 0.0
|
||||
self.position_token = ""
|
||||
self.entry_price: Optional[float] = None
|
||||
self.cost_basis = 0.0 # Track total amount spent on current position for average price calc
|
||||
self.entry_time: Optional[int] = None
|
||||
self.trades: List[Dict[str, Any]] = []
|
||||
self.running = False
|
||||
@@ -163,19 +164,26 @@ class BacktestEngine:
|
||||
await self._execute_actions(price, timestamp, condition)
|
||||
break
|
||||
|
||||
@property
|
||||
def average_entry_price(self) -> Optional[float]:
|
||||
"""Calculate weighted average entry price based on cost basis."""
|
||||
if self.position <= 0 or self.cost_basis <= 0:
|
||||
return None
|
||||
return self.cost_basis / self.position
|
||||
|
||||
def _check_risk_management(
|
||||
self, current_price: float, timestamp: int
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
if self.position <= 0 or self.entry_price is None:
|
||||
if self.position <= 0 or self.average_entry_price is None:
|
||||
return None
|
||||
|
||||
if self.stop_loss_percent is not None:
|
||||
stop_loss_price = self.entry_price * (1 - self.stop_loss_percent / 100)
|
||||
stop_loss_price = self.average_entry_price * (1 - self.stop_loss_percent / 100)
|
||||
if current_price <= stop_loss_price:
|
||||
return {"reason": "stop_loss", "price": stop_loss_price}
|
||||
|
||||
if self.take_profit_percent is not None:
|
||||
take_profit_price = self.entry_price * (1 + self.take_profit_percent / 100)
|
||||
take_profit_price = self.average_entry_price * (1 + self.take_profit_percent / 100)
|
||||
# Use small epsilon to handle floating point precision
|
||||
if current_price >= take_profit_price - 0.001:
|
||||
return {"reason": "take_profit", "price": take_profit_price}
|
||||
@@ -218,6 +226,7 @@ class BacktestEngine:
|
||||
)
|
||||
self.position = 0
|
||||
self.entry_price = None
|
||||
self.cost_basis = 0.0
|
||||
self.entry_time = None
|
||||
|
||||
def _check_condition(
|
||||
@@ -282,10 +291,12 @@ class BacktestEngine:
|
||||
amount = self.current_balance * (amount_percent / 100)
|
||||
|
||||
if action_type == "buy" and self.current_balance >= amount:
|
||||
self.position += amount / price
|
||||
quantity = amount / price
|
||||
self.position += quantity
|
||||
self.current_balance -= amount
|
||||
self.cost_basis += amount # Track total cost for average price
|
||||
self.position_token = token
|
||||
self.entry_price = price
|
||||
self.entry_price = price # Keep last entry price for reference
|
||||
self.entry_time = timestamp
|
||||
self.trades.append(
|
||||
{
|
||||
@@ -293,7 +304,7 @@ class BacktestEngine:
|
||||
"token": token,
|
||||
"price": price,
|
||||
"amount": amount,
|
||||
"quantity": amount / price,
|
||||
"quantity": quantity,
|
||||
"timestamp": timestamp,
|
||||
}
|
||||
)
|
||||
@@ -382,13 +393,13 @@ class BacktestEngine:
|
||||
|
||||
for trade in self.trades:
|
||||
if trade["type"] == "buy":
|
||||
running_position = trade["quantity"]
|
||||
running_position += trade["quantity"] # Add to existing position (DCA)
|
||||
running_balance -= trade["amount"] # Subtract amount spent
|
||||
current_token = trade["token"]
|
||||
last_price = trade["price"]
|
||||
else: # sell
|
||||
running_balance += trade["amount"] # Add amount received
|
||||
running_position = 0
|
||||
running_position = 0 # Close entire position
|
||||
last_price = trade["price"]
|
||||
|
||||
portfolio_value = running_balance + (running_position * last_price)
|
||||
|
||||
Reference in New Issue
Block a user