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 拉取订单
|
# 仅对上述 symbol 拉取订单
|
||||||
all_orders = []
|
all_orders = []
|
||||||
try:
|
try:
|
||||||
logger.info(f"仅对 {len(symbol_list)} 个有 DB 记录的 symbol 拉取订单(时间范围: {days}天,{datetime.fromtimestamp(start_ts_sec)} 至 {datetime.fromtimestamp(end_ts_sec)})")
|
if sync_all_symbols:
|
||||||
for symbol in symbol_list:
|
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:
|
try:
|
||||||
orders = await client.client.futures_get_all_orders(
|
orders = await client.client.futures_get_all_orders(
|
||||||
symbol=symbol,
|
symbol=symbol,
|
||||||
|
|
@ -511,17 +515,35 @@ async def sync_trades_from_binance(
|
||||||
)
|
)
|
||||||
filled_orders = [o for o in orders if o.get('status') == 'FILLED']
|
filled_orders = [o for o in orders if o.get('status') == 'FILLED']
|
||||||
if filled_orders:
|
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)
|
all_orders.extend(filled_orders)
|
||||||
await asyncio.sleep(0.1)
|
await asyncio.sleep(0.1)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"获取 {symbol} 订单失败: {e}")
|
logger.warning(f" [{idx}/{len(symbol_list)}] 获取 {symbol} 订单失败: {e}")
|
||||||
continue
|
continue
|
||||||
logger.info(f"从币安获取到 {len(all_orders)} 个已成交订单(涉及 {len(symbol_list)} 个交易对)")
|
logger.info(f"✓ 从币安获取到 {len(all_orders)} 个已成交订单(涉及 {len(symbol_list)} 个交易对)")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"获取币安订单失败: {e}")
|
logger.error(f"获取币安订单失败: {e}", exc_info=True)
|
||||||
raise HTTPException(status_code=500, detail=f"获取币安订单失败: {str(e)}")
|
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
|
synced_count = 0
|
||||||
updated_count = 0
|
updated_count = 0
|
||||||
|
|
@ -541,6 +563,7 @@ async def sync_trades_from_binance(
|
||||||
logger.info(f"开始同步:平仓订单 {len(close_orders)} 个,开仓订单 {len(open_orders)} 个")
|
logger.info(f"开始同步:平仓订单 {len(close_orders)} 个,开仓订单 {len(open_orders)} 个")
|
||||||
|
|
||||||
# 1. 处理平仓订单
|
# 1. 处理平仓订单
|
||||||
|
logger.info(f"开始处理 {len(close_orders)} 个平仓订单...")
|
||||||
for order in close_orders:
|
for order in close_orders:
|
||||||
symbol = order.get('symbol')
|
symbol = order.get('symbol')
|
||||||
order_id = order.get('orderId')
|
order_id = order.get('orderId')
|
||||||
|
|
@ -562,8 +585,7 @@ async def sync_trades_from_binance(
|
||||||
continue
|
continue
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if reduce_only:
|
# 这是平仓订单(close_orders 已经筛选出 reduceOnly=True 的订单)
|
||||||
# 这是平仓订单
|
|
||||||
# 首先检查是否已经通过订单号同步过(避免重复)
|
# 首先检查是否已经通过订单号同步过(避免重复)
|
||||||
existing_trade = Trade.get_by_exit_order_id(order_id)
|
existing_trade = Trade.get_by_exit_order_id(order_id)
|
||||||
# 如果已有 exit_order_id 且 exit_reason 不是 sync,说明已完整同步,跳过
|
# 如果已有 exit_order_id 且 exit_reason 不是 sync,说明已完整同步,跳过
|
||||||
|
|
@ -725,15 +747,29 @@ async def sync_trades_from_binance(
|
||||||
else:
|
else:
|
||||||
skipped_no_match += 1
|
skipped_no_match += 1
|
||||||
logger.debug(f"平仓订单 {order_id} ({symbol}) 无法匹配到现有记录(无 open 状态且无 exit_order_id 为空的 closed 记录),跳过")
|
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:
|
else:
|
||||||
# 这是开仓订单
|
# 如果不存在,尝试查找没有 entry_order_id 的记录并补全,或创建新记录
|
||||||
existing_trade = Trade.get_by_entry_order_id(order_id)
|
try:
|
||||||
if existing_trade:
|
|
||||||
# 如果已存在,跳过(开仓订单信息通常已完整)
|
|
||||||
logger.debug(f"开仓订单 {order_id} 已存在,跳过")
|
|
||||||
else:
|
|
||||||
# 如果不存在,尝试查找没有 entry_order_id 的记录并补全,或创建新记录
|
|
||||||
try:
|
|
||||||
# 查找该 symbol 下没有 entry_order_id 的记录(按时间匹配)
|
# 查找该 symbol 下没有 entry_order_id 的记录(按时间匹配)
|
||||||
order_time_ms = order.get('time', 0)
|
order_time_ms = order.get('time', 0)
|
||||||
order_time_sec = order_time_ms // 1000 if order_time_ms > 0 else 0
|
order_time_sec = order_time_ms // 1000 if order_time_ms > 0 else 0
|
||||||
|
|
|
||||||
|
|
@ -354,10 +354,65 @@ const TradeList = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="trade-list">
|
<div className="trade-list">
|
||||||
<h2>交易记录</h2>
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '10px' }}>
|
||||||
<p style={{ color: '#666', fontSize: '14px', marginTop: '-10px', marginBottom: '20px' }}>
|
<div>
|
||||||
说明:每条记录代表一笔完整的交易(开仓+平仓),统计总盈亏时每条记录只计算一次。默认「仅可对账」:只显示有开仓/平仓订单号的记录,统计与币安一致。默认「按平仓时间」:选「今天」= 今天平掉的单 + 今天开的未平仓。
|
<h2 style={{ margin: 0 }}>交易记录</h2>
|
||||||
</p>
|
<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">
|
<div className="filter-panel">
|
||||||
|
|
@ -527,45 +582,6 @@ 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', 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 && (
|
{trades.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<button className="btn-export" onClick={() => handleExport('csv')} title="导出Excel/CSV(含入场/离场原因、入场思路等),便于后续分析">
|
<button className="btn-export" onClick={() => handleExport('csv')} title="导出Excel/CSV(含入场/离场原因、入场思路等),便于后续分析">
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user