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();
+ },
};