Add legacy trading bot service layer
Этот коммит содержится в:
@@ -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:]}'
|
||||
Ссылка в новой задаче
Block a user