模型,评分,修复网格策略市场状态监听
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
# 模型推理子包
|
||||
from core.scoring.inference.scorer import GridSeekerPipeline
|
||||
@@ -0,0 +1,217 @@
|
||||
"""
|
||||
grid_seeker v6.6 三级模型推理管道
|
||||
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 模型输入特征 (52维, v3.4, 直接从模型文件的 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):
|
||||
sf = obj.get('selected_features', [])
|
||||
if sf:
|
||||
return sf
|
||||
raise RuntimeError("无法从 rank.pkl 读取 selected_features")
|
||||
|
||||
RANK_FEATURE_COLS = _get_rank_features()
|
||||
|
||||
|
||||
class GridSeekerPipeline:
|
||||
"""
|
||||
grid_seeker v6.6 三级模型评分管道。
|
||||
|
||||
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.6 评分开始 ({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_predicted_rounds)
|
||||
PrintLog(LogLevel.INFO, '[scorer] Stage 2/3: Top 模型...')
|
||||
top_cols = RANK_FEATURE_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)
|
||||
PrintLog(LogLevel.INFO, '[scorer] Stage 3/3: Stacking 模型...')
|
||||
stk_cols = RANK_FEATURE_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]
|
||||
Reference in New Issue
Block a user