update
This commit is contained in:
+152
-29
@@ -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):
|
||||
"""+ 按钮:从评分列表添加标的并打开网格配置"""
|
||||
|
||||
Reference in New Issue
Block a user