diff --git a/backend/api/routes/account.py b/backend/api/routes/account.py index a065500..2c4cfbe 100644 --- a/backend/api/routes/account.py +++ b/backend/api/routes/account.py @@ -305,3 +305,72 @@ async def get_realtime_positions(): error_msg = f"获取持仓数据失败: {str(e)}" logger.error(error_msg, exc_info=True) raise HTTPException(status_code=500, detail=error_msg) + + +@router.post("/positions/{symbol}/close") +async def close_position(symbol: str): + """手动平仓指定交易对的持仓""" + try: + logger.info(f"收到平仓请求: {symbol}") + + # 从数据库读取API密钥 + api_key = TradingConfig.get_value('BINANCE_API_KEY') + api_secret = TradingConfig.get_value('BINANCE_API_SECRET') + use_testnet = TradingConfig.get_value('USE_TESTNET', False) + + if not api_key or not api_secret: + error_msg = "API密钥未配置" + logger.warning(error_msg) + raise HTTPException(status_code=400, detail=error_msg) + + # 导入必要的模块 + try: + from binance_client import BinanceClient + from risk_manager import RiskManager + from position_manager import PositionManager + except ImportError: + trading_system_path = project_root / 'trading_system' + sys.path.insert(0, str(trading_system_path)) + from binance_client import BinanceClient + from risk_manager import RiskManager + from position_manager import PositionManager + + # 创建客户端和仓位管理器 + client = BinanceClient( + api_key=api_key, + api_secret=api_secret, + testnet=use_testnet + ) + + await client.connect() + + try: + risk_manager = RiskManager(client) + position_manager = PositionManager(client, risk_manager) + + # 执行平仓(reason='manual' 表示手动平仓) + success = await position_manager.close_position(symbol, reason='manual') + + if success: + logger.info(f"✓ {symbol} 平仓成功") + return { + "message": f"{symbol} 平仓成功", + "symbol": symbol, + "status": "closed" + } + else: + logger.warning(f"⚠ {symbol} 平仓失败或持仓不存在") + return { + "message": f"{symbol} 平仓失败或持仓不存在", + "symbol": symbol, + "status": "failed" + } + finally: + await client.disconnect() + + except HTTPException: + raise + except Exception as e: + error_msg = f"平仓失败: {str(e)}" + logger.error(error_msg, exc_info=True) + raise HTTPException(status_code=500, detail=error_msg) diff --git a/backend/api/routes/trades.py b/backend/api/routes/trades.py index 1ebadc3..608d33d 100644 --- a/backend/api/routes/trades.py +++ b/backend/api/routes/trades.py @@ -81,9 +81,30 @@ async def get_trades( trades = Trade.get_all(start_date, end_date, symbol, status) logger.info(f"查询到 {len(trades)} 条交易记录") + # 格式化交易记录,添加平仓类型的中文显示 + formatted_trades = [] + for trade in trades[:limit]: + formatted_trade = dict(trade) + + # 将 exit_reason 转换为中文显示 + exit_reason = trade.get('exit_reason', '') + if exit_reason: + exit_reason_map = { + 'manual': '手动平仓', + 'stop_loss': '自动平仓(止损)', + 'take_profit': '自动平仓(止盈)', + 'trailing_stop': '自动平仓(移动止损)', + 'sync': '同步平仓' + } + formatted_trade['exit_reason_display'] = exit_reason_map.get(exit_reason, exit_reason) + else: + formatted_trade['exit_reason_display'] = '' + + formatted_trades.append(formatted_trade) + result = { "total": len(trades), - "trades": trades[:limit], + "trades": formatted_trades, "filters": { "start_date": start_date, "end_date": end_date, diff --git a/frontend/index.html b/frontend/index.html index bc6a0c2..536410b 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -3,7 +3,7 @@ - 自动交易系统 + AutoTrade
diff --git a/frontend/src/components/StatsDashboard.css b/frontend/src/components/StatsDashboard.css index c3495b2..1ad6eb6 100644 --- a/frontend/src/components/StatsDashboard.css +++ b/frontend/src/components/StatsDashboard.css @@ -184,6 +184,21 @@ color: #721c24; } +.trade-actions { + display: flex; + flex-direction: column; + gap: 0.5rem; + align-items: flex-end; +} + +@media (min-width: 768px) { + .trade-actions { + flex-direction: row; + align-items: center; + gap: 1rem; + } +} + .trade-pnl { font-weight: bold; font-size: 1rem; @@ -199,6 +214,63 @@ font-style: italic; } +.close-btn { + padding: 0.5rem 1rem; + background: #e74c3c; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 0.9rem; + font-weight: 500; + transition: all 0.3s; + white-space: nowrap; +} + +.close-btn:hover:not(:disabled) { + background: #c0392b; +} + +.close-btn:active:not(:disabled) { + transform: scale(0.95); +} + +.close-btn:disabled { + opacity: 0.6; + cursor: not-allowed; + background: #95a5a6; +} + +.message { + padding: 1rem; + margin-bottom: 1rem; + border-radius: 4px; + animation: slideIn 0.3s ease-out; +} + +@keyframes slideIn { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.message.success { + background-color: #d4edda; + color: #155724; + border: 1px solid #c3e6cb; +} + +.message.error { + background-color: #f8d7da; + color: #721c24; + border: 1px solid #f5c6cb; +} + @media (min-width: 768px) { .trade-pnl { background: transparent; diff --git a/frontend/src/components/StatsDashboard.jsx b/frontend/src/components/StatsDashboard.jsx index 9dbb8dd..2ec6656 100644 --- a/frontend/src/components/StatsDashboard.jsx +++ b/frontend/src/components/StatsDashboard.jsx @@ -6,6 +6,8 @@ import './StatsDashboard.css' const StatsDashboard = () => { const [dashboardData, setDashboardData] = useState(null) const [loading, setLoading] = useState(true) + const [closingSymbol, setClosingSymbol] = useState(null) + const [message, setMessage] = useState('') useEffect(() => { loadDashboard() @@ -24,6 +26,38 @@ const StatsDashboard = () => { } } + const handleClosePosition = async (symbol) => { + if (!window.confirm(`确定要平仓 ${symbol} 吗?`)) { + return + } + + setClosingSymbol(symbol) + setMessage('') + + try { + const result = await api.closePosition(symbol) + setMessage(result.message || `${symbol} 平仓成功`) + + // 立即刷新数据 + await loadDashboard() + + // 3秒后清除消息 + setTimeout(() => { + setMessage('') + }, 3000) + } catch (error) { + setMessage(`平仓失败: ${error.message}`) + console.error('Close position error:', error) + + // 错误消息5秒后清除 + setTimeout(() => { + setMessage('') + }, 5000) + } finally { + setClosingSymbol(null) + } + } + if (loading) return
加载中...
const account = dashboardData?.account @@ -33,6 +67,12 @@ const StatsDashboard = () => {

仪表板

+ {message && ( +
+ {message} +
+ )} +

账户信息

@@ -117,11 +157,21 @@ const StatsDashboard = () => {
开仓时间: {formatEntryTime(trade.entry_time)}
)}
-
= 0 ? 'positive' : 'negative'}`}> - {parseFloat(trade.pnl || 0).toFixed(2)} USDT - {trade.pnl_percent !== undefined && ( - ({parseFloat(trade.pnl_percent).toFixed(2)}%) - )} +
+
= 0 ? 'positive' : 'negative'}`}> + {parseFloat(trade.pnl || 0).toFixed(2)} USDT + {trade.pnl_percent !== undefined && ( + ({parseFloat(trade.pnl_percent).toFixed(2)}%) + )} +
+
) diff --git a/frontend/src/components/TradeList.jsx b/frontend/src/components/TradeList.jsx index 5c5f233..bcafae8 100644 --- a/frontend/src/components/TradeList.jsx +++ b/frontend/src/components/TradeList.jsx @@ -216,6 +216,7 @@ const TradeList = () => { 出场价 盈亏 状态 + 平仓类型 时间 @@ -231,8 +232,9 @@ const TradeList = () => { {parseFloat(trade.pnl).toFixed(2)} USDT - {trade.status} + {trade.status === 'open' ? '持仓中' : trade.status === 'closed' ? '已平仓' : '已取消'} + {trade.exit_reason_display || '-'} {new Date(trade.entry_time).toLocaleString('zh-CN')} ))} @@ -268,6 +270,12 @@ const TradeList = () => { {parseFloat(trade.pnl).toFixed(2)} USDT
+ {trade.exit_reason_display && ( +
+ 平仓类型 + {trade.exit_reason_display} +
+ )}
{new Date(trade.entry_time).toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })} diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js index 2c9a8d7..d0ea938 100644 --- a/frontend/src/services/api.js +++ b/frontend/src/services/api.js @@ -105,4 +105,19 @@ export const api = { } return response.json(); }, + + // 平仓操作 + closePosition: async (symbol) => { + const response = await fetch(buildUrl(`/api/account/positions/${symbol}/close`), { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }); + if (!response.ok) { + const error = await response.json().catch(() => ({ detail: '平仓失败' })); + throw new Error(error.detail || '平仓失败'); + } + return response.json(); + }, };