This commit is contained in:
薇薇安 2026-01-19 15:04:51 +08:00
parent e1a29011c8
commit fd661d11d4
4 changed files with 388 additions and 141 deletions

View File

@ -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")
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

View File

@ -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:

View File

@ -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,16 +260,183 @@ 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))
# 获取止损止盈百分比(相对于保证金)
# 规则:趋势(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
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
# 追价上限:超过就不追,宁愿错过(避免回到“无脑追价高频打损”)
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
# 总等待/追价参数
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)
# 初始限价(回调入场)
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 = 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:
if not entry_order_id:
entry_order_id = order.get("orderId")
if entry_order_id:
logger.info(f"{symbol} [开仓] 币安订单号: {entry_order_id}")
# 等待订单成交,检查订单状态并获取实际成交价格
# 只有在订单真正成交FILLED后才保存到数据库
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
# 使用实际成交价格和数量
original_entry_price = entry_price
entry_price = actual_entry_price
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)
take_profit_pct_margin = config.TRADING_CONFIG.get('TAKE_PROFIT_PERCENT', 0.05)
# 计算基于保证金的止损止盈
stop_loss_price = self.risk_manager.get_stop_loss_price(
entry_price, side, quantity, leverage,
stop_loss_pct=stop_loss_pct_margin,
@ -184,139 +445,27 @@ class PositionManager:
atr=atr
)
# 计算止损距离(用于盈亏比计算止盈)
stop_distance_for_tp = None
if side == 'BUY':
stop_distance_for_tp = entry_price - stop_loss_price
else: # SELL
else:
stop_distance_for_tp = stop_loss_price - entry_price
# 如果使用ATR策略优先使用ATR计算的止损距离
if atr is not None and atr > 0:
atr_percent = atr / entry_price if entry_price > 0 else None
if atr_percent:
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)
atr_stop_distance = entry_price * atr_percent * atr_multiplier
# 使用ATR计算的止损距离更准确
stop_distance_for_tp = atr_stop_distance
stop_distance_for_tp = entry_price * atr_percent * atr_multiplier
# 计算止盈基于保证金支持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_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, # 传递ATR用于动态止盈
stop_distance=stop_distance_for_tp # 传递止损距离用于盈亏比计算
atr=atr,
stop_distance=stop_distance_for_tp
)
# 下单
order = await self.client.place_order(
symbol=symbol,
side=side,
quantity=quantity,
order_type='MARKET'
)
if order:
# 获取开仓订单号
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 not actual_entry_price or actual_entry_price <= 0:
logger.error(f"{symbol} [开仓] ❌ 无法获取实际成交价格,不保存到数据库")
return None
if filled_quantity <= 0:
logger.error(f"{symbol} [开仓] ❌ 成交数量为0不保存到数据库")
return None
# 使用实际成交价格和数量
original_entry_price = entry_price
entry_price = actual_entry_price
quantity = filled_quantity # 使用实际成交数量
logger.info(f"{symbol} [开仓] ✓ 使用实际成交价格: {entry_price:.4f} USDT (下单时价格: {original_entry_price:.4f}), 成交数量: {quantity:.4f}")
# 分步止盈(基于“实际成交价 + 已计算的止损/止盈”)
if side == 'BUY':
take_profit_1 = entry_price + (entry_price - stop_loss_price) # 盈亏比1:1

View File

@ -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') # 传递布林带数据用于动态止损