Files

154 lines
6.6 KiB
Python

#!/usr/bin/env python3
"""v71_gate.py — v7.1 策略客观入场闸门(2026-07-29 老爸指令:全线按v7.1运行)
所有新买入信号必须通过的硬性条件,与回测引擎 strategy_lab 的 v7.1 配置完全一致:
评分≥45 + 动量≥8 + ADX≥20 + ATR%3.5~5.5 + ROC10~25 + MACD柱0.25~1.3
距MA20≥4% + 量比0.9~2.0 + MA20斜率≤1.5 + 大盘MA20上且斜率≤-0.05
更高高点hh + 更高低点hl + RSI增量≥6 + 板块MA20斜率≤1.0
出场对齐:止损=入场区中值-1.5×ATR,止盈=入场区中值×1.15,最长20天
"""
import sys
from datetime import datetime, timedelta
for p in ("/home/hmo/MoFin", "/home/hmo/web-dashboard"):
if p not in sys.path:
sys.path.insert(0, p)
_V71_FILTERS = {
'adx_min': 20, 'atr_pct_min': 3.5, 'atr_pct_max': 5.5,
'roc_min': 10, 'roc_max': 25,
'macd_hist_min': 0.25, 'macd_hist_max': 1.3,
'dist_ma20_min': 4,
'vol_ratio_min': 0.9, 'vol_ratio_max': 2.0,
'ma20_slope_max': 1.5,
'mkt_above_ma20': True, 'mkt_slope_max': -0.05,
'hh_only': True, 'hl_only': True, 'rsi_delta_min': 6,
'sector_slope_max': 1.0,
}
_MIN_SCORE = 45
_MIN_MOMENTUM = 8
_ctx_ready = False
def _ensure_ctx():
global _ctx_ready
if _ctx_ready:
return
from strategy_lab import prepare_market_context, prepare_sector_context
end = datetime.now().strftime('%Y-%m-%d')
start = (datetime.now() - timedelta(days=220)).strftime('%Y-%m-%d')
prepare_market_context(start, end)
prepare_sector_context(start, end)
_ctx_ready = True
def check_entry_gate(code, price=None):
"""v7.1 入场闸门。返回 {'pass','failed','factors','atr','score','summary'}"""
from backtest_framework import prepare_bars, compute_single_score
from strategy_lab import calc_factors, mkt_ctx, sector_ctx, pass_filters
_ensure_ctx()
end = datetime.now().strftime('%Y-%m-%d')
start = (datetime.now() - timedelta(days=220)).strftime('%Y-%m-%d')
bars = prepare_bars(code, start, end)
if not bars or len(bars) < 25:
return {'pass': False, 'failed': ['历史数据不足'], 'factors': {}, 'atr': None,
'score': 0, 'summary': '历史数据不足'}
sc = compute_single_score(bars)
if sc is None:
return {'pass': False, 'failed': ['评分不可用'], 'factors': {}, 'atr': None,
'score': 0, 'summary': '评分不可用'}
total_score, comp = sc
factors = calc_factors(bars, len(bars) - 1)
date = bars[-1]['date']
mk = mkt_ctx(date, code)
sec = sector_ctx(code, date)
factors['mkt_above_ma20'] = mk.get('above_ma20')
factors['mkt_slope'] = mk.get('ma20_slope')
factors['mkt_roc'] = mk.get('roc')
factors['sector_slope'] = sec.get('slope')
factors['sector_change'] = sec.get('change')
factors['sector_above_ma20'] = sec.get('above_ma20')
failed = []
if total_score < _MIN_SCORE:
failed.append(f"评分{total_score}<{_MIN_SCORE}")
if comp.get('momentum', 0) < _MIN_MOMENTUM:
failed.append(f"动量{comp.get('momentum')}<{_MIN_MOMENTUM}")
if not pass_filters(factors, _V71_FILTERS):
checks = [
('adx', 20, None, 'ADX'), ('atr_pct', 3.5, 5.5, 'ATR%'),
('roc', 10, 25, 'ROC'), ('macd_hist', 0.25, 1.3, 'MACD柱'),
('dist_ma20', 4, None, '距MA20'), ('vol_ratio', 0.9, 2.0, '量比'),
('ma20_slope', None, 1.5, 'MA20斜率'), ('mkt_slope', None, -0.05, '大盘斜率'),
('rsi_delta', 6, None, 'RSI增量'), ('sector_slope', None, 1.0, '板块斜率'),
]
for key, mn, mx, label in checks:
v = factors.get(key)
if mn is not None and (v is None or v < mn):
failed.append(f"{label}{v}<{mn}" if v is not None else f"{label}缺失")
if mx is not None and v is not None and v > mx:
failed.append(f"{label}{v}>{mx}")
if factors.get('mkt_above_ma20') is not True:
failed.append("大盘未在MA20上")
if not factors.get('hh_structure'):
failed.append("无更高高点hh")
if not factors.get('hl_structure'):
failed.append("无更高低点hl")
atr = bars[-1].get('atr') or 0
return {
'pass': len(failed) == 0,
'failed': failed,
'factors': factors,
'atr': atr,
'score': total_score,
'summary': ('; '.join(failed[:4]) + ('...' if len(failed) > 4 else '')) if failed else f'通过(评分{total_score})',
}
def check_breakout_gate(code, price=None):
"""B严格动量入场通道(v_combo 动量族,2026-07-29 落地):
收缩突破签名 = ATR处20日最低1/4位 + 收盘破20日新高 + 量比>1.5 + 评分≥50 + ROC>4 + 大盘MA20上
返回 {'pass','reason','source'} source='momentum' 供出场分派"""
from backtest_framework import prepare_bars, compute_single_score
from strategy_lab import mkt_ctx
end = datetime.now().strftime('%Y-%m-%d')
start = (datetime.now() - timedelta(days=220)).strftime('%Y-%m-%d')
bars = prepare_bars(code, start, end)
if not bars or len(bars) < 25:
return {'pass': False, 'reason': '数据不足', 'source': 'momentum'}
b = bars[-1]
closes = [x['close'] for x in bars]
atr_now = b.get('atr') or 0
atrs = [x.get('atr') or 0 for x in bars[-21:-1]]
if not atrs:
return {'pass': False, 'reason': 'ATR数据不足', 'source': 'momentum'}
atr_low = sorted(atrs)[len(atrs)//4]
high20 = max(x['high'] for x in bars[-21:-1])
vols = [x['volume'] for x in bars[-6:-1]]
vm = sum(vols)/len(vols) if vols else 0
vr = (b['volume']/vm) if vm > 0 else 1
if not (atr_now > 0 and atr_now <= atr_low * 1.05 and closes[-1] > high20 and vr > 1.5):
return {'pass': False, 'reason': f'收缩突破不满足(ATR{atr_now:.2f}vs{atr_low:.2f},量比{vr:.1f})', 'source': 'momentum'}
sc = compute_single_score(bars)
if sc is None or sc[0] < 50:
return {'pass': False, 'reason': f'评分{sc[0] if sc else 0}<50', 'source': 'momentum'}
if (b.get('roc') or 0) < 4:
return {'pass': False, 'reason': f"ROC{b.get('roc')}<4", 'source': 'momentum'}
date = b['date']
mk = mkt_ctx(date, code)
if mk.get('above_ma20') is not True:
return {'pass': False, 'reason': '大盘未在MA20上', 'source': 'momentum'}
return {'pass': True, 'reason': f'收缩突破通过(评分{sc[0]},ROC{b.get("roc")},量比{vr:.1f})', 'source': 'momentum',
'score': sc[0], 'atr': atr_now}
if __name__ == '__main__':
for c in sys.argv[1:] or ['688002', '603599']:
r = check_entry_gate(c)
print(f"{c}: {'✅通过' if r['pass'] else '❌拦截'} | {r['summary']} | ATR={r['atr']}")