Files
trade/app/main.py
T

228 строки
8.9 KiB
Python

from pathlib import Path
from fastapi import Depends, FastAPI, HTTPException, WebSocket, WebSocketDisconnect
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'
app.mount('/static', StaticFiles(directory=static_dir), name='static')
@app.on_event('startup')
def on_startup() -> None:
init_db()
@app.get('/')
async def dashboard() -> FileResponse:
return FileResponse(static_dir / 'index.html')
@app.get('/health')
async def health() -> dict:
return {
'status': 'ok',
'app': settings.app_name,
'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,
}
@app.get('/api/v1/demo/account')
async def demo_account(db: Session = Depends(get_db)) -> dict:
return demo_service.snapshot(db).model_dump(mode='json')
@app.post('/api/v1/demo/reset')
async def reset_demo_account(db: Session = Depends(get_db)) -> dict:
return demo_service.reset(db).model_dump(mode='json')
@app.post('/api/v1/demo/trades')
async def create_demo_trade(
market: str,
side: str,
price: float,
amount: float,
reason: str = 'manual demo trade',
db: Session = Depends(get_db),
) -> dict:
try:
trade = demo_service.add_demo_trade(db=db, market=market, side=side, price=price, amount=amount, reason=reason)
return trade.model_dump(mode='json')
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
@app.get('/api/v1/market/kline')
async def market_kline(market: str | None = None, period: str = '1min', limit: int = 100) -> dict:
return await coinex.get_kline(market or settings.default_market, period, limit)
@app.get('/api/v1/market/ticker')
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()
try:
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