feat: 回测框架全面扩展——支持超跌策略(v_weak/p_oversold)回测: calc_factors加bias60/dist_lo20/r5f/vol_shrink/close_up, pass_filters加超跌/大盘RSI/基本面/新闻因子, _load_index_ctx加大盘RSI/DD60, _get_external_factors查基本面/新闻/行业动量, run_backtest注入外部因子, 注册v_weak+v_oversold到STRATEGIES
This commit is contained in:
+137
@@ -326,6 +326,50 @@ STRATEGIES.update({
|
|||||||
"eval_step": 1,
|
"eval_step": 1,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
# ── 2026-08-11 超跌策略注册(v_weak/p_oversold 回测支持)──
|
||||||
|
"v_weak": {
|
||||||
|
"version": "v_weak",
|
||||||
|
"name": "弱市超跌确认(ADX甜区+6条件)",
|
||||||
|
"summary": "大盘MA20下+ADX[25,30]甜区 + bias60[-35,-20] + RSI<=25 + 5日急跌 + 距低点近 + 收阳缩量",
|
||||||
|
"hypothesis": "弱市深超跌反弹,六步方法论+12维框架定稿",
|
||||||
|
"parent": "v_mr",
|
||||||
|
"config": {
|
||||||
|
"entry": {"min_score": 45, "min_momentum": 8, "filters": {
|
||||||
|
"bias60_min": -35, "bias60_max": -20,
|
||||||
|
"rsi_max": 25,
|
||||||
|
"r5f_max": -3,
|
||||||
|
"dist_lo20_max": 5,
|
||||||
|
"require_close_up": True,
|
||||||
|
"vol_shrink_max": 1.0,
|
||||||
|
"mkt_above_ma20": False,
|
||||||
|
"mkt_adx_min": 25, "mkt_adx_max": 30,
|
||||||
|
}},
|
||||||
|
"exit": {"tp_pct": 0.30, "sl_pct": 0.12, "max_hold_days": 40},
|
||||||
|
"sizing": {"kelly": False},
|
||||||
|
"eval_step": 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"v_oversold": {
|
||||||
|
"version": "v_oversold",
|
||||||
|
"name": "预测超跌反弹(12维因子)",
|
||||||
|
"summary": "弱市+小市值+低PE+新闻+行业弱+深跌",
|
||||||
|
"hypothesis": "由果及因:预测超跌反弹",
|
||||||
|
"parent": "v_weak",
|
||||||
|
"config": {
|
||||||
|
"entry": {"min_score": 45, "min_momentum": 8, "filters": {
|
||||||
|
"mkt_rsi_max": 50,
|
||||||
|
"mkt_dd60_max": -5,
|
||||||
|
"mcap_q_max": 0.2,
|
||||||
|
"pe_q_max": 0.2,
|
||||||
|
"news3_min": 1,
|
||||||
|
"sec_ret20_max": 0,
|
||||||
|
"bias60_max": -20,
|
||||||
|
}},
|
||||||
|
"exit": {"tp_pct": None, "sl_pct": 0.05, "max_hold_days": 40},
|
||||||
|
"sizing": {"kelly": False},
|
||||||
|
"eval_step": 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@@ -598,6 +642,7 @@ def _load_index_ctx(index_code, start_date, end_date):
|
|||||||
bars = prepare_bars(index_code, start_date, end_date)
|
bars = prepare_bars(index_code, start_date, end_date)
|
||||||
if not bars:
|
if not bars:
|
||||||
return ctx
|
return ctx
|
||||||
|
closes = [b.get('close') or 0 for b in bars]
|
||||||
for i, b in enumerate(bars):
|
for i, b in enumerate(bars):
|
||||||
slope = None
|
slope = None
|
||||||
if i >= 5:
|
if i >= 5:
|
||||||
@@ -605,14 +650,72 @@ def _load_index_ctx(index_code, start_date, end_date):
|
|||||||
if m0 and m1:
|
if m0 and m1:
|
||||||
slope = round((m1 - m0) / m0 * 100, 3)
|
slope = round((m1 - m0) / m0 * 100, 3)
|
||||||
ma20 = b.get('ma20') or 0
|
ma20 = b.get('ma20') or 0
|
||||||
|
# 2026-08-11: mkt_dd60 大盘距60日高点回撤 + mkt_rsi(p_oversold 门控)
|
||||||
|
hi60 = max(closes[max(0, i-59):i+1]) if closes else 0
|
||||||
|
mkt_dd60 = round(((b.get('close') or 0) - hi60) / hi60 * 100, 2) if hi60 > 0 else None
|
||||||
ctx[b['date']] = {
|
ctx[b['date']] = {
|
||||||
'above_ma20': (b.get('close') or 0) > ma20 if ma20 > 0 else None,
|
'above_ma20': (b.get('close') or 0) > ma20 if ma20 > 0 else None,
|
||||||
'ma20_slope': slope,
|
'ma20_slope': slope,
|
||||||
'roc': b.get('roc'),
|
'roc': b.get('roc'),
|
||||||
'adx': b.get('adx'), # 趋势强度(choppy市<20,趋势市>25,2026-07-30强化趋势过滤)
|
'adx': b.get('adx'), # 趋势强度(choppy市<20,趋势市>25,2026-07-30强化趋势过滤)
|
||||||
|
'rsi': b.get('rsi'), # 2026-08-11: 大盘RSI(p_oversold 门控)
|
||||||
|
'mkt_dd60': mkt_dd60, # 2026-08-11: 大盘距60日高点回撤
|
||||||
}
|
}
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
|
# ── 2026-08-11 外部因子缓存(基本面/新闻/行业动量,回测超跌策略用)──
|
||||||
|
_EXTERNAL_CACHE = {}
|
||||||
|
|
||||||
|
def _days_ago(dt, n):
|
||||||
|
"""返回 dt 前 n 天的日期字符串"""
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
try:
|
||||||
|
return (datetime.strptime(dt, '%Y-%m-%d') - timedelta(days=n)).strftime('%Y-%m-%d')
|
||||||
|
except:
|
||||||
|
return dt
|
||||||
|
|
||||||
|
def _get_external_factors(code, dt):
|
||||||
|
"""惰性查询 + 缓存:返回该 code 在 dt 日的外部因子(mcap_q/pe_q/news3/sec_ret20)"""
|
||||||
|
key = (code, dt)
|
||||||
|
if key in _EXTERNAL_CACHE:
|
||||||
|
return _EXTERNAL_CACHE[key]
|
||||||
|
result = {}
|
||||||
|
try:
|
||||||
|
# 行业动量 sec_ret20(sector_index_daily 20日涨跌)
|
||||||
|
_srow = conn.execute("SELECT sector_name FROM stock_sectors WHERE code=? LIMIT 1", (code,)).fetchone()
|
||||||
|
if _srow and _srow[0]:
|
||||||
|
_sector = _srow[0]
|
||||||
|
_prev = conn.execute(
|
||||||
|
"SELECT close FROM sector_index_daily WHERE sector=? AND date<=? ORDER BY date DESC LIMIT 21",
|
||||||
|
(_sector, dt)).fetchall()
|
||||||
|
if len(_prev) >= 20 and _prev[-1][0] and _prev[-1][0] > 0:
|
||||||
|
result['sec_ret20'] = round((_prev[0][0] - _prev[-1][0]) / _prev[-1][0] * 100, 2)
|
||||||
|
# 新闻 3 日计数 news3
|
||||||
|
_nrow = conn.execute(
|
||||||
|
"SELECT COUNT(*) FROM stock_news WHERE code=? AND date>? AND date<=?",
|
||||||
|
(code, _days_ago(dt, 3), dt)).fetchone()
|
||||||
|
if _nrow:
|
||||||
|
result['news3'] = _nrow[0]
|
||||||
|
# 基本面分位 mcap_q/pe_q
|
||||||
|
_frow = conn.execute(
|
||||||
|
"SELECT mcap_total, pe FROM stock_fundamentals WHERE code=?", (code,)).fetchone()
|
||||||
|
if _frow and _frow[0]:
|
||||||
|
_mcap = _frow[0]
|
||||||
|
_t = conn.execute("SELECT COUNT(*) FROM stock_fundamentals WHERE mcap_total>0 AND mcap_total<?", (_mcap,)).fetchone()[0]
|
||||||
|
_tot = conn.execute("SELECT COUNT(*) FROM stock_fundamentals WHERE mcap_total>0").fetchone()[0]
|
||||||
|
if _tot:
|
||||||
|
result['mcap_q'] = round(_t / _tot, 3)
|
||||||
|
if _frow and _frow[1]:
|
||||||
|
_pe = _frow[1]
|
||||||
|
_t = conn.execute("SELECT COUNT(*) FROM stock_fundamentals WHERE pe>0 AND pe<?", (_pe,)).fetchone()[0]
|
||||||
|
_tot = conn.execute("SELECT COUNT(*) FROM stock_fundamentals WHERE pe>0").fetchone()[0]
|
||||||
|
if _tot:
|
||||||
|
result['pe_q'] = round(_t / _tot, 3)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
_EXTERNAL_CACHE[key] = result
|
||||||
|
return result
|
||||||
|
|
||||||
def is_hk_code(code):
|
def is_hk_code(code):
|
||||||
return len(code) == 5 and code.startswith('0')
|
return len(code) == 5 and code.startswith('0')
|
||||||
|
|
||||||
@@ -951,6 +1054,20 @@ def pass_filters(factors, filters):
|
|||||||
if filters.get('weekly_aligned') and factors.get('weekly_aligned') is not True: return False
|
if filters.get('weekly_aligned') and factors.get('weekly_aligned') is not True: return False
|
||||||
if filters.get('weekly_down_only') and factors.get('weekly_up') is not False: return False
|
if filters.get('weekly_down_only') and factors.get('weekly_up') is not False: return False
|
||||||
if not chk('weekly_dist', filters.get('weekly_dist_min'), filters.get('weekly_dist_max')): return False
|
if not chk('weekly_dist', filters.get('weekly_dist_min'), filters.get('weekly_dist_max')): return False
|
||||||
|
# ── 2026-08-11 超跌/大盘RSI/基本面/新闻因子(v_weak/p_oversold 回测支持)──
|
||||||
|
if not chk('bias60', filters.get('bias60_min'), filters.get('bias60_max')): return False
|
||||||
|
if not chk('dist_lo20', filters.get('dist_lo20_min'), filters.get('dist_lo20_max')): return False
|
||||||
|
if not chk('r5f', filters.get('r5f_min'), filters.get('r5f_max')): return False
|
||||||
|
if not chk('vol_shrink', filters.get('vol_shrink_min'), filters.get('vol_shrink_max')): return False
|
||||||
|
if filters.get('require_close_up') and not factors.get('close_up'): return False
|
||||||
|
# 大盘因子(mkt_rsi/mkt_dd60)
|
||||||
|
if not chk('mkt_rsi', filters.get('mkt_rsi_min'), filters.get('mkt_rsi_max')): return False
|
||||||
|
if not chk('mkt_dd60', filters.get('mkt_dd60_min'), filters.get('mkt_dd60_max')): return False
|
||||||
|
# 基本面/新闻因子(mcap_q/pe_q/news3/sec_ret20)
|
||||||
|
if not chk('mcap_q', filters.get('mcap_q_min'), filters.get('mcap_q_max')): return False
|
||||||
|
if not chk('pe_q', filters.get('pe_q_min'), filters.get('pe_q_max')): return False
|
||||||
|
if not chk('news3', filters.get('news3_min'), filters.get('news3_max')): return False
|
||||||
|
if not chk('sec_ret20', filters.get('sec_ret20_min'), filters.get('sec_ret20_max')): return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@@ -996,6 +1113,20 @@ def calc_factors(bars, idx):
|
|||||||
l10 = min(x.get('low') or 1e9 for x in bars[idx-9:idx-4])
|
l10 = min(x.get('low') or 1e9 for x in bars[idx-9:idx-4])
|
||||||
f['hh_structure'] = h5 > h10 # 更高的高点 = 上升结构
|
f['hh_structure'] = h5 > h10 # 更高的高点 = 上升结构
|
||||||
f['hl_structure'] = l5 > l10 # 更高的低点 = 上升结构
|
f['hl_structure'] = l5 > l10 # 更高的低点 = 上升结构
|
||||||
|
# ── 2026-08-11 超跌因子(v_weak/p_oversold 回测支持)──
|
||||||
|
ma60 = b.get('ma60') or 0
|
||||||
|
f['bias60'] = round((close - ma60) / ma60 * 100, 2) if ma60 > 0 else None
|
||||||
|
if idx >= 19:
|
||||||
|
lo20 = min((x.get('low') or 1e9) for x in bars[idx-19:idx+1])
|
||||||
|
f['dist_lo20'] = round((close - lo20) / lo20 * 100, 2) if lo20 and lo20 > 0 else None
|
||||||
|
if idx >= 5:
|
||||||
|
prev5c = bars[idx-5].get('close') or 0
|
||||||
|
f['r5f'] = round((close - prev5c) / prev5c * 100, 2) if prev5c > 0 else None
|
||||||
|
if idx >= 19:
|
||||||
|
vol5 = sum((x.get('volume') or 0) for x in bars[idx-4:idx+1]) / 5
|
||||||
|
vol20 = sum((x.get('volume') or 0) for x in bars[idx-19:idx+1]) / 20
|
||||||
|
f['vol_shrink'] = round(vol5 / vol20, 2) if vol20 > 0 else None
|
||||||
|
f['close_up'] = close > (bars[idx-1].get('close') or 0) if idx >= 1 else None
|
||||||
return f
|
return f
|
||||||
|
|
||||||
|
|
||||||
@@ -1097,6 +1228,12 @@ def run_backtest(strategy_version, start_date, end_date, capital=913000, save=Tr
|
|||||||
factors.update(fl)
|
factors.update(fl)
|
||||||
# 周线因子
|
# 周线因子
|
||||||
factors.update(weekly_ctx(code, date))
|
factors.update(weekly_ctx(code, date))
|
||||||
|
# ── 2026-08-11 外部因子注入(超跌策略回测支持)──
|
||||||
|
factors['mkt_rsi'] = mk.get('rsi')
|
||||||
|
factors['mkt_dd60'] = mk.get('mkt_dd60')
|
||||||
|
_lazy = _get_external_factors(code, date)
|
||||||
|
if _lazy:
|
||||||
|
factors.update(_lazy)
|
||||||
|
|
||||||
if pass_filters(factors, filters):
|
if pass_filters(factors, filters):
|
||||||
# ── 次日开盘价入场(杜绝"信号日收盘买"的未来幻觉)──
|
# ── 次日开盘价入场(杜绝"信号日收盘买"的未来幻觉)──
|
||||||
|
|||||||
Reference in New Issue
Block a user