feat: v7.1全线对齐 — 新买入信号硬闸门(v71_gate.py),入场17条件+出场15%/1.5ATR机械对齐,研究Tab色条+当前策略高亮
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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, '"') + '">' + (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>';
|
||||
|
||||
Reference in New Issue
Block a user