模型,评分,修复网格策略市场状态监听
This commit is contained in:
+328
-6
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user