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__) logger = logging.getLogger(__name__)
router = APIRouter() 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("")
@router.get("/") @router.get("/")
@ -36,6 +100,11 @@ async def get_all_configs():
'category': config['category'], 'category': config['category'],
'description': config['description'] 'description': config['description']
} }
# 合并“默认但未入库”的配置项(用于新功能上线时直接在配置页可见)
for k, meta in SMART_ENTRY_CONFIG_DEFAULTS.items():
if k not in result:
result[k] = meta
return result return result
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))
@ -357,12 +426,16 @@ async def update_config(key: str, item: ConfigUpdate):
try: try:
# 获取现有配置以确定类型和分类 # 获取现有配置以确定类型和分类
existing = TradingConfig.get(key) existing = TradingConfig.get(key)
if not existing: if existing:
raise HTTPException(status_code=404, detail="Config not found")
config_type = item.type or existing['config_type'] config_type = item.type or existing['config_type']
category = item.category or existing['category'] category = item.category or existing['category']
description = item.description or existing['description'] 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': if config_type == 'number':
@ -429,8 +502,8 @@ async def update_configs_batch(configs: list[ConfigItem]):
errors.append(f"{item.key}: Invalid number value") errors.append(f"{item.key}: Invalid number value")
continue continue
# 特殊验证:百分比配置 # 特殊验证:百分比配置(兼容 PERCENT / PCT
if 'PERCENT' in item.key and item.type == 'number': if ('PERCENT' in item.key or 'PCT' in item.key) and item.type == 'number':
if not (0 <= float(item.value) <= 1): if not (0 <= float(item.value) <= 1):
errors.append(f"{item.key}: Must be between 0 and 1") errors.append(f"{item.key}: Must be between 0 and 1")
continue continue

View File

@ -207,6 +207,18 @@ def _get_trading_config():
'TRAILING_STOP_ACTIVATION': 0.10, # 移动止损激活提高到10%盈利10%后激活,给趋势更多空间) 'TRAILING_STOP_ACTIVATION': 0.10, # 移动止损激活提高到10%盈利10%后激活,给趋势更多空间)
'TRAILING_STOP_PROTECT': 0.05, # 保护利润提高到5%保护5%利润,更合理) 'TRAILING_STOP_PROTECT': 0.05, # 保护利润提高到5%保护5%利润,更合理)
'POSITION_SYNC_INTERVAL': 60, # 持仓状态同步间隔缩短到1分钟确保状态及时同步 '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配置优先从数据库回退到环境变量和默认值 # 币安API配置优先从数据库回退到环境变量和默认值
@ -227,6 +239,16 @@ defaults = {
'CONFIRM_INTERVAL': '4h', 'CONFIRM_INTERVAL': '4h',
'ENTRY_INTERVAL': '15m', 'ENTRY_INTERVAL': '15m',
'LIMIT_ORDER_OFFSET_PCT': 0.5, # 限价单偏移百分比默认0.5% '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(): for key, value in defaults.items():
if key not in TRADING_CONFIG: if key not in TRADING_CONFIG:

View File

@ -5,6 +5,7 @@ import asyncio
import logging import logging
import json import json
import aiohttp import aiohttp
import time
from typing import Dict, List, Optional from typing import Dict, List, Optional
from datetime import datetime from datetime import datetime
try: try:
@ -69,6 +70,80 @@ class PositionManager:
self.active_positions: Dict[str, Dict] = {} self.active_positions: Dict[str, Dict] = {}
self._monitor_tasks: Dict[str, asyncio.Task] = {} # WebSocket监控任务字典 self._monitor_tasks: Dict[str, asyncio.Task] = {} # WebSocket监控任务字典
self._monitoring_enabled = True # 是否启用实时监控 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( async def open_position(
self, self,
@ -77,6 +152,9 @@ class PositionManager:
leverage: int = 10, leverage: int = 10,
trade_direction: Optional[str] = None, trade_direction: Optional[str] = None,
entry_reason: str = '', entry_reason: str = '',
signal_strength: Optional[int] = None,
market_regime: Optional[str] = None,
trend_4h: Optional[str] = None,
atr: Optional[float] = None, atr: Optional[float] = None,
klines: Optional[List] = None, klines: Optional[List] = None,
bollinger: Optional[Dict] = None bollinger: Optional[Dict] = None
@ -93,6 +171,22 @@ class PositionManager:
订单信息失败返回None 订单信息失败返回None
""" """
try: 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): if not await self.risk_manager.should_trade(symbol, change_percent):
return None return None
@ -166,16 +260,183 @@ class PositionManager:
except Exception as e: except Exception as e:
logger.debug(f"从Redis重新加载配置失败: {e}") logger.debug(f"从Redis重新加载配置失败: {e}")
# 使用基于保证金的止损止盈(结合技术分析) # ===== 智能入场方案C趋势强更少错过震荡更保守=====
# 计算保证金和仓位价值 smart_entry_enabled = bool(config.TRADING_CONFIG.get("SMART_ENTRY_ENABLED", True))
position_value = entry_price * quantity # LIMIT_ORDER_OFFSET_PCT兼容“比例/百分比”两种存储方式
margin = position_value / leverage if leverage > 0 else position_value 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) 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( stop_loss_price = self.risk_manager.get_stop_loss_price(
entry_price, side, quantity, leverage, entry_price, side, quantity, leverage,
stop_loss_pct=stop_loss_pct_margin, stop_loss_pct=stop_loss_pct_margin,
@ -184,139 +445,27 @@ class PositionManager:
atr=atr atr=atr
) )
# 计算止损距离(用于盈亏比计算止盈)
stop_distance_for_tp = None stop_distance_for_tp = None
if side == 'BUY': if side == 'BUY':
stop_distance_for_tp = entry_price - stop_loss_price stop_distance_for_tp = entry_price - stop_loss_price
else: # SELL else:
stop_distance_for_tp = stop_loss_price - entry_price stop_distance_for_tp = stop_loss_price - entry_price
# 如果使用ATR策略优先使用ATR计算的止损距离 if atr is not None and atr > 0 and entry_price > 0:
if atr is not None and atr > 0: atr_percent = atr / entry_price
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_multiplier = config.TRADING_CONFIG.get('ATR_STOP_LOSS_MULTIPLIER', 1.8)
atr_stop_distance = entry_price * atr_percent * atr_multiplier stop_distance_for_tp = entry_price * atr_percent * atr_multiplier
# 使用ATR计算的止损距离更准确
stop_distance_for_tp = atr_stop_distance
# 计算止盈基于保证金支持ATR动态止盈
# 优先使用配置的止盈百分比如果没有配置则使用止损的3倍盈亏比3:1
take_profit_pct_margin = config.TRADING_CONFIG.get('TAKE_PROFIT_PERCENT', 0.30) 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: 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( take_profit_price = self.risk_manager.get_take_profit_price(
entry_price, side, quantity, leverage, entry_price, side, quantity, leverage,
take_profit_pct=take_profit_pct_margin, take_profit_pct=take_profit_pct_margin,
atr=atr, # 传递ATR用于动态止盈 atr=atr,
stop_distance=stop_distance_for_tp # 传递止损距离用于盈亏比计算 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': if side == 'BUY':
take_profit_1 = entry_price + (entry_price - stop_loss_price) # 盈亏比1:1 take_profit_1 = entry_price + (entry_price - stop_loss_price) # 盈亏比1:1

View File

@ -180,6 +180,9 @@ class TradingStrategy:
leverage=dynamic_leverage, leverage=dynamic_leverage,
trade_direction=trade_direction, trade_direction=trade_direction,
entry_reason=entry_reason, entry_reason=entry_reason,
signal_strength=signal_strength,
market_regime=market_regime,
trend_4h=trade_signal.get('trend_4h'),
atr=symbol_info.get('atr'), atr=symbol_info.get('atr'),
klines=symbol_info.get('klines'), # 传递K线数据用于动态止损 klines=symbol_info.get('klines'), # 传递K线数据用于动态止损
bollinger=symbol_info.get('bollinger') # 传递布林带数据用于动态止损 bollinger=symbol_info.get('bollinger') # 传递布林带数据用于动态止损