This commit is contained in:
2026-07-06 09:12:38 +08:00
parent fd5337f163
commit de83f97c05
6 changed files with 381 additions and 61 deletions
+152 -29
View File
@@ -23,13 +23,15 @@ from core.sfgrid.bus_events import EventTradeTargetUpdate, EventPoolMark
_ORDER_STATUS = {48: '未报', 49: '待报', 50: '已报', 51: '已报待撤', 52: '部成待撤',
53: '部撤', 54: '已撤', 55: '部成', 56: '已成', 57: '废单'}
from datetime import date as _date
def _fmt_time(t) -> str:
if not t: return ''
import datetime
from datetime import datetime as _dt
try:
ts = int(t)
if ts > 1e12: ts //= 1000
return datetime.datetime.fromtimestamp(ts).strftime('%H:%M:%S')
return _dt.fromtimestamp(ts).strftime('%H:%M:%S')
except (ValueError, OSError):
return str(t)
@@ -563,7 +565,6 @@ class _DrawerPanel:
self._refresh_trades()
self._refresh_market()
self._refresh_dataset()
self._refresh_scoring()
def _refresh_grid(self):
# (width, expand): 0=固定宽, >0=弹性比重; 股票列 expand 自动填充剩余空间
@@ -908,34 +909,91 @@ class _DrawerPanel:
# ── Tab 6: 每日评分 ──
def _build_scoring_tab(self) -> ft.Control:
from datetime import date, timedelta
self._score_cur_date = date.today()
from datetime import timedelta
from peewee import fn
from core.scoring.models import ScoringResult
# 初始日期:优先最近一个有评分的过去日期,今天盘中不能评分
today = _date.today()
latest_scored = (ScoringResult
.select(fn.MAX(ScoringResult.trade_date))
.scalar())
# 只取过去的评分日期,今天(未收盘)不算有效评分
if latest_scored and latest_scored < today:
self._score_cur_date = latest_scored
elif latest_scored:
self._score_cur_date = latest_scored # 今天有评分(可能是收盘后补跑的),也显示
else:
self._score_cur_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')
self._score_refreshing = False
self._score_lock = threading.Lock()
self._score_max_date = today # 导航上限:不允许看未来日期
def _prev_day(_):
if not self._score_lock.acquire(blocking=False):
return
self._score_cur_date -= timedelta(days=1)
self._score_date_label.value = str(self._score_cur_date)
self._score_date_label.update()
self._score_refreshing = True
for btn in self._score_nav_btns:
btn.disabled = True
for btn in self._score_nav_btns:
btn.update()
self._refresh_scoring()
def _next_day(_):
if not self._score_lock.acquire(blocking=False):
return
if self._score_cur_date >= self._score_max_date:
self._score_lock.release()
return
self._score_cur_date += timedelta(days=1)
self._score_date_label.value = str(self._score_cur_date)
self._score_date_label.update()
self._score_refreshing = True
for btn in self._score_nav_btns:
btn.disabled = True
for btn in self._score_nav_btns:
btn.update()
self._refresh_scoring()
def _today(_):
self._score_cur_date = date.today()
if not self._score_lock.acquire(blocking=False):
return
if self._score_cur_date >= self._score_max_date:
self._score_lock.release()
return
self._score_cur_date = self._score_max_date
self._score_date_label.value = str(self._score_cur_date)
self._score_date_label.update()
self._score_refreshing = True
for btn in self._score_nav_btns:
btn.disabled = True
for btn in self._score_nav_btns:
btn.update()
self._refresh_scoring()
self._score_nav_btns = [] # 占位,后面重新赋值
prev_btn = ft.IconButton(ft.Icons.CHEVRON_LEFT, icon_size=22, tooltip="前一天", on_click=_prev_day)
next_btn = ft.IconButton(ft.Icons.CHEVRON_RIGHT, icon_size=22, tooltip="后一天", on_click=_next_day)
today_btn = ft.IconButton(ft.Icons.TODAY, icon_size=20, tooltip="回到今天", on_click=_today)
self._score_nav_btns = [prev_btn, next_btn, today_btn]
# 初始时若已在上限日期,禁用"明天"和"今天"按钮
if self._score_cur_date >= self._score_max_date:
next_btn.disabled = True
today_btn.disabled = True
date_row = ft.Row([
ft.IconButton(ft.Icons.CHEVRON_LEFT, icon_size=22, tooltip="前一天", on_click=_prev_day),
prev_btn,
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),
next_btn,
today_btn,
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),
@@ -958,34 +1016,68 @@ class _DrawerPanel:
from core.scoring.inference.scorer import GridSeekerPipeline
trade_date = self._score_cur_date
# 检查当日 K线数据是否已同步
# 检查 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},请先盘后同步"
if latest_kline is None:
self._score_status.value = "K线数据未同步,请先同步数据"
self._score_status.color = '#F44336'
self._score_status.update()
return
# 评分日期以实际可用数据日期为准
# 规则: 只能评过去已收盘的日期,今天(06-26)盘中不能评分
# - trade_date < today: 过去日期,若有数据则用,若无数据则fallback到latest_kline
# - trade_date == today: 今天盘中,拒绝评分
today = _date.today()
if trade_date >= today:
self._score_status.value = f"今天({today})未收盘,无法评分,请切换到过去日期"
self._score_status.color = '#F44336'
self._score_status.update()
self._revert_nav_btns()
return
from core.scoring.models import KlineStock
has_trade_date = (KlineStock
.select(fn.COUNT(KlineStock.stock_code))
.where(KlineStock.trade_date == trade_date)
.scalar() or 0) > 0
persist_date = trade_date if has_trade_date else latest_kline
if not has_trade_date:
self._score_status.value = f"{trade_date} K线未同步,实际用 {persist_date} 数据评分"
self._score_status.color = '#FF9800'
self._score_status.update()
try:
engine = GridSeekerPipeline()
rankings = engine.run(trade_date)
rankings = engine.run(persist_date)
if not rankings.empty:
engine.persist(rankings, trade_date)
self._score_status.value = f"{trade_date} 评分完成"
engine.persist(rankings, persist_date)
self._score_status.value = f"{persist_date} 评分完成"
self._score_status.color = '#4CAF50'
# 刷新后跳到实际评分日期的那一页
self._score_cur_date = persist_date
self._score_date_label.value = str(persist_date)
except FileNotFoundError:
self._score_status.value = "模型文件缺失"
except Exception as ex:
self._score_status.value = f"失败: {ex}"
self._score_status.color = '#F44336'
self._score_status.update()
self._refresh_scoring()
threading.Thread(target=_do, daemon=True).start()
def _refresh_scoring(self):
import time as _time
from core.scoring.models import ScoringResult
from core.qmt import qmtv
t0 = _time.time()
PrintLog(LogLevel.DEBUG, '[_refresh_scoring] START date=%s' % self._score_cur_date)
# (width, expand): 排名60 + 名称弹性 + 概率65 + 轮数58 + 堆叠概率65 + 操作32
_C = [(60, 0), (0, 1), (65, 1), (58, 1), (65, 1), (32, 1)]
@@ -1016,28 +1108,40 @@ class _DrawerPanel:
if not scored_rows:
self._score_status.value = f"{trade_date} 无评分数据"
self._score_status.color = '#F44336'
# 空状态时在表格内也显示日期占位,让切换日期有明确的视觉反馈
rows.append(ft.Container(
ft.Text(f'{trade_date} 暂无评分数据 →',
size=14, color='#bbbbbb', text_align=ft.TextAlign.CENTER),
padding=ft.Padding(0, 20, 0, 20),
))
self._scoring_table.controls = rows
# 一次性批量更新:标签 + 状态 + 导航按钮
self._score_date_label.update()
self._score_status.update()
self._revert_nav_btns()
return
shown = 0
# 批量预加载: 1次DB查询 (替代原来每行1+1次,最快方案)
plain_codes = []
full_codes = []
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':
plain_codes.append(plain)
full_codes.append(f'{plain}.SH' if plain.startswith(('6', '5', '9')) else f'{plain}.SZ')
from core.scoring.models import StockInfo
st_map = {}
name_map = {}
for row in StockInfo.select(StockInfo.code, StockInfo.listing_status, StockInfo.name).where(StockInfo.code.in_(full_codes)).dicts():
st_map[row['code']] = row['listing_status']
name_map[row['code'].split('.')[0]] = row['name']
shown = 0
for r, plain, full_code in zip(scored_rows, plain_codes, full_codes):
if st_map.get(full_code) == 'ST':
continue
name = ''
try:
name = qmtv.getInstrumentName(plain)
except Exception:
pass
name = name_map.get(plain, '')
already_in = plain in self._data.stockCodeIdMap
shown += 1
@@ -1068,7 +1172,26 @@ class _DrawerPanel:
self._scoring_table.controls = rows
self._score_status.value = f"{trade_date} 候选 {shown}"
self._score_status.color = '#4CAF50'
# 一次性批量更新:标签 + 状态 + 导航按钮
self._score_date_label.update()
self._score_status.update()
self._revert_nav_btns()
PrintLog(LogLevel.DEBUG, '[_refresh_scoring] END t=%.2fs rows=%s' % (_time.time()-t0, shown))
def _revert_nav_btns(self):
"""重新启用导航按钮并解除刷新锁;已达上限日期时禁用'明天''今天'按钮"""
self._score_refreshing = False
at_max = self._score_cur_date >= self._score_max_date
for btn in self._score_nav_btns:
btn.disabled = False
if at_max:
self._score_nav_btns[1].disabled = True # next
self._score_nav_btns[2].disabled = True # today
for btn in self._score_nav_btns:
btn.update()
if self._score_refreshing:
self._score_lock.release()
def _on_add_from_scoring(self, stock_code: str, stock_name: str):
"""+ 按钮:从评分列表添加标的并打开网格配置"""