a
This commit is contained in:
parent
e1a29011c8
commit
fd661d11d4
|
|
@ -18,6 +18,70 @@ from database.models import TradingConfig
|
|||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter()
|
||||
|
||||
# 智能入场(方案C)配置:为了“配置页可见”,即使数据库尚未创建,也在 GET /api/config 返回默认项
|
||||
SMART_ENTRY_CONFIG_DEFAULTS = {
|
||||
"SMART_ENTRY_ENABLED": {
|
||||
"value": True,
|
||||
"type": "boolean",
|
||||
"category": "strategy",
|
||||
"description": "智能入场开关(方案C):趋势时减少错过,震荡时避免追价打损。",
|
||||
},
|
||||
"SMART_ENTRY_STRONG_SIGNAL": {
|
||||
"value": 8,
|
||||
"type": "number",
|
||||
"category": "strategy",
|
||||
"description": "强信号阈值(0-10)。≥该值且4H趋势明确时,允许更积极的入场(可控市价兜底)。",
|
||||
},
|
||||
"ENTRY_SYMBOL_COOLDOWN_SEC": {
|
||||
"value": 120,
|
||||
"type": "number",
|
||||
"category": "strategy",
|
||||
"description": "同一交易对入场冷却时间(秒)。避免短时间内反复挂单/重入导致高频噪音单。",
|
||||
},
|
||||
"ENTRY_TIMEOUT_SEC": {
|
||||
"value": 180,
|
||||
"type": "number",
|
||||
"category": "strategy",
|
||||
"description": "智能入场总预算时间(秒)。超过预算仍未成交将根据规则取消/兜底。",
|
||||
},
|
||||
"ENTRY_STEP_WAIT_SEC": {
|
||||
"value": 15,
|
||||
"type": "number",
|
||||
"category": "strategy",
|
||||
"description": "每次追价/调整前等待成交时间(秒)。",
|
||||
},
|
||||
"ENTRY_CHASE_MAX_STEPS": {
|
||||
"value": 4,
|
||||
"type": "number",
|
||||
"category": "strategy",
|
||||
"description": "最大追价步数(逐步减小限价回调幅度,靠近当前价)。",
|
||||
},
|
||||
"ENTRY_MARKET_FALLBACK_AFTER_SEC": {
|
||||
"value": 45,
|
||||
"type": "number",
|
||||
"category": "strategy",
|
||||
"description": "趋势强时:超过该时间仍未成交,会在偏离不超过上限时转市价兜底(减少错过)。",
|
||||
},
|
||||
"ENTRY_CONFIRM_TIMEOUT_SEC": {
|
||||
"value": 30,
|
||||
"type": "number",
|
||||
"category": "strategy",
|
||||
"description": "下单后确认成交等待时间(秒)。",
|
||||
},
|
||||
"ENTRY_MAX_DRIFT_PCT_TRENDING": {
|
||||
"value": 0.006, # 0.6%
|
||||
"type": "number",
|
||||
"category": "strategy",
|
||||
"description": "趋势强时最大追价偏离(%)。例如 0.6 表示 0.6%。越小越保守。",
|
||||
},
|
||||
"ENTRY_MAX_DRIFT_PCT_RANGING": {
|
||||
"value": 0.003, # 0.3%
|
||||
"type": "number",
|
||||
"category": "strategy",
|
||||
"description": "震荡/弱趋势最大追价偏离(%)。例如 0.3 表示 0.3%。越小越保守。",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@router.get("")
|
||||
@router.get("/")
|
||||
|
|
@ -36,6 +100,11 @@ async def get_all_configs():
|
|||
'category': config['category'],
|
||||
'description': config['description']
|
||||
}
|
||||
|
||||
# 合并“默认但未入库”的配置项(用于新功能上线时直接在配置页可见)
|
||||
for k, meta in SMART_ENTRY_CONFIG_DEFAULTS.items():
|
||||
if k not in result:
|
||||
result[k] = meta
|
||||
return result
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
|
@ -357,12 +426,16 @@ async def update_config(key: str, item: ConfigUpdate):
|
|||
try:
|
||||
# 获取现有配置以确定类型和分类
|
||||
existing = TradingConfig.get(key)
|
||||
if not existing:
|
||||
raise HTTPException(status_code=404, detail="Config not found")
|
||||
|
||||
config_type = item.type or existing['config_type']
|
||||
category = item.category or existing['category']
|
||||
description = item.description or existing['description']
|
||||
if existing:
|
||||
config_type = item.type or existing['config_type']
|
||||
category = item.category or existing['category']
|
||||
description = item.description or existing['description']
|
||||
else:
|
||||
# 允许创建新配置(用于新功能首次上线,DB 里还没有 key 的情况)
|
||||
meta = SMART_ENTRY_CONFIG_DEFAULTS.get(key)
|
||||
config_type = item.type or (meta.get("type") if meta else "string")
|
||||
category = item.category or (meta.get("category") if meta else "strategy")
|
||||
description = item.description or (meta.get("description") if meta else f"{key}配置")
|
||||
|
||||
# 验证配置值
|
||||
if config_type == 'number':
|
||||
|
|
@ -429,8 +502,8 @@ async def update_configs_batch(configs: list[ConfigItem]):
|
|||
errors.append(f"{item.key}: Invalid number value")
|
||||
continue
|
||||
|
||||
# 特殊验证:百分比配置
|
||||
if 'PERCENT' in item.key and item.type == 'number':
|
||||
# 特殊验证:百分比配置(兼容 PERCENT / PCT)
|
||||
if ('PERCENT' in item.key or 'PCT' in item.key) and item.type == 'number':
|
||||
if not (0 <= float(item.value) <= 1):
|
||||
errors.append(f"{item.key}: Must be between 0 and 1")
|
||||
continue
|
||||
|
|
|
|||
|
|
@ -207,6 +207,18 @@ def _get_trading_config():
|
|||
'TRAILING_STOP_ACTIVATION': 0.10, # 移动止损激活提高到10%(盈利10%后激活,给趋势更多空间)
|
||||
'TRAILING_STOP_PROTECT': 0.05, # 保护利润提高到5%(保护5%利润,更合理)
|
||||
'POSITION_SYNC_INTERVAL': 60, # 持仓状态同步间隔(秒),缩短到1分钟,确保状态及时同步
|
||||
|
||||
# ===== 智能入场(方案C:趋势少错过,震荡不追价)=====
|
||||
'SMART_ENTRY_ENABLED': True,
|
||||
'SMART_ENTRY_STRONG_SIGNAL': 8, # 强信号阈值:≥8 更倾向趋势模式(允许市价兜底)
|
||||
'ENTRY_SYMBOL_COOLDOWN_SEC': 120, # 同一symbol两次入场尝试的冷却时间(避免反复挂单/重入)
|
||||
'ENTRY_TIMEOUT_SEC': 180, # 智能入场总预算(秒)(限价/追价逻辑内部使用)
|
||||
'ENTRY_STEP_WAIT_SEC': 15, # 每步等待成交时间(秒)
|
||||
'ENTRY_CHASE_MAX_STEPS': 4, # 最多追价步数(逐步减少 offset)
|
||||
'ENTRY_MARKET_FALLBACK_AFTER_SEC': 45, # 趋势强时:超过该秒数仍未成交 -> 评估是否市价兜底
|
||||
'ENTRY_CONFIRM_TIMEOUT_SEC': 30, # 下单后最终确认成交的等待时间(秒)
|
||||
'ENTRY_MAX_DRIFT_PCT_TRENDING': 0.6, # 趋势强时允许的最大追价偏离(相对初始限价)
|
||||
'ENTRY_MAX_DRIFT_PCT_RANGING': 0.3, # 震荡/弱趋势时允许的最大追价偏离
|
||||
}
|
||||
|
||||
# 币安API配置(优先从数据库,回退到环境变量和默认值)
|
||||
|
|
@ -227,6 +239,16 @@ defaults = {
|
|||
'CONFIRM_INTERVAL': '4h',
|
||||
'ENTRY_INTERVAL': '15m',
|
||||
'LIMIT_ORDER_OFFSET_PCT': 0.5, # 限价单偏移百分比(默认0.5%)
|
||||
# 智能入场默认值(即使DB里没配置,也能用)
|
||||
'SMART_ENTRY_ENABLED': True,
|
||||
'ENTRY_SYMBOL_COOLDOWN_SEC': 120,
|
||||
'ENTRY_TIMEOUT_SEC': 180,
|
||||
'ENTRY_STEP_WAIT_SEC': 15,
|
||||
'ENTRY_CHASE_MAX_STEPS': 4,
|
||||
'ENTRY_MARKET_FALLBACK_AFTER_SEC': 45,
|
||||
'ENTRY_CONFIRM_TIMEOUT_SEC': 30,
|
||||
'ENTRY_MAX_DRIFT_PCT_TRENDING': 0.6,
|
||||
'ENTRY_MAX_DRIFT_PCT_RANGING': 0.3,
|
||||
}
|
||||
for key, value in defaults.items():
|
||||
if key not in TRADING_CONFIG:
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import asyncio
|
|||
import logging
|
||||
import json
|
||||
import aiohttp
|
||||
import time
|
||||
from typing import Dict, List, Optional
|
||||
from datetime import datetime
|
||||
try:
|
||||
|
|
@ -69,6 +70,80 @@ class PositionManager:
|
|||
self.active_positions: Dict[str, Dict] = {}
|
||||
self._monitor_tasks: Dict[str, asyncio.Task] = {} # WebSocket监控任务字典
|
||||
self._monitoring_enabled = True # 是否启用实时监控
|
||||
self._pending_entry_orders: Dict[str, Dict] = {} # 未成交的入场订单(避免重复挂单)
|
||||
self._last_entry_attempt_ms: Dict[str, int] = {} # 每个symbol的最近一次尝试(冷却/去抖)
|
||||
|
||||
@staticmethod
|
||||
def _pct_like_to_ratio(v: float) -> float:
|
||||
"""
|
||||
将“看起来像百分比”的值转换为比例(0~1)。
|
||||
|
||||
兼容两种来源:
|
||||
- 前端/后端按“比例”存储:0.006 表示 0.6%
|
||||
- 历史/默认值按“百分比数值”存储:0.6 表示 0.6%
|
||||
|
||||
经验规则:
|
||||
- v > 1: 认为是 60/100 这种百分数,除以100
|
||||
- 0.05 < v <= 1: 也更可能是“0.6% 这种写法”,除以100
|
||||
- v <= 0.05: 更可能已经是比例(<=5%)
|
||||
"""
|
||||
try:
|
||||
x = float(v or 0.0)
|
||||
except Exception:
|
||||
x = 0.0
|
||||
if x <= 0:
|
||||
return 0.0
|
||||
if x > 1.0:
|
||||
return x / 100.0
|
||||
if x > 0.05:
|
||||
return x / 100.0
|
||||
return x
|
||||
|
||||
def _calc_limit_entry_price(self, current_price: float, side: str, offset_ratio: float) -> float:
|
||||
"""根据当前价与偏移比例,计算限价入场价(BUY: 下方回调;SELL: 上方回调)"""
|
||||
try:
|
||||
cp = float(current_price)
|
||||
except Exception:
|
||||
cp = 0.0
|
||||
try:
|
||||
off = float(offset_ratio or 0.0)
|
||||
except Exception:
|
||||
off = 0.0
|
||||
if cp <= 0:
|
||||
return 0.0
|
||||
if (side or "").upper() == "BUY":
|
||||
return cp * (1 - off)
|
||||
return cp * (1 + off)
|
||||
|
||||
async def _wait_for_order_filled(
|
||||
self,
|
||||
symbol: str,
|
||||
order_id: int,
|
||||
timeout_sec: int = 30,
|
||||
poll_sec: float = 1.0,
|
||||
) -> Dict:
|
||||
"""
|
||||
等待订单成交(FILLED),返回:
|
||||
{ ok, status, avg_price, executed_qty, raw }
|
||||
"""
|
||||
deadline = time.time() + max(1, int(timeout_sec or 1))
|
||||
last_status = None
|
||||
while time.time() < deadline:
|
||||
try:
|
||||
info = await self.client.client.futures_get_order(symbol=symbol, orderId=order_id)
|
||||
status = info.get("status")
|
||||
last_status = status
|
||||
if status == "FILLED":
|
||||
avg_price = float(info.get("avgPrice", 0) or 0) or float(info.get("price", 0) or 0)
|
||||
executed_qty = float(info.get("executedQty", 0) or 0)
|
||||
return {"ok": True, "status": status, "avg_price": avg_price, "executed_qty": executed_qty, "raw": info}
|
||||
if status in ("CANCELED", "REJECTED", "EXPIRED"):
|
||||
return {"ok": False, "status": status, "avg_price": 0, "executed_qty": float(info.get("executedQty", 0) or 0), "raw": info}
|
||||
except Exception:
|
||||
# 忽略单次失败,继续轮询
|
||||
pass
|
||||
await asyncio.sleep(max(0.2, float(poll_sec or 1.0)))
|
||||
return {"ok": False, "status": last_status or "TIMEOUT", "avg_price": 0, "executed_qty": 0, "raw": None}
|
||||
|
||||
async def open_position(
|
||||
self,
|
||||
|
|
@ -77,6 +152,9 @@ class PositionManager:
|
|||
leverage: int = 10,
|
||||
trade_direction: Optional[str] = None,
|
||||
entry_reason: str = '',
|
||||
signal_strength: Optional[int] = None,
|
||||
market_regime: Optional[str] = None,
|
||||
trend_4h: Optional[str] = None,
|
||||
atr: Optional[float] = None,
|
||||
klines: Optional[List] = None,
|
||||
bollinger: Optional[Dict] = None
|
||||
|
|
@ -93,6 +171,22 @@ class PositionManager:
|
|||
订单信息,失败返回None
|
||||
"""
|
||||
try:
|
||||
# 0) 防止同一 symbol 重复挂入场单/快速重复尝试(去抖 + 冷却)
|
||||
now_ms = int(time.time() * 1000)
|
||||
cooldown_sec = int(config.TRADING_CONFIG.get("ENTRY_SYMBOL_COOLDOWN_SEC", 120) or 0)
|
||||
last_ms = self._last_entry_attempt_ms.get(symbol)
|
||||
if last_ms and cooldown_sec > 0 and now_ms - last_ms < cooldown_sec * 1000:
|
||||
logger.info(f"{symbol} [入场] 冷却中({cooldown_sec}s),跳过本次自动开仓")
|
||||
return None
|
||||
|
||||
pending = self._pending_entry_orders.get(symbol)
|
||||
if pending and pending.get("order_id"):
|
||||
logger.info(f"{symbol} [入场] 已有未完成入场订单(orderId={pending.get('order_id')}),跳过重复开仓")
|
||||
return None
|
||||
|
||||
# 标记本次尝试(无论最终是否成交,都避免短时间内反复开仓/挂单)
|
||||
self._last_entry_attempt_ms[symbol] = now_ms
|
||||
|
||||
# 判断是否应该交易
|
||||
if not await self.risk_manager.should_trade(symbol, change_percent):
|
||||
return None
|
||||
|
|
@ -166,149 +260,170 @@ class PositionManager:
|
|||
except Exception as e:
|
||||
logger.debug(f"从Redis重新加载配置失败: {e}")
|
||||
|
||||
# 使用基于保证金的止损止盈(结合技术分析)
|
||||
# 计算保证金和仓位价值
|
||||
position_value = entry_price * quantity
|
||||
margin = position_value / leverage if leverage > 0 else position_value
|
||||
# ===== 智能入场(方案C:趋势强更少错过,震荡更保守)=====
|
||||
smart_entry_enabled = bool(config.TRADING_CONFIG.get("SMART_ENTRY_ENABLED", True))
|
||||
# LIMIT_ORDER_OFFSET_PCT:兼容“比例/百分比”两种存储方式
|
||||
limit_offset_ratio = self._pct_like_to_ratio(float(config.TRADING_CONFIG.get("LIMIT_ORDER_OFFSET_PCT", 0.5) or 0.0))
|
||||
|
||||
# 获取止损止盈百分比(相对于保证金)
|
||||
stop_loss_pct_margin = config.TRADING_CONFIG.get('STOP_LOSS_PERCENT', 0.03)
|
||||
take_profit_pct_margin = config.TRADING_CONFIG.get('TAKE_PROFIT_PERCENT', 0.05)
|
||||
# 规则:趋势(trending) 或 4H共振明显 + 强信号 -> 允许“超时后市价兜底(有追价上限)”
|
||||
mr = (market_regime or "").strip().lower() if market_regime else ""
|
||||
t4 = (trend_4h or "").strip().lower() if trend_4h else ""
|
||||
ss = int(signal_strength) if signal_strength is not None else 0
|
||||
|
||||
# 计算基于保证金的止损止盈
|
||||
stop_loss_price = self.risk_manager.get_stop_loss_price(
|
||||
entry_price, side, quantity, leverage,
|
||||
stop_loss_pct=stop_loss_pct_margin,
|
||||
klines=klines,
|
||||
bollinger=bollinger,
|
||||
atr=atr
|
||||
)
|
||||
allow_market_fallback = False
|
||||
if mr == "trending":
|
||||
allow_market_fallback = True
|
||||
if ss >= int(config.TRADING_CONFIG.get("SMART_ENTRY_STRONG_SIGNAL", 8) or 8) and t4 in ("up", "down"):
|
||||
allow_market_fallback = True
|
||||
|
||||
# 计算止损距离(用于盈亏比计算止盈)
|
||||
stop_distance_for_tp = None
|
||||
if side == 'BUY':
|
||||
stop_distance_for_tp = entry_price - stop_loss_price
|
||||
else: # SELL
|
||||
stop_distance_for_tp = stop_loss_price - entry_price
|
||||
# 追价上限:超过就不追,宁愿错过(避免回到“无脑追价高频打损”)
|
||||
drift_ratio_trending = self._pct_like_to_ratio(float(config.TRADING_CONFIG.get("ENTRY_MAX_DRIFT_PCT_TRENDING", 0.6) or 0.6))
|
||||
drift_ratio_ranging = self._pct_like_to_ratio(float(config.TRADING_CONFIG.get("ENTRY_MAX_DRIFT_PCT_RANGING", 0.3) or 0.3))
|
||||
max_drift_ratio = drift_ratio_trending if allow_market_fallback else drift_ratio_ranging
|
||||
|
||||
# 如果使用ATR策略,优先使用ATR计算的止损距离
|
||||
if atr is not None and atr > 0:
|
||||
atr_percent = atr / entry_price if entry_price > 0 else None
|
||||
if atr_percent:
|
||||
atr_multiplier = config.TRADING_CONFIG.get('ATR_STOP_LOSS_MULTIPLIER', 1.8)
|
||||
atr_stop_distance = entry_price * atr_percent * atr_multiplier
|
||||
# 使用ATR计算的止损距离(更准确)
|
||||
stop_distance_for_tp = atr_stop_distance
|
||||
# 总等待/追价参数
|
||||
timeout_sec = int(config.TRADING_CONFIG.get("ENTRY_TIMEOUT_SEC", 180) or 180)
|
||||
step_wait_sec = int(config.TRADING_CONFIG.get("ENTRY_STEP_WAIT_SEC", 15) or 15)
|
||||
chase_steps = int(config.TRADING_CONFIG.get("ENTRY_CHASE_MAX_STEPS", 4) or 4)
|
||||
market_fallback_after_sec = int(config.TRADING_CONFIG.get("ENTRY_MARKET_FALLBACK_AFTER_SEC", 45) or 45)
|
||||
|
||||
# 计算止盈(基于保证金,支持ATR动态止盈)
|
||||
# 优先使用配置的止盈百分比,如果没有配置则使用止损的3倍(盈亏比3:1)
|
||||
take_profit_pct_margin = config.TRADING_CONFIG.get('TAKE_PROFIT_PERCENT', 0.30)
|
||||
# 如果配置中没有设置止盈,则使用止损的3倍作为默认(盈亏比3:1)
|
||||
if take_profit_pct_margin is None or take_profit_pct_margin == 0:
|
||||
take_profit_pct_margin = stop_loss_pct_margin * 3.0
|
||||
take_profit_price = self.risk_manager.get_take_profit_price(
|
||||
entry_price, side, quantity, leverage,
|
||||
take_profit_pct=take_profit_pct_margin,
|
||||
atr=atr, # 传递ATR用于动态止盈
|
||||
stop_distance=stop_distance_for_tp # 传递止损距离用于盈亏比计算
|
||||
)
|
||||
# 初始限价(回调入场)
|
||||
current_px = float(entry_price)
|
||||
initial_limit = self._calc_limit_entry_price(current_px, side, limit_offset_ratio)
|
||||
if initial_limit <= 0:
|
||||
return None
|
||||
|
||||
# 下单
|
||||
order = await self.client.place_order(
|
||||
symbol=symbol,
|
||||
side=side,
|
||||
quantity=quantity,
|
||||
order_type='MARKET'
|
||||
)
|
||||
order = None
|
||||
entry_order_id = None
|
||||
order_status = None
|
||||
actual_entry_price = None
|
||||
filled_quantity = 0.0
|
||||
entry_mode_used = "market" if not smart_entry_enabled else ("limit+fallback" if allow_market_fallback else "limit-chase")
|
||||
|
||||
if not smart_entry_enabled:
|
||||
order = await self.client.place_order(symbol=symbol, side=side, quantity=quantity, order_type="MARKET")
|
||||
else:
|
||||
# 1) 先挂限价单
|
||||
logger.info(
|
||||
f"{symbol} [智能入场] 模式={entry_mode_used} | side={side} | "
|
||||
f"marketRegime={market_regime} trend_4h={trend_4h} strength={ss}/10 | "
|
||||
f"初始限价={initial_limit:.6f} (offset={limit_offset_ratio*100:.2f}%) | "
|
||||
f"追价上限={max_drift_ratio*100:.2f}%"
|
||||
)
|
||||
|
||||
order = await self.client.place_order(symbol=symbol, side=side, quantity=quantity, order_type="LIMIT", price=initial_limit)
|
||||
if not order:
|
||||
return None
|
||||
entry_order_id = order.get("orderId")
|
||||
if entry_order_id:
|
||||
self._pending_entry_orders[symbol] = {"order_id": entry_order_id, "created_at_ms": int(time.time() * 1000)}
|
||||
|
||||
start_ts = time.time()
|
||||
# 2) 分步等待 + 追价(逐步减少 offset),并在趋势强时允许市价兜底(有追价上限)
|
||||
for step in range(max(1, chase_steps)):
|
||||
# 先等待一段时间看是否成交
|
||||
wait_res = await self._wait_for_order_filled(symbol, int(entry_order_id), timeout_sec=step_wait_sec, poll_sec=1.0)
|
||||
order_status = wait_res.get("status")
|
||||
if wait_res.get("ok"):
|
||||
actual_entry_price = float(wait_res.get("avg_price") or 0)
|
||||
filled_quantity = float(wait_res.get("executed_qty") or 0)
|
||||
break
|
||||
|
||||
# 未成交:如果超时太久且允许市价兜底,检查追价上限后转市价
|
||||
elapsed = time.time() - start_ts
|
||||
ticker2 = await self.client.get_ticker_24h(symbol)
|
||||
cur2 = float(ticker2.get("price")) if ticker2 else current_px
|
||||
drift_ratio = 0.0
|
||||
try:
|
||||
base = float(initial_limit) if float(initial_limit) > 0 else cur2
|
||||
drift_ratio = abs((cur2 - base) / base)
|
||||
except Exception:
|
||||
drift_ratio = 0.0
|
||||
|
||||
if allow_market_fallback and elapsed >= market_fallback_after_sec:
|
||||
if drift_ratio <= max_drift_ratio:
|
||||
try:
|
||||
await self.client.cancel_order(symbol, int(entry_order_id))
|
||||
except Exception:
|
||||
pass
|
||||
self._pending_entry_orders.pop(symbol, None)
|
||||
logger.info(f"{symbol} [智能入场] 限价超时,且偏离{drift_ratio*100:.2f}%≤{max_drift_ratio*100:.2f}%,转市价兜底")
|
||||
order = await self.client.place_order(symbol=symbol, side=side, quantity=quantity, order_type="MARKET")
|
||||
break
|
||||
else:
|
||||
logger.info(f"{symbol} [智能入场] 限价超时,但偏离{drift_ratio*100:.2f}%>{max_drift_ratio*100:.2f}%,取消并放弃本次交易")
|
||||
try:
|
||||
await self.client.cancel_order(symbol, int(entry_order_id))
|
||||
except Exception:
|
||||
pass
|
||||
self._pending_entry_orders.pop(symbol, None)
|
||||
return None
|
||||
|
||||
# 震荡/不允许市价兜底:尝试追价(减小 offset -> 更靠近当前价),但不突破追价上限
|
||||
try:
|
||||
await self.client.cancel_order(symbol, int(entry_order_id))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# offset 逐步减少到 0(越追越接近当前价)
|
||||
step_ratio = (step + 1) / max(1, chase_steps)
|
||||
cur_offset_ratio = max(0.0, limit_offset_ratio * (1.0 - step_ratio))
|
||||
desired = self._calc_limit_entry_price(cur2, side, cur_offset_ratio)
|
||||
if side == "BUY":
|
||||
cap = initial_limit * (1 + max_drift_ratio)
|
||||
desired = min(desired, cap)
|
||||
else:
|
||||
cap = initial_limit * (1 - max_drift_ratio)
|
||||
desired = max(desired, cap)
|
||||
|
||||
if desired <= 0:
|
||||
self._pending_entry_orders.pop(symbol, None)
|
||||
return None
|
||||
|
||||
logger.info(
|
||||
f"{symbol} [智能入场] 追价 step={step+1}/{chase_steps} | 当前价={cur2:.6f} | "
|
||||
f"offset={cur_offset_ratio*100:.3f}% -> 限价={desired:.6f} | 偏离={drift_ratio*100:.2f}%"
|
||||
)
|
||||
order = await self.client.place_order(symbol=symbol, side=side, quantity=quantity, order_type="LIMIT", price=desired)
|
||||
if not order:
|
||||
self._pending_entry_orders.pop(symbol, None)
|
||||
return None
|
||||
entry_order_id = order.get("orderId")
|
||||
if entry_order_id:
|
||||
self._pending_entry_orders[symbol] = {"order_id": entry_order_id, "created_at_ms": int(time.time() * 1000)}
|
||||
|
||||
# 如果是市价兜底或最终限价成交,这里统一继续后续流程(下面会再查实际成交)
|
||||
|
||||
# ===== 统一处理:确认订单成交并获取实际成交价/数量 =====
|
||||
if order:
|
||||
# 获取开仓订单号
|
||||
entry_order_id = order.get('orderId')
|
||||
if not entry_order_id:
|
||||
entry_order_id = order.get("orderId")
|
||||
if entry_order_id:
|
||||
logger.info(f"{symbol} [开仓] 币安订单号: {entry_order_id}")
|
||||
|
||||
# 等待订单成交,检查订单状态并获取实际成交价格
|
||||
# 只有在订单真正成交(FILLED)后才保存到数据库
|
||||
actual_entry_price = None
|
||||
order_status = None
|
||||
filled_quantity = 0
|
||||
max_retries = 5 # 最多重试5次,每次等待1秒
|
||||
retry_count = 0
|
||||
|
||||
while retry_count < max_retries:
|
||||
try:
|
||||
# 等待一小段时间让订单成交
|
||||
await asyncio.sleep(1)
|
||||
|
||||
# 从币安获取订单详情,检查订单状态
|
||||
try:
|
||||
order_info = await self.client.client.futures_get_order(symbol=symbol, orderId=entry_order_id)
|
||||
if order_info:
|
||||
order_status = order_info.get('status')
|
||||
logger.info(f"{symbol} [开仓] 订单状态: {order_status} (重试 {retry_count + 1}/{max_retries})")
|
||||
|
||||
# 检查订单是否已成交
|
||||
if order_status == 'FILLED':
|
||||
# 订单已完全成交,获取实际成交价格和数量
|
||||
actual_entry_price = float(order_info.get('avgPrice', 0)) or float(order_info.get('price', 0))
|
||||
filled_quantity = float(order_info.get('executedQty', 0))
|
||||
|
||||
if actual_entry_price > 0 and filled_quantity > 0:
|
||||
logger.info(f"{symbol} [开仓] ✓ 订单已成交,成交价格: {actual_entry_price:.4f} USDT, 成交数量: {filled_quantity:.4f}")
|
||||
break
|
||||
elif 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:
|
||||
actual_entry_price = total_value / total_qty
|
||||
filled_quantity = total_qty
|
||||
logger.info(f"{symbol} [开仓] ✓ 订单已成交,从成交记录计算平均成交价格: {actual_entry_price:.4f} USDT, 成交数量: {filled_quantity:.4f}")
|
||||
break
|
||||
elif order_status == 'PARTIALLY_FILLED':
|
||||
# 部分成交,继续等待
|
||||
filled_quantity = float(order_info.get('executedQty', 0))
|
||||
logger.info(f"{symbol} [开仓] ⏳ 订单部分成交 ({filled_quantity:.4f}/{quantity:.4f}),继续等待...")
|
||||
retry_count += 1
|
||||
continue
|
||||
elif order_status in ['NEW', 'PENDING_NEW']:
|
||||
# 订单已提交但未成交,继续等待
|
||||
logger.info(f"{symbol} [开仓] ⏳ 订单已提交但未成交,继续等待...")
|
||||
retry_count += 1
|
||||
continue
|
||||
elif order_status in ['CANCELED', 'REJECTED', 'EXPIRED']:
|
||||
# 订单被取消、拒绝或过期
|
||||
logger.error(f"{symbol} [开仓] ❌ 订单状态异常: {order_status},订单未成交")
|
||||
return None
|
||||
else:
|
||||
logger.warning(f"{symbol} [开仓] ⚠️ 未知订单状态: {order_status},继续等待...")
|
||||
retry_count += 1
|
||||
continue
|
||||
except Exception as order_error:
|
||||
logger.warning(f"{symbol} [开仓] 获取订单详情失败: {order_error},重试中...")
|
||||
retry_count += 1
|
||||
continue
|
||||
except Exception as price_error:
|
||||
logger.warning(f"{symbol} [开仓] 检查订单状态时出错: {price_error},重试中...")
|
||||
retry_count += 1
|
||||
continue
|
||||
|
||||
# 检查订单是否最终成交
|
||||
if order_status != 'FILLED':
|
||||
logger.error(f"{symbol} [开仓] ❌ 订单未成交,状态: {order_status},不保存到数据库")
|
||||
return None
|
||||
if entry_order_id:
|
||||
# 智能入场的限价订单可能需要更长等待,这里给一个总等待兜底(默认 30s)
|
||||
confirm_timeout = int(config.TRADING_CONFIG.get("ENTRY_CONFIRM_TIMEOUT_SEC", 30) or 30)
|
||||
res = await self._wait_for_order_filled(symbol, int(entry_order_id), timeout_sec=confirm_timeout, poll_sec=1.0)
|
||||
order_status = res.get("status")
|
||||
if res.get("ok"):
|
||||
actual_entry_price = float(res.get("avg_price") or 0)
|
||||
filled_quantity = float(res.get("executed_qty") or 0)
|
||||
else:
|
||||
logger.error(f"{symbol} [开仓] ❌ 订单未成交,状态: {order_status},不保存到数据库")
|
||||
self._pending_entry_orders.pop(symbol, None)
|
||||
return None
|
||||
|
||||
if not actual_entry_price or actual_entry_price <= 0:
|
||||
logger.error(f"{symbol} [开仓] ❌ 无法获取实际成交价格,不保存到数据库")
|
||||
self._pending_entry_orders.pop(symbol, None)
|
||||
return None
|
||||
|
||||
if filled_quantity <= 0:
|
||||
logger.error(f"{symbol} [开仓] ❌ 成交数量为0,不保存到数据库")
|
||||
self._pending_entry_orders.pop(symbol, None)
|
||||
return None
|
||||
|
||||
# 使用实际成交价格和数量
|
||||
|
|
@ -317,6 +432,40 @@ class PositionManager:
|
|||
quantity = filled_quantity # 使用实际成交数量
|
||||
logger.info(f"{symbol} [开仓] ✓ 使用实际成交价格: {entry_price:.4f} USDT (下单时价格: {original_entry_price:.4f}), 成交数量: {quantity:.4f}")
|
||||
|
||||
# 成交后清理 pending
|
||||
self._pending_entry_orders.pop(symbol, None)
|
||||
|
||||
# ===== 成交后基于“实际成交价/数量”重新计算止损止盈(修复限价/滑点导致的偏差)=====
|
||||
stop_loss_pct_margin = config.TRADING_CONFIG.get('STOP_LOSS_PERCENT', 0.03)
|
||||
stop_loss_price = self.risk_manager.get_stop_loss_price(
|
||||
entry_price, side, quantity, leverage,
|
||||
stop_loss_pct=stop_loss_pct_margin,
|
||||
klines=klines,
|
||||
bollinger=bollinger,
|
||||
atr=atr
|
||||
)
|
||||
|
||||
stop_distance_for_tp = None
|
||||
if side == 'BUY':
|
||||
stop_distance_for_tp = entry_price - stop_loss_price
|
||||
else:
|
||||
stop_distance_for_tp = stop_loss_price - entry_price
|
||||
|
||||
if atr is not None and atr > 0 and entry_price > 0:
|
||||
atr_percent = atr / entry_price
|
||||
atr_multiplier = config.TRADING_CONFIG.get('ATR_STOP_LOSS_MULTIPLIER', 1.8)
|
||||
stop_distance_for_tp = entry_price * atr_percent * atr_multiplier
|
||||
|
||||
take_profit_pct_margin = config.TRADING_CONFIG.get('TAKE_PROFIT_PERCENT', 0.30)
|
||||
if take_profit_pct_margin is None or take_profit_pct_margin == 0:
|
||||
take_profit_pct_margin = float(stop_loss_pct_margin or 0) * 3.0
|
||||
take_profit_price = self.risk_manager.get_take_profit_price(
|
||||
entry_price, side, quantity, leverage,
|
||||
take_profit_pct=take_profit_pct_margin,
|
||||
atr=atr,
|
||||
stop_distance=stop_distance_for_tp
|
||||
)
|
||||
|
||||
# 分步止盈(基于“实际成交价 + 已计算的止损/止盈”)
|
||||
if side == 'BUY':
|
||||
take_profit_1 = entry_price + (entry_price - stop_loss_price) # 盈亏比1:1
|
||||
|
|
|
|||
|
|
@ -180,6 +180,9 @@ class TradingStrategy:
|
|||
leverage=dynamic_leverage,
|
||||
trade_direction=trade_direction,
|
||||
entry_reason=entry_reason,
|
||||
signal_strength=signal_strength,
|
||||
market_regime=market_regime,
|
||||
trend_4h=trade_signal.get('trend_4h'),
|
||||
atr=symbol_info.get('atr'),
|
||||
klines=symbol_info.get('klines'), # 传递K线数据用于动态止损
|
||||
bollinger=symbol_info.get('bollinger') # 传递布林带数据用于动态止损
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user