154 lines
6.6 KiB
Python
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']}")
|