226 lines
8.3 KiB
Python
226 lines
8.3 KiB
Python
"""
|
||
grid_seeker v6.7r3 三级模型推理管道
|
||
Rank → Top → Stacking → stacking_probability (最终排序)
|
||
"""
|
||
import pickle
|
||
import numpy as np
|
||
import pandas as pd
|
||
from pathlib import Path
|
||
from datetime import date
|
||
from core.scoring.config import (
|
||
get_model_path, RANK_MODEL, TOP_MODEL, STACKING_MODEL,
|
||
STACKING_THRESHOLD,
|
||
)
|
||
from core.scoring.features.pipeline import FeaturePipeline
|
||
from core.scoring.models import ScoringResult
|
||
from core.database import db
|
||
from core.logger import LogLevel, PrintLog
|
||
|
||
|
||
# ============================================================
|
||
# Rank 模型输入特征 (56维 v6.7/v3.4, 同时支持 feat_names 和 selected_features)
|
||
# ============================================================
|
||
def _get_rank_features() -> list:
|
||
import pickle
|
||
from core.scoring.config import get_model_path
|
||
path = get_model_path(RANK_MODEL)
|
||
with open(path, 'rb') as f:
|
||
obj = pickle.load(f)
|
||
if isinstance(obj, dict):
|
||
# 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 读取 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.7r3 三级模型评分管道。
|
||
|
||
Usage:
|
||
engine = GridSeekerPipeline()
|
||
rankings = engine.run(trade_date=date.today())
|
||
# 返回 DataFrame: stock_code, stacking_probability, rank 等
|
||
"""
|
||
|
||
def __init__(self, model_dir: Path = None):
|
||
self._rank_model = None
|
||
self._top_model = None
|
||
self._stacking_model = None
|
||
|
||
# ---- 模型加载 ----
|
||
|
||
def _load_model(self, name: str):
|
||
"""加载单个 .pkl 模型"""
|
||
path = get_model_path(name)
|
||
if not path.exists():
|
||
raise FileNotFoundError(f'模型文件不存在: {path}')
|
||
with open(path, 'rb') as f:
|
||
obj = pickle.load(f)
|
||
# 支持 dict 格式 {"model": lgbm_model, ...} 或直接返回模型对象
|
||
if isinstance(obj, dict):
|
||
return obj.get('model', obj)
|
||
return obj
|
||
|
||
@property
|
||
def rank_model(self):
|
||
if self._rank_model is None:
|
||
self._rank_model = self._load_model(RANK_MODEL)
|
||
return self._rank_model
|
||
|
||
@property
|
||
def top_model(self):
|
||
if self._top_model is None:
|
||
self._top_model = self._load_model(TOP_MODEL)
|
||
return self._top_model
|
||
|
||
@property
|
||
def stacking_model(self):
|
||
if self._stacking_model is None:
|
||
self._stacking_model = self._load_model(STACKING_MODEL)
|
||
return self._stacking_model
|
||
|
||
# ---- 预测 ----
|
||
|
||
def _predict_with_model(self, model, X: pd.DataFrame, feature_cols: list) -> np.ndarray:
|
||
"""
|
||
使用模型预测。自动选择特征子集,兼容 sklearn API (predict/predict_proba)。
|
||
"""
|
||
available = [c for c in feature_cols if c in X.columns]
|
||
missing = set(feature_cols) - set(available)
|
||
if missing:
|
||
PrintLog(LogLevel.WARNING,
|
||
f'[scorer] 缺少特征列 ({len(missing)}): {list(missing)[:5]}...')
|
||
|
||
X_sub = X[available].fillna(0).values
|
||
|
||
try:
|
||
if hasattr(model, 'predict_proba'):
|
||
proba = model.predict_proba(X_sub)
|
||
if proba.shape[1] >= 2:
|
||
return proba[:, 1]
|
||
return proba[:, 0]
|
||
elif hasattr(model, 'predict'):
|
||
return model.predict(X_sub)
|
||
else:
|
||
return model.predict(X_sub)
|
||
except Exception as e:
|
||
PrintLog(LogLevel.ERROR, f'[scorer] 模型预测失败: {e}')
|
||
raise
|
||
|
||
# ---- 主流程 ----
|
||
|
||
def run(self, trade_date: date) -> pd.DataFrame:
|
||
"""
|
||
执行完整的 3 级评分管道。
|
||
|
||
Returns:
|
||
DataFrame indexed by stock_code, 含 stacking_probability / rank 等列,
|
||
按 stacking_probability 降序排列
|
||
"""
|
||
PrintLog(LogLevel.INFO, f'[scorer] ===== grid_seeker v6.7r3 评分开始 ({trade_date}) =====')
|
||
|
||
# 1. 特征工程
|
||
pipeline = FeaturePipeline(trade_date)
|
||
feature_df = pipeline.run()
|
||
|
||
if feature_df.empty:
|
||
PrintLog(LogLevel.WARNING, '[scorer] 无股票通过特征工程, 终止')
|
||
return pd.DataFrame()
|
||
|
||
PrintLog(LogLevel.INFO,
|
||
f'[scorer] 特征工程完成: {len(feature_df)} stocks, '
|
||
f'{len(feature_df.columns)} dims')
|
||
|
||
# 2. Stage 1: Rank 模型 → rank_predicted_rounds (52维)
|
||
PrintLog(LogLevel.INFO, '[scorer] Stage 1/3: Rank 模型...')
|
||
feature_df['rank_predicted_rounds'] = self._predict_with_model(
|
||
self.rank_model, feature_df, RANK_FEATURE_COLS
|
||
)
|
||
|
||
# 3. Stage 2: Top 模型 → top_elite_prob (53维 = 52基础 + rank)
|
||
PrintLog(LogLevel.INFO, '[scorer] Stage 2/3: Top 模型...')
|
||
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 (55维 = 52基础 + rank + top)
|
||
PrintLog(LogLevel.INFO, '[scorer] Stage 3/3: Stacking 模型...')
|
||
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
|
||
)
|
||
|
||
# 5. 排序(直接用 stacking_probability)
|
||
feature_df['score_rank'] = feature_df['stacking_probability'].rank(
|
||
ascending=False, method='min'
|
||
).astype(int)
|
||
feature_df['candidate_count'] = len(feature_df)
|
||
feature_df = feature_df.sort_values('score_rank')
|
||
|
||
n_above = (feature_df['stacking_probability'] >= STACKING_THRESHOLD).sum()
|
||
PrintLog(LogLevel.INFO,
|
||
f'[scorer] 评分完成: {len(feature_df)} 只候选, '
|
||
f'{n_above} 只高于阈值 {STACKING_THRESHOLD}')
|
||
PrintLog(LogLevel.INFO,
|
||
f'[scorer] Top-5: '
|
||
f'{feature_df.head(5)[["stacking_probability", "rank_predicted_rounds"]].to_dict("index")}')
|
||
|
||
return feature_df
|
||
|
||
def persist(self, rankings: pd.DataFrame, trade_date: date):
|
||
"""将评分结果持久化到 ScoringResult 表"""
|
||
if rankings.empty:
|
||
return
|
||
|
||
records = []
|
||
for code, row in rankings.iterrows():
|
||
records.append({
|
||
'stock_code': str(code),
|
||
'trade_date': trade_date,
|
||
'predicted_profit': float(row.get('stacking_probability', 0)),
|
||
'rank_predicted_rounds': float(row.get('rank_predicted_rounds', 0))
|
||
if 'rank_predicted_rounds' in row else None,
|
||
'top_elite_prob': float(row.get('top_elite_prob', 0))
|
||
if 'top_elite_prob' in row else None,
|
||
'stacking_probability': float(row.get('stacking_probability', 0))
|
||
if 'stacking_probability' in row else None,
|
||
'score_rank': int(row.get('score_rank', 0)),
|
||
'candidate_count': int(row.get('candidate_count', 0)),
|
||
})
|
||
|
||
with db.atomic():
|
||
for batch in _chunked(records, 500):
|
||
ScoringResult.insert_many(batch).on_conflict_replace().execute()
|
||
|
||
PrintLog(LogLevel.INFO,
|
||
f'[scorer] 评分结果已持久化: {len(records)} 条')
|
||
|
||
def get_top_n(self, trade_date: date, n: int = 50) -> list[dict]:
|
||
"""查询历史评分 Top-N"""
|
||
rows = (ScoringResult
|
||
.select()
|
||
.where(
|
||
(ScoringResult.trade_date == trade_date) &
|
||
(ScoringResult.score_rank <= n)
|
||
)
|
||
.order_by(ScoringResult.score_rank)
|
||
.dicts())
|
||
return list(rows)
|
||
|
||
|
||
def _chunked(lst: list, n: int):
|
||
for i in range(0, len(lst), n):
|
||
yield lst[i:i + n] |