使用自定义订单号确保与币安一致

This commit is contained in:
薇薇安 2026-02-14 18:38:56 +08:00
parent a52b8c4738
commit 3d9f58f049
5 changed files with 185 additions and 9 deletions

View File

@ -1538,8 +1538,17 @@ async def open_position_from_recommendation(
raise HTTPException(status_code=500, detail=error_msg)
def _order_is_sltp(o: dict, type_key: str = "type") -> bool:
"""判断是否为止损/止盈类订单(含普通单与 Algo 条件单)"""
t = str(o.get(type_key) or o.get("orderType") or "").upper()
return t in ("STOP_MARKET", "TAKE_PROFIT_MARKET", "STOP", "TAKE_PROFIT")
@router.post("/positions/sync")
async def sync_positions(account_id: int = Depends(get_account_id)):
async def sync_positions(
account_id: int = Depends(get_account_id),
only_recover_when_has_sltp: bool = Query(True, description="仅当该持仓存在止损/止盈单时才补建记录(用于区分系统单,减少手动单误建)"),
):
"""同步币安实际持仓状态与数据库状态"""
try:
logger.info("=" * 60)
@ -1750,11 +1759,21 @@ async def sync_positions(account_id: int = Depends(get_account_id)):
else:
logger.info("✓ 数据库与币安状态一致,无需更新")
# 4. 币安有仓但数据库无记录:从币安成交里取 orderId 并补建交易记录,便于在「订单记录」和统计中展示(含系统挂单/条件单)
# 4. 币安有仓但数据库无记录:优先用「开仓订单 clientOrderId 前缀」判断是否系统单,仅对系统单补建
missing_in_db = binance_symbols - db_open_symbols
recovered_count = 0
system_order_prefix = ""
try:
from database.models import TradingConfig
system_order_prefix = (TradingConfig.get_value("SYSTEM_ORDER_ID_PREFIX", "", account_id=account_id) or "").strip()
except Exception:
pass
if missing_in_db:
logger.info(f"发现 {len(missing_in_db)} 个持仓在币安存在但数据库中没有记录: {', '.join(missing_in_db)}")
if system_order_prefix:
logger.info(f" → 仅对开仓订单 clientOrderId 前缀为「{system_order_prefix}」的持仓补建(系统单标识)")
elif only_recover_when_has_sltp:
logger.info(" → 仅对「存在止损/止盈单」的持仓补建记录(视为系统单),避免手动单误建")
for symbol in missing_in_db:
try:
pos = next((p for p in binance_positions if p.get('symbol') == symbol), None)
@ -1778,6 +1797,38 @@ async def sync_positions(account_id: int = Depends(get_account_id)):
entry_order_id = same_side[0].get('orderId')
except Exception as e:
logger.debug(f"获取 {symbol} 成交记录失败: {e}")
if system_order_prefix:
if not entry_order_id:
logger.debug(f" {symbol} 无法获取开仓订单号,跳过补建")
continue
try:
order_info = await client.client.futures_get_order(symbol=symbol, orderId=int(entry_order_id), recvWindow=20000)
cid = (order_info or {}).get("clientOrderId") or ""
if not cid.startswith(system_order_prefix):
logger.debug(f" {symbol} 开仓订单 clientOrderId={cid!r} 非系统前缀,跳过补建")
continue
except Exception as e:
logger.debug(f" {symbol} 查询开仓订单失败: {e},跳过补建")
continue
elif only_recover_when_has_sltp:
has_sltp = False
try:
normal = await client.get_open_orders(symbol)
for o in (normal or []):
if _order_is_sltp(o, "type"):
has_sltp = True
break
if not has_sltp:
algo = await client.futures_get_open_algo_orders(symbol=symbol, algo_type="CONDITIONAL")
for o in (algo or []):
if _order_is_sltp(o, "orderType"):
has_sltp = True
break
except Exception as e:
logger.debug(f"检查 {symbol} 止盈止损单失败: {e}")
if not has_sltp:
logger.debug(f" {symbol} 无止损/止盈单,跳过补建(视为非系统单)")
continue
if entry_order_id and hasattr(Trade, 'get_by_entry_order_id'):
try:
existing = Trade.get_by_entry_order_id(entry_order_id)

View File

@ -60,6 +60,9 @@ USER_VISIBLE_DEFAULTS = {
"MAX_TOTAL_POSITION_PERCENT": {"value": 20.0, "type": "number", "category": "position", "description": "总仓位最大保证金占比(%"},
"AUTO_TRADE_ENABLED": {"value": True, "type": "boolean", "category": "risk", "description": "自动交易总开关:关闭后仅生成推荐,不会自动下单"},
"MAX_OPEN_POSITIONS": {"value": 3, "type": "number", "category": "position", "description": "同时持仓数量上限"},
"SYNC_RECOVER_MISSING_POSITIONS": {"value": True, "type": "boolean", "category": "position", "description": "同步时补建「币安有仓、DB 无记录」的交易记录(便于订单记录与统计)"},
"SYNC_RECOVER_ONLY_WHEN_HAS_SLTP": {"value": True, "type": "boolean", "category": "position", "description": "仅当该持仓存在止损/止盈单时才补建(未配置 SYSTEM_ORDER_ID_PREFIX 时生效)"},
"SYSTEM_ORDER_ID_PREFIX": {"value": "SYS", "type": "string", "category": "position", "description": "系统单标识:下单时写入 newClientOrderId 前缀,同步时仅对「开仓订单 clientOrderId 以此前缀开头」的持仓补建;设空则用「是否有止损止盈单」判断"},
"MAX_DAILY_ENTRIES": {"value": 8, "type": "number", "category": "risk", "description": "每日最多开仓次数"},
"TOP_N_SYMBOLS": {"value": 8, "type": "number", "category": "scan", "description": "每次扫描后优先处理的交易对数量"},
"MIN_SIGNAL_STRENGTH": {"value": 8, "type": "number", "category": "scan", "description": "最小信号强度0-10"},

View File

@ -3,6 +3,7 @@
"""
import asyncio
import logging
import random
import time
from typing import Dict, List, Optional, Any
from binance import AsyncClient, BinanceSocketManager
@ -1429,6 +1430,13 @@ class BinanceClient:
if position_side:
logger.info(f"{symbol} 单向模式下忽略 positionSide={position_side}(避免 -4061")
# 开仓单写入自定义订单号前缀,便于同步时根据 clientOrderId 区分系统单(仅本系统开的仓才补建记录)
if not reduce_only:
prefix = (config.TRADING_CONFIG.get('SYSTEM_ORDER_ID_PREFIX') or '').strip()
if prefix:
# 币安 newClientOrderId 最长 36 字符,格式: PREFIX_timestamp_hex
order_params['newClientOrderId'] = f"{prefix}_{int(time.time() * 1000)}_{random.randint(0, 0xFFFF):04x}"[:36]
# 如果是平仓订单,添加 reduceOnly 参数
# 根据币安API文档reduceOnly 应该是字符串 "true" 或 "false"
if reduce_only:
@ -1470,7 +1478,12 @@ class BinanceClient:
# 让 python-binance 重新生成,否则会报 -1022 Signature invalid
retry_params.pop('timestamp', None)
retry_params.pop('signature', None)
# 重试时生成新的 newClientOrderId避免与首次提交冲突
if 'newClientOrderId' in retry_params:
prefix = (config.TRADING_CONFIG.get('SYSTEM_ORDER_ID_PREFIX') or '').strip()
if prefix:
retry_params['newClientOrderId'] = f"{prefix}_{int(time.time() * 1000)}_{random.randint(0, 0xFFFF):04x}"[:36]
if code == -4061:
logger.error(f"{symbol} 触发 -4061持仓模式不匹配尝试自动兜底重试一次")
if "positionSide" in retry_params:

View File

@ -306,6 +306,10 @@ DEFAULT_TRADING_CONFIG = {
'ENTRY_MAX_DRIFT_PCT_RANGING': 0.3, # 震荡/弱趋势时允许的最大追价偏离
'LIMIT_ORDER_OFFSET_PCT': 0.5, # 限价单偏移百分比默认0.5%
'MIN_HOLD_TIME_SEC': 0, # 默认30分钟1800秒已取消
# ===== 系统单标识(用于同步时区分本系统开仓 vs 手动开仓)=====
# 下单时写入 newClientOrderId = SYSTEM_ORDER_ID_PREFIX_时间戳_随机同步/补建时根据订单 clientOrderId 前缀判断是否系统单
'SYSTEM_ORDER_ID_PREFIX': 'SYS',
}
def _get_trading_config():

View File

@ -2780,7 +2780,6 @@ class PositionManager:
logger.info("✓ 持仓状态同步完成,数据库与币安状态一致")
# 5. 检查币安有但数据库没有记录的持仓(可能是本策略开仓后未正确落库、或其它来源)
# 默认不再自动创建「手动开仓」记录,避免产生大量无 entry_order_id 的怪单(与币安实际订单对不上)
missing_in_db = binance_symbols - db_open_symbols
if missing_in_db:
logger.info(
@ -2788,13 +2787,119 @@ class PositionManager:
f"{', '.join(missing_in_db)}"
)
sync_create_manual = config.TRADING_CONFIG.get("SYNC_CREATE_MANUAL_ENTRY_RECORD", False)
if not sync_create_manual:
sync_recover = config.TRADING_CONFIG.get("SYNC_RECOVER_MISSING_POSITIONS", True)
sync_recover_only_has_sltp = config.TRADING_CONFIG.get("SYNC_RECOVER_ONLY_WHEN_HAS_SLTP", True)
def _order_is_sltp(o, type_key="type"):
t = str(o.get(type_key) or o.get("orderType") or "").upper()
return t in ("STOP_MARKET", "TAKE_PROFIT_MARKET", "STOP", "TAKE_PROFIT")
async def _symbol_has_sltp(sym):
try:
for o in (await self.client.get_open_orders(sym)) or []:
if _order_is_sltp(o, "type"):
return True
for o in (await self.client.futures_get_open_algo_orders(sym, algo_type="CONDITIONAL")) or []:
if _order_is_sltp(o, "orderType"):
return True
except Exception:
pass
return False
if sync_recover:
system_order_prefix = (config.TRADING_CONFIG.get("SYSTEM_ORDER_ID_PREFIX") or "").strip()
if system_order_prefix:
logger.info(f" → 补建「系统单」记录(仅当开仓订单 clientOrderId 前缀为 {system_order_prefix!r} 时创建)")
else:
logger.info(" → 补建「系统单」记录(仅当存在止损/止盈单时创建,避免手动单误建)" if sync_recover_only_has_sltp else " → 补建缺失持仓记录")
for symbol in missing_in_db:
try:
binance_position = next((p for p in binance_positions if p.get("symbol") == symbol), None)
if not binance_position or float(binance_position.get("positionAmt", 0)) == 0:
continue
position_amt = float(binance_position["positionAmt"])
quantity = abs(position_amt)
side = "BUY" if position_amt > 0 else "SELL"
entry_price = float(binance_position.get("entryPrice", 0))
notional = quantity * entry_price
if notional < 1.0:
continue
entry_order_id = None
try:
recent = await self.client.get_recent_trades(symbol, limit=30)
if recent:
same_side = [t for t in recent if str(t.get("side", "")).upper() == side]
if same_side:
same_side.sort(key=lambda x: int(x.get("time", 0)), reverse=True)
entry_order_id = same_side[0].get("orderId")
except Exception:
pass
if system_order_prefix:
if not entry_order_id:
logger.debug(f" {symbol} 无法获取开仓订单号,跳过补建")
continue
try:
order_info = await self.client.client.futures_get_order(symbol=symbol, orderId=int(entry_order_id), recvWindow=20000)
cid = (order_info or {}).get("clientOrderId") or ""
if not cid.startswith(system_order_prefix):
logger.debug(f" {symbol} 开仓订单 clientOrderId={cid!r} 非系统前缀,跳过补建")
continue
except Exception as e:
logger.debug(f" {symbol} 查询开仓订单失败: {e},跳过补建")
continue
elif sync_recover_only_has_sltp and not (await _symbol_has_sltp(symbol)):
logger.debug(f" {symbol} 无止损/止盈单,跳过补建")
continue
if entry_order_id and hasattr(Trade, "get_by_entry_order_id"):
try:
if Trade.get_by_entry_order_id(entry_order_id):
continue
except Exception:
pass
trade_id = Trade.create(
symbol=symbol,
side=side,
quantity=quantity,
entry_price=entry_price,
leverage=binance_position.get("leverage", 10),
entry_reason="sync_recovered",
entry_order_id=entry_order_id,
notional_usdt=notional,
margin_usdt=(notional / float(binance_position.get("leverage", 10) or 10)) if float(binance_position.get("leverage", 10) or 0) > 0 else None,
account_id=self.account_id,
)
logger.info(f"{symbol} [状态同步] 已补建交易记录 (ID: {trade_id}, orderId: {entry_order_id or '-'})")
ticker = await self.client.get_ticker_24h(symbol)
current_price = ticker["price"] if ticker else entry_price
lev = float(binance_position.get("leverage", 10))
stop_loss_pct = config.TRADING_CONFIG.get("STOP_LOSS_PERCENT", 0.08)
if stop_loss_pct is not None and stop_loss_pct > 1:
stop_loss_pct = stop_loss_pct / 100.0
take_profit_pct = config.TRADING_CONFIG.get("TAKE_PROFIT_PERCENT", 0.15)
if take_profit_pct is not None and take_profit_pct > 1:
take_profit_pct = take_profit_pct / 100.0
if not take_profit_pct:
take_profit_pct = (stop_loss_pct or 0.08) * 2.0
stop_loss_price = self.risk_manager.get_stop_loss_price(entry_price, side, quantity, lev, stop_loss_pct=stop_loss_pct)
take_profit_price = self.risk_manager.get_take_profit_price(entry_price, side, quantity, lev, take_profit_pct=take_profit_pct)
position_info = {
"symbol": symbol, "side": side, "quantity": quantity, "entryPrice": entry_price,
"changePercent": 0, "orderId": entry_order_id, "tradeId": trade_id,
"stopLoss": stop_loss_price, "takeProfit": take_profit_price, "initialStopLoss": stop_loss_price,
"leverage": lev, "entryReason": "sync_recovered", "atr": None, "maxProfit": 0.0, "trailingStopActivated": False,
}
self.active_positions[symbol] = position_info
if self._monitoring_enabled:
await self._start_position_monitoring(symbol)
except Exception as e:
logger.warning(f"{symbol} [状态同步] 补建失败: {e}")
elif not sync_create_manual:
logger.info(
" → 已跳过自动创建交易记录SYNC_CREATE_MANUAL_ENTRY_RECORD=False"
" 若确认为本策略开仓可检查开仓时是否保存了 entry_order_id若为手动开仓且需纳入列表可设该配置为 True。"
" → 已跳过自动创建交易记录SYNC_CREATE_MANUAL_ENTRY_RECORD=False, SYNC_RECOVER_MISSING_POSITIONS 未开启)。"
" 若确认为本策略开仓可开启 SYNC_RECOVER_MISSING_POSITIONS=True仅补建有止损止盈单的"
)
else:
# 为手动开仓的持仓创建数据库记录并启动监控(仅当显式开启时)
elif sync_create_manual:
# 为手动开仓的持仓创建数据库记录并启动监控(仅当显式开启且未走上面的「补建系统单」时)
for symbol in missing_in_db:
try:
# 获取币安持仓详情