模型,评分,修复网格策略市场状态监听

This commit is contained in:
2026-06-22 10:48:40 +08:00
parent 2e3202968d
commit f938c453e1
42 changed files with 3803 additions and 108 deletions
+318 -1
View File
@@ -341,6 +341,19 @@ class TradeTargetUI(ttk.Frame):
self.right_notebook.add(self.trade_tab, text="当日成交")
self._create_trade_table(self.trade_tab)
# Tab 4: 数据集管理
self.dataset_tab = ttk.Frame(self.right_notebook)
self.right_notebook.add(self.dataset_tab, text="数据集管理")
self._create_dataset_tab(self.dataset_tab)
# Tab 5: 每日评分
self.scoring_tab = ttk.Frame(self.right_notebook)
self.right_notebook.add(self.scoring_tab, text="每日评分")
self._create_scoring_tab(self.scoring_tab)
# 评分数据缓存
self._scoring_data: dict = {} # {stock_code: {score, rank, ...}}
# Tab 切换时自动刷新
self.right_notebook.bind("<<NotebookTabChanged>>", self._on_tab_changed)
@@ -506,10 +519,314 @@ class TradeTargetUI(ttk.Frame):
self._refresh_orders()
elif tab_text == "当日成交":
self._refresh_trades()
elif tab_text == "数据集管理":
self._refresh_dataset_status()
elif tab_text == "每日评分":
self._refresh_scoring_table()
except Exception:
pass
def _refresh_orders(self):
# ================================================================
# Tab 4: 数据集管理
# ================================================================
def _create_dataset_tab(self, parent):
"""创建数据集管理 Tab"""
# 顶部按钮栏
btn_frame = ttk.Frame(parent)
btn_frame.pack(fill=tk.X, pady=(0, 5))
syncs = [
("同步K线(个股+指数)", lambda: self._run_dataset_sync(['kline'])),
("同步股票信息", lambda: self._run_dataset_sync(['stocks'])),
("同步行业映射", lambda: self._run_dataset_sync(['industry'])),
("计算市场状态", lambda: self._run_dataset_sync(['market'])),
("计算行业指数", lambda: self._run_dataset_sync(['sector'])),
("同步全部", lambda: self._run_dataset_sync(['all'])),
]
for label, cmd in syncs:
ttk.Button(btn_frame, text=label, command=cmd, width=18).pack(
side=tk.LEFT, padx=2)
# 数据表状态表格
cols = ("表名", "记录数", "最新日期", "数据覆盖")
self.dataset_tree = ttk.Treeview(parent, columns=cols, show='headings', height=12)
widths = {"表名": 180, "记录数": 80, "最新日期": 100, "数据覆盖": 200}
for c in cols:
self.dataset_tree.heading(c, text=c)
self.dataset_tree.column(c, width=widths.get(c, 80), anchor=tk.W)
sb = ttk.Scrollbar(parent, orient=tk.VERTICAL, command=self.dataset_tree.yview)
self.dataset_tree.configure(yscrollcommand=sb.set)
self.dataset_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
sb.pack(side=tk.RIGHT, fill=tk.Y)
# 状态栏
self.dataset_status = ttk.Label(parent, text='就绪', foreground='gray')
self.dataset_status.pack(fill=tk.X, pady=(5, 0))
def _refresh_dataset_status(self):
"""刷新数据集状态"""
from core.scoring.models import (
KlineStock, KlineIndex, StockInfo, IndustryMapping,
MarketRegimeDaily, SectorFeaturesDaily, ScoringResult,
)
# (显示名, 模型类, 日期字段名)
tables = [
('kline_stock', KlineStock, 'trade_date'),
('kline_index', KlineIndex, 'trade_date'),
('stocks', StockInfo, 'share_updated_at'),
('industry', IndustryMapping, 'update_date'),
('market_regime', MarketRegimeDaily, 'trade_date'),
('sector_features', SectorFeaturesDaily, 'trade_date'),
('ScoringResult', ScoringResult, 'trade_date'),
]
self.dataset_tree.delete(*self.dataset_tree.get_children())
for name, model, date_col in tables:
try:
count = model.select().count()
df = getattr(model, date_col, None)
if df is not None:
last = (model.select(df)
.order_by(df.desc())
.first())
last_date = str(getattr(last, date_col, '')) if last else ''
else:
last_date = ''
self.dataset_tree.insert('', tk.END, values=(name, count, last_date, ''))
except Exception as e:
self.dataset_tree.insert('', tk.END, values=(name, 'ERR', str(e), ''))
def _run_dataset_sync(self, targets: list):
"""后台线程执行数据同步"""
import threading
self.dataset_status.config(text='同步中...', foreground='orange')
def _do_sync():
from core.scoring.sync import (
KlineStockSync, KlineIndexSync, StocksSync,
IndustrySync, MarketRegimeSync, SectorFeaturesSync,
)
from core.eventbus import event_bus
from core.sfgrid.bus_events import EventSyncProgress
all_targets = {
'kline': [('K线-个股', KlineStockSync(count=300)),
('K线-指数', KlineIndexSync(count=300))],
'stocks': [('股票信息', StocksSync())],
'industry': [('行业映射', IndustrySync())],
'market': [('市场状态', MarketRegimeSync())],
'sector': [('行业指数', SectorFeaturesSync())],
}
if 'all' in targets:
order = (all_targets['kline'] + all_targets['stocks'] +
all_targets['industry'] + all_targets['market'] +
all_targets['sector'])
else:
order = []
for t in targets:
order.extend(all_targets.get(t, []))
for label, sync in order:
self.after(0, lambda l=label: self.dataset_status.config(
text=f'同步中: {l}...', foreground='orange'))
try:
stats = sync.run()
event_bus.publish(EventSyncProgress,
{'source': label, 'status': 'ok', 'stats': stats})
except Exception as e:
event_bus.publish(EventSyncProgress,
{'source': label, 'status': 'error', 'stats': str(e)})
self.after(0, lambda: self.dataset_status.config(
text='同步完成', foreground='green'))
self.after(100, self._refresh_dataset_status)
threading.Thread(target=_do_sync, daemon=True).start()
# ================================================================
# Tab 5: 每日评分
# ================================================================
def _create_scoring_tab(self, parent):
"""创建每日评分 Tab"""
# 顶部控制栏
ctrl = ttk.Frame(parent)
ctrl.pack(fill=tk.X, pady=(0, 5))
ttk.Label(ctrl, text="评分日期:").pack(side=tk.LEFT, padx=(0, 5))
self.score_date_var = tk.StringVar(value='')
self.score_date_entry = ttk.Entry(ctrl, textvariable=self.score_date_var, width=10)
self.score_date_entry.pack(side=tk.LEFT, padx=2)
ttk.Button(ctrl, text="执行评分", command=self._run_scoring, width=10).pack(
side=tk.LEFT, padx=5)
ttk.Button(ctrl, text="刷新", command=self._refresh_scoring_table, width=6).pack(
side=tk.LEFT, padx=2)
ttk.Button(ctrl, text="加入网格",
command=self._add_scored_to_grid, width=10).pack(
side=tk.LEFT, padx=5)
self.scoring_status = ttk.Label(ctrl, text='', foreground='gray')
self.scoring_status.pack(side=tk.LEFT, padx=10)
# 评分结果表格
cols = ("排名", "代码", "名称", "预测利润", "预测轮数", "堆叠概率")
self.scoring_tree = ttk.Treeview(parent, columns=cols, show='headings', height=14)
widths = {"排名": 50, "代码": 70, "名称": 80, "预测利润": 80, "预测轮数": 80, "堆叠概率": 80}
for c in cols:
self.scoring_tree.heading(c, text=c)
self.scoring_tree.column(c, width=widths.get(c, 60), anchor=tk.CENTER)
sb = ttk.Scrollbar(parent, orient=tk.VERTICAL, command=self.scoring_tree.yview)
self.scoring_tree.configure(yscrollcommand=sb.set)
self.scoring_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
sb.pack(side=tk.RIGHT, fill=tk.Y)
# 双击加入网格
self.scoring_tree.bind("<Double-1>", self._on_scoring_double_click)
def _refresh_scoring_table(self):
"""从 ScoringResult 表加载最新评分"""
from core.scoring.models import ScoringResult
from core.qmt import qmtv
self.scoring_tree.delete(*self.scoring_tree.get_children())
self._scoring_data.clear()
try:
# 取最新评分日
latest = (ScoringResult
.select(ScoringResult.trade_date)
.distinct()
.order_by(ScoringResult.trade_date.desc())
.first())
if not latest:
self.scoring_status.config(text='无评分数据', foreground='gray')
return
trade_date = latest.trade_date
self.score_date_var.set(str(trade_date))
rows = (ScoringResult
.select()
.where(ScoringResult.trade_date == trade_date)
.order_by(ScoringResult.score_rank)
.limit(200)
.dicts())
for r in rows:
code = r['stock_code']
# 尝试从 QMT 获取名称
name = ''
try:
name = qmtv.getInstrumentName(code)
except Exception:
pass
values = (
r.get('score_rank', ''),
code,
name,
f'{r.get("predicted_profit", 0):.4f}',
f'{r.get("rank_predicted_rounds", 0) or 0:.2f}',
f'{r.get("stacking_probability", 0) or 0:.4f}',
)
self.scoring_tree.insert('', tk.END, values=values)
self._scoring_data[code] = {'name': name, **r}
self.scoring_status.config(
text=f'{trade_date}{len(rows)} 只候选股',
foreground='green')
except Exception as e:
self.scoring_status.config(text=f'加载失败: {e}', foreground='red')
def _run_scoring(self):
"""后台线程执行评分管道"""
import threading
from datetime import date, datetime
date_str = self.score_date_var.get().strip()
if date_str:
try:
trade_date = datetime.strptime(date_str, '%Y-%m-%d').date()
except ValueError:
self.scoring_status.config(text='日期格式错误 (YYYY-MM-DD)', foreground='red')
return
else:
trade_date = date.today()
self.scoring_status.config(text='评分中...', foreground='orange')
def _do_score():
from core.scoring.inference.scorer import GridSeekerPipeline
from core.eventbus import event_bus
from core.sfgrid.bus_events import EventScoringCompleted
try:
engine = GridSeekerPipeline()
rankings = engine.run(trade_date)
if not rankings.empty:
engine.persist(rankings, trade_date)
event_bus.publish(EventScoringCompleted,
{'date': str(trade_date), 'count': len(rankings)})
except FileNotFoundError as e:
self.after(0, lambda: self.scoring_status.config(
text=f'模型文件缺失: {e}', foreground='red'))
return
except Exception as e:
self.after(0, lambda: self.scoring_status.config(
text=f'评分失败: {e}', foreground='red'))
return
self.after(100, self._refresh_scoring_table)
self.after(0, lambda: self.scoring_status.config(
text=f'{trade_date} — 评分完成, {len(rankings)}', foreground='green'))
threading.Thread(target=_do_score, daemon=True).start()
def _on_scoring_double_click(self, event):
"""双击评分行 → 加入网格策略"""
self._add_scored_to_grid()
def _add_scored_to_grid(self):
"""将选中的评分股票加入网格策略"""
selected = self.scoring_tree.selection()
if not selected:
return
for item in selected:
values = self.scoring_tree.item(item)['values']
if not values:
continue
stock_code = str(values[1])
stock_name = str(values[2]) if values[2] else stock_code
# 检查是否已存在
from core.sfgrid.model import SFGridTradeTarget
existing = SFGridTradeTarget.get_or_none(
SFGridTradeTarget.stock_code == stock_code)
if existing:
continue
# 添加到未分类持仓
from core.qmt import qmtv
pos = qmtv.getStockPosition(stock_code)
position = int(pos.volume) if pos else 0
target = SFGridTradeTarget.create(
stock_code=stock_code,
stock_name=stock_name,
current_position=position,
strategy_type=0, # 未分类
enabled=False,
)
self.tradeTargetData[target.id] = target
self.stockCodeIdMap[stock_code] = target.id
self.refresh_table()
# ---- 原有方法继续 ----
"""从 QMT 读取当日委托并刷新表格"""
from core.qmt import qmtv
try: