feat(trades, trade_list): 增强订单同步功能与用户界面优化

在 `trades.py` 中更新 `sync_trades_from_binance` 方法,改进日志记录以区分全量与增量同步模式,并添加对获取订单数为零的警告处理。更新 `TradeList.jsx` 组件,优化用户界面,新增订单同步选项和状态显示,提升用户体验。此改动增强了系统的灵活性和数据完整性。
This commit is contained in:
薇薇安 2026-02-17 23:02:49 +08:00
parent 1430ddc532
commit 60a7e15100
2 changed files with 111 additions and 59 deletions

View File

@ -501,8 +501,12 @@ async def sync_trades_from_binance(
# 仅对上述 symbol 拉取订单
all_orders = []
try:
logger.info(f"仅对 {len(symbol_list)} 个有 DB 记录的 symbol 拉取订单(时间范围: {days}天,{datetime.fromtimestamp(start_ts_sec)}{datetime.fromtimestamp(end_ts_sec)}")
for symbol in symbol_list:
if sync_all_symbols:
logger.info(f"全量同步模式:对 {len(symbol_list)} 个交易对拉取订单(时间范围: {days}天,{datetime.fromtimestamp(start_ts_sec)}{datetime.fromtimestamp(end_ts_sec)}")
else:
logger.info(f"增量同步模式:对 {len(symbol_list)} 个有 DB 记录的 symbol 拉取订单(时间范围: {days}天,{datetime.fromtimestamp(start_ts_sec)}{datetime.fromtimestamp(end_ts_sec)}")
for idx, symbol in enumerate(symbol_list, 1):
try:
orders = await client.client.futures_get_all_orders(
symbol=symbol,
@ -511,17 +515,35 @@ async def sync_trades_from_binance(
)
filled_orders = [o for o in orders if o.get('status') == 'FILLED']
if filled_orders:
logger.debug(f" {symbol}: 获取到 {len(filled_orders)} 个已成交订单")
logger.info(f" [{idx}/{len(symbol_list)}] {symbol}: 获取到 {len(filled_orders)} 个已成交订单")
elif orders:
logger.debug(f" [{idx}/{len(symbol_list)}] {symbol}: 获取到 {len(orders)} 个订单,但无已成交订单")
all_orders.extend(filled_orders)
await asyncio.sleep(0.1)
except Exception as e:
logger.warning(f"获取 {symbol} 订单失败: {e}")
logger.warning(f" [{idx}/{len(symbol_list)}] 获取 {symbol} 订单失败: {e}")
continue
logger.info(f"从币安获取到 {len(all_orders)} 个已成交订单(涉及 {len(symbol_list)} 个交易对)")
logger.info(f"从币安获取到 {len(all_orders)} 个已成交订单(涉及 {len(symbol_list)} 个交易对)")
except Exception as e:
logger.error(f"获取币安订单失败: {e}")
logger.error(f"获取币安订单失败: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"获取币安订单失败: {str(e)}")
if len(all_orders) == 0:
logger.warning(f"⚠️ 从币安获取到的订单数为 0可能原因1) 时间范围内确实没有订单 2) 交易对列表为空 3) API 调用失败")
return {
"success": True,
"message": f"从币安获取到 0 个订单(时间范围: {days}天,交易对数量: {len(symbol_list)})。可能原因:时间范围内确实没有订单,或请检查币安 API 连接。",
"total_orders": 0,
"updated_trades": 0,
"created_trades": 0,
"entry_order_id_filled": 0,
"exit_order_id_filled": 0,
"skipped_existing": 0,
"skipped_no_match": 0,
"close_orders": 0,
"open_orders": 0,
}
# 同步订单到数据库(仅当前账号)
synced_count = 0
updated_count = 0
@ -541,6 +563,7 @@ async def sync_trades_from_binance(
logger.info(f"开始同步:平仓订单 {len(close_orders)} 个,开仓订单 {len(open_orders)}")
# 1. 处理平仓订单
logger.info(f"开始处理 {len(close_orders)} 个平仓订单...")
for order in close_orders:
symbol = order.get('symbol')
order_id = order.get('orderId')
@ -562,8 +585,7 @@ async def sync_trades_from_binance(
continue
try:
if reduce_only:
# 这是平仓订单
# 这是平仓订单close_orders 已经筛选出 reduceOnly=True 的订单)
# 首先检查是否已经通过订单号同步过(避免重复)
existing_trade = Trade.get_by_exit_order_id(order_id)
# 如果已有 exit_order_id 且 exit_reason 不是 sync说明已完整同步跳过
@ -725,15 +747,29 @@ async def sync_trades_from_binance(
else:
skipped_no_match += 1
logger.debug(f"平仓订单 {order_id} ({symbol}) 无法匹配到现有记录(无 open 状态且无 exit_order_id 为空的 closed 记录),跳过")
# 2. 处理开仓订单
logger.info(f"开始处理 {len(open_orders)} 个开仓订单...")
for order in open_orders:
symbol = order.get('symbol')
order_id = order.get('orderId')
side = order.get('side')
quantity = float(order.get('executedQty', 0))
avg_price = float(order.get('avgPrice', 0))
order_time = datetime.fromtimestamp(order.get('time', 0) / 1000)
otype = str(order.get('type') or order.get('origType') or '').upper()
if quantity <= 0 or avg_price <= 0:
continue
try:
# 这是开仓订单
existing_trade = Trade.get_by_entry_order_id(order_id)
if existing_trade:
# 如果已存在,跳过(开仓订单信息通常已完整)
logger.debug(f"开仓订单 {order_id} 已存在,跳过")
else:
# 这是开仓订单
existing_trade = Trade.get_by_entry_order_id(order_id)
if existing_trade:
# 如果已存在,跳过(开仓订单信息通常已完整)
logger.debug(f"开仓订单 {order_id} 已存在,跳过")
else:
# 如果不存在,尝试查找没有 entry_order_id 的记录并补全,或创建新记录
try:
# 如果不存在,尝试查找没有 entry_order_id 的记录并补全,或创建新记录
try:
# 查找该 symbol 下没有 entry_order_id 的记录(按时间匹配)
order_time_ms = order.get('time', 0)
order_time_sec = order_time_ms // 1000 if order_time_ms > 0 else 0

View File

@ -354,10 +354,65 @@ const TradeList = () => {
return (
<div className="trade-list">
<h2>交易记录</h2>
<p style={{ color: '#666', fontSize: '14px', marginTop: '-10px', marginBottom: '20px' }}>
说明每条记录代表一笔完整的交易开仓+平仓统计总盈亏时每条记录只计算一次默认仅可对账只显示有开仓/平仓订单号的记录统计与币安一致默认按平仓时间今天= 今天平掉的单 + 今天开的未平仓
</p>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '10px' }}>
<div>
<h2 style={{ margin: 0 }}>交易记录</h2>
<p style={{ color: '#666', fontSize: '14px', marginTop: '5px', marginBottom: '0' }}>
说明每条记录代表一笔完整的交易开仓+平仓统计总盈亏时每条记录只计算一次默认仅可对账只显示有开仓/平仓订单号的记录统计与币安一致默认按平仓时间今天= 今天平掉的单 + 今天开的未平仓
</p>
</div>
<div style={{
display: 'flex',
flexDirection: 'column',
gap: '8px',
alignItems: 'flex-end',
padding: '10px',
backgroundColor: '#f5f5f5',
borderRadius: '4px',
border: '1px solid #ddd'
}}>
<div style={{ fontSize: '12px', fontWeight: 'bold', color: '#666', marginBottom: '4px' }}>订单同步</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', flexWrap: 'wrap' }}>
<select
value={syncDays}
onChange={(e) => setSyncDays(Number(e.target.value))}
disabled={syncing}
style={{ padding: '4px 8px', fontSize: '12px', width: '70px' }}
>
<option value={3}>3</option>
<option value={7}>7</option>
<option value={14}>14</option>
<option value={30}>30</option>
</select>
<label style={{ display: 'flex', alignItems: 'center', gap: '4px', fontSize: '12px', cursor: 'pointer' }}>
<input
type="checkbox"
checked={syncAllSymbols}
onChange={(e) => setSyncAllSymbols(e.target.checked)}
disabled={syncing}
style={{ cursor: 'pointer' }}
/>
<span title="勾选后将同步所有交易对的订单(不限于数据库中的),用于补全缺失的交易记录">全量同步</span>
</label>
<button
onClick={handleSyncOrders}
disabled={syncing}
style={{
backgroundColor: syncing ? '#ccc' : '#4CAF50',
color: 'white',
border: 'none',
padding: '6px 12px',
fontSize: '12px',
borderRadius: '4px',
cursor: syncing ? 'not-allowed' : 'pointer'
}}
title={syncAllSymbols ? "从币安同步所有交易对的历史订单,并创建缺失的交易记录(会请求大量数据)" : "从币安同步历史订单,补全缺失的订单号(仅限数据库中已有的交易对,不会重复同步)"}
>
{syncing ? '同步中...' : '同步订单'}
</button>
</div>
</div>
</div>
{/* 筛选面板 */}
<div className="filter-panel">
@ -527,45 +582,6 @@ const TradeList = () => {
<button className="btn-secondary" onClick={handleReset}>
重置
</button>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginLeft: '10px', flexWrap: 'wrap' }}>
<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>
<label style={{ display: 'flex', alignItems: 'center', gap: '4px', fontSize: '12px', cursor: 'pointer', marginLeft: '8px' }}>
<input
type="checkbox"
checked={syncAllSymbols}
onChange={(e) => setSyncAllSymbols(e.target.checked)}
disabled={syncing}
style={{ cursor: 'pointer' }}
/>
<span title="勾选后将同步所有交易对的订单(不限于数据库中的),用于补全缺失的交易记录">同步所有交易对</span>
</label>
<button
className="btn-secondary"
onClick={handleSyncOrders}
disabled={syncing}
style={{
backgroundColor: syncing ? '#ccc' : '#4CAF50',
color: 'white',
border: 'none',
padding: '6px 12px',
fontSize: '12px'
}}
title={syncAllSymbols ? "从币安同步所有交易对的历史订单,并创建缺失的交易记录(会请求大量数据)" : "从币安同步历史订单,补全缺失的订单号(仅限数据库中已有的交易对,不会重复同步)"}
>
{syncing ? '同步中...' : '同步订单'}
</button>
</div>
{trades.length > 0 && (
<>
<button className="btn-export" onClick={() => handleExport('csv')} title="导出Excel/CSV含入场/离场原因、入场思路等),便于后续分析">