a
This commit is contained in:
parent
c7444d884c
commit
f737c32ea2
|
|
@ -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)}")
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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'])
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user