1
This commit is contained in:
parent
b0392f358e
commit
c7f1361d99
103
backend/TROUBLESHOOTING.md
Normal file
103
backend/TROUBLESHOOTING.md
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
# Backend 启动问题排查指南
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 1. 语法错误
|
||||
**错误信息**: `SyntaxError: expected 'except' or 'finally' block`
|
||||
|
||||
**解决方法**:
|
||||
- 检查代码中的 `try:` 块是否都有对应的 `except` 或 `finally`
|
||||
- 检查缩进是否正确
|
||||
- 运行 `python3 -m py_compile api/routes/trades.py` 检查语法
|
||||
|
||||
### 2. 缺少依赖模块
|
||||
**错误信息**: `ModuleNotFoundError: No module named 'xxx'`
|
||||
|
||||
**解决方法**:
|
||||
```bash
|
||||
# 激活虚拟环境
|
||||
source ../.venv/bin/activate # 或 source .venv/bin/activate
|
||||
|
||||
# 安装依赖
|
||||
pip install -r requirements.txt
|
||||
|
||||
# 或者单独安装缺失的模块
|
||||
pip install python-jose[cryptography]
|
||||
```
|
||||
|
||||
### 3. 导入错误
|
||||
**错误信息**: `ModuleNotFoundError: No module named 'api'`
|
||||
|
||||
**解决方法**:
|
||||
- 确保在 `backend` 目录下运行
|
||||
- 检查 `PYTHONPATH` 是否正确设置
|
||||
- 使用 `cd backend && python3 -m uvicorn api.main:app` 启动
|
||||
|
||||
## 排查步骤
|
||||
|
||||
### 步骤 1: 检查依赖
|
||||
```bash
|
||||
cd backend
|
||||
bash check_dependencies.sh
|
||||
```
|
||||
|
||||
### 步骤 2: 检查语法
|
||||
```bash
|
||||
cd backend
|
||||
source ../.venv/bin/activate
|
||||
python3 -m py_compile api/main.py
|
||||
python3 -m py_compile api/routes/*.py
|
||||
```
|
||||
|
||||
### 步骤 3: 测试导入
|
||||
```bash
|
||||
cd backend
|
||||
source ../.venv/bin/activate
|
||||
python3 -c "import api.main; print('✓ 导入成功')"
|
||||
```
|
||||
|
||||
### 步骤 4: 查看日志
|
||||
```bash
|
||||
# 查看最新的错误日志
|
||||
tail -50 backend/logs/api.log
|
||||
tail -50 backend/logs/uvicorn.log
|
||||
```
|
||||
|
||||
### 步骤 5: 手动启动测试
|
||||
```bash
|
||||
cd backend
|
||||
source ../.venv/bin/activate
|
||||
export DB_HOST=your_db_host
|
||||
export DB_PORT=3306
|
||||
export DB_USER=your_db_user
|
||||
export DB_PASSWORD=your_db_password
|
||||
export DB_NAME=auto_trade_sys
|
||||
uvicorn api.main:app --host 0.0.0.0 --port 8001 --log-level info
|
||||
```
|
||||
|
||||
## 启动脚本
|
||||
|
||||
### 开发模式(自动重载)
|
||||
```bash
|
||||
cd backend
|
||||
./start_dev.sh
|
||||
```
|
||||
|
||||
### 生产模式(后台运行)
|
||||
```bash
|
||||
cd backend
|
||||
./start.sh
|
||||
```
|
||||
|
||||
## 检查服务状态
|
||||
|
||||
```bash
|
||||
# 检查进程
|
||||
ps aux | grep uvicorn
|
||||
|
||||
# 检查端口
|
||||
lsof -i :8001
|
||||
|
||||
# 测试健康检查
|
||||
curl http://localhost:8001/api/health
|
||||
```
|
||||
|
|
@ -588,167 +588,170 @@ async def sync_trades_from_binance(
|
|||
|
||||
try:
|
||||
# 这是平仓订单(close_orders 已经筛选出 reduceOnly=True 的订单)
|
||||
# 首先检查是否已经通过订单号同步过(避免重复)
|
||||
existing_trade = Trade.get_by_exit_order_id(order_id)
|
||||
# 如果已有 exit_order_id 且 exit_reason 不是 sync,说明已完整同步,跳过
|
||||
if existing_trade and existing_trade.get("exit_order_id") and existing_trade.get("exit_reason") not in (None, "", "sync"):
|
||||
skipped_existing += 1
|
||||
logger.debug(f"订单 {order_id} 已完整同步过(exit_reason={existing_trade.get('exit_reason')}),跳过")
|
||||
continue
|
||||
|
||||
# 查找数据库中该交易对的 open 状态记录(仅当前账号),或已平仓但 exit_order_id 为空的记录
|
||||
open_trades = Trade.get_by_symbol(symbol, status='open', account_id=account_id or DEFAULT_ACCOUNT_ID)
|
||||
closed_trades_no_exit_id = []
|
||||
if not existing_trade and not open_trades:
|
||||
# 如果没有 open 记录,查找已平仓但 exit_order_id 为空的记录
|
||||
try:
|
||||
closed_trades = Trade.get_by_symbol(symbol, status='closed', account_id=account_id or DEFAULT_ACCOUNT_ID)
|
||||
closed_trades_no_exit_id = [
|
||||
t for t in closed_trades
|
||||
if not t.get("exit_order_id") or str(t.get("exit_order_id")).strip() in ("", "0")
|
||||
]
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if existing_trade or open_trades or closed_trades_no_exit_id:
|
||||
# 找到匹配的交易记录(优先用 existing_trade,其次 open_trades,最后 closed_trades_no_exit_id)
|
||||
if existing_trade:
|
||||
trade = existing_trade
|
||||
elif open_trades:
|
||||
trade = open_trades[0] # 取第一个 open 记录
|
||||
else:
|
||||
# 从已平仓但无 exit_order_id 的记录中选择(按 entry_time 最近的一条)
|
||||
closed_trades_no_exit_id.sort(key=lambda x: x.get('entry_time', 0) or 0, reverse=True)
|
||||
trade = closed_trades_no_exit_id[0]
|
||||
trade_id = trade['id']
|
||||
|
||||
# 如果之前没有 exit_order_id,记录为补全
|
||||
if not trade.get("exit_order_id") or str(trade.get("exit_order_id")).strip() in ("", "0"):
|
||||
exit_order_id_filled += 1
|
||||
|
||||
# 计算盈亏
|
||||
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
|
||||
|
||||
# 细分 exit_reason:优先使用币安订单类型,其次用价格接近止损/止盈做兜底
|
||||
exit_reason = "sync"
|
||||
# 检查订单的 reduceOnly 字段:如果是 true,说明是自动平仓,不应该标记为 manual
|
||||
is_reduce_only = order.get("reduceOnly", False) if isinstance(order, dict) else False
|
||||
|
||||
if "TRAILING" in otype:
|
||||
exit_reason = "trailing_stop"
|
||||
elif "TAKE_PROFIT" in otype:
|
||||
exit_reason = "take_profit"
|
||||
elif "STOP" in otype:
|
||||
exit_reason = "stop_loss"
|
||||
elif otype in ("MARKET", "LIMIT"):
|
||||
# 如果是 reduceOnly 订单,说明是自动平仓(可能是保护单触发的),先标记为 sync,后续用价格判断
|
||||
if is_reduce_only:
|
||||
exit_reason = "sync" # 临时标记,后续用价格判断
|
||||
else:
|
||||
exit_reason = "manual" # 非 reduceOnly 的 MARKET/LIMIT 订单才是真正的手动平仓
|
||||
|
||||
try:
|
||||
def _close_to(a: float, b: float, max_pct: float = 0.02) -> bool: # 放宽到2%,因为滑点可能导致价格不完全一致
|
||||
if a <= 0 or b <= 0:
|
||||
return False
|
||||
return abs((a - b) / b) <= max_pct
|
||||
|
||||
ep = float(avg_price or 0)
|
||||
if ep > 0:
|
||||
sl = trade.get("stop_loss_price")
|
||||
tp = trade.get("take_profit_price")
|
||||
tp1 = trade.get("take_profit_1")
|
||||
tp2 = trade.get("take_profit_2")
|
||||
# 优先检查止损
|
||||
if sl is not None and _close_to(ep, float(sl), max_pct=0.02):
|
||||
exit_reason = "stop_loss"
|
||||
# 然后检查止盈
|
||||
elif tp is not None and _close_to(ep, float(tp), max_pct=0.02):
|
||||
exit_reason = "take_profit"
|
||||
elif tp1 is not None and _close_to(ep, float(tp1), max_pct=0.02):
|
||||
exit_reason = "take_profit"
|
||||
elif tp2 is not None and _close_to(ep, float(tp2), max_pct=0.02):
|
||||
exit_reason = "take_profit"
|
||||
# 如果价格接近入场价,可能是移动止损触发的
|
||||
elif is_reduce_only and exit_reason == "sync":
|
||||
entry_price_val = float(trade.get("entry_price", 0) or 0)
|
||||
if entry_price_val > 0 and _close_to(ep, entry_price_val, max_pct=0.01):
|
||||
exit_reason = "trailing_stop"
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 从币安成交获取手续费与实际盈亏,保证统计与币安一致
|
||||
sync_commission = None
|
||||
sync_commission_asset = None
|
||||
sync_realized_pnl = None
|
||||
try:
|
||||
recent_trades = await client.get_recent_trades(symbol, limit=30)
|
||||
related = [t for t in recent_trades if str(t.get('orderId')) == str(order_id)]
|
||||
if related:
|
||||
sync_commission = sum(float(t.get('commission', 0)) for t in related)
|
||||
assets = {t.get('commissionAsset') for t in related if t.get('commissionAsset')}
|
||||
sync_commission_asset = "/".join(assets) if assets else None
|
||||
sync_realized_pnl = sum(float(t.get('realizedPnl', 0)) for t in related)
|
||||
except Exception as fee_err:
|
||||
logger.debug(f"同步订单 {order_id} 手续费失败: {fee_err}")
|
||||
|
||||
# 持仓持续时间(分钟)
|
||||
duration_minutes = None
|
||||
try:
|
||||
et = trade.get("entry_time")
|
||||
if et is not None and exit_time_ts is not None:
|
||||
et_i = int(et)
|
||||
if exit_time_ts >= et_i:
|
||||
duration_minutes = int((exit_time_ts - et_i) / 60)
|
||||
except Exception:
|
||||
duration_minutes = None
|
||||
|
||||
# 更新数据库(包含订单号、手续费与实际盈亏)
|
||||
Trade.update_exit(
|
||||
trade_id=trade_id,
|
||||
exit_price=avg_price,
|
||||
exit_reason=exit_reason,
|
||||
pnl=pnl,
|
||||
pnl_percent=pnl_percent,
|
||||
exit_order_id=order_id, # 保存订单号,确保唯一性
|
||||
duration_minutes=duration_minutes,
|
||||
exit_time_ts=exit_time_ts,
|
||||
commission=sync_commission,
|
||||
commission_asset=sync_commission_asset,
|
||||
realized_pnl=sync_realized_pnl,
|
||||
)
|
||||
updated_count += 1
|
||||
logger.info(
|
||||
f"✓ 更新平仓记录: {symbol} (ID: {trade_id}, 订单号: {order_id}, "
|
||||
f"类型: {otype or '-'}, 原因: {exit_reason}, 成交价: {avg_price:.4f})"
|
||||
)
|
||||
# 首先检查是否已经通过订单号同步过(避免重复)
|
||||
existing_trade = Trade.get_by_exit_order_id(order_id)
|
||||
# 如果已有 exit_order_id 且 exit_reason 不是 sync,说明已完整同步,跳过
|
||||
if existing_trade and existing_trade.get("exit_order_id") and existing_trade.get("exit_reason") not in (None, "", "sync"):
|
||||
skipped_existing += 1
|
||||
logger.debug(f"订单 {order_id} 已完整同步过(exit_reason={existing_trade.get('exit_reason')}),跳过")
|
||||
continue
|
||||
|
||||
# 查找数据库中该交易对的 open 状态记录(仅当前账号),或已平仓但 exit_order_id 为空的记录
|
||||
open_trades = Trade.get_by_symbol(symbol, status='open', account_id=account_id or DEFAULT_ACCOUNT_ID)
|
||||
closed_trades_no_exit_id = []
|
||||
if not existing_trade and not open_trades:
|
||||
# 如果没有 open 记录,查找已平仓但 exit_order_id 为空的记录
|
||||
try:
|
||||
closed_trades = Trade.get_by_symbol(symbol, status='closed', account_id=account_id or DEFAULT_ACCOUNT_ID)
|
||||
closed_trades_no_exit_id = [
|
||||
t for t in closed_trades
|
||||
if not t.get("exit_order_id") or str(t.get("exit_order_id")).strip() in ("", "0")
|
||||
]
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if existing_trade or open_trades or closed_trades_no_exit_id:
|
||||
# 找到匹配的交易记录(优先用 existing_trade,其次 open_trades,最后 closed_trades_no_exit_id)
|
||||
if existing_trade:
|
||||
trade = existing_trade
|
||||
elif open_trades:
|
||||
trade = open_trades[0] # 取第一个 open 记录
|
||||
else:
|
||||
# 没有找到匹配的记录
|
||||
if sync_all_symbols:
|
||||
# 如果启用了同步所有交易对,尝试创建完整的交易记录(开仓+平仓)
|
||||
try:
|
||||
# 查找是否有对应的开仓订单(通过时间窗口和价格匹配)
|
||||
# 注意:平仓订单通常有 reduceOnly=True,我们需要找到对应的开仓订单
|
||||
# 由于币安订单历史可能不完整,这里先跳过创建,只记录日志
|
||||
logger.debug(f"平仓订单 {order_id} ({symbol}) 无法匹配到现有记录,且 sync_all_symbols=True,但创建完整交易记录需要开仓订单信息,暂时跳过")
|
||||
skipped_no_match += 1
|
||||
except Exception as e:
|
||||
logger.debug(f"处理平仓订单失败 {order_id}: {e}")
|
||||
skipped_no_match += 1
|
||||
# 从已平仓但无 exit_order_id 的记录中选择(按 entry_time 最近的一条)
|
||||
closed_trades_no_exit_id.sort(key=lambda x: x.get('entry_time', 0) or 0, reverse=True)
|
||||
trade = closed_trades_no_exit_id[0]
|
||||
trade_id = trade['id']
|
||||
|
||||
# 如果之前没有 exit_order_id,记录为补全
|
||||
if not trade.get("exit_order_id") or str(trade.get("exit_order_id")).strip() in ("", "0"):
|
||||
exit_order_id_filled += 1
|
||||
|
||||
# 计算盈亏
|
||||
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
|
||||
|
||||
# 细分 exit_reason:优先使用币安订单类型,其次用价格接近止损/止盈做兜底
|
||||
exit_reason = "sync"
|
||||
# 检查订单的 reduceOnly 字段:如果是 true,说明是自动平仓,不应该标记为 manual
|
||||
is_reduce_only = order.get("reduceOnly", False) if isinstance(order, dict) else False
|
||||
|
||||
if "TRAILING" in otype:
|
||||
exit_reason = "trailing_stop"
|
||||
elif "TAKE_PROFIT" in otype:
|
||||
exit_reason = "take_profit"
|
||||
elif "STOP" in otype:
|
||||
exit_reason = "stop_loss"
|
||||
elif otype in ("MARKET", "LIMIT"):
|
||||
# 如果是 reduceOnly 订单,说明是自动平仓(可能是保护单触发的),先标记为 sync,后续用价格判断
|
||||
if is_reduce_only:
|
||||
exit_reason = "sync" # 临时标记,后续用价格判断
|
||||
else:
|
||||
exit_reason = "manual" # 非 reduceOnly 的 MARKET/LIMIT 订单才是真正的手动平仓
|
||||
|
||||
try:
|
||||
def _close_to(a: float, b: float, max_pct: float = 0.02) -> bool: # 放宽到2%,因为滑点可能导致价格不完全一致
|
||||
if a <= 0 or b <= 0:
|
||||
return False
|
||||
return abs((a - b) / b) <= max_pct
|
||||
|
||||
ep = float(avg_price or 0)
|
||||
if ep > 0:
|
||||
sl = trade.get("stop_loss_price")
|
||||
tp = trade.get("take_profit_price")
|
||||
tp1 = trade.get("take_profit_1")
|
||||
tp2 = trade.get("take_profit_2")
|
||||
# 优先检查止损
|
||||
if sl is not None and _close_to(ep, float(sl), max_pct=0.02):
|
||||
exit_reason = "stop_loss"
|
||||
# 然后检查止盈
|
||||
elif tp is not None and _close_to(ep, float(tp), max_pct=0.02):
|
||||
exit_reason = "take_profit"
|
||||
elif tp1 is not None and _close_to(ep, float(tp1), max_pct=0.02):
|
||||
exit_reason = "take_profit"
|
||||
elif tp2 is not None and _close_to(ep, float(tp2), max_pct=0.02):
|
||||
exit_reason = "take_profit"
|
||||
# 如果价格接近入场价,可能是移动止损触发的
|
||||
elif is_reduce_only and exit_reason == "sync":
|
||||
entry_price_val = float(trade.get("entry_price", 0) or 0)
|
||||
if entry_price_val > 0 and _close_to(ep, entry_price_val, max_pct=0.01):
|
||||
exit_reason = "trailing_stop"
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 从币安成交获取手续费与实际盈亏,保证统计与币安一致
|
||||
sync_commission = None
|
||||
sync_commission_asset = None
|
||||
sync_realized_pnl = None
|
||||
try:
|
||||
recent_trades = await client.get_recent_trades(symbol, limit=30)
|
||||
related = [t for t in recent_trades if str(t.get('orderId')) == str(order_id)]
|
||||
if related:
|
||||
sync_commission = sum(float(t.get('commission', 0)) for t in related)
|
||||
assets = {t.get('commissionAsset') for t in related if t.get('commissionAsset')}
|
||||
sync_commission_asset = "/".join(assets) if assets else None
|
||||
sync_realized_pnl = sum(float(t.get('realizedPnl', 0)) for t in related)
|
||||
except Exception as fee_err:
|
||||
logger.debug(f"同步订单 {order_id} 手续费失败: {fee_err}")
|
||||
|
||||
# 持仓持续时间(分钟)
|
||||
duration_minutes = None
|
||||
try:
|
||||
et = trade.get("entry_time")
|
||||
if et is not None and exit_time_ts is not None:
|
||||
et_i = int(et)
|
||||
if exit_time_ts >= et_i:
|
||||
duration_minutes = int((exit_time_ts - et_i) / 60)
|
||||
except Exception:
|
||||
duration_minutes = None
|
||||
|
||||
# 更新数据库(包含订单号、手续费与实际盈亏)
|
||||
Trade.update_exit(
|
||||
trade_id=trade_id,
|
||||
exit_price=avg_price,
|
||||
exit_reason=exit_reason,
|
||||
pnl=pnl,
|
||||
pnl_percent=pnl_percent,
|
||||
exit_order_id=order_id, # 保存订单号,确保唯一性
|
||||
duration_minutes=duration_minutes,
|
||||
exit_time_ts=exit_time_ts,
|
||||
commission=sync_commission,
|
||||
commission_asset=sync_commission_asset,
|
||||
realized_pnl=sync_realized_pnl,
|
||||
)
|
||||
updated_count += 1
|
||||
logger.info(
|
||||
f"✓ 更新平仓记录: {symbol} (ID: {trade_id}, 订单号: {order_id}, "
|
||||
f"类型: {otype or '-'}, 原因: {exit_reason}, 成交价: {avg_price:.4f})"
|
||||
)
|
||||
else:
|
||||
# 没有找到匹配的记录
|
||||
if sync_all_symbols:
|
||||
# 如果启用了同步所有交易对,尝试创建完整的交易记录(开仓+平仓)
|
||||
try:
|
||||
# 查找是否有对应的开仓订单(通过时间窗口和价格匹配)
|
||||
# 注意:平仓订单通常有 reduceOnly=True,我们需要找到对应的开仓订单
|
||||
# 由于币安订单历史可能不完整,这里先跳过创建,只记录日志
|
||||
logger.debug(f"平仓订单 {order_id} ({symbol}) 无法匹配到现有记录,且 sync_all_symbols=True,但创建完整交易记录需要开仓订单信息,暂时跳过")
|
||||
skipped_no_match += 1
|
||||
logger.debug(f"平仓订单 {order_id} ({symbol}) 无法匹配到现有记录(无 open 状态且无 exit_order_id 为空的 closed 记录),跳过")
|
||||
except Exception as e:
|
||||
logger.debug(f"处理平仓订单失败 {order_id}: {e}")
|
||||
skipped_no_match += 1
|
||||
else:
|
||||
skipped_no_match += 1
|
||||
logger.debug(f"平仓订单 {order_id} ({symbol}) 无法匹配到现有记录(无 open 状态且无 exit_order_id 为空的 closed 记录),跳过")
|
||||
except Exception as e:
|
||||
logger.warning(f"同步平仓订单失败 {symbol} (订单ID: {order_id}): {e}")
|
||||
continue
|
||||
# 2. 处理开仓订单
|
||||
logger.info(f"开始处理 {len(open_orders)} 个开仓订单...")
|
||||
for order in open_orders:
|
||||
|
|
@ -772,86 +775,86 @@ async def sync_trades_from_binance(
|
|||
else:
|
||||
# 如果不存在,尝试查找没有 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
|
||||
|
||||
# 查找时间范围内(订单时间前后 1 小时)且没有 entry_order_id 的记录
|
||||
time_window_start = order_time_sec - 3600
|
||||
time_window_end = order_time_sec + 3600
|
||||
|
||||
trades_no_entry_id = Trade.get_all(
|
||||
account_id=account_id or DEFAULT_ACCOUNT_ID,
|
||||
symbol=symbol,
|
||||
start_timestamp=time_window_start,
|
||||
end_timestamp=time_window_end,
|
||||
time_filter="entry", # 使用 entry_time 过滤
|
||||
)
|
||||
|
||||
# 过滤出没有 entry_order_id 的记录
|
||||
trades_no_entry_id = [
|
||||
t for t in trades_no_entry_id
|
||||
if not t.get("entry_order_id") or str(t.get("entry_order_id")).strip() in ("", "0")
|
||||
]
|
||||
|
||||
# 按价格和数量匹配(允许 5% 误差)
|
||||
matched_trade = None
|
||||
order_qty = float(order.get('executedQty', 0))
|
||||
order_price = float(order.get('avgPrice', 0))
|
||||
order_side = order.get('side', '').upper()
|
||||
|
||||
for t in trades_no_entry_id:
|
||||
t_qty = float(t.get('quantity', 0))
|
||||
t_price = float(t.get('entry_price', 0))
|
||||
# 数量匹配(允许 5% 误差)且价格匹配(允许 2% 误差)
|
||||
if (order_qty > 0 and t_qty > 0 and
|
||||
abs(t_qty - order_qty) / max(order_qty, 1e-8) <= 0.05 and
|
||||
order_price > 0 and t_price > 0 and
|
||||
abs(t_price - order_price) / max(order_price, 1e-8) <= 0.02):
|
||||
matched_trade = t
|
||||
break
|
||||
|
||||
if matched_trade:
|
||||
# 补全 entry_order_id
|
||||
from database.models import Trade as TradeModel
|
||||
if TradeModel.update_entry_order_id(matched_trade['id'], order_id):
|
||||
entry_order_id_filled += 1
|
||||
logger.info(f"✓ 补全开仓订单号: {symbol} (ID: {matched_trade['id']}, orderId: {order_id}, qty={order_qty}, price={order_price:.4f})")
|
||||
else:
|
||||
logger.debug(f"补全开仓订单号失败(可能已有订单号): {symbol} (ID: {matched_trade['id']}, orderId: {order_id})")
|
||||
elif sync_all_symbols:
|
||||
# 如果启用了同步所有交易对,且无法匹配到现有记录,创建新记录
|
||||
try:
|
||||
# 从订单信息中提取杠杆(如果有)
|
||||
leverage = 10 # 默认杠杆
|
||||
try:
|
||||
# 尝试从订单的 positionSide 或其他字段获取杠杆信息
|
||||
# 如果没有,使用默认值
|
||||
pass
|
||||
except:
|
||||
pass
|
||||
|
||||
# 创建新的交易记录
|
||||
trade_id = Trade.create(
|
||||
symbol=symbol,
|
||||
side=order_side,
|
||||
quantity=order_qty,
|
||||
entry_price=order_price,
|
||||
leverage=leverage,
|
||||
entry_reason='sync_from_binance',
|
||||
entry_order_id=order_id,
|
||||
client_order_id=order.get('clientOrderId'),
|
||||
account_id=account_id or DEFAULT_ACCOUNT_ID,
|
||||
status='open', # 先标记为 open,如果后续有平仓订单会更新
|
||||
)
|
||||
created_count += 1
|
||||
logger.info(f"✓ 创建新交易记录: {symbol} (ID: {trade_id}, orderId: {order_id}, side={order_side}, qty={order_qty}, price={order_price:.4f})")
|
||||
except Exception as create_err:
|
||||
logger.warning(f"创建交易记录失败 {symbol} (orderId: {order_id}): {create_err}")
|
||||
# 查找该 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
|
||||
|
||||
# 查找时间范围内(订单时间前后 1 小时)且没有 entry_order_id 的记录
|
||||
time_window_start = order_time_sec - 3600
|
||||
time_window_end = order_time_sec + 3600
|
||||
|
||||
trades_no_entry_id = Trade.get_all(
|
||||
account_id=account_id or DEFAULT_ACCOUNT_ID,
|
||||
symbol=symbol,
|
||||
start_timestamp=time_window_start,
|
||||
end_timestamp=time_window_end,
|
||||
time_filter="entry", # 使用 entry_time 过滤
|
||||
)
|
||||
|
||||
# 过滤出没有 entry_order_id 的记录
|
||||
trades_no_entry_id = [
|
||||
t for t in trades_no_entry_id
|
||||
if not t.get("entry_order_id") or str(t.get("entry_order_id")).strip() in ("", "0")
|
||||
]
|
||||
|
||||
# 按价格和数量匹配(允许 5% 误差)
|
||||
matched_trade = None
|
||||
order_qty = float(order.get('executedQty', 0))
|
||||
order_price = float(order.get('avgPrice', 0))
|
||||
order_side = order.get('side', '').upper()
|
||||
|
||||
for t in trades_no_entry_id:
|
||||
t_qty = float(t.get('quantity', 0))
|
||||
t_price = float(t.get('entry_price', 0))
|
||||
# 数量匹配(允许 5% 误差)且价格匹配(允许 2% 误差)
|
||||
if (order_qty > 0 and t_qty > 0 and
|
||||
abs(t_qty - order_qty) / max(order_qty, 1e-8) <= 0.05 and
|
||||
order_price > 0 and t_price > 0 and
|
||||
abs(t_price - order_price) / max(order_price, 1e-8) <= 0.02):
|
||||
matched_trade = t
|
||||
break
|
||||
|
||||
if matched_trade:
|
||||
# 补全 entry_order_id
|
||||
from database.models import Trade as TradeModel
|
||||
if TradeModel.update_entry_order_id(matched_trade['id'], order_id):
|
||||
entry_order_id_filled += 1
|
||||
logger.info(f"✓ 补全开仓订单号: {symbol} (ID: {matched_trade['id']}, orderId: {order_id}, qty={order_qty}, price={order_price:.4f})")
|
||||
else:
|
||||
logger.debug(f"发现新的开仓订单 {order_id} ({symbol}, qty={order_qty}, price={order_price:.4f}),但无法匹配到现有记录(sync_all_symbols=False,跳过创建)")
|
||||
except Exception as e:
|
||||
logger.debug(f"处理开仓订单失败 {order_id}: {e}")
|
||||
logger.debug(f"补全开仓订单号失败(可能已有订单号): {symbol} (ID: {matched_trade['id']}, orderId: {order_id})")
|
||||
elif sync_all_symbols:
|
||||
# 如果启用了同步所有交易对,且无法匹配到现有记录,创建新记录
|
||||
try:
|
||||
# 从订单信息中提取杠杆(如果有)
|
||||
leverage = 10 # 默认杠杆
|
||||
try:
|
||||
# 尝试从订单的 positionSide 或其他字段获取杠杆信息
|
||||
# 如果没有,使用默认值
|
||||
pass
|
||||
except:
|
||||
pass
|
||||
|
||||
# 创建新的交易记录
|
||||
trade_id = Trade.create(
|
||||
symbol=symbol,
|
||||
side=order_side,
|
||||
quantity=order_qty,
|
||||
entry_price=order_price,
|
||||
leverage=leverage,
|
||||
entry_reason='sync_from_binance',
|
||||
entry_order_id=order_id,
|
||||
client_order_id=order.get('clientOrderId'),
|
||||
account_id=account_id or DEFAULT_ACCOUNT_ID,
|
||||
status='open', # 先标记为 open,如果后续有平仓订单会更新
|
||||
)
|
||||
created_count += 1
|
||||
logger.info(f"✓ 创建新交易记录: {symbol} (ID: {trade_id}, orderId: {order_id}, side={order_side}, qty={order_qty}, price={order_price:.4f})")
|
||||
except Exception as create_err:
|
||||
logger.warning(f"创建交易记录失败 {symbol} (orderId: {order_id}): {create_err}")
|
||||
else:
|
||||
logger.debug(f"发现新的开仓订单 {order_id} ({symbol}, qty={order_qty}, price={order_price:.4f}),但无法匹配到现有记录(sync_all_symbols=False,跳过创建)")
|
||||
except Exception as e:
|
||||
logger.debug(f"处理开仓订单失败 {order_id}: {e}")
|
||||
except Exception as e:
|
||||
logger.warning(f"同步订单失败 {symbol} (订单ID: {order_id}): {e}")
|
||||
continue
|
||||
|
|
|
|||
46
backend/check_dependencies.sh
Executable file
46
backend/check_dependencies.sh
Executable file
|
|
@ -0,0 +1,46 @@
|
|||
#!/bin/bash
|
||||
# 检查 backend 依赖是否完整安装
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
echo "=== 检查 Backend 依赖 ==="
|
||||
echo ""
|
||||
|
||||
# 检查虚拟环境
|
||||
if [ -d "../.venv" ]; then
|
||||
echo "✓ 找到虚拟环境: ../.venv"
|
||||
source ../.venv/bin/activate
|
||||
elif [ -d ".venv" ]; then
|
||||
echo "✓ 找到虚拟环境: .venv"
|
||||
source .venv/bin/activate
|
||||
else
|
||||
echo "⚠ 未找到虚拟环境,使用系统 Python"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Python 路径: $(which python3)"
|
||||
echo "Python 版本: $(python3 --version)"
|
||||
echo ""
|
||||
|
||||
# 检查关键依赖
|
||||
echo "检查关键依赖..."
|
||||
python3 -c "import fastapi; print('✓ fastapi:', fastapi.__version__)" 2>&1 || echo "✗ fastapi 未安装"
|
||||
python3 -c "import uvicorn; print('✓ uvicorn:', uvicorn.__version__)" 2>&1 || echo "✗ uvicorn 未安装"
|
||||
python3 -c "from jose import jwt; print('✓ python-jose: 已安装')" 2>&1 || echo "✗ python-jose 未安装"
|
||||
python3 -c "import pymysql; print('✓ pymysql:', pymysql.__version__)" 2>&1 || echo "✗ pymysql 未安装"
|
||||
python3 -c "import redis; print('✓ redis:', redis.__version__)" 2>&1 || echo "✗ redis 未安装"
|
||||
python3 -c "from cryptography.fernet import Fernet; print('✓ cryptography: 已安装')" 2>&1 || echo "✗ cryptography 未安装"
|
||||
|
||||
echo ""
|
||||
echo "=== 尝试导入 api.main ==="
|
||||
python3 -c "import api.main; print('✓ api.main 导入成功')" 2>&1 || echo "✗ api.main 导入失败"
|
||||
|
||||
echo ""
|
||||
echo "=== 检查完成 ==="
|
||||
echo ""
|
||||
echo "如果缺少依赖,请运行:"
|
||||
echo " pip install -r backend/requirements.txt"
|
||||
echo ""
|
||||
echo "或者激活虚拟环境后运行:"
|
||||
echo " source .venv/bin/activate"
|
||||
echo " pip install -r backend/requirements.txt"
|
||||
Loading…
Reference in New Issue
Block a user