This commit is contained in:
薇薇安 2026-01-19 22:07:48 +08:00
parent e4d72057eb
commit 6e922d7921
3 changed files with 105 additions and 2 deletions

View File

@ -174,6 +174,37 @@ async def _ensure_exchange_sltp_for_symbol(symbol: str):
sl_order = await client.client.futures_create_order(**sl_params)
tp_order = await client.client.futures_create_order(**tp_params)
# 再查一次未成交委托,确认是否真的挂上(并用于前端展示/排查)
open_orders = []
try:
oo = await client.client.futures_get_open_orders(symbol=symbol)
if isinstance(oo, list):
for o in oo:
try:
if not isinstance(o, dict):
continue
otype2 = str(o.get("type") or "").upper()
if otype2 in {"STOP_MARKET", "TAKE_PROFIT_MARKET", "TRAILING_STOP_MARKET"}:
open_orders.append(
{
"orderId": o.get("orderId"),
"type": otype2,
"side": o.get("side"),
"stopPrice": o.get("stopPrice"),
"price": o.get("price"),
"workingType": o.get("workingType"),
"positionSide": o.get("positionSide"),
"closePosition": o.get("closePosition"),
"status": o.get("status"),
"updateTime": o.get("updateTime"),
}
)
except Exception:
continue
except Exception:
open_orders = []
return {
"symbol": symbol,
"position_side": side,
@ -184,6 +215,8 @@ async def _ensure_exchange_sltp_for_symbol(symbol: str):
"stop_market": sl_order,
"take_profit_market": tp_order,
},
"open_protection_orders": open_orders,
"ui_hint": "在币安【U本位合约】里这类 STOP/TP 通常显示在【条件单/止盈止损/计划委托】而不一定在普通【当前委托(限价)】列表。",
}
finally:
await client.disconnect()

View File

@ -446,6 +446,44 @@
font-style: normal;
}
.positions-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.positions-header h3 {
margin: 0;
}
.sltp-all-btn {
padding: 0.5rem 0.9rem;
background: #1f7aec;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.85rem;
font-weight: 600;
transition: all 0.3s;
white-space: nowrap;
}
.sltp-all-btn:hover:not(:disabled) {
background: #175fb8;
}
.sltp-all-btn:active:not(:disabled) {
transform: scale(0.98);
}
.sltp-all-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
background: #95a5a6;
}
.close-btn {
padding: 0.5rem 1rem;
background: #e74c3c;

View File

@ -8,6 +8,7 @@ const StatsDashboard = () => {
const [loading, setLoading] = useState(true)
const [closingSymbol, setClosingSymbol] = useState(null)
const [sltpSymbol, setSltpSymbol] = useState(null)
const [sltpAllBusy, setSltpAllBusy] = useState(false)
const [message, setMessage] = useState('')
const [tradingConfig, setTradingConfig] = useState(null)
@ -116,7 +117,8 @@ const StatsDashboard = () => {
const res = await api.ensurePositionSLTP(symbol)
const slId = res?.orders?.stop_market?.orderId
const tpId = res?.orders?.take_profit_market?.orderId
setMessage(`${symbol} 已补挂保护单SL=${slId || '-'} / TP=${tpId || '-'}`)
const cnt = Array.isArray(res?.open_protection_orders) ? res.open_protection_orders.length : 0
setMessage(`${symbol} 已补挂保护单SL=${slId || '-'} / TP=${tpId || '-'}(币安条件单可见,当前检测到保护单 ${cnt} 条)`)
await loadDashboard()
} catch (error) {
setMessage(`补挂失败 ${symbol}: ${error.message || '未知错误'}`)
@ -126,6 +128,26 @@ const StatsDashboard = () => {
}
}
const handleEnsureAllSLTP = async () => {
if (!window.confirm('确定要为【所有当前持仓】一键补挂“币安止损/止盈保护单”吗?\n\n说明会对每个持仓 symbol 自动取消旧的 STOP/TP 保护单并重新挂单(避免重复)。')) {
return
}
setSltpAllBusy(true)
setMessage('')
try {
const res = await api.ensureAllPositionsSLTP(50)
const ok = res?.ok ?? 0
const failed = res?.failed ?? 0
setMessage(`一键补挂完成:成功 ${ok} / 失败 ${failed}(请在币安【条件单/止盈止损】里查看)`)
await loadDashboard()
} catch (error) {
setMessage(`一键补挂失败: ${error.message || '未知错误'}`)
} finally {
setSltpAllBusy(false)
setTimeout(() => setMessage(''), 5000)
}
}
const handleSyncPositions = async () => {
if (!window.confirm('确定要同步持仓状态吗?这将检查币安实际持仓并更新数据库状态。')) {
return
@ -337,7 +359,17 @@ const StatsDashboard = () => {
)}
<div className="dashboard-card">
<h3>当前持仓</h3>
<div className="positions-header">
<h3>当前持仓</h3>
<button
className="sltp-all-btn"
onClick={handleEnsureAllSLTP}
disabled={sltpAllBusy || openTrades.length === 0}
title="为所有持仓在币安侧补挂 STOP_MARKET + TAKE_PROFIT_MARKET 保护单closePosition"
>
{sltpAllBusy ? '一键补挂中...' : '一键补挂止盈止损'}
</button>
</div>
<div className="entry-type-summary">
<span className="entry-type-badge limit">限价入场: {entryTypeCounts.limit}</span>
<span className="entry-type-badge market">市价入场: {entryTypeCounts.market}</span>