feat(trade_list, api): 添加订单同步功能以补全缺失的历史订单

在 `TradeList.jsx` 中新增订单同步功能,允许用户从币安同步最近的历史订单并补全缺失的订单号。引入 `syncTradesFromBinance` 方法于 `api.js`,实现与后端的交互,处理同步请求并返回结果。更新前端界面以显示同步状态和结果,提升用户体验和数据完整性。
This commit is contained in:
薇薇安 2026-02-17 22:27:10 +08:00
parent 415589e625
commit 55ae7b5b08
3 changed files with 140 additions and 0 deletions

View File

@ -22,6 +22,9 @@ const TradeList = () => {
const [exitReason, setExitReason] = useState('')
const [reconciledOnly, setReconciledOnly] = useState(true) //
const [timeFilter, setTimeFilter] = useState('exit') // 'exit' =, 'entry'
const [syncing, setSyncing] = useState(false) //
const [syncResult, setSyncResult] = useState(null) //
const [syncDays, setSyncDays] = useState(7) //
useEffect(() => {
loadData()
@ -87,6 +90,41 @@ const TradeList = () => {
setReconciledOnly(true)
}
//
const handleSyncOrders = async () => {
if (syncing) {
return //
}
if (!window.confirm(`确定要同步最近 ${syncDays} 天的订单吗?\n这将从币安拉取历史订单并补全缺失的订单号。`)) {
return
}
setSyncing(true)
setSyncResult(null)
try {
const result = await api.syncTradesFromBinance(syncDays)
setSyncResult({
success: true,
message: `同步完成:共处理 ${result.total_orders || 0} 个订单,更新 ${result.updated_trades || 0} 条记录`,
details: result
})
//
setTimeout(() => {
loadData()
}, 1000)
} catch (error) {
setSyncResult({
success: false,
message: `同步失败:${error.message || '未知错误'}`,
details: null
})
} finally {
setSyncing(false)
}
}
// /便
// type: 'csv' | 'json'
const handleExport = (type = 'csv') => {
@ -484,6 +522,35 @@ const TradeList = () => {
<button className="btn-secondary" onClick={handleReset}>
重置
</button>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginLeft: '10px' }}>
<label style={{ fontSize: '12px', color: '#666' }}>同步天数</label>
<select
value={syncDays}
onChange={(e) => setSyncDays(Number(e.target.value))}
disabled={syncing}
style={{ width: '70px', padding: '4px', fontSize: '12px' }}
>
<option value={3}>3</option>
<option value={7}>7</option>
<option value={14}>14</option>
<option value={30}>30</option>
</select>
<button
className="btn-secondary"
onClick={handleSyncOrders}
disabled={syncing}
style={{
backgroundColor: syncing ? '#ccc' : '#4CAF50',
color: 'white',
border: 'none',
padding: '6px 12px',
fontSize: '12px'
}}
title="从币安同步历史订单,补全缺失的订单号(不会重复同步)"
>
{syncing ? '同步中...' : '同步订单'}
</button>
</div>
{trades.length > 0 && (
<>
<button className="btn-export" onClick={() => handleExport('csv')} title="导出Excel/CSV含入场/离场原因、入场思路等),便于后续分析">
@ -495,6 +562,39 @@ const TradeList = () => {
</>
)}
</div>
{syncResult && (
<div style={{
marginTop: '10px',
padding: '10px',
backgroundColor: syncResult.success ? '#e8f5e9' : '#ffebee',
border: `1px solid ${syncResult.success ? '#4CAF50' : '#f44336'}`,
borderRadius: '4px',
fontSize: '13px',
color: syncResult.success ? '#2e7d32' : '#c62828'
}}>
{syncResult.message}
{syncResult.details && syncResult.success && (
<div style={{ marginTop: '8px', fontSize: '12px', color: '#666' }}>
开仓订单{syncResult.details.open_orders || 0} |
平仓订单{syncResult.details.close_orders || 0} |
更新记录{syncResult.details.updated_trades || 0}
</div>
)}
<button
onClick={() => setSyncResult(null)}
style={{
float: 'right',
background: 'none',
border: 'none',
color: '#666',
cursor: 'pointer',
fontSize: '16px'
}}
>
×
</button>
</div>
)}
</div>
{

View File

@ -405,6 +405,36 @@ export const api = {
return response.json();
},
// 同步订单:从币安同步历史订单,补全缺失的订单号
syncTradesFromBinance: async (days = 7) => {
const response = await fetch(buildUrl(`/api/trades/sync-binance?days=${days}`), {
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();
},
// 验证订单一致性:校验订单记录是否与币安一致
verifyTradesAgainstBinance: async (days = 30, limit = 100) => {
const response = await fetch(buildUrl(`/api/trades/verify-binance?days=${days}&limit=${limit}`), {
method: 'GET',
headers: {
...withAccountHeaders({ 'Content-Type': 'application/json' }),
},
});
if (!response.ok) {
const error = await response.json().catch(() => ({ detail: '验证订单失败' }));
throw new Error(error.detail || '验证订单失败');
}
return response.json();
},
// 统计
getPerformance: async (days = 7) => {
const response = await fetch(buildUrl(`/api/stats/performance?days=${days}`), { headers: withAccountHeaders() });

View File

@ -249,17 +249,23 @@ class UserDataStream:
try:
data = json.loads(raw)
except Exception:
logger.debug(f"UserDataStream: 无法解析推送消息: {raw[:200]}")
return False
e = data.get("e")
if e == "listenKeyExpired":
logger.warning("UserDataStream: 收到 listenKeyExpired将换新 key 重连")
return True
if e == "ORDER_TRADE_UPDATE":
logger.debug(f"UserDataStream: 收到 ORDER_TRADE_UPDATE 推送")
self._on_order_trade_update(data.get("o") or {})
elif e == "ACCOUNT_UPDATE":
logger.debug(f"UserDataStream: 收到 ACCOUNT_UPDATE 推送")
self._on_account_update(data.get("a") or {})
elif e == "ALGO_UPDATE":
logger.debug(f"UserDataStream: 收到 ALGO_UPDATE 推送")
self._on_algo_update(data.get("o") or {})
else:
logger.debug(f"UserDataStream: 收到未知事件类型: {e}")
return False
def _on_order_trade_update(self, o: Dict):
@ -267,7 +273,11 @@ class UserDataStream:
# ap=均价, z=累计成交量, R=只减仓, rp=该交易实现盈亏, s=交易对
event_type = (o.get("x") or "").strip().upper()
status = (o.get("X") or "").strip().upper()
symbol = (o.get("s") or "").strip()
order_id = o.get("i")
logger.debug(f"UserDataStream: ORDER_TRADE_UPDATE symbol={symbol!r} orderId={order_id} event={event_type} status={status}")
if status != "FILLED":
logger.debug(f"UserDataStream: 订单状态非 FILLED跳过 symbol={symbol!r} orderId={order_id} status={status}")
return
client_order_id = (o.get("c") or "").strip() or None
order_id = o.get("i")