完成模型更新

This commit is contained in:
2026-06-24 12:19:08 +08:00
parent ee8fc60eae
commit 6494f43ddd
22 changed files with 4029 additions and 271 deletions
+2 -2
View File
@@ -50,5 +50,5 @@ RANK_MODEL = 'rank'
TOP_MODEL = 'top'
STACKING_MODEL = 'stacking'
# Stacking 选股阈值
STACKING_THRESHOLD = 0.35
# Stacking 选股阈值 (v6.7r3 最优阈值 0.331)
STACKING_THRESHOLD = 0.33
+25 -1
View File
@@ -1,9 +1,10 @@
"""
扩展特征 v3.4 (16维)
扩展特征 v3.4 + v6.7新增 (20维: 16维 v3.4 + 4维 v6.7新增)
"""
import numpy as np
import pandas as pd
from core.scoring.features.v3_2_features import _ols_slope
from core.scoring.features.v3_3_features import _grid_touch_count
from core.scoring.config import GRID_LOW, GRID_HIGH
@@ -104,6 +105,29 @@ def calculate_features_v3_4(ctx) -> pd.DataFrame:
feat['wick_ratio_20d'] = np.mean(
wick_len / np.where(total_len > 0, total_len, 1)) * 100
# ── v6.7 新增 4 维特征 ──────────────────────────────
# 53. vol_decay_5d: 近5日波动率 / 近20日波动率 (波动率用对数收益std)
log_ret = np.diff(np.log(np.maximum(closes, 1e-10)))
vol_5d = np.std(log_ret[-5:], ddof=1) if len(log_ret) >= 5 else 0
vol_20d = np.std(log_ret[-20:], ddof=1) if len(log_ret) >= 20 else vol_5d
feat['vol_decay_5d'] = float(vol_5d / vol_20d) if vol_20d > 0 else 0.0
# 54. grid_touch_relative_10d: 10日振幅比 / 60日振幅比
range_10d = float(np.max(highs[-10:]) - np.min(lows[-10:]))
range_60d = float(np.max(highs[-60:]) - np.min(lows[-60:])) if len(highs) >= 60 else range_10d
close_now = float(closes[-1])
close_60d_mean = float(np.mean(closes[-60:])) if len(closes) >= 60 else close_now
if close_now > 0 and close_60d_mean > 0 and range_60d > 0:
feat['grid_touch_relative_10d'] = (range_10d / close_now) / (range_60d / close_60d_mean)
else:
feat['grid_touch_relative_10d'] = 0.0
# 55. vol_decay_x_grid_balance: vol_decay × 网格均衡度
feat['vol_decay_x_grid_balance'] = feat['vol_decay_5d'] * feat.get('grid_room_balance', 0.0)
# 56. vol_decay_x_dist_lower: vol_decay × 下轨距离
feat['vol_decay_x_dist_lower'] = feat['vol_decay_5d'] * feat.get('dist_to_grid_lower', 0.0)
features[code] = feat
return pd.DataFrame.from_dict(features, orient='index')
+19 -10
View File
@@ -1,5 +1,5 @@
"""
grid_seeker v6.6 三级模型推理管道
grid_seeker v6.7r3 三级模型推理管道
Rank → Top → Stacking → stacking_probability (最终排序)
"""
import pickle
@@ -18,7 +18,7 @@ from core.logger import LogLevel, PrintLog
# ============================================================
# Rank 模型输入特征 (52维, v3.4, 直接从模型文件的 selected_features 读取)
# Rank 模型输入特征 (56维 v6.7/v3.4, 同时支持 feat_names 和 selected_features)
# ============================================================
def _get_rank_features() -> list:
import pickle
@@ -27,17 +27,26 @@ def _get_rank_features() -> list:
with open(path, 'rb') as f:
obj = pickle.load(f)
if isinstance(obj, dict):
sf = obj.get('selected_features', [])
# v6.7r3 使用 feat_names, v6.6 使用 selected_features
sf = obj.get('feat_names', []) or obj.get('selected_features', [])
if sf:
return sf
raise RuntimeError("无法从 rank.pkl 读取 selected_features")
raise RuntimeError("无法从 rank.pkl 读取 feat_names 或 selected_features")
RANK_FEATURE_COLS = _get_rank_features()
# Top/Stacking 模型只用 52 维基础特征(不含 v6.7 新增的4维)
# v6.7 新增: vol_decay_5d, grid_touch_relative_10d, vol_decay_x_grid_balance, vol_decay_x_dist_lower
_V67_NEW_FEATS = {
'vol_decay_5d', 'grid_touch_relative_10d',
'vol_decay_x_grid_balance', 'vol_decay_x_dist_lower'
}
BASE_52_COLS = [f for f in RANK_FEATURE_COLS if f not in _V67_NEW_FEATS]
class GridSeekerPipeline:
"""
grid_seeker v6.6 三级模型评分管道。
grid_seeker v6.7r3 三级模型评分管道。
Usage:
engine = GridSeekerPipeline()
@@ -120,7 +129,7 @@ class GridSeekerPipeline:
DataFrame indexed by stock_code, 含 stacking_probability / rank 等列,
按 stacking_probability 降序排列
"""
PrintLog(LogLevel.INFO, f'[scorer] ===== grid_seeker v6.6 评分开始 ({trade_date}) =====')
PrintLog(LogLevel.INFO, f'[scorer] ===== grid_seeker v6.7r3 评分开始 ({trade_date}) =====')
# 1. 特征工程
pipeline = FeaturePipeline(trade_date)
@@ -140,16 +149,16 @@ class GridSeekerPipeline:
self.rank_model, feature_df, RANK_FEATURE_COLS
)
# 3. Stage 2: Top 模型 → top_elite_prob (53维 = 52 + rank_predicted_rounds)
# 3. Stage 2: Top 模型 → top_elite_prob (53维 = 52基础 + rank)
PrintLog(LogLevel.INFO, '[scorer] Stage 2/3: Top 模型...')
top_cols = RANK_FEATURE_COLS + ['rank_predicted_rounds']
top_cols = BASE_52_COLS + ['rank_predicted_rounds']
feature_df['top_elite_prob'] = self._predict_with_model(
self.top_model, feature_df, top_cols
)
# 4. Stage 3: Stacking 模型 → stacking_probability (54维 = 52 + rank + top)
# 4. Stage 3: Stacking 模型 → stacking_probability (55维 = 52基础 + rank + top)
PrintLog(LogLevel.INFO, '[scorer] Stage 3/3: Stacking 模型...')
stk_cols = RANK_FEATURE_COLS + ['rank_predicted_rounds', 'top_elite_prob']
stk_cols = BASE_52_COLS + ['rank_predicted_rounds', 'top_elite_prob']
feature_df['stacking_probability'] = self._predict_with_model(
self.stacking_model, feature_df, stk_cols
)
+10 -3
View File
@@ -5,7 +5,7 @@ K线数据同步 — 个股日K + 指数日K
线程锁: KlineStockSync / KlineIndexSync 各自内部锁
"""
import pandas as pd
from datetime import date, timedelta
from datetime import date, datetime, timedelta
from core.scoring.sync.base import BaseSync
from core.scoring.models import KlineStock, KlineIndex
from core.scoring.config import TRACKED_INDICES
@@ -111,7 +111,11 @@ class KlineStockSync(BaseSync):
stock_code = full_code.split('.')[0]
records = []
for td in close_df.columns:
td_date = td.date() if hasattr(td, 'date') else td
# xtdata 返回的列名可能是字符串 'YYYYMMDD' 或 datetime,需统一转成 date
if isinstance(td, str):
td_date = datetime.strptime(td, '%Y%m%d').date()
else:
td_date = td.date() if hasattr(td, 'date') else td
if start_date is not None and td_date <= start_date:
self.stats['skipped'] = self.stats.get('skipped', 0) + 1
continue
@@ -193,7 +197,10 @@ class KlineIndexSync(BaseSync):
for full_code in close_df.index:
index_code = full_code.split('.')[0]
for td in close_df.columns:
td_date = td.date() if hasattr(td, 'date') else td
if isinstance(td, str):
td_date = datetime.strptime(td, '%Y%m%d').date()
else:
td_date = td.date() if hasattr(td, 'date') else td
if latest is not None and td_date <= latest:
self.stats['skipped'] = self.stats.get('skipped', 0) + 1
continue
+73 -41
View File
@@ -13,7 +13,7 @@ class SectorFeaturesSync(BaseSync):
"""行业聚合指数同步 — kline_stock + industry → sector_features_daily"""
def _fetch(self, **kwargs):
"""从数据库加载原始数据, 计算行业指数特征"""
"""从数据库加载原始数据, 计算行业指数特征(分块处理避免内存溢出)"""
PrintLog(LogLevel.INFO, '[sync] SectorFeatures: 加载原始数据...')
# 1. 加载行业映射: code → industry_name
@@ -24,49 +24,81 @@ class SectorFeaturesSync(BaseSync):
code_to_industry = {row['code']: row['industry_name'] for row in industries}
PrintLog(LogLevel.INFO, f'[sync] SectorFeatures: {len(code_to_industry)} 条行业映射')
# 2. 加载 K 线数据
kline_rows = (KlineStock
.select(
KlineStock.stock_code,
KlineStock.trade_date,
KlineStock.open,
KlineStock.high,
KlineStock.low,
KlineStock.close,
)
.order_by(KlineStock.stock_code, KlineStock.trade_date)
.dicts())
# 2. 分块加载 K 线数据,避免内存溢出
# 聚合结果: {(trade_date, sector_name): [sum_pct_chg, sum_amp, count]}
sector_daily_agg = {} # key: (date, sector) -> {'ret_sum': float, 'amp_sum': float, 'count': int}
CHUNK_SIZE = 50000
last_date_per_stock = {} # stock_code -> prev_close
if not kline_rows:
PrintLog(LogLevel.WARNING, '[sync] SectorFeatures: KlineStock 表为空')
PrintLog(LogLevel.INFO, '[sync] SectorFeatures: 分块处理K线数据...')
chunk_num = 0
while True:
chunk_num += 1
rows = list(KlineStock
.select(
KlineStock.stock_code,
KlineStock.trade_date,
KlineStock.open,
KlineStock.high,
KlineStock.low,
KlineStock.close,
)
.order_by(KlineStock.stock_code, KlineStock.trade_date)
.offset((chunk_num - 1) * CHUNK_SIZE)
.limit(CHUNK_SIZE)
.dicts())
if not rows:
break
PrintLog(LogLevel.INFO, f'[sync] SectorFeatures: 处理块 {chunk_num} ({len(rows)} 行)...')
for row in rows:
code = str(row['stock_code'])
td = row['trade_date']
open_p = float(row['open'])
high = float(row['high'])
low = float(row['low'])
close = float(row['close'])
sector = code_to_industry.get(code)
if sector is None:
continue
# 计算日收益率和振幅
prev_close = last_date_per_stock.get(code)
if prev_close is not None and prev_close > 0 and open_p > 0 and close > 0:
pct_chg = (close - prev_close) / prev_close * 100
amp = (high - low) / open_p * 100
key = (td, sector)
if key not in sector_daily_agg:
sector_daily_agg[key] = {'ret_sum': 0.0, 'amp_sum': 0.0, 'count': 0}
sector_daily_agg[key]['ret_sum'] += pct_chg
sector_daily_agg[key]['amp_sum'] += amp
sector_daily_agg[key]['count'] += 1
last_date_per_stock[code] = close
if not sector_daily_agg:
PrintLog(LogLevel.WARNING, '[sync] SectorFeatures: 无有效K线数据')
return None
df = pd.DataFrame(kline_rows)
df['trade_date'] = pd.to_datetime(df['trade_date'])
PrintLog(LogLevel.INFO, f'[sync] SectorFeatures: {len(df)} 条K线数据')
PrintLog(LogLevel.INFO, f'[sync] SectorFeatures: 聚合完成, {len(sector_daily_agg)} 个行业-日组合')
# 3. 映射行业
df['sector_name'] = df['stock_code'].map(code_to_industry)
df = df.dropna(subset=['sector_name'])
# 3. 构建聚合 DataFrame
agg_data = []
for (td, sector), vals in sector_daily_agg.items():
agg_data.append({
'trade_date': td,
'sector_name': sector,
'sector_ret': vals['ret_sum'] / vals['count'],
'sector_amplitude': vals['amp_sum'] / vals['count'],
})
agg = pd.DataFrame(agg_data)
agg = agg.sort_values(['sector_name', 'trade_date'])
agg['trade_date'] = pd.to_datetime(agg['trade_date'])
PrintLog(LogLevel.INFO, f'[sync] SectorFeatures: {len(agg)} 行, {agg["sector_name"].nunique()} 个行业')
# 4. 逐股计算日收益率和振幅
df = df.sort_values(['stock_code', 'trade_date'])
df['prev_close'] = df.groupby('stock_code')['close'].shift(1)
df['pct_chg'] = (df['close'] - df['prev_close']) / df['prev_close'] * 100
df['amplitude'] = (df['high'] - df['low']) / df['open'] * 100
# 清理无效值
df = df.dropna(subset=['pct_chg', 'amplitude'])
# 5. 按行业+日期聚合
agg = (df.groupby(['trade_date', 'sector_name'])
.agg(
sector_ret=('pct_chg', 'mean'),
sector_amplitude=('amplitude', 'mean'),
)
.reset_index())
# 6. 构建行业指数 (基值=100)
# 4. 构建行业指数 (基值=100)
agg = agg.sort_values(['sector_name', 'trade_date'])
agg['sector_index'] = agg.groupby('sector_name')['sector_ret'].transform(
lambda x: (1 + x / 100).cumprod() * 100
@@ -77,7 +109,7 @@ class SectorFeaturesSync(BaseSync):
first_val = group['sector_index'].iloc[0]
agg.loc[idx, 'sector_index'] = group['sector_index'] / first_val * 100
# 7. 计算 EMA 均线
# 5. 计算 EMA 均线
agg['ema10'] = (agg.groupby('sector_name')['sector_index']
.transform(lambda x: x.ewm(span=10, min_periods=1).mean()))
agg['ema20'] = (agg.groupby('sector_name')['sector_index']
@@ -85,7 +117,7 @@ class SectorFeaturesSync(BaseSync):
agg['ema200'] = (agg.groupby('sector_name')['sector_index']
.transform(lambda x: x.ewm(span=200, min_periods=1).mean()))
# 8. 趋势评分: close>ema200 得1分 + ema10>ema20 得1分
# 6. 趋势评分: close>ema200 得1分 + ema10>ema20 得1分
agg['score'] = (
(agg['sector_index'] > agg['ema200']).astype(int) +
(agg['ema10'] > agg['ema20']).astype(int)