This commit is contained in:
薇薇安 2026-01-22 19:30:57 +08:00
parent b1f4cbddac
commit e5a281569c
7 changed files with 189 additions and 34 deletions

View File

@ -1153,6 +1153,9 @@ async def sync_positions(account_id: int = Depends(get_account_id)):
except Exception:
exit_time_ts = None
# 检查订单的 reduceOnly 字段:如果是 true说明是自动平仓不应该标记为 manual
is_reduce_only = latest_close_order.get("reduceOnly", False) if latest_close_order else False
if "TRAILING" in otype:
exit_reason = "trailing_stop"
elif "TAKE_PROFIT" in otype:
@ -1160,7 +1163,44 @@ async def sync_positions(account_id: int = Depends(get_account_id)):
elif "STOP" in otype:
exit_reason = "stop_loss"
elif otype in ("MARKET", "LIMIT"):
exit_reason = "manual"
# 如果是 reduceOnly 订单,说明是自动平仓(可能是保护单触发的),先标记为 sync后续用价格判断
if is_reduce_only:
exit_reason = "sync" # 临时标记,后续用价格判断
else:
exit_reason = "manual" # 非 reduceOnly 的 MARKET/LIMIT 订单才是真正的手动平仓
# 价格兜底:如果能明显命中止损/止盈价,则覆盖 exit_reason
# 这对于保护单触发的 MARKET 订单特别重要
if exit_reason == "sync" or exit_reason == "manual":
try:
def _close_to(a: float, b: float, max_pct: float = 0.02) -> bool:
if a <= 0 or b <= 0:
return False
return abs((a - b) / b) <= max_pct
ep = float(exit_price or 0)
if ep > 0:
sl = trade.get("stop_loss_price")
tp = trade.get("take_profit_price")
tp1 = trade.get("take_profit_1")
tp2 = trade.get("take_profit_2")
# 优先检查止损
if sl is not None and _close_to(ep, float(sl), max_pct=0.02):
exit_reason = "stop_loss"
# 然后检查止盈
elif tp is not None and _close_to(ep, float(tp), max_pct=0.02):
exit_reason = "take_profit"
elif tp1 is not None and _close_to(ep, float(tp1), max_pct=0.02):
exit_reason = "take_profit"
elif tp2 is not None and _close_to(ep, float(tp2), max_pct=0.02):
exit_reason = "take_profit"
# 如果价格接近入场价,可能是移动止损触发的
elif is_reduce_only:
entry_price_val = float(trade.get("entry_price", 0) or 0)
if entry_price_val > 0 and _close_to(ep, entry_price_val, max_pct=0.01):
exit_reason = "trailing_stop"
except Exception:
pass
if not exit_price or exit_price <= 0:
ticker = await client.get_ticker_24h(symbol)

View File

@ -460,6 +460,9 @@ async def sync_trades_from_binance(
# 细分 exit_reason优先使用币安订单类型其次用价格接近止损/止盈做兜底
exit_reason = "sync"
# 检查订单的 reduceOnly 字段:如果是 true说明是自动平仓不应该标记为 manual
is_reduce_only = order.get("reduceOnly", False) if isinstance(order, dict) else False
if "TRAILING" in otype:
exit_reason = "trailing_stop"
elif "TAKE_PROFIT" in otype:
@ -467,10 +470,14 @@ async def sync_trades_from_binance(
elif "STOP" in otype:
exit_reason = "stop_loss"
elif otype in ("MARKET", "LIMIT"):
exit_reason = "manual"
# 如果是 reduceOnly 订单,说明是自动平仓(可能是保护单触发的),先标记为 sync后续用价格判断
if is_reduce_only:
exit_reason = "sync" # 临时标记,后续用价格判断
else:
exit_reason = "manual" # 非 reduceOnly 的 MARKET/LIMIT 订单才是真正的手动平仓
try:
def _close_to(a: float, b: float, max_pct: float = 0.01) -> bool:
def _close_to(a: float, b: float, max_pct: float = 0.02) -> bool: # 放宽到2%,因为滑点可能导致价格不完全一致
if a <= 0 or b <= 0:
return False
return abs((a - b) / b) <= max_pct
@ -481,14 +488,21 @@ async def sync_trades_from_binance(
tp = trade.get("take_profit_price")
tp1 = trade.get("take_profit_1")
tp2 = trade.get("take_profit_2")
if sl is not None and _close_to(ep, float(sl), max_pct=0.01):
# 优先检查止损
if sl is not None and _close_to(ep, float(sl), max_pct=0.02):
exit_reason = "stop_loss"
elif tp is not None and _close_to(ep, float(tp), max_pct=0.01):
# 然后检查止盈
elif tp is not None and _close_to(ep, float(tp), max_pct=0.02):
exit_reason = "take_profit"
elif tp1 is not None and _close_to(ep, float(tp1), max_pct=0.01):
elif tp1 is not None and _close_to(ep, float(tp1), max_pct=0.02):
exit_reason = "take_profit"
elif tp2 is not None and _close_to(ep, float(tp2), max_pct=0.01):
elif tp2 is not None and _close_to(ep, float(tp2), max_pct=0.02):
exit_reason = "take_profit"
# 如果价格接近入场价,可能是移动止损触发的
elif is_reduce_only and exit_reason == "sync":
entry_price_val = float(trade.get("entry_price", 0) or 0)
if entry_price_val > 0 and _close_to(ep, entry_price_val, max_pct=0.01):
exit_reason = "trailing_stop"
except Exception:
pass

View File

@ -541,11 +541,11 @@ class ConfigManager:
'STOP_LOSS_PERCENT': eff_get('STOP_LOSS_PERCENT', 0.10), # 默认10%
'TAKE_PROFIT_PERCENT': eff_get('TAKE_PROFIT_PERCENT', 0.25), # 默认25%从30%放宽到25%配合ATR止盈放大盈亏比
'MIN_STOP_LOSS_PRICE_PCT': eff_get('MIN_STOP_LOSS_PRICE_PCT', 0.02), # 默认2%
'MIN_TAKE_PROFIT_PRICE_PCT': eff_get('MIN_TAKE_PROFIT_PRICE_PCT', 0.03), # 默认3%
'MIN_TAKE_PROFIT_PRICE_PCT': eff_get('MIN_TAKE_PROFIT_PRICE_PCT', 0.02), # 默认2%防止ATR过小时计算出不切实际的微小止盈距离
'USE_ATR_STOP_LOSS': eff_get('USE_ATR_STOP_LOSS', True), # 是否使用ATR动态止损
'ATR_STOP_LOSS_MULTIPLIER': eff_get('ATR_STOP_LOSS_MULTIPLIER', 1.8), # ATR止损倍数1.5-2倍
'ATR_TAKE_PROFIT_MULTIPLIER': eff_get('ATR_TAKE_PROFIT_MULTIPLIER', 4.5), # ATR止盈倍数从3.0提升到4.5,放大盈亏比
'RISK_REWARD_RATIO': eff_get('RISK_REWARD_RATIO', 3.0), # 盈亏比(止损距离的倍数
'ATR_TAKE_PROFIT_MULTIPLIER': eff_get('ATR_TAKE_PROFIT_MULTIPLIER', 1.5), # ATR止盈倍数从4.5降至1.5将盈亏比从3:1降至更现实、可达成的1.5:1提升止盈触发率
'RISK_REWARD_RATIO': eff_get('RISK_REWARD_RATIO', 1.5), # 盈亏比(止损距离的倍数从3.0降至1.5,更容易达成
'ATR_PERIOD': eff_get('ATR_PERIOD', 14), # ATR计算周期
'USE_DYNAMIC_ATR_MULTIPLIER': eff_get('USE_DYNAMIC_ATR_MULTIPLIER', False), # 是否根据波动率动态调整ATR倍数
'ATR_MULTIPLIER_MIN': eff_get('ATR_MULTIPLIER_MIN', 1.5), # 动态ATR倍数最小值

View File

@ -208,11 +208,11 @@ INSERT INTO `trading_config` (`config_key`, `config_value`, `config_type`, `cate
('STOP_LOSS_PERCENT', '0.10', 'number', 'risk', '止损10%(相对于保证金)'),
('TAKE_PROFIT_PERCENT', '0.30', 'number', 'risk', '止盈30%相对于保证金盈亏比3:1'),
('MIN_STOP_LOSS_PRICE_PCT', '0.02', 'number', 'risk', '最小止损价格变动2%(防止止损过紧)'),
('MIN_TAKE_PROFIT_PRICE_PCT', '0.03', 'number', 'risk', '最小止盈价格变动:3%(防止止盈过紧'),
('MIN_TAKE_PROFIT_PRICE_PCT', '0.02', 'number', 'risk', '最小止盈价格变动:2%防止ATR过小时计算出不切实际的微小止盈距离'),
('USE_ATR_STOP_LOSS', 'true', 'boolean', 'risk', '是否使用ATR动态止损优先于固定百分比'),
('ATR_STOP_LOSS_MULTIPLIER', '1.8', 'number', 'risk', 'ATR止损倍数1.5-2倍ATR默认1.8'),
('ATR_TAKE_PROFIT_MULTIPLIER', '3.0', 'number', 'risk', 'ATR止盈倍数3倍ATR对应3:1盈亏比'),
('RISK_REWARD_RATIO', '3.0', 'number', 'risk', '盈亏比(止损距离的倍数,用于计算止盈'),
('ATR_TAKE_PROFIT_MULTIPLIER', '1.5', 'number', 'risk', 'ATR止盈倍数从4.5降至1.5将盈亏比从3:1降至更现实、可达成的1.5:1提升止盈触发率'),
('RISK_REWARD_RATIO', '1.5', 'number', 'risk', '盈亏比(止损距离的倍数,用于计算止盈从3.0降至1.5,更容易达成'),
('ATR_PERIOD', '14', 'number', 'risk', 'ATR计算周期默认14'),
('USE_DYNAMIC_ATR_MULTIPLIER', 'false', 'boolean', 'risk', '是否根据波动率动态调整ATR倍数'),
('ATR_MULTIPLIER_MIN', '1.5', 'number', 'risk', '动态ATR倍数最小值'),

View File

@ -20,8 +20,8 @@ class ATRStrategy:
"""初始化ATR策略"""
self.use_atr = config.TRADING_CONFIG.get('USE_ATR_STOP_LOSS', True)
self.atr_sl_multiplier = config.TRADING_CONFIG.get('ATR_STOP_LOSS_MULTIPLIER', 1.8)
self.atr_tp_multiplier = config.TRADING_CONFIG.get('ATR_TAKE_PROFIT_MULTIPLIER', 3.0)
self.risk_reward_ratio = config.TRADING_CONFIG.get('RISK_REWARD_RATIO', 3.0)
self.atr_tp_multiplier = config.TRADING_CONFIG.get('ATR_TAKE_PROFIT_MULTIPLIER', 1.5)
self.risk_reward_ratio = config.TRADING_CONFIG.get('RISK_REWARD_RATIO', 1.5)
self.atr_period = config.TRADING_CONFIG.get('ATR_PERIOD', 14)
# 动态调整ATR倍数的参数
@ -38,8 +38,8 @@ class ATRStrategy:
cfg = getattr(config, "TRADING_CONFIG", {}) or {}
self.use_atr = bool(cfg.get('USE_ATR_STOP_LOSS', True))
self.atr_sl_multiplier = float(cfg.get('ATR_STOP_LOSS_MULTIPLIER', 1.8))
self.atr_tp_multiplier = float(cfg.get('ATR_TAKE_PROFIT_MULTIPLIER', 3.0))
self.risk_reward_ratio = float(cfg.get('RISK_REWARD_RATIO', 3.0))
self.atr_tp_multiplier = float(cfg.get('ATR_TAKE_PROFIT_MULTIPLIER', 1.5))
self.risk_reward_ratio = float(cfg.get('RISK_REWARD_RATIO', 1.5))
self.atr_period = int(cfg.get('ATR_PERIOD', 14))
self.use_dynamic_atr_multiplier = bool(cfg.get('USE_DYNAMIC_ATR_MULTIPLIER', False))
self.atr_multiplier_min = float(cfg.get('ATR_MULTIPLIER_MIN', 1.5))

View File

@ -190,11 +190,11 @@ def _get_trading_config():
'STOP_LOSS_PERCENT': 0.10, # 止损百分比相对于保证金默认10%
'TAKE_PROFIT_PERCENT': 0.25, # 止盈百分比相对于保证金默认25%从30%放宽配合ATR止盈放大盈亏比
'MIN_STOP_LOSS_PRICE_PCT': 0.02, # 最小止损价格变动百分比如0.02表示2%防止止损过紧默认2%
'MIN_TAKE_PROFIT_PRICE_PCT': 0.03, # 最小止盈价格变动百分比如0.03表示3%防止止盈过紧默认3%
'MIN_TAKE_PROFIT_PRICE_PCT': 0.02, # 最小止盈价格变动百分比如0.02表示2%防止ATR过小时计算出不切实际的微小止盈距离默认2%
'USE_ATR_STOP_LOSS': True, # 是否使用ATR动态止损优先于固定百分比
'ATR_STOP_LOSS_MULTIPLIER': 1.8, # ATR止损倍数1.5-2倍ATR默认1.8
'ATR_TAKE_PROFIT_MULTIPLIER': 4.5, # ATR止盈倍数从3.0提升到4.5,放大盈亏比,让利润奔跑
'RISK_REWARD_RATIO': 3.0, # 盈亏比(止损距离的倍数,用于计算止盈
'ATR_TAKE_PROFIT_MULTIPLIER': 1.5, # ATR止盈倍数从4.5降至1.5将盈亏比从3:1降至更现实、可达成的1.5:1提升止盈触发率
'RISK_REWARD_RATIO': 1.5, # 盈亏比(止损距离的倍数,用于计算止盈从3.0降至1.5,更容易达成
'ATR_PERIOD': 14, # ATR计算周期默认14
'USE_DYNAMIC_ATR_MULTIPLIER': False, # 是否根据波动率动态调整ATR倍数
'ATR_MULTIPLIER_MIN': 1.5, # 动态ATR倍数最小值

View File

@ -1466,13 +1466,13 @@ class PositionManager:
# 分步止盈后的“保本”处理:
# - 若启用 USE_TRAILING_STOP允许把剩余仓位止损移至成本价并进入移动止损阶段
# - 若关闭 USE_TRAILING_STOP严格不自动移动止损避免你说的“仍然保本/仍然移动止损”)
use_trailing = config.TRADING_CONFIG.get('USE_TRAILING_STOP', True)
if use_trailing:
position_info['stopLoss'] = entry_price
position_info['trailingStopActivated'] = True
logger.info(f"{symbol} 剩余仓位止损移至成本价(保本),配合移动止损博取更大利润")
else:
logger.info(f"{symbol} 已部分止盈,但已关闭移动止损:不自动将止损移至成本价")
# 无论是否启用移动止损,分步止盈后都将剩余仓位止损移至成本价(保本)
# 这样既不错失后续行情,又彻底杜绝了该笔交易亏损的可能
position_info['stopLoss'] = entry_price
logger.info(
f"{symbol} 部分止盈后:剩余仓位止损移至成本价 {entry_price:.4f}(保本),"
f"剩余50%仓位追求1.5:1止盈目标"
)
else:
# 兜底:可能遇到 -2022reduceOnly rejected等竞态重新查一次持仓
try:
@ -1878,6 +1878,9 @@ class PositionManager:
or latest_close_order.get("origType")
or ""
).upper()
# 检查订单的 reduceOnly 字段:如果是 true说明是自动平仓不应该标记为 manual
is_reduce_only = latest_close_order.get("reduceOnly", False)
if "TRAILING" in otype:
exit_reason = "trailing_stop"
elif "TAKE_PROFIT" in otype:
@ -1886,7 +1889,11 @@ class PositionManager:
# STOP / STOP_MARKET 通常对应止损触发
exit_reason = "stop_loss"
elif otype in ("MARKET", "LIMIT"):
exit_reason = "manual"
# 如果是 reduceOnly 订单,说明是自动平仓(可能是保护单触发的),先标记为 sync后续用价格判断
if is_reduce_only:
exit_reason = "sync" # 临时标记,后续用价格判断
else:
exit_reason = "manual" # 非 reduceOnly 的 MARKET/LIMIT 订单才是真正的手动平仓
ms = latest_close_order.get("updateTime") or latest_close_order.get("time")
try:
@ -1900,8 +1907,9 @@ class PositionManager:
pass
# 价格兜底:如果能明显命中止损/止盈价,则覆盖 exit_reason
# 这对于保护单触发的 MARKET 订单特别重要(币安的保护单触发后会生成 MARKET 订单)
try:
def _close_to(a: float, b: float, max_pct: float = 0.01) -> bool:
def _close_to(a: float, b: float, max_pct: float = 0.02) -> bool: # 放宽到2%,因为滑点可能导致价格不完全一致
if a <= 0 or b <= 0:
return False
return abs((a - b) / b) <= max_pct
@ -1912,14 +1920,22 @@ class PositionManager:
tp = trade.get("take_profit_price")
tp1 = trade.get("take_profit_1")
tp2 = trade.get("take_profit_2")
if sl is not None and _close_to(ep, float(sl), max_pct=0.01):
# 优先检查止损(因为止损更关键)
if sl is not None and _close_to(ep, float(sl), max_pct=0.02):
exit_reason = "stop_loss"
elif tp is not None and _close_to(ep, float(tp), max_pct=0.01):
# 然后检查止盈
elif tp is not None and _close_to(ep, float(tp), max_pct=0.02):
exit_reason = "take_profit"
elif tp1 is not None and _close_to(ep, float(tp1), max_pct=0.01):
elif tp1 is not None and _close_to(ep, float(tp1), max_pct=0.02):
exit_reason = "take_profit"
elif tp2 is not None and _close_to(ep, float(tp2), max_pct=0.01):
elif tp2 is not None and _close_to(ep, float(tp2), max_pct=0.02):
exit_reason = "take_profit"
# 如果之前标记为 sync 且是 reduceOnly 订单,但价格不匹配止损/止盈,可能是其他自动平仓(如移动止损)
elif exit_reason == "sync" and is_reduce_only:
# 检查是否是移动止损:如果价格接近入场价,可能是移动止损触发的
entry_price_val = float(trade.get("entry_price", 0) or 0)
if entry_price_val > 0 and _close_to(ep, entry_price_val, max_pct=0.01):
exit_reason = "trailing_stop"
except Exception:
pass
@ -2555,7 +2571,92 @@ class PositionManager:
logger.warning(f" 移动止损: 已激活(从初始止损 {position_info.get('initialStopLoss', 'N/A')} 调整)")
logger.warning("=" * 80)
# 检查止盈(基于保证金收益比)
# 检查分步止盈(实时监控)
if not should_close:
take_profit_1 = position_info.get('takeProfit1') # 第一目标盈亏比1:1
take_profit_2 = position_info.get('takeProfit2', position_info.get('takeProfit')) # 第二目标1.5:1
partial_profit_taken = position_info.get('partialProfitTaken', False)
remaining_quantity = position_info.get('remainingQuantity', quantity)
# 第一目标盈亏比1:1了结50%仓位
if not partial_profit_taken and take_profit_1 is not None:
# 计算第一目标对应的保证金百分比
if position_info['side'] == 'BUY':
take_profit_1_amount = (take_profit_1 - entry_price) * quantity
else: # SELL
take_profit_1_amount = (entry_price - take_profit_1) * quantity
take_profit_1_pct_margin = (take_profit_1_amount / margin * 100) if margin > 0 else 0
# 直接比较当前盈亏百分比与第一目标(基于保证金)
if pnl_percent_margin >= take_profit_1_pct_margin:
logger.info(
f"{symbol} [实时监控] 触发第一目标止盈盈亏比1:1基于保证金: "
f"当前盈亏={pnl_percent_margin:.2f}% of margin >= 目标={take_profit_1_pct_margin:.2f}% of margin | "
f"当前价={current_price_float:.4f}, 目标价={take_profit_1:.4f}"
)
# 部分平仓50%
partial_quantity = quantity * 0.5
try:
close_side = 'SELL' if position_info['side'] == 'BUY' else 'BUY'
close_position_side = 'LONG' if position_info['side'] == 'BUY' else 'SHORT'
live_amt = await self._get_live_position_amt(symbol, position_side=close_position_side)
if live_amt is None or abs(live_amt) <= 0:
logger.warning(f"{symbol} [实时监控] 部分止盈实时持仓已为0跳过部分平仓")
else:
partial_quantity = min(partial_quantity, abs(live_amt))
partial_quantity = await self._adjust_close_quantity(symbol, partial_quantity)
if partial_quantity > 0:
partial_order = await self.client.place_order(
symbol=symbol,
side=close_side,
quantity=partial_quantity,
order_type='MARKET',
reduce_only=True,
position_side=close_position_side,
)
if partial_order:
position_info['partialProfitTaken'] = True
position_info['remainingQuantity'] = remaining_quantity - partial_quantity
logger.info(
f"{symbol} [实时监控] 部分止盈成功: 平仓{partial_quantity:.4f},剩余{position_info['remainingQuantity']:.4f}"
)
# 分步止盈后的"保本"处理:将剩余仓位止损移至成本价(保本)
position_info['stopLoss'] = entry_price
logger.info(
f"{symbol} [实时监控] 部分止盈后:剩余仓位止损移至成本价 {entry_price:.4f}(保本),"
f"剩余50%仓位追求1.5:1止盈目标"
)
except Exception as e:
logger.error(f"{symbol} [实时监控] 部分止盈失败: {e}")
# 第二目标1.5:1止盈平掉剩余仓位
if partial_profit_taken and take_profit_2 is not None and not should_close:
# 计算第二目标对应的保证金百分比(基于剩余仓位)
if position_info['side'] == 'BUY':
take_profit_2_amount = (take_profit_2 - entry_price) * remaining_quantity
else: # SELL
take_profit_2_amount = (entry_price - take_profit_2) * remaining_quantity
remaining_margin = (entry_price * remaining_quantity) / leverage if leverage > 0 else (entry_price * remaining_quantity)
take_profit_2_pct_margin = (take_profit_2_amount / remaining_margin * 100) if remaining_margin > 0 else 0
# 计算剩余仓位的当前盈亏
if position_info['side'] == 'BUY':
remaining_pnl_amount = (current_price_float - entry_price) * remaining_quantity
else:
remaining_pnl_amount = (entry_price - current_price_float) * remaining_quantity
remaining_pnl_pct_margin = (remaining_pnl_amount / remaining_margin * 100) if remaining_margin > 0 else 0
# 直接比较剩余仓位盈亏百分比与第二目标(基于保证金)
if remaining_pnl_pct_margin >= take_profit_2_pct_margin:
should_close = True
exit_reason = 'take_profit'
logger.info(
f"{symbol} [实时监控] 触发第二目标止盈1.5:1基于保证金: "
f"剩余仓位盈亏={remaining_pnl_pct_margin:.2f}% of margin >= 目标={take_profit_2_pct_margin:.2f}% of margin | "
f"当前价={current_price_float:.4f}, 目标价={take_profit_2:.4f}, "
f"剩余数量={remaining_quantity:.4f}"
)
# 检查止盈(基于保证金收益比)- 用于未启用分步止盈的情况
if not should_close:
take_profit = position_info.get('takeProfit')
if take_profit is not None: