This commit is contained in:
薇薇安 2026-07-08 13:11:48 +08:00
parent 013f8df750
commit bc962d93f9
7 changed files with 927 additions and 44 deletions

View File

@ -0,0 +1,251 @@
[
{
"key": "VOLATILITY_PULLBACK_ENABLED",
"value": true,
"type": "boolean",
"category": "strategy",
"description": "启用波动回调策略4H+1H同向1H区间极值15m波动触发"
},
{
"key": "VOLATILITY_PULLBACK_AUTO_ONLY",
"value": true,
"type": "boolean",
"category": "strategy",
"description": "自动交易仅使用VP信号不fallback MACD趋势"
},
{
"key": "VOLATILITY_PULLBACK_SKIP_REGIME_GATE",
"value": true,
"type": "boolean",
"category": "strategy",
"description": "VP信号跳过 ranging/trending 门禁"
},
{
"key": "VP_INSTANT_FLUSH_ENABLED",
"value": true,
"type": "boolean",
"category": "strategy",
"description": "即时模式进行中1H快速波动+前结构低/高点入场"
},
{
"key": "VP_PRIOR_STRUCTURE_BARS",
"value": 12,
"type": "number",
"category": "strategy",
"description": "前结构位已完成1H K线根数(不含当前进行中一根)"
},
{
"key": "VP_1H_FLUSH_MIN_PCT",
"value": 0.005,
"type": "number",
"category": "strategy",
"description": "进行中1H从开盘下冲/上冲至少0.5%"
},
{
"key": "VP_NEAR_PRIOR_LOW_PCT",
"value": 0.008,
"type": "number",
"category": "strategy",
"description": "做多现价在前低上方0.8%内"
},
{
"key": "VP_MAX_BELOW_PRIOR_LOW_PCT",
"value": 0.004,
"type": "number",
"category": "strategy",
"description": "做多允许刺破前低0.4%仍算入场区"
},
{
"key": "VP_NEAR_PRIOR_HIGH_PCT",
"value": 0.008,
"type": "number",
"category": "strategy"
},
{
"key": "VP_MAX_ABOVE_PRIOR_HIGH_PCT",
"value": 0.004,
"type": "number",
"category": "strategy"
},
{
"key": "VP_VOL_LOOKBACK_BARS",
"value": 3,
"type": "number",
"category": "strategy",
"description": "15m波动近3根(约45分钟)"
},
{
"key": "VP_LONG_MIN_DROP_PCT",
"value": 0.006,
"type": "number",
"category": "strategy",
"description": "15m合计跌0.6%可作为辅助触发"
},
{
"key": "VP_SHORT_MIN_RISE_PCT",
"value": 0.006,
"type": "number",
"category": "strategy"
},
{
"key": "VP_SCAN_INTERVAL_SEC",
"value": 180,
"type": "number",
"category": "scan",
"description": "VP启用时扫描间隔180秒(3分钟)"
},
{
"key": "SCAN_INTERVAL",
"value": 180,
"type": "number",
"category": "scan",
"description": "与VP_SCAN一致提高抓取频率"
},
{
"key": "VP_REQUIRE_1H_4H_ALIGN",
"value": false,
"type": "boolean",
"category": "strategy",
"description": "即时模式下仅4H定方向(急跌抄底不要求1H同向)"
},
{
"key": "VP_RANGE_INTERVAL",
"value": "1h",
"type": "string",
"category": "strategy",
"description": "区间锚定周期"
},
{
"key": "VP_RANGE_LOOKBACK_BARS",
"value": 24,
"type": "number",
"category": "strategy",
"description": "1H区间回看根数"
},
{
"key": "VP_MAX_LONG_IN_RANGE",
"value": 0.42,
"type": "number",
"category": "strategy",
"description": "做多1H区间位置上限(靠近前低)"
},
{
"key": "VP_MIN_SHORT_IN_RANGE",
"value": 0.58,
"type": "number",
"category": "strategy",
"description": "做空1H区间位置下限(靠近前高)"
},
{
"key": "VP_VOL_INTERVAL",
"value": "15m",
"type": "string",
"category": "strategy",
"description": "瞬时波动观察周期"
},
{
"key": "VP_VOL_LOOKBACK_BARS",
"value": 5,
"type": "number",
"category": "strategy",
"description": "15m波动计算根数"
},
{
"key": "VP_LONG_MIN_DROP_PCT",
"value": 0.008,
"type": "number",
"category": "strategy",
"description": "做多15m合计跌幅≥0.8%"
},
{
"key": "VP_SHORT_MIN_RISE_PCT",
"value": 0.008,
"type": "number",
"category": "strategy",
"description": "做空15m合计涨幅≥0.8%"
},
{
"key": "VP_SIGNAL_STRENGTH",
"value": 8,
"type": "number",
"category": "strategy",
"description": "VP信号强度(用于杠杆/日志)"
},
{
"key": "VP_ATR_STOP_MULTIPLIER",
"value": 1.2,
"type": "number",
"category": "risk",
"description": "VP止损ATR倍数(相对1H ATR)"
},
{
"key": "VP_STRUCTURE_STOP_BUFFER_PCT",
"value": 0.003,
"type": "number",
"category": "risk",
"description": "结构止损1H前低/前高外缓冲0.3%"
},
{
"key": "AUTO_TRADE_SYMBOL_WHITELIST",
"value": "BTCUSDT,ETHUSDT,BNBUSDT,SOLUSDT,LINKUSDT,DOGEUSDT,AVAXUSDT,XRPUSDT",
"type": "string",
"category": "strategy",
"description": "混合白名单"
},
{
"key": "MANUAL_REDUCED_SYMBOLS",
"value": "LINKUSDT,DOGEUSDT,AVAXUSDT,XRPUSDT",
"type": "string",
"category": "strategy",
"description": "山寨减仓名单"
},
{
"key": "MANUAL_REDUCED_SYMBOL_POSITION_FACTOR",
"value": 0.5,
"type": "number",
"category": "strategy"
},
{
"key": "MANUAL_REDUCED_SYMBOL_SIGNAL_BOOST",
"value": 1,
"type": "number",
"category": "strategy"
},
{
"key": "AUTO_TRADE_ONLY_TRENDING",
"value": false,
"type": "boolean",
"category": "strategy"
},
{
"key": "AUTO_TRADE_ALLOW_RANGING",
"value": true,
"type": "boolean",
"category": "strategy"
},
{
"key": "BETA_FILTER_ENABLED",
"value": true,
"type": "boolean",
"category": "strategy"
},
{
"key": "USE_TREND_ENTRY_FILTER",
"value": false,
"type": "boolean",
"category": "strategy",
"description": "VP模式下关闭趋势追价过滤"
},
{
"key": "NO_OPEN_HOURS_BJ",
"value": "",
"type": "string",
"category": "risk"
},
{
"key": "NIGHT_HOURS_NO_OPEN_ENABLED",
"value": false,
"type": "boolean",
"category": "risk"
}
]

View File

@ -0,0 +1,26 @@
#!/usr/bin/env bash
# 波动回调策略 + 混合白名单(多空)
#
# 规则4H+1H 同向 | 1H 区间靠近前低/前高 | 15m 急跌/急涨触发
# 详见 config/my_batch_volatility_pullback.json
#
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
PYTHON_BIN=""
for _py in \
"${ROOT}/.venv/bin/python" \
"${ROOT}/trading_system/.venv/bin/python" \
"${ROOT}/backend/.venv/bin/python"; do
[ -x "$_py" ] && PYTHON_BIN="$_py" && break
done
PYTHON_BIN="${PYTHON_BIN:-$(command -v python3)}"
DRY_RUN=()
[[ "${1:-}" == "--dry-run" ]] && DRY_RUN=(--dry-run)
exec "$PYTHON_BIN" "$ROOT/scripts/apply_config_patch_only.py" \
--from "$ROOT/config/my_batch_volatility_pullback.json" \
--updated-by "volatility_pullback" \
"${DRY_RUN[@]}"

View File

@ -389,6 +389,42 @@ DEFAULT_TRADING_CONFIG = {
'ENTRY_PULLBACK_MAX_LONG_IN_RANGE': 0.62, # 做多pos 超过此值则不开(默认拒绝最上约 38% 区域)
'ENTRY_PULLBACK_MIN_SHORT_IN_RANGE': 0.38, # 做空pos 低于此值则不开(默认拒绝最下约 38% 区域)
# ===== 波动回调入场4H+1H 定方向1H 区间低点/高点15m 波动触发)=====
'VOLATILITY_PULLBACK_ENABLED': False,
'VOLATILITY_PULLBACK_AUTO_ONLY': True,
'VOLATILITY_PULLBACK_SKIP_REGIME_GATE': True,
'VP_INSTANT_FLUSH_ENABLED': True,
'VP_SCAN_INTERVAL_SEC': 180,
'VP_PRIOR_STRUCTURE_BARS': 12,
'VP_1H_FLUSH_MIN_PCT': 0.005,
'VP_NEAR_PRIOR_LOW_PCT': 0.008,
'VP_MAX_BELOW_PRIOR_LOW_PCT': 0.004,
'VP_NEAR_PRIOR_HIGH_PCT': 0.008,
'VP_MAX_ABOVE_PRIOR_HIGH_PCT': 0.004,
'VP_REQUIRE_1H_4H_ALIGN': True,
'VP_RANGE_INTERVAL': '1h',
'VP_RANGE_LOOKBACK_BARS': 24,
'VP_RANGE_MIN_BARS': 5,
'VP_MAX_LONG_IN_RANGE': 0.42,
'VP_MIN_SHORT_IN_RANGE': 0.58,
'VP_VOL_INTERVAL': '15m',
'VP_VOL_LOOKBACK_BARS': 5,
'VP_LONG_MIN_DROP_PCT': 0.008,
'VP_SHORT_MIN_RISE_PCT': 0.008,
'VP_SIGNAL_STRENGTH': 8,
'VP_ATR_STOP_MULTIPLIER': 1.2,
'VP_STRUCTURE_STOP_BUFFER_PCT': 0.003,
# 即时波动模式:进行中 1H K 线快速波动 + 前结构低/高点
'VP_INSTANT_FLUSH_ENABLED': True,
'VP_PRIOR_STRUCTURE_BARS': 12,
'VP_1H_FLUSH_MIN_PCT': 0.005,
'VP_NEAR_PRIOR_LOW_PCT': 0.008,
'VP_MAX_BELOW_PRIOR_LOW_PCT': 0.004,
'VP_NEAR_PRIOR_HIGH_PCT': 0.008,
'VP_MAX_ABOVE_PRIOR_HIGH_PCT': 0.004,
'VP_SCAN_INTERVAL_SEC': 180,
# ===== 智能入场方案C=====
# 根治方案:默认关闭。关闭后回归“纯限价单模式”(不追价/不市价兜底/未成交撤单跳过)
'SMART_ENTRY_ENABLED': True, # 开启智能入场,提高成交率

View File

@ -444,6 +444,11 @@ class PositionManager:
# ===== 趋势入场过滤(防止在趋势尾部追价)=====
try:
use_trend_filter = bool(config.TRADING_CONFIG.get("USE_TREND_ENTRY_FILTER", False))
if (
entry_context
and entry_context.get("strategy_type") == "volatility_pullback"
):
use_trend_filter = False
if use_trend_filter and getattr(self.client, "redis_cache", None):
trend_state_key = f"trend_state:{symbol}"
trend_state = await self.client.redis_cache.get(trend_state_key)
@ -493,6 +498,11 @@ class PositionManager:
# ===== 回撤/区间入场过滤(参考近 N 根 K 线的前低/前高,避免买在区间顶部、卖在区间底部)=====
try:
pb_enabled = bool(config.TRADING_CONFIG.get("ENTRY_PULLBACK_FILTER_ENABLED", False))
if (
entry_context
and entry_context.get("strategy_type") == "volatility_pullback"
):
pb_enabled = False
if pb_enabled:
min_bars = max(3, int(config.TRADING_CONFIG.get("ENTRY_PULLBACK_MIN_BARS", 5) or 5))
lookback = max(min_bars, int(config.TRADING_CONFIG.get("ENTRY_PULLBACK_LOOKBACK_BARS", 24) or 24))
@ -545,12 +555,40 @@ class PositionManager:
# 估算止损价格(用于固定风险计算)
estimated_stop_loss = None
vp_meta = (entry_context or {}).get("volatility_pullback") if entry_context else None
vp_mult = None
if entry_context and entry_context.get("strategy_type") == "volatility_pullback":
try:
vp_mult = float(config.TRADING_CONFIG.get("VP_ATR_STOP_MULTIPLIER", 1.2) or 1.2)
except (TypeError, ValueError):
vp_mult = 1.2
atr_multiplier = vp_mult if vp_mult else config.TRADING_CONFIG.get('ATR_STOP_LOSS_MULTIPLIER', 2.5)
if atr and atr > 0:
atr_multiplier = config.TRADING_CONFIG.get('ATR_STOP_LOSS_MULTIPLIER', 2.5) # 默认2.5,放宽止损提升胜率
if estimated_side == 'BUY':
estimated_stop_loss = estimated_entry_price - (atr * atr_multiplier)
else: # SELL
else:
estimated_stop_loss = estimated_entry_price + (atr * atr_multiplier)
if isinstance(vp_meta, dict) and vp_meta:
try:
buf = float(config.TRADING_CONFIG.get("VP_STRUCTURE_STOP_BUFFER_PCT", 0.003) or 0.003)
except (TypeError, ValueError):
buf = 0.003
if estimated_side == 'BUY':
ref_low = vp_meta.get("prior_structure_low") or vp_meta.get("recent_low")
if ref_low:
struct_sl = float(ref_low) * (1.0 - buf)
if estimated_stop_loss is None:
estimated_stop_loss = struct_sl
else:
estimated_stop_loss = max(estimated_stop_loss, struct_sl)
elif estimated_side == 'SELL':
ref_high = vp_meta.get("prior_structure_high") or vp_meta.get("recent_high")
if ref_high:
struct_sl = float(ref_high) * (1.0 + buf)
if estimated_stop_loss is None:
estimated_stop_loss = struct_sl
else:
estimated_stop_loss = min(estimated_stop_loss, struct_sl)
quantity, adjusted_leverage = await self.risk_manager.calculate_position_size(
symbol, change_percent, leverage=leverage,

View File

@ -55,6 +55,13 @@ async def apply_signal_filters(ctx: SignalFilterContext) -> SignalFilterResult:
reasons: List[str] = []
accepted = True
if ctx.trade_signal.get("strategy_type") == "volatility_pullback":
if not _filter_by_volatility(ctx, reasons):
accepted = False
if accepted and not _filter_by_strict_volume(ctx, reasons):
accepted = False
return SignalFilterResult(accepted=accepted, reasons=reasons)
# 1) 波动率过滤:避免在接近「死币」的环境下做趋势单
if not _filter_by_volatility(ctx, reasons):
accepted = False

View File

@ -15,6 +15,7 @@ try:
from . import config
from .symbol_policy import resolve_symbol_trading_policy, auto_trade_whitelist_allows
from .shadow_mode import evaluate_shadow_mode, invalidate_cache as shadow_invalidate_cache
from .volatility_pullback import evaluate_volatility_pullback
except ImportError:
from binance_client import BinanceClient
from market_scanner import MarketScanner
@ -24,6 +25,7 @@ except ImportError:
import config
from symbol_policy import resolve_symbol_trading_policy, auto_trade_whitelist_allows
from shadow_mode import evaluate_shadow_mode, invalidate_cache as shadow_invalidate_cache
from volatility_pullback import evaluate_volatility_pullback
logger = logging.getLogger(__name__)
@ -124,7 +126,7 @@ class TradingStrategy:
if not top_symbols:
logger.warning("未找到符合条件的交易对,等待下次扫描...")
await self._wait_for_next_scan(config.TRADING_CONFIG.get('SCAN_INTERVAL', 3600))
await self._wait_for_next_scan(self._effective_scan_interval())
continue
# 2. 对每个交易对执行交易逻辑(使用技术指标)
@ -142,7 +144,7 @@ class TradingStrategy:
f"({direction} {change_percent:.2f}%, 市场状态: {market_regime})"
)
# 使用技术指标判断交易信号(高胜率策略)
# 使用技术指标判断交易信号(推荐/日志);自动交易可另走波动回调(VP)
trade_signal = await self._analyze_trade_signal(symbol_info)
# 记录交易信号到数据库(只有当有明确方向时才记录)
@ -191,30 +193,6 @@ class TradingStrategy:
)
continue
# 提升胜率:可配置的“仅 trending 自动交易”过滤
only_trending = bool(config.TRADING_CONFIG.get("AUTO_TRADE_ONLY_TRENDING", True))
allow_ranging = bool(config.TRADING_CONFIG.get("AUTO_TRADE_ALLOW_RANGING", False))
allow_unknown = bool(config.TRADING_CONFIG.get("AUTO_TRADE_ALLOW_UNKNOWN", False))
if only_trending and market_regime != 'trending':
logger.info(
f"{symbol} 市场状态={market_regime},跳过自动交易(仅生成推荐)"
f"原因AUTO_TRADE_ONLY_TRENDING=true"
)
continue
if not only_trending:
if market_regime == 'ranging' and not allow_ranging:
logger.info(
f"{symbol} 市场状态={market_regime},跳过自动交易(仅生成推荐)"
f"原因AUTO_TRADE_ALLOW_RANGING=false"
)
continue
if market_regime not in ('trending', 'ranging') and not allow_unknown:
logger.info(
f"{symbol} 市场状态={market_regime},跳过自动交易(仅生成推荐)"
f"原因AUTO_TRADE_ALLOW_UNKNOWN=false"
)
continue
# 检查是否应该自动交易(已有持仓则跳过自动交易,但推荐已生成)
if not await self.risk_manager.should_trade(symbol, change_percent):
continue
@ -224,34 +202,64 @@ class TradingStrategy:
logger.info(f"{symbol} 成交量确认失败,跳过")
continue
if not trade_signal['should_trade']:
auto_signal = await self._resolve_auto_trade_signal(symbol, symbol_info, trade_signal)
if not auto_signal['should_trade']:
logger.info(
f"{symbol} 技术指标分析: {trade_signal['reason']}, 跳过自动交易"
f"{symbol} 信号未通过: {auto_signal['reason']}, 跳过自动交易"
)
continue
# 提升胜率:可配置的“仅 trending 自动交易”过滤(波动回调策略可跳过)
skip_regime = bool(config.TRADING_CONFIG.get("VOLATILITY_PULLBACK_SKIP_REGIME_GATE", False)) and (
auto_signal.get("strategy_type") == "volatility_pullback"
)
only_trending = bool(config.TRADING_CONFIG.get("AUTO_TRADE_ONLY_TRENDING", True))
allow_ranging = bool(config.TRADING_CONFIG.get("AUTO_TRADE_ALLOW_RANGING", False))
allow_unknown = bool(config.TRADING_CONFIG.get("AUTO_TRADE_ALLOW_UNKNOWN", False))
if not skip_regime:
if only_trending and market_regime != 'trending':
logger.info(
f"{symbol} 市场状态={market_regime},跳过自动交易(仅生成推荐)"
f"原因AUTO_TRADE_ONLY_TRENDING=true"
)
continue
if not only_trending:
if market_regime == 'ranging' and not allow_ranging:
logger.info(
f"{symbol} 市场状态={market_regime},跳过自动交易(仅生成推荐)"
f"原因AUTO_TRADE_ALLOW_RANGING=false"
)
continue
if market_regime not in ('trending', 'ranging') and not allow_unknown:
logger.info(
f"{symbol} 市场状态={market_regime},跳过自动交易(仅生成推荐)"
f"原因AUTO_TRADE_ALLOW_UNKNOWN=false"
)
continue
# 统一的信号过滤插件层(第一阶段:波动率等「进一步收紧」过滤)
try:
sf_ctx = SignalFilterContext(
symbol=symbol,
symbol_info=symbol_info,
trade_signal=trade_signal,
trade_signal=auto_signal,
market_regime=market_regime,
)
sf_result = await apply_signal_filters(sf_ctx)
if not sf_result.accepted:
# 将过滤原因拼接到 trade_signal 的 reason 里,便于后续分析
extra_reason = " | ".join(sf_result.reasons) if sf_result.reasons else "信号过滤未通过"
base_reason = trade_signal.get("reason") or ""
base_reason = auto_signal.get("reason") or ""
combined_reason = f"{base_reason} | {extra_reason}" if base_reason else extra_reason
trade_signal["reason"] = combined_reason
auto_signal["reason"] = combined_reason
logger.info(f"{symbol} 信号过滤未通过,跳过自动交易: {combined_reason}")
continue
elif sf_result.reasons:
# 若过滤层仅追加了评语accepted=True也拼接到 reason 便于排查
extra_reason = " | ".join(sf_result.reasons)
base_reason = trade_signal.get("reason") or ""
trade_signal["reason"] = f"{base_reason} | {extra_reason}" if base_reason else extra_reason
base_reason = auto_signal.get("reason") or ""
auto_signal["reason"] = f"{base_reason} | {extra_reason}" if base_reason else extra_reason
except Exception as e:
logger.warning(f"{symbol} 应用信号过滤插件时出错(忽略,回退为原逻辑): {e}")
@ -275,18 +283,18 @@ class TradingStrategy:
)
continue
# 确定交易方向(基于技术指标
trade_direction = trade_signal['direction']
# 确定交易方向(自动交易信号,含 VP
trade_direction = auto_signal['direction']
# 4H 下跌禁止开多(熊市/保守方案:避免逆势抄底)
block_long_4h_down = bool(config.TRADING_CONFIG.get("BLOCK_LONG_WHEN_4H_DOWN", False))
if block_long_4h_down and trade_direction == "BUY" and trade_signal.get("trend_4h") == "down":
if block_long_4h_down and trade_direction == "BUY" and auto_signal.get("trend_4h") == "down":
logger.info(
f"{symbol} 4H 趋势下跌禁止开多BLOCK_LONG_WHEN_4H_DOWN=true跳过"
)
continue
# 4H 上涨禁止开空(对称过滤:避免逆势做空,减少上涨行情中空单止损)
block_short_4h_up = bool(config.TRADING_CONFIG.get("BLOCK_SHORT_WHEN_4H_UP", True))
if block_short_4h_up and trade_direction == "SELL" and trade_signal.get("trend_4h") == "up":
if block_short_4h_up and trade_direction == "SELL" and auto_signal.get("trend_4h") == "up":
logger.info(
f"{symbol} 4H 趋势上涨禁止开空BLOCK_SHORT_WHEN_4H_UP=true跳过"
)
@ -313,9 +321,9 @@ class TradingStrategy:
if not await self._check_liquidity_before_entry(symbol, symbol_info.get('price')):
logger.info(f"{symbol} 流动性检查未通过,跳过开仓")
continue
entry_reason = trade_signal['reason']
entry_reason = auto_signal['reason']
signal_strength = trade_signal.get('strength', 0)
signal_strength = auto_signal.get('strength', 0)
logger.info(
f"{symbol} 交易信号: {trade_direction} | "
f"原因: {entry_reason} | "
@ -349,7 +357,10 @@ class TradingStrategy:
entry_context = {
'signal_strength': signal_strength,
'market_regime': market_regime,
'trend_4h': trade_signal.get('trend_4h'),
'trend_4h': auto_signal.get('trend_4h'),
'trend_1h': auto_signal.get('trend_1h'),
'strategy_type': auto_signal.get('strategy_type'),
'volatility_pullback': auto_signal.get('volatility_pullback') or {},
'change_percent': change_percent,
'direction': trade_direction,
'reason': entry_reason,
@ -456,7 +467,7 @@ class TradingStrategy:
logger.debug(f"重新加载配置失败: {e}")
# 等待下次扫描(支持手动触发)
scan_interval = config.TRADING_CONFIG.get('SCAN_INTERVAL', 3600)
scan_interval = self._effective_scan_interval()
await self._wait_for_next_scan(scan_interval)
except Exception as e:
@ -478,6 +489,22 @@ class TradingStrategy:
logger.warning(f"停止持仓监控时出错: {e}")
logger.info("交易策略已停止")
def _effective_scan_interval(self) -> int:
"""VP 启用时可使用更短扫描间隔以捕捉即时波动。"""
try:
base = int(config.TRADING_CONFIG.get("SCAN_INTERVAL", 3600) or 3600)
except (TypeError, ValueError):
base = 3600
if not bool(config.TRADING_CONFIG.get("VOLATILITY_PULLBACK_ENABLED", False)):
return max(30, base)
try:
vp_sec = int(config.TRADING_CONFIG.get("VP_SCAN_INTERVAL_SEC", 0) or 0)
except (TypeError, ValueError):
vp_sec = 0
if vp_sec > 0:
return max(30, min(base, vp_sec))
return max(30, base)
async def _wait_for_next_scan(self, scan_interval: int):
"""
等待下次扫描同时监听手动触发信号
@ -631,6 +658,43 @@ class TradingStrategy:
hour_bj = datetime.now(bj).hour
return evaluate_shadow_mode(symbol, hour_bj, config.TRADING_CONFIG)
async def _resolve_auto_trade_signal(
self, symbol: str, symbol_info: Dict, macd_signal: Dict
) -> Dict:
"""
自动交易专用信号优先波动回调(VP)未启用或未通过时可回退 MACD 趋势信号
"""
vp_enabled = bool(config.TRADING_CONFIG.get("VOLATILITY_PULLBACK_ENABLED", False))
vp_auto_only = bool(config.TRADING_CONFIG.get("VOLATILITY_PULLBACK_AUTO_ONLY", True))
if not vp_enabled:
return macd_signal
trend_4h = symbol_info.get("trend_4h")
if not trend_4h:
price_4h = symbol_info.get("price_4h", symbol_info.get("price"))
trend_4h = self._judge_trend_4h(
price_4h,
symbol_info.get("ema20_4h"),
symbol_info.get("ema50_4h"),
symbol_info.get("macd_4h"),
)
vp_signal = await evaluate_volatility_pullback(
self.client,
symbol,
symbol_info,
trend_4h=trend_4h,
beta_checker=self._check_beta_filter,
)
if vp_signal.get("should_trade"):
logger.info(f"{symbol} [VP] {vp_signal.get('reason')}")
return vp_signal
if vp_auto_only:
logger.info(f"{symbol} [VP] 未满足: {vp_signal.get('reason')}")
return {**vp_signal, "should_trade": False}
return macd_signal
async def _analyze_trade_signal(self, symbol_info: Dict) -> Dict:
"""
使用技术指标分析交易信号高胜率策略

View File

@ -0,0 +1,461 @@
"""
波动回调入场Volatility Pullback
即时波动模式推荐
- 大方向4H 定多空急跌抄底只要求 4H 向上
- 触发当前进行中的 1H K 线快速下冲/上冲 15m 短周期波动
- 结构位已完成 1H K 线的前低/前高非整段区间极值作入场与止损参考
"""
from __future__ import annotations
import logging
from typing import Any, Awaitable, Callable, Dict, List, Optional, Tuple
from . import config
logger = logging.getLogger(__name__)
BetaChecker = Callable[[], Awaitable[Dict[str, Any]]]
def _cfg(key: str, default: Any) -> Any:
return config.TRADING_CONFIG.get(key, default)
def _pullback_range_metrics(
klines: Optional[List],
current_price: float,
) -> Optional[Tuple[float, float, float]]:
if not klines or current_price is None:
return None
try:
cur = float(current_price)
except (TypeError, ValueError):
return None
if cur <= 0:
return None
highs: List[float] = []
lows: List[float] = []
for k in klines:
try:
highs.append(float(k[2]))
lows.append(float(k[3]))
except (IndexError, TypeError, ValueError):
continue
if len(highs) < 2:
return None
hi = max(highs)
lo = min(lows)
if hi <= lo:
return None
pos = (cur - lo) / (hi - lo)
return lo, hi, pos
def judge_trend_1h(
price: Optional[float],
ema20: Optional[float],
ema50: Optional[float],
) -> str:
if price is None or ema20 is None:
return "neutral"
try:
p = float(price)
e20 = float(ema20)
except (TypeError, ValueError):
return "neutral"
e50 = None
if ema50 is not None:
try:
e50 = float(ema50)
except (TypeError, ValueError):
e50 = None
if p >= e20 and (e50 is None or e20 >= e50):
return "up"
if p <= e20 and (e50 is None or e20 <= e50):
return "down"
return "neutral"
def _prior_structure_levels(
klines_1h: List,
prior_bars: int,
) -> Optional[Tuple[float, float]]:
"""取已完成 1H K 线(不含当前进行中一根)的前低/前高。"""
if not klines_1h or len(klines_1h) < 2:
return None
completed = klines_1h[:-1]
if not completed:
return None
slice_bars = completed[-max(1, prior_bars):]
lows: List[float] = []
highs: List[float] = []
for k in slice_bars:
try:
lows.append(float(k[3]))
highs.append(float(k[2]))
except (IndexError, TypeError, ValueError):
continue
if not lows or not highs:
return None
return min(lows), max(highs)
def _forming_1h_flush_pcts(klines_1h: List, current_price: float) -> Optional[Dict[str, float]]:
"""
当前进行中 1H K 线内的瞬时波动幅度相对开盘价
flush_down: open -> min(low, price)
flush_up: max(high, price) -> open
"""
if not klines_1h:
return None
forming = klines_1h[-1]
try:
o = float(forming[1])
h = float(forming[2])
lo = float(forming[3])
cur = float(current_price)
except (IndexError, TypeError, ValueError):
return None
if o <= 0:
return None
trough = min(lo, cur)
peak = max(h, cur)
flush_down = max(0.0, (o - trough) / o)
flush_up = max(0.0, (peak - o) / o)
return {
"open": o,
"high": h,
"low": lo,
"flush_down_pct": flush_down,
"flush_up_pct": flush_up,
}
def _price_near_prior_low(price: float, prior_low: float, above_pct: float, below_pct: float) -> bool:
if prior_low <= 0:
return False
upper = prior_low * (1.0 + above_pct)
lower = prior_low * (1.0 - below_pct)
return lower <= price <= upper
def _price_near_prior_high(price: float, prior_high: float, below_pct: float, above_pct: float) -> bool:
if prior_high <= 0:
return False
lower = prior_high * (1.0 - below_pct)
upper = prior_high * (1.0 + above_pct)
return lower <= price <= upper
async def _resolve_live_price(client, symbol: str, symbol_info: Dict) -> Optional[float]:
for src in (
lambda: client.get_realtime_price(symbol) if hasattr(client, "get_realtime_price") else None,
):
try:
v = src()
if v is not None:
return float(v)
except Exception:
pass
raw = symbol_info.get("ticker_price") or symbol_info.get("price")
try:
return float(raw) if raw is not None else None
except (TypeError, ValueError):
return None
async def _period_change(client, symbol: str, interval: str, periods: int) -> Optional[float]:
try:
klines = await client.get_klines(symbol, interval, limit=max(periods + 1, 3))
if not klines or len(klines) < 2:
return None
first_close = float(klines[0][4])
last_close = float(klines[-1][4])
if first_close <= 0:
return None
return (last_close - first_close) / first_close
except Exception as e:
logger.debug(f"{symbol} {interval} 涨跌幅获取失败: {e}")
return None
async def _fetch_klines(client, symbol: str, interval: str, limit: int) -> List:
try:
rows = await client.get_klines(symbol, interval, limit=limit)
return rows or []
except Exception as e:
logger.debug(f"{symbol} 拉取 {interval} K线失败: {e}")
return []
def _fail(reason: str, trend_4h: Optional[str]) -> Dict[str, Any]:
return {
"should_trade": False,
"direction": None,
"reason": reason,
"strength": 0,
"trend_4h": trend_4h,
"trend_1h": None,
"strategy_type": "volatility_pullback",
"volatility_pullback": {},
}
async def _evaluate_instant_flush(
client,
symbol: str,
symbol_info: Dict,
trend_4h: Optional[str],
price_f: float,
beta_checker: Optional[BetaChecker],
) -> Dict[str, Any]:
"""1H 进行中 K 线快速波动 + 前结构低/高点入场(更贴近手动抄底/摸顶)。"""
t4 = (trend_4h or "neutral").strip().lower()
if t4 not in ("up", "down"):
return _fail(f"4H 趋势未明确(4H={t4}),即时模式需 up/down", trend_4h)
direction = "BUY" if t4 == "up" else "SELL"
lookback = max(8, int(_cfg("VP_RANGE_LOOKBACK_BARS", 24) or 24))
prior_bars = max(3, int(_cfg("VP_PRIOR_STRUCTURE_BARS", 12) or 12))
bars_1h = await _fetch_klines(client, symbol, "1h", lookback + 1)
if len(bars_1h) < 4:
return _fail(f"1H K线不足({len(bars_1h)})", trend_4h)
structure = _prior_structure_levels(bars_1h, prior_bars)
if structure is None:
return _fail("无法计算前结构低/高点", trend_4h)
prior_low, prior_high = structure
flush = _forming_1h_flush_pcts(bars_1h, price_f)
if flush is None:
return _fail("无法解析进行中 1H K线", trend_4h)
flush_min = float(_cfg("VP_1H_FLUSH_MIN_PCT", 0.005) or 0.005)
near_above = float(_cfg("VP_NEAR_PRIOR_LOW_PCT", 0.008) or 0.008)
near_below = float(_cfg("VP_MAX_BELOW_PRIOR_LOW_PCT", 0.004) or 0.004)
near_high_below = float(_cfg("VP_NEAR_PRIOR_HIGH_PCT", 0.008) or 0.008)
near_high_above = float(_cfg("VP_MAX_ABOVE_PRIOR_HIGH_PCT", 0.004) or 0.004)
vol_iv = str(_cfg("VP_VOL_INTERVAL", "15m") or "15m")
vol_bars = max(2, int(_cfg("VP_VOL_LOOKBACK_BARS", 3) or 3))
long_min_drop = float(_cfg("VP_LONG_MIN_DROP_PCT", 0.006) or 0.006)
short_min_rise = float(_cfg("VP_SHORT_MIN_RISE_PCT", 0.006) or 0.006)
vol_change = await _period_change(client, symbol, vol_iv, vol_bars)
trend_1h = judge_trend_1h(price_f, symbol_info.get("ema20"), symbol_info.get("ema50"))
reasons: List[str] = []
if direction == "BUY":
flush_ok = flush["flush_down_pct"] >= flush_min
vol_ok = vol_change is not None and vol_change <= -long_min_drop
if not flush_ok and not vol_ok:
fd = flush["flush_down_pct"] * 100
vc = (vol_change or 0) * 100
return _fail(
f"即时波动不足1H下冲{fd:.2f}%<{flush_min*100:.2f}% 且 "
f"{vol_iv}{vc:.2f}%不足-{long_min_drop*100:.2f}%",
trend_4h,
)
if not _price_near_prior_low(price_f, prior_low, near_above, near_below):
return _fail(
f"未贴近前低:现价={price_f:.6f} 前低={prior_low:.6f} "
f"(允许上方{near_above*100:.1f}%/下方{near_below*100:.1f}%)",
trend_4h,
)
if beta_checker is not None:
try:
beta = await beta_checker()
if beta.get("should_block_buy"):
return _fail(f"大盘共振过滤:{beta.get('reason', '禁多')}", trend_4h)
except Exception as e:
logger.debug(f"{symbol} Beta 检查异常(放行): {e}")
if flush_ok:
reasons.append(f"1H进行中下冲{flush['flush_down_pct']*100:.2f}%")
if vol_ok:
reasons.append(f"{vol_iv}{(vol_change or 0)*100:.2f}%")
reasons.append(f"贴近前低{prior_low:.6f}")
reasons.append(f"4H向上")
struct_key, struct_val = "prior_structure_low", prior_low
else:
flush_ok = flush["flush_up_pct"] >= flush_min
vol_ok = vol_change is not None and vol_change >= short_min_rise
if not flush_ok and not vol_ok:
fu = flush["flush_up_pct"] * 100
vc = (vol_change or 0) * 100
return _fail(
f"即时波动不足1H上冲{fu:.2f}%<{flush_min*100:.2f}% 且 "
f"{vol_iv}{vc:.2f}%不足{short_min_rise*100:.2f}%",
trend_4h,
)
if not _price_near_prior_high(price_f, prior_high, near_high_below, near_high_above):
return _fail(
f"未贴近前高:现价={price_f:.6f} 前高={prior_high:.6f} "
f"(允许下方{near_high_below*100:.1f}%/上方{near_high_above*100:.1f}%)",
trend_4h,
)
if flush_ok:
reasons.append(f"1H进行中上冲{flush['flush_up_pct']*100:.2f}%")
if vol_ok:
reasons.append(f"{vol_iv}{(vol_change or 0)*100:.2f}%")
reasons.append(f"贴近前高{prior_high:.6f}")
reasons.append(f"4H向下")
struct_key, struct_val = "prior_structure_high", prior_high
strength = int(_cfg("VP_SIGNAL_STRENGTH", 8) or 8)
strength = max(1, min(10, strength))
metrics = _pullback_range_metrics(bars_1h, price_f)
meta = {
"entry_mode": "instant_flush",
"trend_1h": trend_1h,
"prior_structure_low": prior_low,
"prior_structure_high": prior_high,
"forming_1h_open": flush.get("open"),
"forming_1h_flush_down_pct": round(flush["flush_down_pct"] * 100, 4),
"forming_1h_flush_up_pct": round(flush["flush_up_pct"] * 100, 4),
"vol_interval": vol_iv,
"vol_change_pct": round((vol_change or 0) * 100, 4),
struct_key: struct_val,
"recent_low": prior_low,
"recent_high": prior_high,
}
if metrics:
meta["range_pos"] = round(metrics[2], 4)
return {
"should_trade": True,
"direction": direction,
"reason": "VP即时波动: " + ", ".join(reasons),
"strength": strength,
"trend_4h": t4,
"trend_1h": trend_1h,
"strategy_type": "volatility_pullback",
"volatility_pullback": meta,
}
async def _evaluate_classic_align(
client,
symbol: str,
symbol_info: Dict,
trend_4h: Optional[str],
price_f: float,
beta_checker: Optional[BetaChecker],
) -> Dict[str, Any]:
"""原 4H+1H 同向 + 区间位置 + 15m 波动模式。"""
trend_1h = judge_trend_1h(price_f, symbol_info.get("ema20"), symbol_info.get("ema50"))
t4 = (trend_4h or "neutral").strip().lower()
t1 = trend_1h
require_align = bool(_cfg("VP_REQUIRE_1H_4H_ALIGN", True))
if require_align:
if t4 not in ("up", "down") or t1 not in ("up", "down"):
return _fail(f"4H/1H 趋势未明确(4H={t4}, 1H={t1})", trend_4h)
if t4 != t1:
return _fail(f"4H与1H方向不一致(4H={t4}, 1H={t1})", trend_4h)
direction = "BUY" if t4 == "up" else "SELL"
else:
if t4 == "up":
direction = "BUY"
elif t4 == "down":
direction = "SELL"
else:
return _fail(f"4H 趋势中性,跳过(4H={t4})", trend_4h)
range_iv = str(_cfg("VP_RANGE_INTERVAL", "1h") or "1h")
lookback = max(5, int(_cfg("VP_RANGE_LOOKBACK_BARS", 24) or 24))
min_bars = max(3, int(_cfg("VP_RANGE_MIN_BARS", 5) or 5))
max_long_pos = float(_cfg("VP_MAX_LONG_IN_RANGE", 0.42) or 0.42)
min_short_pos = float(_cfg("VP_MIN_SHORT_IN_RANGE", 0.58) or 0.58)
bars_1h = await _fetch_klines(client, symbol, range_iv, lookback)
metrics = _pullback_range_metrics(bars_1h, price_f)
if metrics is None or len(bars_1h) < min_bars:
return _fail(f"1H 区间数据不足({len(bars_1h)}<{min_bars})", trend_4h)
recent_low, recent_high, pos = metrics
vol_iv = str(_cfg("VP_VOL_INTERVAL", "15m") or "15m")
vol_bars = max(2, int(_cfg("VP_VOL_LOOKBACK_BARS", 5) or 5))
long_min_drop = float(_cfg("VP_LONG_MIN_DROP_PCT", 0.008) or 0.008)
short_min_rise = float(_cfg("VP_SHORT_MIN_RISE_PCT", 0.008) or 0.008)
vol_change = await _period_change(client, symbol, vol_iv, vol_bars)
if vol_change is None:
return _fail(f"{vol_iv} 波动数据不可用", trend_4h)
reasons: List[str] = []
if direction == "BUY":
if pos > max_long_pos:
return _fail(
f"1H 区间位置={pos:.2f} > 做多上限{max_long_pos:.2f}",
trend_4h,
)
if vol_change > -long_min_drop:
return _fail(
f"{vol_iv}×{vol_bars} 跌幅不足:{vol_change*100:.2f}%",
trend_4h,
)
if beta_checker is not None:
try:
beta = await beta_checker()
if beta.get("should_block_buy"):
return _fail(f"大盘共振过滤:{beta.get('reason', '禁多')}", trend_4h)
except Exception as e:
logger.debug(f"{symbol} Beta 检查异常(放行): {e}")
reasons.extend([f"4H/1H同向多", f"1H区间pos={pos:.2f}", f"{vol_iv}{vol_change*100:.2f}%"])
struct_key, struct_val = "recent_low", recent_low
else:
if pos < min_short_pos:
return _fail(f"1H 区间位置={pos:.2f} < 做空下限{min_short_pos:.2f}", trend_4h)
if vol_change < short_min_rise:
return _fail(f"{vol_iv}×{vol_bars} 涨幅不足:{vol_change*100:.2f}%", trend_4h)
reasons.extend([f"4H/1H同向空", f"1H区间pos={pos:.2f}", f"{vol_iv}{vol_change*100:.2f}%"])
struct_key, struct_val = "recent_high", recent_high
strength = max(1, min(10, int(_cfg("VP_SIGNAL_STRENGTH", 8) or 8)))
meta = {
"entry_mode": "classic_align",
"trend_1h": t1,
"range_interval": range_iv,
"range_pos": round(pos, 4),
"recent_low": recent_low,
"recent_high": recent_high,
"vol_interval": vol_iv,
"vol_change_pct": round(vol_change * 100, 4),
struct_key: struct_val,
}
return {
"should_trade": True,
"direction": direction,
"reason": "VP波动回调: " + ", ".join(reasons),
"strength": strength,
"trend_4h": t4,
"trend_1h": t1,
"strategy_type": "volatility_pullback",
"volatility_pullback": meta,
}
async def evaluate_volatility_pullback(
client,
symbol: str,
symbol_info: Dict,
trend_4h: Optional[str],
beta_checker: Optional[BetaChecker] = None,
) -> Dict[str, Any]:
if not bool(_cfg("VOLATILITY_PULLBACK_ENABLED", False)):
return _fail("波动回调策略未启用", trend_4h)
price_f = await _resolve_live_price(client, symbol, symbol_info)
if not price_f or price_f <= 0:
return _fail("无法获取有效价格", trend_4h)
if bool(_cfg("VP_INSTANT_FLUSH_ENABLED", True)):
return await _evaluate_instant_flush(
client, symbol, symbol_info, trend_4h, price_f, beta_checker
)
return await _evaluate_classic_align(
client, symbol, symbol_info, trend_4h, price_f, beta_checker
)