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 [exitReason, setExitReason] = useState('')
|
||||||
const [reconciledOnly, setReconciledOnly] = useState(true) // 默认仅可对账,与币安一致
|
const [reconciledOnly, setReconciledOnly] = useState(true) // 默认仅可对账,与币安一致
|
||||||
const [timeFilter, setTimeFilter] = useState('exit') // 'exit' 按平仓时间(今天=今天平掉的单), 'entry' 按开仓时间
|
const [timeFilter, setTimeFilter] = useState('exit') // 'exit' 按平仓时间(今天=今天平掉的单), 'entry' 按开仓时间
|
||||||
|
const [syncing, setSyncing] = useState(false) // 同步订单状态
|
||||||
|
const [syncResult, setSyncResult] = useState(null) // 同步结果
|
||||||
|
const [syncDays, setSyncDays] = useState(7) // 同步天数
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadData()
|
loadData()
|
||||||
|
|
@ -87,6 +90,41 @@ const TradeList = () => {
|
||||||
setReconciledOnly(true)
|
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'
|
// type: 'csv' | 'json'
|
||||||
const handleExport = (type = 'csv') => {
|
const handleExport = (type = 'csv') => {
|
||||||
|
|
@ -484,6 +522,35 @@ const TradeList = () => {
|
||||||
<button className="btn-secondary" onClick={handleReset}>
|
<button className="btn-secondary" onClick={handleReset}>
|
||||||
重置
|
重置
|
||||||
</button>
|
</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 && (
|
{trades.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<button className="btn-export" onClick={() => handleExport('csv')} title="导出Excel/CSV(含入场/离场原因、入场思路等),便于后续分析">
|
<button className="btn-export" onClick={() => handleExport('csv')} title="导出Excel/CSV(含入场/离场原因、入场思路等),便于后续分析">
|
||||||
|
|
@ -495,6 +562,39 @@ const TradeList = () => {
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -405,6 +405,36 @@ export const api = {
|
||||||
return response.json();
|
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) => {
|
getPerformance: async (days = 7) => {
|
||||||
const response = await fetch(buildUrl(`/api/stats/performance?days=${days}`), { headers: withAccountHeaders() });
|
const response = await fetch(buildUrl(`/api/stats/performance?days=${days}`), { headers: withAccountHeaders() });
|
||||||
|
|
|
||||||
|
|
@ -249,17 +249,23 @@ class UserDataStream:
|
||||||
try:
|
try:
|
||||||
data = json.loads(raw)
|
data = json.loads(raw)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
logger.debug(f"UserDataStream: 无法解析推送消息: {raw[:200]}")
|
||||||
return False
|
return False
|
||||||
e = data.get("e")
|
e = data.get("e")
|
||||||
if e == "listenKeyExpired":
|
if e == "listenKeyExpired":
|
||||||
logger.warning("UserDataStream: 收到 listenKeyExpired,将换新 key 重连")
|
logger.warning("UserDataStream: 收到 listenKeyExpired,将换新 key 重连")
|
||||||
return True
|
return True
|
||||||
if e == "ORDER_TRADE_UPDATE":
|
if e == "ORDER_TRADE_UPDATE":
|
||||||
|
logger.debug(f"UserDataStream: 收到 ORDER_TRADE_UPDATE 推送")
|
||||||
self._on_order_trade_update(data.get("o") or {})
|
self._on_order_trade_update(data.get("o") or {})
|
||||||
elif e == "ACCOUNT_UPDATE":
|
elif e == "ACCOUNT_UPDATE":
|
||||||
|
logger.debug(f"UserDataStream: 收到 ACCOUNT_UPDATE 推送")
|
||||||
self._on_account_update(data.get("a") or {})
|
self._on_account_update(data.get("a") or {})
|
||||||
elif e == "ALGO_UPDATE":
|
elif e == "ALGO_UPDATE":
|
||||||
|
logger.debug(f"UserDataStream: 收到 ALGO_UPDATE 推送")
|
||||||
self._on_algo_update(data.get("o") or {})
|
self._on_algo_update(data.get("o") or {})
|
||||||
|
else:
|
||||||
|
logger.debug(f"UserDataStream: 收到未知事件类型: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _on_order_trade_update(self, o: Dict):
|
def _on_order_trade_update(self, o: Dict):
|
||||||
|
|
@ -267,7 +273,11 @@ class UserDataStream:
|
||||||
# ap=均价, z=累计成交量, R=只减仓, rp=该交易实现盈亏, s=交易对
|
# ap=均价, z=累计成交量, R=只减仓, rp=该交易实现盈亏, s=交易对
|
||||||
event_type = (o.get("x") or "").strip().upper()
|
event_type = (o.get("x") or "").strip().upper()
|
||||||
status = (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":
|
if status != "FILLED":
|
||||||
|
logger.debug(f"UserDataStream: 订单状态非 FILLED,跳过 symbol={symbol!r} orderId={order_id} status={status}")
|
||||||
return
|
return
|
||||||
client_order_id = (o.get("c") or "").strip() or None
|
client_order_id = (o.get("c") or "").strip() or None
|
||||||
order_id = o.get("i")
|
order_id = o.get("i")
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user