This commit is contained in:
薇薇安 2026-01-16 11:45:43 +08:00
parent c7444d884c
commit f737c32ea2
4 changed files with 265 additions and 17 deletions

View File

@ -7,6 +7,7 @@ from datetime import datetime, timedelta
import sys import sys
from pathlib import Path from pathlib import Path
import logging import logging
import asyncio
project_root = Path(__file__).parent.parent.parent.parent project_root = Path(__file__).parent.parent.parent.parent
sys.path.insert(0, str(project_root)) sys.path.insert(0, str(project_root))
@ -173,3 +174,164 @@ async def get_trade_stats(
except Exception as e: except Exception as e:
logger.error(f"获取交易统计失败: {e}", exc_info=True) logger.error(f"获取交易统计失败: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))
@router.post("/sync-binance")
async def sync_trades_from_binance(
days: int = Query(7, ge=1, le=30, description="同步最近N天的订单")
):
"""
从币安同步历史订单确保数据库与币安一致
Args:
days: 同步最近N天的订单默认7天
"""
try:
logger.info(f"开始从币安同步历史订单(最近{days}天)...")
# 导入必要的模块
trading_system_path = project_root / 'trading_system'
if not trading_system_path.exists():
alternative_path = project_root / 'backend' / 'trading_system'
if alternative_path.exists():
trading_system_path = alternative_path
else:
raise HTTPException(status_code=500, detail="交易系统模块不存在")
sys.path.insert(0, str(trading_system_path))
sys.path.insert(0, str(project_root))
from binance_client import BinanceClient
import config
# 初始化客户端
client = BinanceClient(
api_key=config.BINANCE_API_KEY,
api_secret=config.BINANCE_API_SECRET,
testnet=config.USE_TESTNET
)
await client.connect()
try:
import time
from datetime import datetime, timedelta
# 计算时间范围
end_time = int(time.time() * 1000) # 当前时间(毫秒)
start_time = int((datetime.now() - timedelta(days=days)).timestamp() * 1000)
# 获取所有已成交的订单(包括开仓和平仓)
all_orders = []
try:
# 获取所有交易对的订单
# 注意币安API可能需要分交易对查询这里先获取所有交易对
symbols = await client.client.futures_exchange_info()
symbol_list = [s['symbol'] for s in symbols.get('symbols', []) if s.get('contractType') == 'PERPETUAL']
logger.info(f"开始同步 {len(symbol_list)} 个交易对的订单...")
for symbol in symbol_list:
try:
# 获取该交易对的历史订单
orders = await client.client.futures_get_all_orders(
symbol=symbol,
startTime=start_time,
endTime=end_time
)
# 只保留已成交的订单
filled_orders = [o for o in orders if o.get('status') == 'FILLED']
all_orders.extend(filled_orders)
# 避免请求过快
await asyncio.sleep(0.1)
except Exception as e:
logger.debug(f"获取 {symbol} 订单失败: {e}")
continue
logger.info(f"从币安获取到 {len(all_orders)} 个已成交订单")
except Exception as e:
logger.error(f"获取币安订单失败: {e}")
raise HTTPException(status_code=500, detail=f"获取币安订单失败: {str(e)}")
# 同步订单到数据库
synced_count = 0
updated_count = 0
# 按时间排序,从旧到新
all_orders.sort(key=lambda x: x.get('time', 0))
for order in all_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)
reduce_only = order.get('reduceOnly', False)
if quantity <= 0 or avg_price <= 0:
continue
try:
if reduce_only:
# 这是平仓订单,更新数据库中的对应记录
# 查找数据库中该交易对的open状态记录
open_trades = Trade.get_by_symbol(symbol, status='open')
if open_trades:
# 找到匹配的交易记录通过symbol匹配如果有多个则取最近的
trade = open_trades[0] # 取第一个
trade_id = trade['id']
# 计算盈亏
entry_price = float(trade['entry_price'])
entry_quantity = float(trade['quantity'])
# 使用实际成交数量(可能部分平仓)
actual_quantity = min(quantity, entry_quantity)
if trade['side'] == 'BUY':
pnl = (avg_price - entry_price) * actual_quantity
pnl_percent = ((avg_price - entry_price) / entry_price) * 100
else: # SELL
pnl = (entry_price - avg_price) * actual_quantity
pnl_percent = ((entry_price - avg_price) / entry_price) * 100
# 更新数据库
Trade.update_exit(
trade_id=trade_id,
exit_price=avg_price,
exit_reason='sync',
pnl=pnl,
pnl_percent=pnl_percent
)
updated_count += 1
logger.debug(f"✓ 更新平仓记录: {symbol} (ID: {trade_id}, 成交价: {avg_price:.4f})")
else:
# 这是开仓订单,检查数据库中是否已存在
# 这里可以添加逻辑来创建缺失的开仓记录
# 但为了简化,暂时只处理平仓订单
pass
except Exception as e:
logger.warning(f"同步订单失败 {symbol} (订单ID: {order_id}): {e}")
continue
result = {
"success": True,
"message": f"同步完成:更新了 {updated_count} 条平仓记录",
"total_orders": len(all_orders),
"updated_trades": updated_count
}
logger.info(f"✓ 同步完成:处理了 {len(all_orders)} 个订单,更新了 {updated_count} 条记录")
return result
finally:
await client.disconnect()
except HTTPException:
raise
except Exception as e:
logger.error(f"同步币安订单失败: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"同步币安订单失败: {str(e)}")

View File

@ -136,7 +136,8 @@ class Trade:
query += " AND exit_reason = %s" query += " AND exit_reason = %s"
params.append(exit_reason) params.append(exit_reason)
query += " ORDER BY entry_time DESC" # 按平仓时间倒序排序,如果没有平仓时间则按入场时间倒序
query += " ORDER BY COALESCE(exit_time, entry_time) DESC, entry_time DESC"
return db.execute_query(query, params) return db.execute_query(query, params)
@staticmethod @staticmethod

View File

@ -250,7 +250,8 @@ const TradeList = () => {
<th>盈亏比例</th> <th>盈亏比例</th>
<th>状态</th> <th>状态</th>
<th>平仓类型</th> <th>平仓类型</th>
<th>时间</th> <th>入场时间</th>
<th>平仓时间</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -304,6 +305,7 @@ const TradeList = () => {
</td> </td>
<td>{trade.exit_reason_display || '-'}</td> <td>{trade.exit_reason_display || '-'}</td>
<td>{formatTime(trade.entry_time)}</td> <td>{formatTime(trade.entry_time)}</td>
<td>{trade.exit_time ? formatTime(trade.exit_time) : '-'}</td>
</tr> </tr>
) )
})} })}
@ -385,9 +387,15 @@ const TradeList = () => {
)} )}
</div> </div>
<div className="trade-card-footer"> <div className="trade-card-footer">
<div className="trade-time-item">
<span className="time-label">入场:</span>
<span>{formatTime(trade.entry_time)}</span> <span>{formatTime(trade.entry_time)}</span>
</div>
{trade.exit_time && ( {trade.exit_time && (
<span>平仓: {formatTime(trade.exit_time)}</span> <div className="trade-time-item">
<span className="time-label">平仓:</span>
<span>{formatTime(trade.exit_time)}</span>
</div>
)} )}
</div> </div>
</div> </div>

View File

@ -347,14 +347,55 @@ class PositionManager:
raise # 重新抛出异常,让外层捕获 raise # 重新抛出异常,让外层捕获
if order: if order:
logger.info(f"{symbol} [平仓] ✓ 平仓订单已提交 (订单ID: {order.get('orderId', 'N/A')})") order_id = order.get('orderId')
# 获取平仓价格确保是float类型 logger.info(f"{symbol} [平仓] ✓ 平仓订单已提交 (订单ID: {order_id})")
ticker = await self.client.get_ticker_24h(symbol)
if not ticker: # 等待订单成交,然后从币安获取实际成交价格
logger.warning(f"无法获取 {symbol} 价格,使用订单价格") exit_price = None
exit_price = float(order.get('avgPrice', 0)) or float(order.get('price', 0)) try:
# 等待一小段时间让订单成交
await asyncio.sleep(1)
# 从币安获取订单详情,获取实际成交价格
try:
order_info = await self.client.client.futures_get_order(symbol=symbol, orderId=order_id)
if order_info:
# 优先使用平均成交价格avgPrice如果没有则使用价格字段
exit_price = float(order_info.get('avgPrice', 0)) or float(order_info.get('price', 0))
if exit_price > 0:
logger.info(f"{symbol} [平仓] 从币安订单获取实际成交价格: {exit_price:.4f} USDT")
else: else:
# 如果订单还没有完全成交,尝试从成交记录获取
if order_info.get('status') == 'FILLED' and order_info.get('fills'):
# 计算加权平均成交价格
total_qty = 0
total_value = 0
for fill in order_info.get('fills', []):
qty = float(fill.get('qty', 0))
price = float(fill.get('price', 0))
total_qty += qty
total_value += qty * price
if total_qty > 0:
exit_price = total_value / total_qty
logger.info(f"{symbol} [平仓] 从成交记录计算平均成交价格: {exit_price:.4f} USDT")
except Exception as order_error:
logger.warning(f"{symbol} [平仓] 获取订单详情失败: {order_error},使用备用方法")
# 如果无法从订单获取价格,使用当前价格作为备用
if not exit_price or exit_price <= 0:
ticker = await self.client.get_ticker_24h(symbol)
if ticker:
exit_price = float(ticker['price']) exit_price = float(ticker['price'])
logger.warning(f"{symbol} [平仓] 使用当前价格作为平仓价格: {exit_price:.4f} USDT")
else:
exit_price = float(order.get('avgPrice', 0)) or float(order.get('price', 0))
if exit_price <= 0:
logger.error(f"{symbol} [平仓] 无法获取平仓价格,使用订单价格字段")
exit_price = float(order.get('price', 0))
except Exception as price_error:
logger.warning(f"{symbol} [平仓] 获取成交价格时出错: {price_error},使用当前价格")
ticker = await self.client.get_ticker_24h(symbol)
exit_price = float(ticker['price']) if ticker else float(order.get('price', 0))
# 更新数据库记录 # 更新数据库记录
if DB_AVAILABLE and Trade and symbol in self.active_positions: if DB_AVAILABLE and Trade and symbol in self.active_positions:
@ -825,9 +866,45 @@ class PositionManager:
try: try:
logger.info(f"{symbol} [状态同步] 更新交易记录状态 (ID: {trade_id})...") logger.info(f"{symbol} [状态同步] 更新交易记录状态 (ID: {trade_id})...")
# 获取当前价格作为平仓价格 # 尝试从币安历史订单获取实际平仓价格
exit_price = None
try:
# 获取最近的平仓订单reduceOnly=True的订单
import time
end_time = int(time.time() * 1000) # 当前时间(毫秒)
start_time = end_time - (7 * 24 * 60 * 60 * 1000) # 最近7天
# 获取历史订单
orders = await self.client.client.futures_get_all_orders(
symbol=symbol,
startTime=start_time,
endTime=end_time
)
# 查找最近的平仓订单reduceOnly=True且已成交
close_orders = [
o for o in orders
if o.get('reduceOnly') == True
and o.get('status') == 'FILLED'
]
if close_orders:
# 按时间倒序排序,取最近的
close_orders.sort(key=lambda x: x.get('updateTime', 0), reverse=True)
latest_order = close_orders[0]
# 获取平均成交价格
exit_price = float(latest_order.get('avgPrice', 0))
if exit_price > 0:
logger.info(f"{symbol} [状态同步] 从币安历史订单获取平仓价格: {exit_price:.4f} USDT")
except Exception as order_error:
logger.debug(f"{symbol} [状态同步] 获取历史订单失败: {order_error}")
# 如果无法从订单获取,使用当前价格
if not exit_price or exit_price <= 0:
ticker = await self.client.get_ticker_24h(symbol) ticker = await self.client.get_ticker_24h(symbol)
exit_price = float(ticker['price']) if ticker else float(trade['entry_price']) exit_price = float(ticker['price']) if ticker else float(trade['entry_price'])
logger.warning(f"{symbol} [状态同步] 使用当前价格作为平仓价格: {exit_price:.4f} USDT")
# 计算盈亏确保所有值都是float类型避免Decimal类型问题 # 计算盈亏确保所有值都是float类型避免Decimal类型问题
entry_price = float(trade['entry_price']) entry_price = float(trade['entry_price'])