This commit is contained in:
薇薇安 2026-01-15 09:19:27 +08:00
parent 9e178547b3
commit d29abf9055
7 changed files with 243 additions and 8 deletions

View File

@ -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)

View File

@ -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,

View File

@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0, user-scalable=yes" />
<title>自动交易系统</title>
<title>AutoTrade</title>
</head>
<body>
<div id="root"></div>

View File

@ -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;

View File

@ -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 <div className="loading">加载中...</div>
const account = dashboardData?.account
@ -33,6 +67,12 @@ const StatsDashboard = () => {
<div className="dashboard">
<h2>仪表板</h2>
{message && (
<div className={`message ${message.includes('失败') || message.includes('错误') ? 'error' : 'success'}`}>
{message}
</div>
)}
<div className="dashboard-grid">
<div className="dashboard-card">
<h3>账户信息</h3>
@ -117,12 +157,22 @@ const StatsDashboard = () => {
<div className="entry-time">开仓时间: {formatEntryTime(trade.entry_time)}</div>
)}
</div>
<div className="trade-actions">
<div className={`trade-pnl ${parseFloat(trade.pnl || 0) >= 0 ? 'positive' : 'negative'}`}>
{parseFloat(trade.pnl || 0).toFixed(2)} USDT
{trade.pnl_percent !== undefined && (
<span> ({parseFloat(trade.pnl_percent).toFixed(2)}%)</span>
)}
</div>
<button
className="close-btn"
onClick={() => handleClosePosition(trade.symbol)}
disabled={closingSymbol === trade.symbol}
title={`平仓 ${trade.symbol}`}
>
{closingSymbol === trade.symbol ? '平仓中...' : '平仓'}
</button>
</div>
</div>
)
})}

View File

@ -216,6 +216,7 @@ const TradeList = () => {
<th>出场价</th>
<th>盈亏</th>
<th>状态</th>
<th>平仓类型</th>
<th>时间</th>
</tr>
</thead>
@ -231,8 +232,9 @@ const TradeList = () => {
{parseFloat(trade.pnl).toFixed(2)} USDT
</td>
<td>
<span className={`status ${trade.status}`}>{trade.status}</span>
<span className={`status ${trade.status}`}>{trade.status === 'open' ? '持仓中' : trade.status === 'closed' ? '已平仓' : '已取消'}</span>
</td>
<td>{trade.exit_reason_display || '-'}</td>
<td>{new Date(trade.entry_time).toLocaleString('zh-CN')}</td>
</tr>
))}
@ -268,6 +270,12 @@ const TradeList = () => {
{parseFloat(trade.pnl).toFixed(2)} USDT
</span>
</div>
{trade.exit_reason_display && (
<div className="trade-card-field">
<span className="trade-card-label">平仓类型</span>
<span className="trade-card-value">{trade.exit_reason_display}</span>
</div>
)}
</div>
<div className="trade-card-footer">
<span>{new Date(trade.entry_time).toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })}</span>

View File

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