a
This commit is contained in:
parent
cb7b091280
commit
1fcd692368
|
|
@ -286,8 +286,8 @@ async def ensure_all_positions_sltp(
|
|||
批量补挂当前所有持仓的止盈止损保护单。
|
||||
"""
|
||||
# 先拿当前持仓symbol列表
|
||||
api_key, api_secret, use_testnet = Account.get_credentials(account_id)
|
||||
if not api_key or not api_secret:
|
||||
api_key, api_secret, use_testnet, status = Account.get_credentials(account_id)
|
||||
if (not api_key or not api_secret) and status == "active":
|
||||
logger.error(f"[account_id={account_id}] API密钥未配置")
|
||||
raise HTTPException(status_code=400, detail=f"API密钥未配置(account_id={account_id})")
|
||||
|
||||
|
|
@ -351,7 +351,7 @@ async def get_realtime_account_data(account_id: int = 1):
|
|||
try:
|
||||
# 从 accounts 表读取账号私有API密钥
|
||||
logger.info(f"步骤1: 从accounts读取API配置... (account_id={account_id})")
|
||||
api_key, api_secret, use_testnet = Account.get_credentials(account_id)
|
||||
api_key, api_secret, use_testnet, status = Account.get_credentials(account_id)
|
||||
|
||||
logger.info(f" - API密钥存在: {bool(api_key)}")
|
||||
if api_key:
|
||||
|
|
@ -565,7 +565,7 @@ async def get_realtime_positions(account_id: int = Depends(get_account_id)):
|
|||
"""获取实时持仓数据"""
|
||||
client = None
|
||||
try:
|
||||
api_key, api_secret, use_testnet = Account.get_credentials(account_id)
|
||||
api_key, api_secret, use_testnet, status = Account.get_credentials(account_id)
|
||||
|
||||
logger.info(f"尝试获取实时持仓数据 (testnet={use_testnet}, account_id={account_id})")
|
||||
|
||||
|
|
@ -738,9 +738,9 @@ async def close_position(symbol: str, account_id: int = Depends(get_account_id))
|
|||
logger.info(f"收到平仓请求: {symbol}")
|
||||
logger.info(f"=" * 60)
|
||||
|
||||
api_key, api_secret, use_testnet = Account.get_credentials(account_id)
|
||||
api_key, api_secret, use_testnet, status = Account.get_credentials(account_id)
|
||||
|
||||
if not api_key or not api_secret:
|
||||
if (not api_key or not api_secret) and status == "active":
|
||||
error_msg = f"API密钥未配置(account_id={account_id})"
|
||||
logger.warning(f"[account_id={account_id}] {error_msg}")
|
||||
raise HTTPException(status_code=400, detail=error_msg)
|
||||
|
|
@ -1041,6 +1041,169 @@ async def close_position(symbol: str, account_id: int = Depends(get_account_id))
|
|||
raise HTTPException(status_code=500, detail=error_msg)
|
||||
|
||||
|
||||
@router.post("/positions/close-all")
|
||||
async def close_all_positions(account_id: int = Depends(get_account_id)):
|
||||
"""一键全平:平仓所有持仓"""
|
||||
try:
|
||||
logger.info("=" * 60)
|
||||
logger.info("收到一键全平请求")
|
||||
logger.info("=" * 60)
|
||||
|
||||
api_key, api_secret, use_testnet, status = Account.get_credentials(account_id)
|
||||
|
||||
if (not api_key or not api_secret) and status == "active":
|
||||
error_msg = f"API密钥未配置(account_id={account_id})"
|
||||
logger.warning(f"[account_id={account_id}] {error_msg}")
|
||||
raise HTTPException(status_code=400, detail=error_msg)
|
||||
|
||||
# 导入必要的模块
|
||||
try:
|
||||
from binance_client import BinanceClient
|
||||
logger.info("✓ 成功导入交易系统模块")
|
||||
except ImportError as import_error:
|
||||
logger.warning(f"首次导入失败: {import_error},尝试从trading_system路径导入")
|
||||
trading_system_path = project_root / 'trading_system'
|
||||
sys.path.insert(0, str(trading_system_path))
|
||||
from binance_client import BinanceClient
|
||||
logger.info("✓ 从trading_system路径导入成功")
|
||||
|
||||
# 导入数据库模型
|
||||
from database.models import Trade
|
||||
|
||||
# 创建客户端
|
||||
logger.info(f"创建BinanceClient (testnet={use_testnet})...")
|
||||
client = BinanceClient(
|
||||
api_key=api_key,
|
||||
api_secret=api_secret,
|
||||
testnet=use_testnet
|
||||
)
|
||||
|
||||
logger.info("连接币安API...")
|
||||
await client.connect()
|
||||
logger.info("✓ 币安API连接成功")
|
||||
|
||||
try:
|
||||
# 获取所有持仓
|
||||
positions = await client.get_open_positions()
|
||||
if not positions:
|
||||
logger.info("当前没有持仓")
|
||||
return {
|
||||
"message": "当前没有持仓",
|
||||
"closed": 0,
|
||||
"failed": 0,
|
||||
"results": []
|
||||
}
|
||||
|
||||
logger.info(f"发现 {len(positions)} 个持仓,开始逐一平仓...")
|
||||
|
||||
results = []
|
||||
closed_count = 0
|
||||
failed_count = 0
|
||||
|
||||
for position in positions:
|
||||
symbol = position.get('symbol')
|
||||
position_amt = float(position.get('positionAmt', 0))
|
||||
|
||||
if abs(position_amt) <= 0:
|
||||
continue
|
||||
|
||||
try:
|
||||
logger.info(f"开始平仓 {symbol} (数量: {position_amt})...")
|
||||
|
||||
# 确定平仓方向
|
||||
side = 'SELL' if position_amt > 0 else 'BUY'
|
||||
|
||||
# 使用市价单平仓
|
||||
order = await client.place_order(
|
||||
symbol=symbol,
|
||||
side=side,
|
||||
order_type='MARKET',
|
||||
quantity=abs(position_amt),
|
||||
reduce_only=True
|
||||
)
|
||||
|
||||
if order and order.get('orderId'):
|
||||
logger.info(f"✓ {symbol} 平仓订单已提交: {order.get('orderId')}")
|
||||
|
||||
# 获取成交价格
|
||||
exit_price = float(order.get('avgPrice', 0)) or float(order.get('price', 0))
|
||||
if not exit_price:
|
||||
# 如果订单中没有价格,获取当前价格
|
||||
ticker = await client.get_ticker_24h(symbol)
|
||||
exit_price = float(ticker['price']) if ticker else 0
|
||||
|
||||
# 更新数据库记录
|
||||
open_trades = Trade.get_by_symbol(symbol, status='open')
|
||||
for trade in open_trades:
|
||||
entry_price = float(trade['entry_price'])
|
||||
quantity = float(trade['quantity'])
|
||||
|
||||
if trade['side'] == 'BUY':
|
||||
pnl = (exit_price - entry_price) * quantity
|
||||
pnl_percent = ((exit_price - entry_price) / entry_price) * 100
|
||||
else:
|
||||
pnl = (entry_price - exit_price) * quantity
|
||||
pnl_percent = ((entry_price - exit_price) / entry_price) * 100
|
||||
|
||||
Trade.update_exit(
|
||||
trade_id=trade['id'],
|
||||
exit_price=exit_price,
|
||||
exit_reason='manual',
|
||||
pnl=pnl,
|
||||
pnl_percent=pnl_percent,
|
||||
exit_order_id=order.get('orderId')
|
||||
)
|
||||
logger.info(f"✓ 已更新数据库记录 trade_id={trade['id']} (盈亏: {pnl:.2f} USDT)")
|
||||
|
||||
closed_count += 1
|
||||
results.append({
|
||||
"symbol": symbol,
|
||||
"status": "success",
|
||||
"order_id": order.get('orderId'),
|
||||
"message": f"{symbol} 平仓成功"
|
||||
})
|
||||
else:
|
||||
logger.warning(f"⚠ {symbol} 平仓订单提交失败")
|
||||
failed_count += 1
|
||||
results.append({
|
||||
"symbol": symbol,
|
||||
"status": "failed",
|
||||
"message": f"{symbol} 平仓失败: 订单未提交"
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ {symbol} 平仓失败: {e}")
|
||||
failed_count += 1
|
||||
results.append({
|
||||
"symbol": symbol,
|
||||
"status": "failed",
|
||||
"message": f"{symbol} 平仓失败: {str(e)}"
|
||||
})
|
||||
|
||||
logger.info(f"一键全平完成: 成功 {closed_count} / 失败 {failed_count}")
|
||||
return {
|
||||
"message": f"一键全平完成: 成功 {closed_count} / 失败 {failed_count}",
|
||||
"closed": closed_count,
|
||||
"failed": failed_count,
|
||||
"results": results
|
||||
}
|
||||
|
||||
finally:
|
||||
logger.info("断开币安API连接...")
|
||||
await client.disconnect()
|
||||
logger.info("✓ 已断开连接")
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
error_msg = f"一键全平失败: {str(e)}"
|
||||
logger.error("=" * 60)
|
||||
logger.error(f"一键全平操作异常: {error_msg}")
|
||||
logger.error(f"错误类型: {type(e).__name__}")
|
||||
logger.error("=" * 60, exc_info=True)
|
||||
raise HTTPException(status_code=500, detail=error_msg)
|
||||
|
||||
|
||||
@router.post("/positions/{symbol}/open")
|
||||
async def open_position_from_recommendation(
|
||||
symbol: str,
|
||||
|
|
@ -1068,9 +1231,9 @@ async def open_position_from_recommendation(
|
|||
if entry_price <= 0 or stop_loss_price <= 0:
|
||||
raise HTTPException(status_code=400, detail="入场价和止损价必须大于0")
|
||||
|
||||
api_key, api_secret, use_testnet = Account.get_credentials(account_id)
|
||||
api_key, api_secret, use_testnet, status = Account.get_credentials(account_id)
|
||||
|
||||
if not api_key or not api_secret:
|
||||
if (not api_key or not api_secret) and status == "active":
|
||||
error_msg = f"API密钥未配置(account_id={account_id})"
|
||||
logger.warning(f"[account_id={account_id}] {error_msg}")
|
||||
raise HTTPException(status_code=400, detail=error_msg)
|
||||
|
|
@ -1247,9 +1410,9 @@ async def sync_positions(account_id: int = Depends(get_account_id)):
|
|||
logger.info("收到持仓状态同步请求")
|
||||
logger.info("=" * 60)
|
||||
|
||||
api_key, api_secret, use_testnet = Account.get_credentials(account_id)
|
||||
api_key, api_secret, use_testnet, status = Account.get_credentials(account_id)
|
||||
|
||||
if not api_key or not api_secret:
|
||||
if (not api_key or not api_secret) and status == "active":
|
||||
error_msg = f"API密钥未配置(account_id={account_id})"
|
||||
logger.warning(f"[account_id={account_id}] {error_msg}")
|
||||
raise HTTPException(status_code=400, detail=error_msg)
|
||||
|
|
|
|||
|
|
@ -99,12 +99,12 @@ async def list_accounts(user: Dict[str, Any] = Depends(get_current_user)) -> Lis
|
|||
if not r:
|
||||
continue
|
||||
# 普通用户:不返回密钥明文,但返回“是否已配置”的状态,方便前端提示
|
||||
api_key, api_secret, use_testnet = Account.get_credentials(int(aid))
|
||||
api_key, api_secret, use_testnet, status = Account.get_credentials(int(aid))
|
||||
out.append(
|
||||
{
|
||||
"id": int(aid),
|
||||
"name": r.get("name") or "",
|
||||
"status": r.get("status") or "active",
|
||||
"status": status or r.get("status") or "active",
|
||||
"use_testnet": bool(use_testnet),
|
||||
"role": membership_map.get(int(aid), "viewer"),
|
||||
"has_api_key": bool(api_key),
|
||||
|
|
|
|||
|
|
@ -210,9 +210,9 @@ async def get_all_configs(
|
|||
|
||||
# 合并账号级 API Key/Secret(从 accounts 表读,避免把密钥当普通配置存)
|
||||
try:
|
||||
api_key, api_secret, use_testnet = Account.get_credentials(account_id)
|
||||
api_key, api_secret, use_testnet, status = Account.get_credentials(account_id)
|
||||
except Exception:
|
||||
api_key, api_secret, use_testnet = "", "", False
|
||||
api_key, api_secret, use_testnet, status = "", "", False, "active"
|
||||
# 仅用于配置页展示/更新:不返回 secret 明文;api_key 仅脱敏展示
|
||||
result["BINANCE_API_KEY"] = {
|
||||
"value": _mask(api_key or ""),
|
||||
|
|
@ -733,7 +733,7 @@ async def get_config(
|
|||
try:
|
||||
# 虚拟字段:从 accounts 表读取
|
||||
if key in {"BINANCE_API_KEY", "BINANCE_API_SECRET", "USE_TESTNET"}:
|
||||
api_key, api_secret, use_testnet = Account.get_credentials(account_id)
|
||||
api_key, api_secret, use_testnet, status = Account.get_credentials(account_id)
|
||||
if key == "BINANCE_API_KEY":
|
||||
return {"key": key, "value": _mask(api_key or ""), "type": "string", "category": "api", "description": "币安API密钥(仅脱敏展示)"}
|
||||
if key == "BINANCE_API_SECRET":
|
||||
|
|
|
|||
|
|
@ -114,6 +114,47 @@ const StatsDashboard = () => {
|
|||
}
|
||||
}
|
||||
|
||||
const handleCloseAllPositions = async () => {
|
||||
if (!window.confirm(`确定要一键全平所有持仓吗?\n\n这将使用市价单平仓所有持仓,请谨慎操作!`)) {
|
||||
return
|
||||
}
|
||||
|
||||
setClosingSymbol('ALL') // 使用特殊标记表示全平操作
|
||||
setMessage('')
|
||||
|
||||
try {
|
||||
console.log('开始一键全平...')
|
||||
const result = await api.closeAllPositions()
|
||||
console.log('一键全平结果:', result)
|
||||
|
||||
let message = result.message || '一键全平完成'
|
||||
if (result.closed > 0 || result.failed > 0) {
|
||||
message = `一键全平完成: 成功 ${result.closed} / 失败 ${result.failed}`
|
||||
}
|
||||
|
||||
setMessage(message)
|
||||
|
||||
// 立即刷新数据
|
||||
await loadDashboard()
|
||||
|
||||
// 5秒后清除消息
|
||||
setTimeout(() => {
|
||||
setMessage('')
|
||||
}, 5000)
|
||||
} catch (error) {
|
||||
console.error('Close all positions error:', error)
|
||||
const errorMessage = error.message || error.toString() || '一键全平失败,请检查网络连接或后端服务'
|
||||
setMessage(`一键全平失败: ${errorMessage}`)
|
||||
|
||||
// 错误消息5秒后清除
|
||||
setTimeout(() => {
|
||||
setMessage('')
|
||||
}, 5000)
|
||||
} finally {
|
||||
setClosingSymbol(null)
|
||||
}
|
||||
}
|
||||
|
||||
const handleEnsureSLTP = async (symbol) => {
|
||||
if (!window.confirm(`确定要为 ${symbol} 补挂“币安止损/止盈保护单”吗?\n\n说明:将自动取消该交易对已有的 STOP/TP 保护单并重新挂单(避免重复)。`)) {
|
||||
return
|
||||
|
|
@ -387,14 +428,35 @@ const StatsDashboard = () => {
|
|||
<div className="dashboard-card">
|
||||
<div className="positions-header">
|
||||
<h3>当前持仓</h3>
|
||||
<div style={{ display: 'flex', gap: '8px' }}>
|
||||
<button
|
||||
className="sltp-all-btn"
|
||||
onClick={handleEnsureAllSLTP}
|
||||
disabled={sltpAllBusy || openTrades.length === 0}
|
||||
title="为所有持仓在币安侧补挂 STOP_MARKET + TAKE_PROFIT_MARKET 保护单(closePosition)"
|
||||
title="为所有持仓在币安侧补挂 STOP_MARKET + TAKE_PROFIT_MARKET 保护单"
|
||||
>
|
||||
{sltpAllBusy ? '一键补挂中...' : '一键补挂止盈止损'}
|
||||
</button>
|
||||
<button
|
||||
className="close-all-btn"
|
||||
onClick={handleCloseAllPositions}
|
||||
disabled={closingSymbol !== null || openTrades.length === 0}
|
||||
style={{
|
||||
padding: '6px 12px',
|
||||
backgroundColor: '#ff6b6b',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: (closingSymbol !== null || openTrades.length === 0) ? 'not-allowed' : 'pointer',
|
||||
fontSize: '13px',
|
||||
fontWeight: 'bold',
|
||||
opacity: (closingSymbol !== null || openTrades.length === 0) ? 0.6 : 1
|
||||
}}
|
||||
title="一键平仓所有持仓(市价单)"
|
||||
>
|
||||
{closingSymbol !== null ? '全平中...' : '⚡ 一键全平'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="entry-type-summary">
|
||||
<span className="entry-type-badge limit">限价入场: {entryTypeCounts.limit}</span>
|
||||
|
|
|
|||
|
|
@ -353,7 +353,7 @@ export const api = {
|
|||
|
||||
// 平仓操作
|
||||
closePosition: async (symbol) => {
|
||||
const response = await fetch(buildUrl(`/api/accounts/positions/${symbol}/close`), {
|
||||
const response = await fetch(buildUrl(`/api/account/positions/${symbol}/close`), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
...withAccountHeaders({ 'Content-Type': 'application/json' }),
|
||||
|
|
@ -366,6 +366,21 @@ export const api = {
|
|||
return response.json();
|
||||
},
|
||||
|
||||
// 一键全平(平仓所有持仓)
|
||||
closeAllPositions: async () => {
|
||||
const response = await fetch(buildUrl(`/api/account/positions/close-all`), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
...withAccountHeaders({ 'Content-Type': 'application/json' }),
|
||||
},
|
||||
});
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({ detail: '一键全平失败' }));
|
||||
throw new Error(error.detail || '一键全平失败');
|
||||
}
|
||||
return response.json();
|
||||
},
|
||||
|
||||
// 补挂止盈止损(交易所保护单)
|
||||
ensurePositionSLTP: async (symbol) => {
|
||||
const response = await fetch(buildUrl(`/api/accounts/positions/${symbol}/sltp/ensure`), {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user