使用自定义订单号确保与币安一致
This commit is contained in:
parent
a52b8c4738
commit
3d9f58f049
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)"},
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
# 获取币安持仓详情
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user