feat(trade_list, api): 添加订单同步功能以补全缺失的历史订单
在 `TradeList.jsx` 中新增订单同步功能,允许用户从币安同步最近的历史订单并补全缺失的订单号。引入 `syncTradesFromBinance` 方法于 `api.js`,实现与后端的交互,处理同步请求并返回结果。更新前端界面以显示同步状态和结果,提升用户体验和数据完整性。
This commit is contained in:
parent
415589e625
commit
55ae7b5b08
|
|
@ -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>
|
||||
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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() });
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user