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

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
+328 -6
View File
@@ -414,15 +414,21 @@ class _GridPanel:
class _DrawerPanel:
"""右侧叠加抽屉:市场监控 + 委托 + 成交 + 未分类"""
def __init__(self, data: _DataStore, dialogs=None):
def __init__(self, data: _DataStore, dialogs=None, page=None):
self._data = data
self._dialogs = dialogs
self._tab_orders = ft.Tab(label="当前委托")
self._tab_trades = ft.Tab(label="当日成交")
self._tab_dataset = ft.Tab(label="数据集管理")
self._tab_scoring = ft.Tab(label="每日评分")
self._order_table = None
self._trade_table = None
self._uncl_col = None
self._market_table = None
self._dataset_table = None
self._scoring_table = None
self._sync_lock = False # 同步线程锁
self._sync_btns = [] # 同步按钮引用列表
def build(self) -> ft.Control:
"""返回 overlay Container"""
@@ -430,25 +436,32 @@ class _DrawerPanel:
self._order_table = ft.ListView(expand=True)
self._trade_table = ft.ListView(expand=True)
self._market_table = ft.ListView(expand=True)
self._dataset_table = ft.ListView(expand=True)
self._dataset_col = ft.Column(scroll=ft.ScrollMode.AUTO, expand=True)
self._scoring_table = ft.ListView(expand=True)
bar = ft.TabBar(tabs=[
ft.Tab(label="实时价格监控"),
self._tab_scoring,
self._tab_orders,
self._tab_trades,
self._tab_dataset,
ft.Tab(label="未分类持仓"),
])
view = ft.TabBarView(controls=[
self._build_market_tab(),
self._build_scoring_tab(),
self._order_table,
self._trade_table,
self._build_dataset_tab(),
self._uncl_col,
], expand=True)
panel = ft.Container(
ft.Column([
ft.Tabs(ft.Column([bar, view], expand=True), length=4, expand=True),
ft.Tabs(ft.Column([bar, view], expand=True), length=6, expand=True),
], expand=True),
width=700, bgcolor=ft.Colors.SURFACE,
width=750, bgcolor=ft.Colors.SURFACE,
)
backdrop = ft.Container(bgcolor='#44000000', expand=True)
@@ -477,6 +490,8 @@ class _DrawerPanel:
self._refresh_orders()
self._refresh_trades()
self._refresh_market()
self._refresh_dataset()
self._refresh_scoring()
def _refresh_grid(self):
# (width, expand): 0=固定宽, >0=弹性比重; 股票列 expand 自动填充剩余空间
@@ -551,11 +566,16 @@ class _DrawerPanel:
_data_cell(f"{d['last_price']:.3f}", *_C[2]),
]
if already_in_pool:
cells.append(_data_cell("已在池", *_C[3], color='#AAAAAA', size=11))
op_w, _ = _C[3]
cells.append(ft.Container(
ft.Row([ft.Text("已在池", color='#AAAAAA', size=11, text_align=ft.TextAlign.CENTER)],
alignment=ft.MainAxisAlignment.CENTER),
width=op_w, padding=0))
else:
cells.append(ft.Container(
ft.IconButton(ft.Icons.ADD, icon_size=20, tooltip="添加持仓",
on_click=lambda e, code=sc: self._on_add_from_market(code)),
ft.Row([ft.IconButton(ft.Icons.ADD, icon_size=20, tooltip="添加持仓",
on_click=lambda e, code=sc: self._on_add_from_market(code))],
alignment=ft.MainAxisAlignment.CENTER),
width=_C[3][0], padding=0))
row = ft.Row(cells, spacing=0)
rows.append(ft.Container(row, padding=ft.Padding(0, 2, 0, 2)))
@@ -563,6 +583,307 @@ class _DrawerPanel:
self._market_table.controls = [ft.Column(rows, spacing=0)]
# ── Tab 5: 数据集管理 ──
# 按依赖顺序: kline → stocks → industry → market → sector
_SYNC_DEPS = {
'kline': ['kline'],
'stocks': ['stocks'],
'industry':['industry'],
'market': ['market'],
'sector': ['sector'],
'all': ['kline', 'stocks', 'industry', 'market', 'sector'],
}
_SYNC_LABELS = {
'kline': '同步K线',
'stocks': '同步股票信息',
'industry':'同步行业映射',
'market': '计算市场状态',
'sector': '计算行业指数',
}
def _build_dataset_tab(self) -> ft.Control:
self._dataset_status = ft.Text("就绪", size=12, color='#888888')
return ft.Column([
self._dataset_status,
self._dataset_col,
], expand=True, spacing=6)
def _run_sync(self, targets: list):
# 线程锁
if self._sync_lock:
self._dataset_status.value = "同步中,跳过重复点击"
self._dataset_status.update()
return
self._sync_lock = True
self._set_sync_btns_disabled(True)
self._dataset_status.value = "同步中..."
self._dataset_status.update()
import threading
def _do():
from core.scoring.sync import (
KlineStockSync, KlineIndexSync, StocksSync,
IndustrySync, MarketRegimeSync, SectorFeaturesSync,
)
syncs = {
'kline': [KlineStockSync(count=300), KlineIndexSync(count=300)],
'stocks': [StocksSync()],
'industry':[IndustrySync()],
'market': [MarketRegimeSync()],
'sector': [SectorFeaturesSync()],
}
order = []
for t in targets:
for key in self._SYNC_DEPS.get(t, [t]):
order.extend(syncs.get(key, []))
for s in order:
try:
s.run()
except Exception:
pass
self._sync_lock = False
self._set_sync_btns_disabled(False)
self._dataset_status.value = "同步完成"
self._dataset_status.update()
self._refresh_dataset()
threading.Thread(target=_do, daemon=True).start()
def _set_sync_btns_disabled(self, disabled: bool):
for btn in self._sync_btns:
btn.disabled = disabled
btn.update()
def _refresh_dataset(self):
from core.scoring.models import (
KlineStock, KlineIndex, StockInfo, IndustryMapping,
MarketRegimeDaily, SectorFeaturesDaily, ScoringResult,
)
tables = [
("kline_stock", KlineStock, "kline", "trade_date"),
("kline_index", KlineIndex, "kline", "trade_date"),
("stocks", StockInfo, "stocks", "share_updated_at"),
("industry", IndustryMapping, "industry","update_date"),
("market_regime", MarketRegimeDaily, "market", "trade_date"),
("sector_features", SectorFeaturesDaily, "sector", "trade_date"),
("ScoringResult", ScoringResult, "all", "trade_date"),
]
# 与市场监控Tab一致的列宽布局: (width, expand)
_C = [(150, 0), (80, 0), (100, 0)]
H = ["表名", "记录数", "最新日期"]
def _hcell(text, w, e):
if e > 0:
return ft.Container(
ft.Text(text, weight=ft.FontWeight.BOLD), padding=4, expand=e)
return ft.Container(
ft.Text(text, weight=ft.FontWeight.BOLD), width=w, padding=4)
header = ft.Row([_hcell(h, w, e) for h, (w, e) in zip(H, _C)], spacing=0)
self._sync_btns.clear()
if self._sync_lock:
for btn in self._sync_btns:
btn.disabled = True
rows = [header, ft.Divider(height=1, color='#e0e0e0')]
for name, model_cls, dep_key, date_col in tables:
try:
cnt = model_cls.select().count()
df = getattr(model_cls, date_col, None)
if df is not None:
last = (model_cls.select(df).order_by(df.desc()).first())
date_str = str(getattr(last, date_col, "")) if last else ""
else:
date_str = ""
except Exception:
cnt, date_str = "ERR", ""
def _dcell(text, w, e=0):
if e > 0:
return ft.Container(ft.Text(text), padding=4, expand=e)
return ft.Container(ft.Text(text), width=w, padding=4)
cells = [
_dcell(name, *_C[0]),
_dcell(str(cnt), *_C[1]),
_dcell(date_str, *_C[2]),
]
row = ft.Row(cells, spacing=0)
rows.append(ft.Container(row, padding=ft.Padding(0, 2, 0, 2)))
rows.append(ft.Divider(height=1, color='#f0f0f0'))
self._dataset_col.controls = rows
# ── Tab 6: 每日评分 ──
def _build_scoring_tab(self) -> ft.Control:
from datetime import date, timedelta
self._score_cur_date = date.today()
self._score_date_label = ft.Text(str(self._score_cur_date), size=14, weight=ft.FontWeight.BOLD)
self._score_status = ft.Text("无评分数据", size=12, color='#888888')
def _prev_day(_):
self._score_cur_date -= timedelta(days=1)
self._score_date_label.value = str(self._score_cur_date)
self._score_date_label.update()
self._refresh_scoring()
def _next_day(_):
self._score_cur_date += timedelta(days=1)
self._score_date_label.value = str(self._score_cur_date)
self._score_date_label.update()
self._refresh_scoring()
def _today(_):
self._score_cur_date = date.today()
self._score_date_label.value = str(self._score_cur_date)
self._score_date_label.update()
self._refresh_scoring()
date_row = ft.Row([
ft.IconButton(ft.Icons.CHEVRON_LEFT, icon_size=22, tooltip="前一天", on_click=_prev_day),
self._score_date_label,
ft.IconButton(ft.Icons.CHEVRON_RIGHT, icon_size=22, tooltip="后一天", on_click=_next_day),
ft.IconButton(ft.Icons.TODAY, icon_size=20, tooltip="回到今天", on_click=_today),
ft.Container(ft.Divider(height=20), width=2),
ft.ElevatedButton("同步数据", on_click=lambda e: self._run_sync(['kline', 'stocks', 'industry', 'market', 'sector']), height=32),
ft.ElevatedButton("执行评分", on_click=lambda e: self._run_scoring(), height=32),
self._score_status,
], spacing=6, vertical_alignment=ft.CrossAxisAlignment.CENTER)
return ft.Column([
date_row,
self._scoring_table,
], expand=True, spacing=6)
def _run_scoring(self):
self._score_status.value = "评分中..."
self._score_status.update()
import threading
def _do():
from core.scoring.inference.scorer import GridSeekerPipeline
trade_date = self._score_cur_date
# 检查当日 K线数据是否已同步
from peewee import fn
from core.scoring.models import KlineStock
latest_kline = KlineStock.select(fn.MAX(KlineStock.trade_date)).scalar()
if latest_kline is None or latest_kline < trade_date:
self._score_status.value = f"K线未同步至 {trade_date},请先盘后同步"
self._score_status.color = '#F44336'
self._score_status.update()
return
try:
engine = GridSeekerPipeline()
rankings = engine.run(trade_date)
if not rankings.empty:
engine.persist(rankings, trade_date)
self._score_status.value = f"{trade_date} 评分完成"
except FileNotFoundError:
self._score_status.value = "模型文件缺失"
except Exception as ex:
self._score_status.value = f"失败: {ex}"
self._score_status.update()
self._refresh_scoring()
threading.Thread(target=_do, daemon=True).start()
def _refresh_scoring(self):
from core.scoring.models import ScoringResult
from core.qmt import qmtv
# (width, expand): 排名60 + 名称弹性 + 概率65 + 轮数58 + 堆叠概率65 + 操作32
_C = [(60, 0), (0, 1), (65, 1), (58, 1), (65, 1), (32, 1)]
H = ["排名", "代码 / 名称", "Rank轮数", "Top概率", "Stacking概率", "操作"]
def _hcell(text, w, e):
if e > 0:
return ft.Container(_text(text, bold=True), padding=4, expand=e)
return ft.Container(_text(text, bold=True), width=w, padding=4)
def _dcell(text, w, e, color=None, size=None):
if e > 0:
return ft.Container(_text(text, color=color, size=size), padding=4, expand=e)
return ft.Container(_text(text, color=color, size=size), width=w, padding=4)
header = ft.Row([_hcell(h, w, e) for h, (w, e) in zip(H, _C)], spacing=0)
rows = [header, ft.Divider(height=1, color='#e0e0e0')]
# 用导航日期查询
trade_date = self._score_cur_date
scored_rows = list((ScoringResult
.select()
.where(ScoringResult.trade_date == trade_date)
.order_by(ScoringResult.score_rank)
.limit(200)
.dicts()))
if not scored_rows:
self._score_status.value = f"{trade_date} 无评分数据"
self._score_status.color = '#F44336'
self._scoring_table.controls = rows
self._score_status.update()
return
shown = 0
for r in scored_rows:
code = r['stock_code']
plain = code.split('.')[0] if '.' in code else code
# 过滤 ST
from core.scoring.models import StockInfo
if plain.startswith(('6', '5', '9')):
full_code = f'{plain}.SH'
else:
full_code = f'{plain}.SZ'
st = StockInfo.get_or_none(StockInfo.code == full_code)
if st and st.listing_status == 'ST':
continue
name = ''
try:
name = qmtv.getInstrumentName(plain)
except Exception:
pass
already_in = plain in self._data.stockCodeIdMap
shown += 1
cells = [
_dcell(str(r.get("score_rank", "")), _C[0][0], _C[0][1]),
_dcell(f"{plain} {name}", _C[1][0], _C[1][1]),
_dcell(f"{r.get("rank_predicted_rounds", 0) or 0:.4f}", _C[2][0], _C[2][1]),
_dcell(f"{r.get("top_elite_prob", 0) or 0:.4f}", _C[3][0], _C[3][1]),
_dcell(f"{r.get("stacking_probability", 0) or 0:.4f}", _C[4][0], _C[4][1]),
]
if already_in:
op_w = _C[5][0]
cells.append(ft.Container(
ft.Row([ft.Text('已添加', color='#AAAAAA', size=11, text_align=ft.TextAlign.CENTER)],
alignment=ft.MainAxisAlignment.CENTER),
width=op_w, padding=0))
else:
op_w = _C[5][0]
cells.append(ft.Container(
ft.Row([ft.IconButton(ft.Icons.ADD, icon_size=20, tooltip='加入网格',
on_click=lambda e, c=plain, n=name: self._on_add_from_scoring(c, n))],
alignment=ft.MainAxisAlignment.CENTER),
width=op_w, padding=0))
row = ft.Row(cells, spacing=0)
rows.append(ft.Container(row, padding=ft.Padding(0, 2, 0, 2)))
rows.append(ft.Divider(height=1, color='#f0f0f0'))
self._scoring_table.controls = rows
self._score_status.value = f"{trade_date} 候选 {shown}"
self._score_status.color = '#4CAF50'
self._score_status.update()
def _on_add_from_scoring(self, stock_code: str, stock_name: str):
"""+ 按钮:从评分列表添加标的并打开网格配置"""
tid = self._data.add_from_market(stock_code, stock_name)
target = self._data.tradeTargets.get(tid)
if target is None:
return
self._dialogs.open_config(target)
self._refresh_scoring() # 刷新状态(显示"已添加")
def _on_add_from_market(self, stock_code: str):
"""+ 按钮:从市场监控添加标的并打开网格配置"""
info = self._data.marketLog.get(stock_code)
@@ -797,6 +1118,7 @@ class QmtApp:
def __init__(self, page: ft.Page):
self.page = page
self.page.title = "神之一手"
self.page.theme_mode = ft.ThemeMode.LIGHT
self.page.window.width = 1400
self.page.window.height = 800
self.page.padding = 0