feat(trades, trade_list): 增强订单同步功能与用户界面优化
在 `trades.py` 中更新 `sync_trades_from_binance` 方法,改进日志记录以区分全量与增量同步模式,并添加对获取订单数为零的警告处理。更新 `TradeList.jsx` 组件,优化用户界面,新增订单同步选项和状态显示,提升用户体验。此改动增强了系统的灵活性和数据完整性。
This commit is contained in:
parent
1430ddc532
commit
60a7e15100
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(含入场/离场原因、入场思路等),便于后续分析">
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user