diff --git a/src/backend/app/services/backtest/engine.py b/src/backend/app/services/backtest/engine.py index e9cd682..e5b716b 100644 --- a/src/backend/app/services/backtest/engine.py +++ b/src/backend/app/services/backtest/engine.py @@ -20,10 +20,15 @@ class BacktestEngine: self.strategy_config = config.get("strategy_config", {}) self.conditions = self.strategy_config.get("conditions", []) self.actions = self.strategy_config.get("actions", []) + self.risk_management = self.strategy_config.get("risk_management", {}) + self.stop_loss_percent = self.risk_management.get("stop_loss_percent") + self.take_profit_percent = self.risk_management.get("take_profit_percent") self.initial_balance = config.get("initial_balance", 10000.0) self.current_balance = self.initial_balance self.position = 0.0 self.position_token = "" + self.entry_price: Optional[float] = None + self.entry_time: Optional[int] = None self.trades: List[Dict[str, Any]] = [] self.running = False @@ -103,11 +108,73 @@ class BacktestEngine: timestamp = kline.get("timestamp", 0) + if self.position > 0 and self.entry_price is not None: + exit_info = self._check_risk_management(price, timestamp) + if exit_info: + await self._execute_risk_exit(price, timestamp, exit_info) + continue + for condition in self.conditions: if self._check_condition(condition, klines, i, price): await self._execute_actions(price, timestamp, condition) break + def _check_risk_management( + self, current_price: float, timestamp: int + ) -> Optional[Dict[str, Any]]: + if self.position <= 0 or self.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) + 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) + if current_price >= take_profit_price: + return {"reason": "take_profit", "price": take_profit_price} + + return None + + async def _execute_risk_exit( + self, price: float, timestamp: int, exit_info: Dict[str, Any] + ): + if self.position <= 0: + return + + reason = exit_info["reason"] + sell_amount = self.position * price + self.current_balance += sell_amount + self.trades.append( + { + "type": "sell", + "token": self.position_token, + "price": price, + "amount": sell_amount, + "quantity": self.position, + "timestamp": timestamp, + "exit_reason": reason, + } + ) + self.signals.append( + { + "id": str(uuid.uuid4()), + "bot_id": self.bot_id, + "run_id": self.run_id, + "signal_type": "sell", + "token": self.position_token, + "price": price, + "confidence": 1.0, + "reasoning": f"Risk management triggered {reason}", + "executed": False, + "created_at": datetime.utcnow(), + } + ) + self.position = 0 + self.entry_price = None + self.entry_time = None + def _check_condition( self, condition: Dict[str, Any], @@ -173,6 +240,8 @@ class BacktestEngine: self.position += amount / price self.current_balance -= amount self.position_token = token + self.entry_price = price + self.entry_time = timestamp self.trades.append( { "type": "buy", @@ -209,9 +278,12 @@ class BacktestEngine: "amount": sell_amount, "quantity": self.position, "timestamp": timestamp, + "exit_reason": "manual", } ) self.position = 0 + self.entry_price = None + self.entry_time = None self.signals.append( { "id": str(uuid.uuid4()), diff --git a/src/backend/app/services/simulate/engine.py b/src/backend/app/services/simulate/engine.py index 9cd32e7..721fb0b 100644 --- a/src/backend/app/services/simulate/engine.py +++ b/src/backend/app/services/simulate/engine.py @@ -20,6 +20,9 @@ class SimulateEngine: self.strategy_config = config.get("strategy_config", {}) self.conditions = self.strategy_config.get("conditions", []) self.actions = self.strategy_config.get("actions", []) + self.risk_management = self.strategy_config.get("risk_management", {}) + self.stop_loss_percent = self.risk_management.get("stop_loss_percent") + self.take_profit_percent = self.risk_management.get("take_profit_percent") self.check_interval = config.get("check_interval", 60) self.duration_seconds = config.get("duration_seconds", 3600) self.auto_execute = config.get("auto_execute", False) @@ -29,6 +32,12 @@ class SimulateEngine: self.started_at: Optional[datetime] = None self.last_price: Optional[float] = None self.last_volume: Optional[float] = None + self.position: float = 0.0 + self.position_token: str = "" + self.entry_price: Optional[float] = None + self.entry_time: Optional[int] = None + self.current_balance: float = config.get("initial_balance", 10000.0) + self.trades: List[Dict[str, Any]] = [] async def run(self) -> Dict[str, Any]: self.running = True @@ -94,11 +103,70 @@ class SimulateEngine: ): timestamp = int(datetime.utcnow().timestamp() * 1000) + if self.position > 0 and self.entry_price is not None: + exit_info = self._check_risk_management(current_price, timestamp) + if exit_info: + await self._execute_risk_exit(current_price, timestamp, exit_info) + return + for condition in self.conditions: if self._check_condition(condition, current_price, current_volume): await self._execute_actions(current_price, timestamp, condition) break + def _check_risk_management( + self, current_price: float, timestamp: int + ) -> Optional[Dict[str, Any]]: + if self.position <= 0 or self.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) + 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) + if current_price >= take_profit_price: + return {"reason": "take_profit", "price": take_profit_price} + + return None + + async def _execute_risk_exit( + self, price: float, timestamp: int, exit_info: Dict[str, Any] + ): + if self.position <= 0: + return + + reason = exit_info["reason"] + self.trades.append( + { + "type": "sell", + "token": self.position_token, + "price": price, + "quantity": self.position, + "timestamp": timestamp, + "exit_reason": reason, + } + ) + self.signals.append( + { + "id": str(uuid.uuid4()), + "bot_id": self.bot_id, + "run_id": self.run_id, + "signal_type": "sell", + "token": self.position_token, + "price": price, + "confidence": 1.0, + "reasoning": f"Risk management triggered {reason}", + "executed": self.auto_execute, + "created_at": datetime.utcnow(), + } + ) + self.position = 0 + self.entry_price = None + self.entry_time = None + def _check_condition( self, condition: Dict[str, Any], @@ -146,20 +214,41 @@ class SimulateEngine: token = matched_condition.get("token", self.token) reasoning = f"Condition {matched_condition.get('type')} triggered" - signal = { - "id": str(uuid.uuid4()), - "bot_id": self.bot_id, - "run_id": self.run_id, - "signal_type": "signal", - "token": token, - "price": price, - "confidence": 0.8, - "reasoning": reasoning, - "executed": self.auto_execute, - "created_at": datetime.utcnow(), - } + for action in self.actions: + action_type = action.get("type", "") + if action_type == "buy": + amount_percent = action.get("amount_percent", 10) + amount = self.current_balance * (amount_percent / 100) + self.position += amount / price + self.position_token = token + self.entry_price = price + self.entry_time = timestamp + self.current_balance -= amount + self.trades.append( + { + "type": "buy", + "token": token, + "price": price, + "amount": amount, + "quantity": amount / price, + "timestamp": timestamp, + } + ) - self.signals.append(signal) + signal = { + "id": str(uuid.uuid4()), + "bot_id": self.bot_id, + "run_id": self.run_id, + "signal_type": action_type, + "token": token, + "price": price, + "confidence": 0.8, + "reasoning": reasoning, + "executed": self.auto_execute, + "created_at": datetime.utcnow(), + } + + self.signals.append(signal) async def stop(self): self.running = False