feat: v7.1全线对齐 — 新买入信号硬闸门(v71_gate.py),入场17条件+出场15%/1.5ATR机械对齐,研究Tab色条+当前策略高亮

This commit is contained in:
hmo
2026-07-29 02:26:56 +08:00
parent 224f4e58c2
commit 94b8942749
3 changed files with 2794 additions and 2624 deletions
File diff suppressed because it is too large Load Diff
+116
View File
@@ -0,0 +1,116 @@
#!/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)
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})',
}
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']}")
+43 -13
View File
@@ -1974,6 +1974,34 @@ function renderStrategyTable(strategies) {
const isBest = invert ? (val === bestVal && bestVal !== 999) : (val === bestVal && bestVal !== -999);
return isBest ? 'text-green-400 font-bold' : '';
};
// 条件格式色条:计算每列 min/max,单元格背景按相对大小画色条
const CURRENT_STRATEGY = 'v7.1';
const ranges = {};
const cols = ['total_return_pct','capital_final','cagr_pct','total_trades','avg_hold_days','win_rate','avg_profit_pct','sharpe_ratio','profit_factor','portfolio_max_dd_pct'];
for (const c of cols) {
let mn = Infinity, mx = -Infinity;
for (const s of strategies) {
const st = s.summary_stats || {}; const pf = st.portfolio || {};
const v = (c in pf) ? pf[c] : st[c];
if (v != null) { mn = Math.min(mn, v); mx = Math.max(mx, v); }
}
ranges[c] = { mn: mn === Infinity ? 0 : mn, mx: mx === -Infinity ? 1 : mx };
}
const bar = (c, v, invert) => {
if (v == null) return '';
const { mn, mx } = ranges[c];
const span = (mx - mn) || 1;
let pct = (v - mn) / span;
if (invert) pct = 1 - pct;
pct = Math.max(0.03, Math.min(1, pct));
const color = invert ? 'rgba(96,165,250,0.28)' : 'rgba(59,130,246,0.30)';
return `background:linear-gradient(to right, ${color} ${pct*100}%, transparent ${pct*100}%);`;
};
const cell = (c, v, fmt, extraCls, invert) => {
const cls = 'text-right px-2 py-1.5 font-mono ' + (extraCls || '');
const sty = bar(c, v, invert);
return `<td class="${cls}" style="${sty}">${v != null ? fmt(v) : '—'}</td>`;
};
let html = '<div class="overflow-x-auto"><table class="w-full text-xs whitespace-nowrap"><thead><tr class="text-slate-500 border-b border-slate-700">' +
'<th class="text-left px-2 py-1.5">版本</th><th class="text-left px-2 py-1.5">名称</th>' +
'<th class="text-right px-2 py-1.5 text-amber-400">总收益</th>' +
@@ -1990,20 +2018,22 @@ function renderStrategyTable(strategies) {
const pf = st.portfolio || {};
const hasResult = st.total_trades != null;
const smallSample = hasResult && st.total_trades < 40;
const retClass = pf.total_return_pct == null ? '' : (pf.total_return_pct >= 0 ? 'text-green-400' : 'text-red-400');
html += '<tr class="border-b border-slate-800/50 hover:bg-slate-800/30 cursor-pointer" onclick="showStrategyDetail(\'' + s.version + '\')">' +
'<td class="px-2 py-1.5 font-mono font-bold text-blue-400">' + s.version + '</td>' +
const isCurrent = s.version === CURRENT_STRATEGY;
const retCls = pf.total_return_pct == null ? '' : (pf.total_return_pct >= 0 ? 'text-green-400' : 'text-red-400');
const rowCls = 'border-b border-slate-800/50 hover:bg-slate-800/30 cursor-pointer' + (isCurrent ? ' bg-emerald-900/20 border-l-2 border-l-emerald-400' : '');
html += '<tr class="' + rowCls + '" onclick="showStrategyDetail(\'' + s.version + '\')">' +
'<td class="px-2 py-1.5 font-mono font-bold ' + (isCurrent ? 'text-emerald-400' : 'text-blue-400') + '">' + s.version + (isCurrent ? ' <span class="text-[10px] bg-emerald-500/20 text-emerald-300 px-1 rounded">当前</span>' : '') + '</td>' +
'<td class="px-2 py-1.5" title="' + (s.hypothesis || s.summary || '').replace(/"/g, '&quot;') + '">' + (s.name || '') + (smallSample ? ' <span class="text-amber-500" title="样本<40笔">⚠️</span>' : '') + '</td>' +
'<td class="text-right px-2 py-1.5 font-mono font-bold ' + retClass + ' ' + hl(pf.total_return_pct, best.total_return_pct) + '">' + (pf.total_return_pct != null ? pf.total_return_pct + '%' : '—') + '</td>' +
'<td class="text-right px-2 py-1.5 font-mono ' + (pf.capital_final != null ? '' : 'text-slate-600') + '">' + (pf.capital_final != null ? '¥' + (pf.capital_final/10000).toFixed(0) + '万' : '—') + '</td>' +
'<td class="text-right px-2 py-1.5 font-mono ' + hl(pf.cagr_pct, best.cagr_pct) + '">' + (pf.cagr_pct != null ? pf.cagr_pct + '%' : '—') + '</td>' +
'<td class="text-right px-2 py-1.5 font-mono">' + (st.total_trades != null ? st.total_trades : '—') + '</td>' +
'<td class="text-right px-2 py-1.5 font-mono">' + (st.avg_hold_days != null ? st.avg_hold_days + 'd' : '—') + '</td>' +
'<td class="text-right px-2 py-1.5 font-mono ' + hl(st.win_rate, best.win_rate) + '">' + (st.win_rate != null ? st.win_rate + '%' : '—') + '</td>' +
'<td class="text-right px-2 py-1.5 font-mono ' + ((st.avg_profit_pct || 0) >= 0 ? 'text-green-400' : 'text-red-400') + '">' + (st.avg_profit_pct != null ? st.avg_profit_pct + '%' : '—') + '</td>' +
'<td class="text-right px-2 py-1.5 font-mono ' + hl(st.sharpe_ratio, best.sharpe_ratio) + '">' + (st.sharpe_ratio != null ? st.sharpe_ratio : '—') + '</td>' +
'<td class="text-right px-2 py-1.5 font-mono ' + hl(st.profit_factor, best.profit_factor) + '">' + (st.profit_factor != null ? st.profit_factor : '—') + '</td>' +
'<td class="text-right px-2 py-1.5 font-mono ' + hl(pf.portfolio_max_dd_pct, best.portfolio_max_dd_pct, true) + '">' + (pf.portfolio_max_dd_pct != null ? pf.portfolio_max_dd_pct + '%' : '—') + '</td>' +
cell('total_return_pct', pf.total_return_pct, v => v + '%', 'font-bold ' + retCls + ' ' + hl(pf.total_return_pct, best.total_return_pct)) +
cell('capital_final', pf.capital_final, v => '¥' + (v/10000).toFixed(0) + '万') +
cell('cagr_pct', pf.cagr_pct, v => v + '%', hl(pf.cagr_pct, best.cagr_pct)) +
cell('total_trades', st.total_trades, v => v) +
cell('avg_hold_days', st.avg_hold_days, v => v + 'd') +
cell('win_rate', st.win_rate, v => v + '%', hl(st.win_rate, best.win_rate)) +
cell('avg_profit_pct', st.avg_profit_pct, v => v + '%', (st.avg_profit_pct || 0) >= 0 ? 'text-green-400' : 'text-red-400') +
cell('sharpe_ratio', st.sharpe_ratio, v => v, hl(st.sharpe_ratio, best.sharpe_ratio)) +
cell('profit_factor', st.profit_factor, v => v, hl(st.profit_factor, best.profit_factor)) +
cell('portfolio_max_dd_pct', pf.portfolio_max_dd_pct, v => v + '%', hl(pf.portfolio_max_dd_pct, best.portfolio_max_dd_pct, true), true) +
'<td class="text-center px-2 py-1.5" onclick="event.stopPropagation()">' +
'<button onclick="runStrategyBacktest(\'' + s.version + '\')" class="px-2 py-0.5 bg-blue-600/60 hover:bg-blue-500 rounded text-xs">▶回测</button>' +
'</td></tr>';