diff --git a/backend/app/api/real_trading.py b/backend/app/api/real_trading.py new file mode 100644 index 0000000..452d294 --- /dev/null +++ b/backend/app/api/real_trading.py @@ -0,0 +1,315 @@ +""" +实盘交易 API +""" +from fastapi import APIRouter, HTTPException, Query +from typing import Optional +from datetime import datetime +from pydantic import BaseModel + +from app.services.real_trading_service import get_real_trading_service +from app.utils.logger import logger + + +router = APIRouter(prefix="/api/real-trading", tags=["实盘交易"]) + + +class OrderResponse(BaseModel): + """订单响应""" + success: bool + message: str + data: Optional[dict] = None + + +@router.get("/orders") +async def get_orders( + symbol: Optional[str] = Query(None, description="交易对筛选"), + status: Optional[str] = Query(None, description="状态筛选: active, closed"), + limit: int = Query(100, description="返回数量限制") +): + """ + 获取实盘订单列表 + + - symbol: 可选,按交易对筛选 + - status: 可选,active=活跃订单, closed=已平仓订单 + - limit: 返回数量限制,默认100 + """ + try: + service = get_real_trading_service() + + if not service: + return { + "success": False, + "message": "实盘交易服务未启用", + "count": 0, + "orders": [] + } + + if status == "active": + orders = service.get_active_orders() + elif status == "closed": + # 从数据库获取历史订单 + from app.services.db_service import db_service + from app.models.real_trading import RealOrder + from app.models.paper_trading import OrderStatus + + db = db_service.get_session() + try: + query = db.query(RealOrder).filter( + RealOrder.status.in_([OrderStatus.CLOSED, OrderStatus.CANCELLED]) + ) + + if symbol: + query = query.filter(RealOrder.symbol == symbol) + + orders = [order.to_dict() for order in query.order_by( + RealOrder.created_at.desc() + ).limit(limit).all()] + finally: + db.close() + else: + # 返回所有订单 + active = service.get_active_orders() + # TODO: 获取历史订单 + orders = active + + return { + "success": True, + "count": len(orders), + "orders": orders + } + except Exception as e: + logger.error(f"获取实盘订单列表失败: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + +@router.get("/orders/active") +async def get_active_orders( + symbol: Optional[str] = Query(None, description="交易对筛选") +): + """获取活跃实盘订单""" + try: + service = get_real_trading_service() + + if not service: + return { + "success": False, + "message": "实盘交易服务未启用", + "count": 0, + "orders": [] + } + + orders = service.get_active_orders() + + # 如果指定了交易对,进行过滤 + if symbol: + orders = [o for o in orders if o.get('symbol') == symbol] + + return { + "success": True, + "count": len(orders), + "orders": orders + } + except Exception as e: + logger.error(f"获取活跃实盘订单失败: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + +@router.get("/orders/{order_id}") +async def get_order(order_id: str): + """获取实盘订单详情""" + try: + service = get_real_trading_service() + + if not service: + raise HTTPException(status_code=404, detail="实盘交易服务未启用") + + order = service.get_order(order_id) + + if not order: + raise HTTPException(status_code=404, detail="订单不存在") + + return { + "success": True, + "order": order + } + except HTTPException: + raise + except Exception as e: + logger.error(f"获取实盘订单详情失败: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + +@router.get("/positions") +async def get_positions(): + """获取实盘持仓(从交易所同步)""" + try: + service = get_real_trading_service() + + if not service: + return { + "success": False, + "message": "实盘交易服务未启用", + "positions": [] + } + + positions = service.sync_positions_from_exchange() + + return { + "success": True, + "count": len(positions), + "positions": positions + } + except Exception as e: + logger.error(f"获取实盘持仓失败: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + +@router.get("/account") +async def get_account_status(): + """获取实盘账户状态""" + try: + service = get_real_trading_service() + + if not service: + return { + "success": False, + "message": "实盘交易服务未启用", + "account": None + } + + account = service.get_account_status() + + return { + "success": True, + "account": account + } + except Exception as e: + logger.error(f"获取实盘账户状态失败: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + +@router.post("/orders/{order_id}/close") +async def close_order(order_id: str, exit_price: float = Query(..., description="平仓价格")): + """ + 手动平仓 + + 注意:实盘交易通常由交易所自动执行止损/止盈, + 此接口主要用于紧急情况下的手动平仓 + """ + try: + service = get_real_trading_service() + + if not service: + raise HTTPException(status_code=404, detail="实盘交易服务未启用") + + # 获取订单 + order = service.get_order(order_id) + if not order: + raise HTTPException(status_code=404, detail="订单不存在") + + # 调用交易所API平仓 + # TODO: 实现手动平仓逻辑 + + return { + "success": True, + "message": "平仓指令已发送", + "order_id": order_id + } + except HTTPException: + raise + except Exception as e: + logger.error(f"实盘手动平仓失败: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + +@router.get("/stats") +async def get_trading_stats(): + """获取实盘交易统计""" + try: + service = get_real_trading_service() + + if not service: + return { + "success": False, + "message": "实盘交易服务未启用", + "stats": None + } + + # 获取账户信息 + account = service.get_account_status() + + # 获取历史订单统计 + from app.services.db_service import db_service + from app.models.real_trading import RealOrder + from app.models.paper_trading import OrderStatus + + db = db_service.get_session() + try: + # 获取已平仓订单 + closed_orders = db.query(RealOrder).filter( + RealOrder.status == OrderStatus.CLOSED + ).all() + + # 计算统计数据 + total_trades = len(closed_orders) + winning_trades = len([o for o in closed_orders if o.pnl > 0]) + losing_trades = len([o for o in closed_orders if o.pnl < 0]) + total_pnl = sum([o.pnl or 0 for o in closed_orders]) + win_rate = (winning_trades / total_trades * 100) if total_trades > 0 else 0 + + # 计算最大回撤等指标 + # TODO: 实现更详细的统计 + + stats = { + "total_trades": total_trades, + "winning_trades": winning_trades, + "losing_trades": losing_trades, + "win_rate": round(win_rate, 2), + "total_pnl": round(total_pnl, 2), + "current_balance": account.get('current_balance', 0), + "available": account.get('available', 0), + "used_margin": account.get('used_margin', 0), + "total_position_value": account.get('total_position_value', 0), + } + finally: + db.close() + + return { + "success": True, + "stats": stats + } + except Exception as e: + logger.error(f"获取实盘交易统计失败: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + +@router.get("/status") +async def get_service_status(): + """获取实盘交易服务状态""" + try: + from app.config import get_settings + settings = get_settings() + + service = get_real_trading_service() + + status = { + "enabled": settings.real_trading_enabled, + "api_configured": bool(settings.bitget_api_key and settings.bitget_api_secret), + "use_testnet": settings.bitget_use_testnet, + "service_running": service is not None, + "max_single_position": settings.real_trading_max_single_position, + "default_leverage": settings.real_trading_default_leverage, + "max_orders": settings.real_trading_max_orders, + } + + if service: + account = service.get_account_status() + status["account"] = account + + return { + "success": True, + "status": status + } + except Exception as e: + logger.error(f"获取实盘交易服务状态失败: {e}") + raise HTTPException(status_code=500, detail=str(e)) diff --git a/backend/app/main.py b/backend/app/main.py index b193efd..d27e238 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -9,7 +9,7 @@ from fastapi.responses import FileResponse from contextlib import asynccontextmanager from app.config import get_settings from app.utils.logger import logger -from app.api import chat, stock, skills, llm, auth, admin, paper_trading, stocks, signals, system +from app.api import chat, stock, skills, llm, auth, admin, paper_trading, stocks, signals, system, real_trading from app.utils.error_handler import setup_global_exception_handler, init_error_notifier from app.utils.system_status import get_system_monitor import os @@ -456,6 +456,7 @@ app.include_router(stock.router, prefix="/api/stock", tags=["股票数据"]) app.include_router(skills.router, prefix="/api/skills", tags=["技能管理"]) app.include_router(llm.router, tags=["LLM模型"]) app.include_router(paper_trading.router, tags=["模拟交易"]) +app.include_router(real_trading.router, tags=["实盘交易"]) app.include_router(stocks.router, prefix="/api/stocks", tags=["美股分析"]) app.include_router(signals.router, tags=["信号管理"]) app.include_router(system.router, prefix="/api/system", tags=["系统状态"]) @@ -494,6 +495,14 @@ async def paper_trading_page(): return FileResponse(page_path) return {"message": "页面不存在"} +@app.get("/real-trading") +async def real_trading_page(): + """实盘交易页面""" + page_path = os.path.join(frontend_path, "real-trading.html") + if os.path.exists(page_path): + return FileResponse(page_path) + return {"message": "页面不存在"} + @app.get("/signals") async def signals_page(): """信号列表页面""" diff --git a/frontend/real-trading.html b/frontend/real-trading.html new file mode 100644 index 0000000..9a3054b --- /dev/null +++ b/frontend/real-trading.html @@ -0,0 +1,703 @@ + + + + + + 实盘交易 | Tradus Auto Trading + + + + +
+
+
+ + + +
+ + + +
+ + +
+
+ + + + + +
+
+
胜率
+
+ {{ stats.win_rate ? stats.win_rate.toFixed(1) : '0' }}% +
+
+
+
盈利交易
+
+ {{ stats.winning_trades || 0 }} +
+
+
+
亏损交易
+
+ {{ stats.losing_trades || 0 }} +
+
+
+
活跃订单
+
+ {{ activeOrders.length }} +
+
+
+ + +
+ + + +
+ + +
+
+
+

加载中...

+
+ +
+ + + +

暂无活跃订单

+
+ +
+ + + +

暂无历史订单

+
+ +
+ + + +

暂无持仓

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
交易对方向等级入场价当前价数量杠杆止损止盈盈亏状态时间
{{ order.symbol }} + + {{ order.side === 'long' ? '做多' : '做空' }} + + + + {{ order.signal_grade || 'D' }} + + ${{ order.entry_price ? order.entry_price.toLocaleString() : '-' }}${{ order.current_price ? order.current_price.toLocaleString() : '-' }}${{ order.quantity ? order.quantity.toLocaleString() : '-' }}{{ order.leverage || 1 }}x${{ order.stop_loss ? order.stop_loss.toLocaleString() : '-' }}${{ order.take_profit ? order.take_profit.toLocaleString() : '-' }} + + {{ order.pnl >= 0 ? '+' : '' }}${{ order.pnl.toFixed(2) }} + + - + + + {{ formatStatus(order.status) }} + + {{ formatTime(order.created_at) }}
+
+
+
+
+ + + + + +