feat: 新闻因子接入回测引擎——prepare_news_context+conviction新闻倍数(叠乘/平铺双模式), v_next3 5y+9.8pp至64.6%
This commit is contained in:
+111
-5
@@ -515,9 +515,9 @@ STRATEGIES.update({
|
||||
"v_next3": _v81_conviction("v_next3", "v8.1+信念叠乘+行业牛杠杆",
|
||||
"v8.1波段基座; DNA×2 + 行业ADX>25×2 + flow_delta>0×2 叠乘, 封顶×4",
|
||||
"多因子共振的票才是极品配最重仓; 行业ADX>25=行业级确认牛, 比>=20更精准",
|
||||
{"model": "叠乘封顶×4: DNA×2 + 行业ADX>25×2 + flow_delta>0×2",
|
||||
{"model": "叠乘封顶×4: DNA×2 + 行业ADX>25×2 + flow_delta>0×2 + 新闻正向×2",
|
||||
"stack": True, "cap": 4.0, "dna_mult": 2.0, "sector_adx_min": 25, "sector_mult": 2.0,
|
||||
"flow_delta_min": 0, "flow_mult": 2.0}),
|
||||
"flow_delta_min": 0, "flow_mult": 2.0, "news_mult": 2.0, "news_days": 3}),
|
||||
})
|
||||
|
||||
STRATEGY_DESCRIPTIONS.update({
|
||||
@@ -528,7 +528,7 @@ STRATEGY_DESCRIPTIONS.update({
|
||||
"evidence": "5年全参与+48.3%(年化8.8%)/回撤4.1%。×2票69%胜率/+14.23% vs ×1票48%/+5.99%。规则经逆向验证103/103笔精确复现(平铺×2)。",
|
||||
},
|
||||
"v_next3": {
|
||||
"title": "v8.1+信念叠乘+行业牛杠杆(DNA×2·行业ADX>25×2·资金加速度×2, 封顶×4)【当前最优】",
|
||||
"title": "v8.1+信念叠乘+行业牛杠杆+新闻因子(DNA×2·行业ADX>25×2·资金加速度×2·新闻正向×2, 封顶×4)【当前最优】",
|
||||
"algorithm": "v8.1波段出场基座 + 信念因子叠乘重仓: 动量基因DNA×2、行业确认牛(行业ADX>25)×2、资金加速度(flow_delta>0)×2; 多因子同时命中则叠乘, 封顶×4; 均不命中×1。基准3仓, 单票最大4倍基准仓。",
|
||||
"rationale": "v_next平铺只区分'有没有信念', 叠乘区分'信念有多强'——多因子共振的票是极品, 配最重仓位。行业ADX>25是行业级确认牛(比≥20更严格), 对应生产端行业牛杠杆(ADX>25升一档仓位)。",
|
||||
"evidence": "5年103笔/57.3%胜率, 全参与+55.6%(年化9.1%)/回撤4.1%, 收益/回撤比12.4全场最优。boost分布×1:56笔/×2:37笔/×4:10笔。规则经逆向验证102/103笔精确复现。",
|
||||
@@ -744,6 +744,87 @@ def flow_ctx(code, date, bars_dates, idx):
|
||||
# ══════════════════════════════════════════════════════
|
||||
_WEEKLY_CTX = {} # code -> sorted [(date, close, ma10w, ma20w)]
|
||||
|
||||
# ══════════════════════════════════════════════════════
|
||||
# 新闻情绪上下文(stock_news 表,同花顺+东财)
|
||||
# ══════════════════════════════════════════════════════
|
||||
_NEWS_CTX = {} # code -> sorted [(date_str, sentiment)]
|
||||
_NEWS_DATES = [] # sorted dates
|
||||
_SECTOR_NEWS_CTX = {} # sector -> sorted [(date_str, sentiment)]
|
||||
|
||||
def prepare_news_context(start_date, end_date):
|
||||
"""加载个股新闻情绪(关键词分类)"""
|
||||
global _NEWS_CTX, _NEWS_DATES, _SECTOR_NEWS_CTX
|
||||
_NEWS_CTX, _SECTOR_NEWS_CTX = {}, {}
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
try:
|
||||
rows = conn.execute("""
|
||||
SELECT code, SUBSTR(date,1,10) as d, title, content
|
||||
FROM stock_news WHERE date >= ? AND date <= ?
|
||||
ORDER BY code, date
|
||||
""", (start_date, end_date)).fetchall()
|
||||
except sqlite3.OperationalError:
|
||||
rows = []
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
POS = ['涨停','大涨','预增','中标','增持','回购','利好','创新高','突破','净买入','资金流入','机构买入','看多','上调','回暖','复苏','放量']
|
||||
NEG = ['跌停','大跌','预亏','减持','亏损','利空','立案','风险','下调','净卖出','资金流出','机构卖出','看空','退市','下滑','萎缩']
|
||||
|
||||
from collections import defaultdict
|
||||
by_code = defaultdict(list)
|
||||
for code, d, title, content in rows:
|
||||
text = (title or '') + ' ' + (content or '')
|
||||
pos = sum(1 for kw in POS if kw in text)
|
||||
neg = sum(1 for kw in NEG if kw in text)
|
||||
if pos > neg:
|
||||
sent = 'positive'
|
||||
elif neg > pos:
|
||||
sent = 'negative'
|
||||
else:
|
||||
continue # 中性不存
|
||||
by_code[code].append((d, sent))
|
||||
|
||||
for code, lst in by_code.items():
|
||||
_NEWS_CTX[code] = lst
|
||||
|
||||
# 行业聚合
|
||||
try:
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
sector_map = dict(conn.execute("SELECT code, sector FROM stock_sectors_em").fetchall())
|
||||
conn.close()
|
||||
sector_news = defaultdict(list)
|
||||
for code, lst in by_code.items():
|
||||
sec = sector_map.get(code)
|
||||
if sec:
|
||||
for d, sent in lst:
|
||||
sector_news[sec].append((d, sent))
|
||||
for sec, lst in sector_news.items():
|
||||
_SECTOR_NEWS_CTX[sec] = sorted(lst)
|
||||
except:
|
||||
pass
|
||||
|
||||
_NEWS_DATES = sorted({d for lst in by_code.values() for d, _ in lst})
|
||||
|
||||
def news_ctx(code, date, days=3):
|
||||
"""返回个股近N日新闻情绪统计"""
|
||||
from datetime import datetime, timedelta
|
||||
d_end = date
|
||||
d_start = (datetime.strptime(date, '%Y-%m-%d') - timedelta(days=days)).strftime('%Y-%m-%d')
|
||||
lst = _NEWS_CTX.get(code, [])
|
||||
pos = sum(1 for d, s in lst if d_start <= d < d_end and s == 'positive')
|
||||
neg = sum(1 for d, s in lst if d_start <= d < d_end and s == 'negative')
|
||||
return {'pos': pos, 'neg': neg, 'total': pos + neg}
|
||||
|
||||
def sector_news_ctx(sector, date, days=3):
|
||||
"""返回行业近N日新闻情绪统计"""
|
||||
from datetime import datetime, timedelta
|
||||
d_end = date
|
||||
d_start = (datetime.strptime(date, '%Y-%m-%d') - timedelta(days=days)).strftime('%Y-%m-%d')
|
||||
lst = _SECTOR_NEWS_CTX.get(sector, [])
|
||||
pos = sum(1 for d, s in lst if d_start <= d < d_end and s == 'positive')
|
||||
neg = sum(1 for d, s in lst if d_start <= d < d_end and s == 'negative')
|
||||
return {'pos': pos, 'neg': neg, 'total': pos + neg}
|
||||
|
||||
def prepare_weekly_context(start_date, end_date):
|
||||
"""加载 stock_weekly,计算每周 MA10/MA20"""
|
||||
global _WEEKLY_CTX
|
||||
@@ -924,6 +1005,7 @@ def run_backtest(strategy_version, start_date, end_date, capital=913000, save=Tr
|
||||
prepare_sector_context(start_date, end_date)
|
||||
prepare_flow_context(fetch_start, end_date)
|
||||
prepare_weekly_context(fetch_start, end_date)
|
||||
prepare_news_context(start_date, end_date)
|
||||
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
stocks = conn.execute("""
|
||||
@@ -1288,6 +1370,8 @@ def run_backtest(strategy_version, start_date, end_date, capital=913000, save=Tr
|
||||
_sec_m = conv.get('sector_mult', 2.0)
|
||||
_flo_t = conv.get('flow_delta_min', 0)
|
||||
_flo_m = conv.get('flow_mult', 2.0)
|
||||
_news_m = conv.get('news_mult', 0) # 新闻因子倍数(0=不启用)
|
||||
_news_days = conv.get('news_days', 3)
|
||||
_cap = conv.get('cap')
|
||||
_stack = conv.get('stack', True)
|
||||
for t in trades:
|
||||
@@ -1296,6 +1380,15 @@ def run_backtest(strategy_version, start_date, end_date, capital=913000, save=Tr
|
||||
_sec_hit = _sa is not None and _sa > _sec_t
|
||||
_fd = _f.get('flow_delta')
|
||||
_flo_hit = _fd is not None and _fd > _flo_t
|
||||
# 新闻因子(个股+行业)
|
||||
_news_hit = False
|
||||
if _news_m > 0:
|
||||
_code = t.get('code')
|
||||
_date = t.get('entry_date')
|
||||
_sector = _STOCK_SECTOR.get(_code, '')
|
||||
_sn = news_ctx(_code, _date, _news_days)
|
||||
_ssn = sector_news_ctx(_sector, _date, _news_days) if _sector else {'pos': 0, 'neg': 0}
|
||||
_news_hit = (_sn['pos'] + _ssn['pos']) > (_sn['neg'] + _ssn['neg']) and (_sn['pos'] + _ssn['pos']) >= 1
|
||||
if _stack:
|
||||
b = 1.0
|
||||
if t.get('dna'):
|
||||
@@ -1304,16 +1397,29 @@ def run_backtest(strategy_version, start_date, end_date, capital=913000, save=Tr
|
||||
b *= _sec_m
|
||||
if _flo_hit:
|
||||
b *= _flo_m
|
||||
if _news_hit:
|
||||
b *= _news_m
|
||||
if _cap:
|
||||
b = min(b, _cap)
|
||||
else:
|
||||
b = _dna_m if (t.get('dna') or _sec_hit or _flo_hit) else 1.0
|
||||
b = _dna_m if (t.get('dna') or _sec_hit or _flo_hit or _news_hit) else 1.0
|
||||
t['boost'] = b
|
||||
summary['conviction_model'] = conv.get('model', '')
|
||||
else:
|
||||
boost_k = cfg.get('exit', {}).get('dna_boost', 2.5)
|
||||
_news_m = cfg.get('news_mult', 0)
|
||||
_news_days = cfg.get('news_days', 3)
|
||||
for t in trades:
|
||||
t['boost'] = boost_k if t.get('dna') else 1.0
|
||||
b = boost_k if t.get('dna') else 1.0
|
||||
if _news_m > 0:
|
||||
_code = t.get('code')
|
||||
_date = t.get('entry_date')
|
||||
_sector = _STOCK_SECTOR.get(_code, '')
|
||||
_sn = news_ctx(_code, _date, _news_days)
|
||||
_ssn = sector_news_ctx(_sector, _date, _news_days) if _sector else {'pos': 0, 'neg': 0}
|
||||
if (_sn['pos'] + _ssn['pos']) > (_sn['neg'] + _ssn['neg']) and (_sn['pos'] + _ssn['pos']) >= 1:
|
||||
b *= _news_m
|
||||
t['boost'] = b
|
||||
# 集中仓位(该策略最优激进仓位)
|
||||
slots = STRATEGY_SIZING.get(strategy_version, 10)
|
||||
summary['portfolio'] = portfolio_sim(trades, capital, slots)
|
||||
|
||||
Reference in New Issue
Block a user