From 7e4317f426ca8c4e844c053af3c8a1924b47bad2 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 03:44:55 +0900 Subject: [PATCH] Wire AI bot settings, decisions and guarded live toggle --- app/main.py | 136 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 136 insertions(+) diff --git a/app/main.py b/app/main.py index c5f3c98..8f2f5db 100644 --- a/app/main.py +++ b/app/main.py @@ -4,15 +4,18 @@ from fastapi.responses import FileResponse from fastapi.staticfiles import StaticFiles from sqlalchemy.orm import Session +from app.ai_bot import AiTradingBot, decision_to_dict, signal_to_dict, trade_marker_from_record from app.coinex import CoinExClient, CoinExLiveStream from app.core import get_settings from app.db import SessionLocal, get_db, init_db from app.demo_account import DemoAccountService +from app.models import DemoTradeRecord settings = get_settings() coinex = CoinExClient(settings.coinex_api_base) live_stream = CoinExLiveStream(settings.coinex_ws_spot, coinex) demo_service = DemoAccountService(settings.demo_initial_balance, settings.demo_quote_asset) +ai_bot = AiTradingBot(coinex, settings.markets) app = FastAPI(title=settings.app_name) static_dir = Path(__file__).parent / 'static' @@ -37,6 +40,7 @@ async def health() -> dict: 'env': settings.app_env, 'trade_mode': settings.trade_mode, 'live_enabled': settings.live_enabled, + 'coinex_credentials_present': settings.coinex_credentials_present, 'default_market': settings.default_market, 'coinex_ws_spot': settings.coinex_ws_spot, } @@ -78,6 +82,136 @@ async def market_ticker(market: str | None = None) -> dict: return await coinex.get_ticker(market or settings.default_market) +@app.get('/api/v1/bot/settings') +async def bot_settings(db: Session = Depends(get_db)) -> dict: + state = ai_bot.get_or_create_state(db) + data = ai_bot.state_to_dict(state) + data['env_live_enabled'] = settings.live_enabled + data['coinex_credentials_present'] = settings.coinex_credentials_present + data['markets'] = settings.markets + data['live_ack_text'] = settings.live_requires_ack_text + return data + + +@app.post('/api/v1/bot/settings') +async def update_bot_settings( + enabled: bool | None = None, + trade_mode: str | None = None, + trade_style_mode: str | None = None, + min_signal_score: float | None = None, + max_open_positions: int | None = None, + max_quote_per_trade: float | None = None, + emergency_stop: bool | None = None, + db: Session = Depends(get_db), +) -> dict: + if trade_mode is not None and trade_mode not in {'demo', 'live'}: + raise HTTPException(status_code=400, detail='trade_mode must be demo or live') + state = ai_bot.update_state( + db, + enabled=enabled, + trade_mode=trade_mode, + trade_style_mode=trade_style_mode, + min_signal_score=min_signal_score, + max_open_positions=max_open_positions, + max_quote_per_trade=max_quote_per_trade, + emergency_stop=emergency_stop, + ) + return ai_bot.state_to_dict(state) + + +@app.post('/api/v1/bot/live/enable') +async def enable_live_mode(ack: str, db: Session = Depends(get_db)) -> dict: + if ack != settings.live_requires_ack_text: + raise HTTPException(status_code=400, detail='wrong acknowledgement text') + if not settings.enable_live_trading or settings.trade_mode != 'live': + raise HTTPException(status_code=400, detail='set TRADE_MODE=live and ENABLE_LIVE_TRADING=true in .env first') + if not settings.coinex_credentials_present: + raise HTTPException(status_code=400, detail='CoinEx credentials are missing in environment') + state = ai_bot.update_state(db, trade_mode='live', live_acknowledged=True, emergency_stop=False) + return {'live_ready': True, 'settings': ai_bot.state_to_dict(state)} + + +@app.post('/api/v1/bot/live/disable') +async def disable_live_mode(db: Session = Depends(get_db)) -> dict: + state = ai_bot.update_state(db, trade_mode='demo', live_acknowledged=False, emergency_stop=True) + return {'live_ready': False, 'settings': ai_bot.state_to_dict(state)} + + +@app.get('/api/v1/bot/analyze') +async def analyze_market(market: str | None = None, db: Session = Depends(get_db)) -> dict: + try: + pack = await ai_bot.analyze_market(db, market or settings.default_market) + return pack.as_dict() + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + + +@app.post('/api/v1/bot/decide') +async def bot_decide(market: str | None = None, db: Session = Depends(get_db)) -> dict: + try: + decision = await ai_bot.make_decision(db, market) + return decision_to_dict(decision) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + + +@app.post('/api/v1/bot/auto-demo-trade') +async def auto_demo_trade(market: str | None = None, db: Session = Depends(get_db)) -> dict: + try: + decision = await ai_bot.make_decision(db, market) + state = ai_bot.get_or_create_state(db) + risk = ai_bot.risk_check(db, decision, state.max_quote_per_trade) + if not risk['allowed']: + return {'executed': False, 'decision': decision_to_dict(decision), 'risk': risk} + if decision.action not in {'buy', 'sell'}: + return {'executed': False, 'decision': decision_to_dict(decision), 'risk': risk} + ticker = await coinex.get_ticker(decision.market) + price = ai_bot.coinex_live_price_from_ticker(ticker) if hasattr(ai_bot, 'coinex_live_price_from_ticker') else None + if price is None: + price = decision_to_dict(decision)['indicators']['last_price'] + amount = state.max_quote_per_trade / price + trade = demo_service.add_demo_trade(db, decision.market, decision.action, price, amount, decision.reason) + decision.executed = True + db.add(decision) + db.commit() + return {'executed': True, 'decision': decision_to_dict(decision), 'trade': trade.model_dump(mode='json'), 'risk': risk} + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + + +@app.post('/api/v1/signals/manual') +async def create_manual_signal( + title: str, + market: str, + sentiment: str = 'neutral', + score: float = 50.0, + source: str = 'manual', + url: str = '', + db: Session = Depends(get_db), +) -> dict: + signal = ai_bot.record_manual_signal(db, title=title, market=market, sentiment=sentiment, score=score, source=source, url=url) + return signal_to_dict(signal) + + +@app.get('/api/v1/signals/recent') +async def recent_signals(limit: int = 50, db: Session = Depends(get_db)) -> dict: + return {'items': [signal_to_dict(item) for item in ai_bot.recent_signals(db, limit)]} + + +@app.get('/api/v1/decisions/recent') +async def recent_decisions(limit: int = 50, db: Session = Depends(get_db)) -> dict: + return {'items': [decision_to_dict(item) for item in ai_bot.recent_decisions(db, limit)]} + + +@app.get('/api/v1/dashboard/markers') +async def dashboard_markers(market: str | None = None, limit: int = 100, db: Session = Depends(get_db)) -> dict: + query = db.query(DemoTradeRecord) + if market: + query = query.filter(DemoTradeRecord.market == market.upper()) + trades = query.order_by(DemoTradeRecord.id.desc()).limit(limit).all() + return {'items': [trade_marker_from_record(item) for item in reversed(trades)]} + + @app.websocket('/ws/market/{market}') async def market_stream(websocket: WebSocket, market: str) -> None: await websocket.accept() @@ -85,7 +219,9 @@ async def market_stream(websocket: WebSocket, market: str) -> None: async for live_event in live_stream.trades(market): with SessionLocal() as db: account = demo_service.snapshot(db).model_dump(mode='json') + state = ai_bot.state_to_dict(ai_bot.get_or_create_state(db)) live_event['demo_account'] = account + live_event['bot_state'] = state await websocket.send_json(live_event) except WebSocketDisconnect: return