From eefdad353c84567faa5d859e20c51a1e352536b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D0=BA=D1=82=D0=BE=D1=80?= <78488229+viktor138irk@users.noreply.github.com> Date: Fri, 8 May 2026 04:06:21 +0900 Subject: [PATCH] Add legacy trading bot service layer --- app/legacy_bot.py | 228 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 228 insertions(+) create mode 100644 app/legacy_bot.py diff --git a/app/legacy_bot.py b/app/legacy_bot.py new file mode 100644 index 0000000..cde14bc --- /dev/null +++ b/app/legacy_bot.py @@ -0,0 +1,228 @@ +from datetime import datetime, timezone +from typing import Any + +from sqlalchemy.orm import Session + +from app.models import ( + AiDecision, + ChartHistoryPoint, + CoinExSettings, + DemoAccountState, + DemoPosition, + DemoTradeRecord, + SystemStatus, + TelegramSettings, + WorkerRunLog, + WorkerStrategy, +) + + +DEFAULT_STRATEGIES = [ + ('AI News Scalper', '1min', 'BTCUSDT,ETHUSDT,SOLUSDT'), + ('Trend Follow', '5min', 'BTCUSDT,ETHUSDT'), + ('Volatility Catcher', '1min', 'SOLUSDT,XRPUSDT,DOGEUSDT'), +] + + +class LegacyBotService: + def ensure_defaults(self, db: Session) -> None: + if db.get(CoinExSettings, 1) is None: + db.add(CoinExSettings(id=1)) + if db.get(TelegramSettings, 1) is None: + db.add(TelegramSettings(id=1)) + for name, timeframe, markets in DEFAULT_STRATEGIES: + exists = db.query(WorkerStrategy).filter(WorkerStrategy.name == name).first() + if exists is None: + db.add(WorkerStrategy(name=name, timeframe=timeframe, markets=markets, enabled=False, dry_run=True)) + db.commit() + + def dashboard(self, db: Session) -> dict[str, Any]: + self.ensure_defaults(db) + account = db.get(DemoAccountState, 1) + balance = account.balance if account else 0.0 + realized = account.realized_pnl if account else 0.0 + trades_total = db.query(DemoTradeRecord).count() + open_positions = db.query(DemoPosition).filter(DemoPosition.is_open.is_(True)).count() + closed_positions = db.query(DemoPosition).filter(DemoPosition.is_open.is_(False)).count() + latest_decision = db.query(AiDecision).order_by(AiDecision.id.desc()).first() + strategies = db.query(WorkerStrategy).order_by(WorkerStrategy.id.asc()).all() + coinex = db.get(CoinExSettings, 1) + telegram = db.get(TelegramSettings, 1) + return { + 'kpi': { + 'balance': balance, + 'realized_pnl': realized, + 'trades_total': trades_total, + 'open_positions': open_positions, + 'closed_positions': closed_positions, + 'active_strategies': sum(1 for s in strategies if s.enabled), + }, + 'latest_decision': self._decision(latest_decision), + 'strategies': [self._strategy(s) for s in strategies], + 'coinex': self._coinex(coinex), + 'telegram': self._telegram(telegram), + } + + def open_positions(self, db: Session) -> list[dict[str, Any]]: + positions = db.query(DemoPosition).filter(DemoPosition.is_open.is_(True)).order_by(DemoPosition.id.desc()).all() + return [self._position(item) for item in positions] + + def closed_positions(self, db: Session, limit: int = 50) -> list[dict[str, Any]]: + positions = db.query(DemoPosition).filter(DemoPosition.is_open.is_(False)).order_by(DemoPosition.id.desc()).limit(limit).all() + return [self._position(item) for item in positions] + + def chart_history(self, db: Session, limit: int = 100) -> list[dict[str, Any]]: + rows = db.query(ChartHistoryPoint).order_by(ChartHistoryPoint.id.desc()).limit(limit).all() + return [ + { + 'time': int(row.created_at.timestamp()), + 'balance': row.balance, + 'pnl_total': row.pnl_total, + 'open_trades': row.open_trades, + 'closed_trades': row.closed_trades, + } + for row in reversed(rows) + ] + + def snapshot_history(self, db: Session) -> dict[str, Any]: + account = db.get(DemoAccountState, 1) + point = ChartHistoryPoint( + balance=account.balance if account else 0.0, + pnl_total=account.realized_pnl if account else 0.0, + open_trades=db.query(DemoPosition).filter(DemoPosition.is_open.is_(True)).count(), + closed_trades=db.query(DemoPosition).filter(DemoPosition.is_open.is_(False)).count(), + ) + db.add(point) + db.commit() + db.refresh(point) + return {'id': point.id, 'balance': point.balance, 'pnl_total': point.pnl_total} + + def update_strategy(self, db: Session, strategy_id: int, enabled: bool | None = None, dry_run: bool | None = None) -> dict[str, Any]: + self.ensure_defaults(db) + strategy = db.get(WorkerStrategy, strategy_id) + if strategy is None: + raise ValueError('strategy not found') + if enabled is not None: + strategy.enabled = enabled + if dry_run is not None: + strategy.dry_run = dry_run + strategy.last_signal = 'waiting' if strategy.enabled else 'stopped' + strategy.last_message = 'Strategy settings updated from dashboard' + db.add(strategy) + db.add(WorkerRunLog(strategy_name=strategy.name, status='ok', message=strategy.last_message)) + db.commit() + db.refresh(strategy) + return self._strategy(strategy) + + def update_coinex(self, db: Session, enabled: bool | None = None, access_id: str | None = None, secret_key: str | None = None, mode: str | None = None) -> dict[str, Any]: + self.ensure_defaults(db) + settings = db.get(CoinExSettings, 1) + if enabled is not None: + settings.enabled = enabled + if access_id is not None: + settings.access_id = access_id + if secret_key is not None and secret_key: + settings.secret_masked = self._mask(secret_key) + if mode is not None: + settings.mode = mode + db.add(settings) + db.commit() + db.refresh(settings) + return self._coinex(settings) + + def update_telegram(self, db: Session, enabled: bool | None = None, chat_id: str | None = None, message_format: str | None = None) -> dict[str, Any]: + self.ensure_defaults(db) + settings = db.get(TelegramSettings, 1) + if enabled is not None: + settings.enabled = enabled + if chat_id is not None: + settings.chat_id = chat_id + if message_format is not None: + settings.message_format = message_format + db.add(settings) + db.commit() + db.refresh(settings) + return self._telegram(settings) + + def updater_status(self, db: Session) -> dict[str, Any]: + db.add(SystemStatus(component='updater', status='ok', message='Updater ready. Manual git pull is available on server.')) + db.commit() + return {'status': 'ready', 'message': 'Обновление готово: git pull + docker compose up -d --build'} + + def terminal_command(self, command: str) -> dict[str, Any]: + allowed = {'status', 'health', 'version'} + if command not in allowed: + return {'status': 'blocked', 'output': 'Разрешены команды: status, health, version'} + return {'status': 'ok', 'output': f'Команда {command} выполнена в безопасном режиме панели'} + + def android_status(self, db: Session) -> dict[str, Any]: + dash = self.dashboard(db) + return { + 'ok': True, + 'mode': dash['coinex']['mode'], + 'balance': dash['kpi']['balance'], + 'pnl': dash['kpi']['realized_pnl'], + 'open_positions': dash['kpi']['open_positions'], + 'active_strategies': dash['kpi']['active_strategies'], + } + + def _position(self, position: DemoPosition) -> dict[str, Any]: + return { + 'id': position.id, + 'market': position.market, + 'side': position.side, + 'amount': position.amount, + 'avg_entry_price': position.avg_entry_price, + 'current_price': position.current_price, + 'take_profit': position.take_profit, + 'stop_loss': position.stop_loss, + 'unrealized_pnl': position.unrealized_pnl, + 'unrealized_pnl_pct': position.unrealized_pnl_pct, + 'realized_pnl': position.realized_pnl, + 'is_open': position.is_open, + } + + def _strategy(self, strategy: WorkerStrategy) -> dict[str, Any]: + return { + 'id': strategy.id, + 'name': strategy.name, + 'enabled': strategy.enabled, + 'dry_run': strategy.dry_run, + 'timeframe': strategy.timeframe, + 'markets': strategy.markets, + 'last_signal': strategy.last_signal, + 'last_message': strategy.last_message, + } + + def _decision(self, decision: AiDecision | None) -> dict[str, Any] | None: + if decision is None: + return None + return { + 'market': decision.market, + 'action': decision.action, + 'score': decision.score, + 'confidence': decision.confidence, + 'reason': decision.reason, + } + + def _coinex(self, settings: CoinExSettings | None) -> dict[str, Any]: + if settings is None: + return {'enabled': False, 'mode': 'demo', 'access_id': '', 'secret_masked': ''} + return { + 'enabled': settings.enabled, + 'mode': settings.mode, + 'access_id': settings.access_id, + 'secret_masked': settings.secret_masked, + 'base_url': settings.base_url, + 'account_type': settings.account_type, + } + + def _telegram(self, settings: TelegramSettings | None) -> dict[str, Any]: + if settings is None: + return {'enabled': False, 'chat_id': ''} + return {'enabled': settings.enabled, 'chat_id': settings.chat_id, 'message_format': settings.message_format} + + def _mask(self, value: str) -> str: + if len(value) <= 8: + return '***' + return f'{value[:4]}***{value[-4:]}'