a
This commit is contained in:
parent
9e178547b3
commit
d29abf9055
|
|
@ -305,3 +305,72 @@ async def get_realtime_positions():
|
||||||
error_msg = f"获取持仓数据失败: {str(e)}"
|
error_msg = f"获取持仓数据失败: {str(e)}"
|
||||||
logger.error(error_msg, exc_info=True)
|
logger.error(error_msg, exc_info=True)
|
||||||
raise HTTPException(status_code=500, detail=error_msg)
|
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)
|
||||||
|
|
|
||||||
|
|
@ -81,9 +81,30 @@ async def get_trades(
|
||||||
trades = Trade.get_all(start_date, end_date, symbol, status)
|
trades = Trade.get_all(start_date, end_date, symbol, status)
|
||||||
logger.info(f"查询到 {len(trades)} 条交易记录")
|
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 = {
|
result = {
|
||||||
"total": len(trades),
|
"total": len(trades),
|
||||||
"trades": trades[:limit],
|
"trades": formatted_trades,
|
||||||
"filters": {
|
"filters": {
|
||||||
"start_date": start_date,
|
"start_date": start_date,
|
||||||
"end_date": end_date,
|
"end_date": end_date,
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0, user-scalable=yes" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0, user-scalable=yes" />
|
||||||
<title>自动交易系统</title>
|
<title>AutoTrade</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|
|
||||||
|
|
@ -184,6 +184,21 @@
|
||||||
color: #721c24;
|
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 {
|
.trade-pnl {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
|
|
@ -199,6 +214,63 @@
|
||||||
font-style: italic;
|
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) {
|
@media (min-width: 768px) {
|
||||||
.trade-pnl {
|
.trade-pnl {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ import './StatsDashboard.css'
|
||||||
const StatsDashboard = () => {
|
const StatsDashboard = () => {
|
||||||
const [dashboardData, setDashboardData] = useState(null)
|
const [dashboardData, setDashboardData] = useState(null)
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [closingSymbol, setClosingSymbol] = useState(null)
|
||||||
|
const [message, setMessage] = useState('')
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadDashboard()
|
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>
|
if (loading) return <div className="loading">加载中...</div>
|
||||||
|
|
||||||
const account = dashboardData?.account
|
const account = dashboardData?.account
|
||||||
|
|
@ -33,6 +67,12 @@ const StatsDashboard = () => {
|
||||||
<div className="dashboard">
|
<div className="dashboard">
|
||||||
<h2>仪表板</h2>
|
<h2>仪表板</h2>
|
||||||
|
|
||||||
|
{message && (
|
||||||
|
<div className={`message ${message.includes('失败') || message.includes('错误') ? 'error' : 'success'}`}>
|
||||||
|
{message}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="dashboard-grid">
|
<div className="dashboard-grid">
|
||||||
<div className="dashboard-card">
|
<div className="dashboard-card">
|
||||||
<h3>账户信息</h3>
|
<h3>账户信息</h3>
|
||||||
|
|
@ -117,12 +157,22 @@ const StatsDashboard = () => {
|
||||||
<div className="entry-time">开仓时间: {formatEntryTime(trade.entry_time)}</div>
|
<div className="entry-time">开仓时间: {formatEntryTime(trade.entry_time)}</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="trade-actions">
|
||||||
<div className={`trade-pnl ${parseFloat(trade.pnl || 0) >= 0 ? 'positive' : 'negative'}`}>
|
<div className={`trade-pnl ${parseFloat(trade.pnl || 0) >= 0 ? 'positive' : 'negative'}`}>
|
||||||
{parseFloat(trade.pnl || 0).toFixed(2)} USDT
|
{parseFloat(trade.pnl || 0).toFixed(2)} USDT
|
||||||
{trade.pnl_percent !== undefined && (
|
{trade.pnl_percent !== undefined && (
|
||||||
<span> ({parseFloat(trade.pnl_percent).toFixed(2)}%)</span>
|
<span> ({parseFloat(trade.pnl_percent).toFixed(2)}%)</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<button
|
||||||
|
className="close-btn"
|
||||||
|
onClick={() => handleClosePosition(trade.symbol)}
|
||||||
|
disabled={closingSymbol === trade.symbol}
|
||||||
|
title={`平仓 ${trade.symbol}`}
|
||||||
|
>
|
||||||
|
{closingSymbol === trade.symbol ? '平仓中...' : '平仓'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|
|
||||||
|
|
@ -216,6 +216,7 @@ const TradeList = () => {
|
||||||
<th>出场价</th>
|
<th>出场价</th>
|
||||||
<th>盈亏</th>
|
<th>盈亏</th>
|
||||||
<th>状态</th>
|
<th>状态</th>
|
||||||
|
<th>平仓类型</th>
|
||||||
<th>时间</th>
|
<th>时间</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
@ -231,8 +232,9 @@ const TradeList = () => {
|
||||||
{parseFloat(trade.pnl).toFixed(2)} USDT
|
{parseFloat(trade.pnl).toFixed(2)} USDT
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span className={`status ${trade.status}`}>{trade.status}</span>
|
<span className={`status ${trade.status}`}>{trade.status === 'open' ? '持仓中' : trade.status === 'closed' ? '已平仓' : '已取消'}</span>
|
||||||
</td>
|
</td>
|
||||||
|
<td>{trade.exit_reason_display || '-'}</td>
|
||||||
<td>{new Date(trade.entry_time).toLocaleString('zh-CN')}</td>
|
<td>{new Date(trade.entry_time).toLocaleString('zh-CN')}</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
|
|
@ -268,6 +270,12 @@ const TradeList = () => {
|
||||||
{parseFloat(trade.pnl).toFixed(2)} USDT
|
{parseFloat(trade.pnl).toFixed(2)} USDT
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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>
|
||||||
<div className="trade-card-footer">
|
<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>
|
<span>{new Date(trade.entry_time).toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })}</span>
|
||||||
|
|
|
||||||
|
|
@ -105,4 +105,19 @@ export const api = {
|
||||||
}
|
}
|
||||||
return response.json();
|
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();
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user