1
This commit is contained in:
parent
013f8df750
commit
bc962d93f9
251
config/my_batch_volatility_pullback.json
Normal file
251
config/my_batch_volatility_pullback.json
Normal 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"
|
||||
}
|
||||
]
|
||||
26
scripts/apply_volatility_pullback.sh
Executable file
26
scripts/apply_volatility_pullback.sh
Executable 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[@]}"
|
||||
|
|
@ -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, # 开启智能入场,提高成交率
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
"""
|
||||
使用技术指标分析交易信号(高胜率策略)
|
||||
|
|
|
|||
461
trading_system/volatility_pullback.py
Normal file
461
trading_system/volatility_pullback.py
Normal 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
|
||||
)
|
||||
Loading…
Reference in New Issue
Block a user