update
This commit is contained in:
@@ -443,6 +443,33 @@ class RealQmtV:
|
||||
"""获取股票名称"""
|
||||
return self.cacheStockDetail(stock_code)['InstrumentName']
|
||||
|
||||
def getInstrumentName_batch(self, stock_codes: list) -> dict:
|
||||
"""批量获取股票名称,返回 {stock_code: name} dict"""
|
||||
result = {}
|
||||
missing = []
|
||||
for code in stock_codes:
|
||||
if code in self.details:
|
||||
result[code] = self.details[code].get('InstrumentName', '')
|
||||
else:
|
||||
missing.append(code)
|
||||
if not missing:
|
||||
return result
|
||||
try:
|
||||
from xtquant import xtdata
|
||||
for code in missing:
|
||||
full_code = self._to_full_code(code)
|
||||
detail = xtdata.get_instrument_detail(full_code)
|
||||
if detail:
|
||||
name = detail.get('instrumentName', detail.get('InstrumentName', ''))
|
||||
self.details[code] = detail
|
||||
result[code] = name
|
||||
else:
|
||||
result[code] = ''
|
||||
except Exception:
|
||||
for code in missing:
|
||||
result[code] = ''
|
||||
return result
|
||||
|
||||
def dailyUpStop(self, stock_code: str):
|
||||
"""获取涨停价"""
|
||||
detail = self.cacheStockDetail(stock_code)
|
||||
|
||||
@@ -58,19 +58,18 @@ class KlineStockSync(BaseSync):
|
||||
def _fetch(self, **kwargs):
|
||||
from xtquant import xtdata
|
||||
|
||||
# 增量判断
|
||||
# 增量判断: 以数据库最新一条记录为准
|
||||
latest = _latest_date(KlineStock)
|
||||
today = date.today()
|
||||
if latest is not None and latest >= today:
|
||||
PrintLog(LogLevel.INFO, f'[sync] KlineStock: 已最新 ({latest}),跳过')
|
||||
self.stats['skipped'] = 0
|
||||
return self.stats
|
||||
|
||||
# 增量起点
|
||||
# 增量起点: last_db_date + 1; 截止: 昨天(盘中不能同步当天数据)
|
||||
# 注意: 不能用 latest >= today 跳过,因为 latest 可能是错误的未收盘数据
|
||||
start_date = (latest + timedelta(days=1)) if latest else None
|
||||
end_date = today - timedelta(days=1) # 固定截止到昨天,收盘后同步昨天数据
|
||||
start_str = start_date.strftime('%Y%m%d') if start_date else ""
|
||||
end_str = end_date.strftime('%Y%m%d')
|
||||
PrintLog(LogLevel.INFO,
|
||||
f'[sync] KlineStock: 增量同步,起点={start_str or "全部"}')
|
||||
f'[sync] KlineStock: 增量同步 {start_str} ~ {end_str}')
|
||||
|
||||
all_stocks = xtdata.get_stock_list_in_sector("沪深A股")
|
||||
PrintLog(LogLevel.INFO, f'[sync] KlineStock: {len(all_stocks)} 只A股')
|
||||
@@ -83,7 +82,7 @@ class KlineStockSync(BaseSync):
|
||||
PrintLog(LogLevel.INFO, f'[sync] KlineStock: {i}/{total} ({i*100//total}%)')
|
||||
|
||||
try:
|
||||
xtdata.download_history_data(code, period='1d', start_time=start_str)
|
||||
xtdata.download_history_data(code, period='1d', start_time=start_str, end_time=end_str)
|
||||
except Exception:
|
||||
self.stats['errors'] += 1
|
||||
continue
|
||||
@@ -91,8 +90,9 @@ class KlineStockSync(BaseSync):
|
||||
try:
|
||||
result = xtdata.get_market_data(
|
||||
field_list=field_list, stock_list=[code], period='1d',
|
||||
count=self.count, dividend_type='none', fill_data=False)
|
||||
inserted += self._upsert_incremental(code, result, start_date)
|
||||
start_time=start_str, end_time=end_str,
|
||||
dividend_type='none', fill_data=False)
|
||||
inserted += self._upsert_incremental(code, result, start_date, end_date)
|
||||
except Exception:
|
||||
self.stats['errors'] += 1
|
||||
|
||||
@@ -102,7 +102,7 @@ class KlineStockSync(BaseSync):
|
||||
f'跳过={self.stats["skipped"]} 错误={self.stats["errors"]}')
|
||||
return self.stats
|
||||
|
||||
def _upsert_incremental(self, full_code: str, result: dict, start_date) -> int:
|
||||
def _upsert_incremental(self, full_code: str, result: dict, start_date, end_date) -> int:
|
||||
if not result:
|
||||
return 0
|
||||
close_df = result.get('close')
|
||||
@@ -110,13 +110,23 @@ class KlineStockSync(BaseSync):
|
||||
return 0
|
||||
stock_code = full_code.split('.')[0]
|
||||
records = []
|
||||
vol_df = result.get('volume')
|
||||
for td in close_df.columns:
|
||||
# xtdata 返回的列名可能是字符串 'YYYYMMDD' 或 datetime,需统一转成 date
|
||||
if isinstance(td, str):
|
||||
td_date = datetime.strptime(td, '%Y%m%d').date()
|
||||
else:
|
||||
td_date = td.date() if hasattr(td, 'date') else td
|
||||
if start_date is not None and td_date <= start_date:
|
||||
# 过滤: 不在增量范围内的跳过 (start_date < td <= end_date)
|
||||
if start_date is not None and td_date < start_date:
|
||||
self.stats['skipped'] = self.stats.get('skipped', 0) + 1
|
||||
continue
|
||||
if end_date is not None and td_date > end_date:
|
||||
self.stats['skipped'] = self.stats.get('skipped', 0) + 1
|
||||
continue
|
||||
# 跳过成交量为0的无效数据(盘中未结算数据)
|
||||
vol = vol_df.loc[full_code, td] if vol_df is not None else None
|
||||
if vol is None or (isinstance(vol, float) and pd.isna(vol)) or vol == 0:
|
||||
self.stats['skipped'] = self.stats.get('skipped', 0) + 1
|
||||
continue
|
||||
close_val = close_df.loc[full_code, td]
|
||||
@@ -130,7 +140,7 @@ class KlineStockSync(BaseSync):
|
||||
'high': _safe_get(result.get('high'), full_code, td),
|
||||
'low': _safe_get(result.get('low'), full_code, td),
|
||||
'close': float(close_val),
|
||||
'volume': _safe_get(result.get('volume'), full_code, td),
|
||||
'volume': float(vol),
|
||||
})
|
||||
if records:
|
||||
with db.atomic():
|
||||
@@ -165,33 +175,35 @@ class KlineIndexSync(BaseSync):
|
||||
|
||||
latest = _latest_date(KlineIndex)
|
||||
today = date.today()
|
||||
if latest is not None and latest >= today:
|
||||
PrintLog(LogLevel.INFO, f'[sync] KlineIndex: 已最新 ({latest}),跳过')
|
||||
return {}
|
||||
|
||||
start_date = (latest + timedelta(days=1)) if latest else None
|
||||
end_date = today - timedelta(days=1) # 截止到昨天
|
||||
start_str = start_date.strftime('%Y%m%d') if start_date else ""
|
||||
end_str = end_date.strftime('%Y%m%d')
|
||||
PrintLog(LogLevel.INFO,
|
||||
f'[sync] KlineIndex: 增量同步,起点={start_str or "全部"}')
|
||||
f'[sync] KlineIndex: 增量同步 {start_str} ~ {end_str}')
|
||||
|
||||
for code in index_codes:
|
||||
try:
|
||||
xtdata.download_history_data(code, period='1d', start_time=start_str)
|
||||
xtdata.download_history_data(code, period='1d', start_time=start_str, end_time=end_str)
|
||||
except Exception:
|
||||
self.stats['errors'] += 1
|
||||
|
||||
field_list = ['open', 'high', 'low', 'close', 'volume']
|
||||
result = xtdata.get_market_data(
|
||||
field_list=field_list, stock_list=index_codes, period='1d',
|
||||
count=self.count, dividend_type='none', fill_data=False)
|
||||
return result or {}
|
||||
start_time=start_str, end_time=end_str,
|
||||
dividend_type='none', fill_data=False)
|
||||
|
||||
def _upsert(self, data):
|
||||
if not data:
|
||||
return
|
||||
latest = _latest_date(KlineIndex)
|
||||
today = date.today()
|
||||
start_date = (latest + timedelta(days=1)) if latest else None
|
||||
end_date = today - timedelta(days=1)
|
||||
records = []
|
||||
close_df = data.get('close')
|
||||
vol_df = data.get('volume')
|
||||
if close_df is None or close_df.empty:
|
||||
return
|
||||
for full_code in close_df.index:
|
||||
@@ -201,7 +213,16 @@ class KlineIndexSync(BaseSync):
|
||||
td_date = datetime.strptime(td, '%Y%m%d').date()
|
||||
else:
|
||||
td_date = td.date() if hasattr(td, 'date') else td
|
||||
if latest is not None and td_date <= latest:
|
||||
# 增量范围过滤 (start_date < td <= end_date)
|
||||
if start_date is not None and td_date < start_date:
|
||||
self.stats['skipped'] = self.stats.get('skipped', 0) + 1
|
||||
continue
|
||||
if end_date is not None and td_date > end_date:
|
||||
self.stats['skipped'] = self.stats.get('skipped', 0) + 1
|
||||
continue
|
||||
# 过滤成交量为0的无效数据
|
||||
vol = vol_df.loc[full_code, td] if vol_df is not None else None
|
||||
if vol is None or (isinstance(vol, float) and pd.isna(vol)) or vol == 0:
|
||||
self.stats['skipped'] = self.stats.get('skipped', 0) + 1
|
||||
continue
|
||||
close_val = close_df.loc[full_code, td]
|
||||
@@ -215,7 +236,7 @@ class KlineIndexSync(BaseSync):
|
||||
'high': _safe_get(data.get('high'), full_code, td),
|
||||
'low': _safe_get(data.get('low'), full_code, td),
|
||||
'close': float(close_val),
|
||||
'volume': _safe_get(data.get('volume'), full_code, td),
|
||||
'volume': float(vol),
|
||||
})
|
||||
if records:
|
||||
with db.atomic():
|
||||
|
||||
@@ -342,7 +342,6 @@ class PoolManager:
|
||||
f'[PoolManager] 沉寂检测异常 {tgt.stock_code}: {e}')
|
||||
|
||||
if marked:
|
||||
from core.sfgrid.bus import event_bus
|
||||
event_bus.publish(EventPoolMark, {
|
||||
'action': 'liquidate',
|
||||
'stock_codes': marked,
|
||||
@@ -404,7 +403,6 @@ class PoolManager:
|
||||
f'[PoolManager] 淘汰标记: {code},历史: {recent}')
|
||||
|
||||
if marked:
|
||||
from core.sfgrid.bus import event_bus
|
||||
event_bus.publish(EventPoolMark, {
|
||||
'action': 'eliminate',
|
||||
'stock_codes': marked,
|
||||
|
||||
@@ -54,10 +54,17 @@ class SFGridStrategy:
|
||||
"""
|
||||
self.tradeTarget: model.SFGridTradeTarget = tradeTarget
|
||||
|
||||
# orderGrid 必须在所有可能触发回调的操作之前初始化
|
||||
# orderGrid: 网格索引 → 订单编号(seq 或 order_id)的映射
|
||||
# seq 是 xtquant 返回的下单序号(下单瞬间),order_id 是交易所返回的正式订单号(异步回调后更新)
|
||||
self.orderGrid = {} # {grid_index: order_seq | order_id}
|
||||
|
||||
# 数据更新锁:保护 orderGrid 和 tradeTarget 的并发访问
|
||||
# QMT 回调在独立线程中触发,必须在可能触发回调的操作之前创建
|
||||
# 注意:这个锁必须在订阅事件之前创建,防止事件在初始化期间触发
|
||||
self.dataUpdateLock = threading.Lock()
|
||||
# 注意:必须使用 RLock 而非 Lock,因为 refreshGridOrder 在持有此锁时也会被调用
|
||||
#(如 onOrderTrade 回调中),Lock 会导致同一线程重复获取时永久阻塞(死锁)
|
||||
self.dataUpdateLock = threading.RLock()
|
||||
|
||||
# 订阅事件总线:监听订单创建、成交、失败三种事件
|
||||
event_bus.subscribe(eBus.MarketOrderCreated, self.onOrderCreateAsync)
|
||||
@@ -73,10 +80,6 @@ class SFGridStrategy:
|
||||
f'|- [DEBUG] 标的{tradeTarget.targetName()} 构造开始: '
|
||||
f'网格={tradeTarget.grid_index}, 启用={tradeTarget.enabled}')
|
||||
|
||||
# orderGrid: 网格索引 → 订单编号(seq 或 order_id)的映射
|
||||
# seq 是 xtquant 返回的下单序号(下单瞬间),order_id 是交易所返回的正式订单号(异步回调后更新)
|
||||
self.orderGrid = {} # {grid_index: order_seq | order_id}
|
||||
|
||||
# 加载券商侧已存在的未成交订单,恢复到 orderGrid 中
|
||||
self.loadExistOrders()
|
||||
|
||||
@@ -171,7 +174,14 @@ class SFGridStrategy:
|
||||
sell_remark = self._make_remark(OrderTypeSell, sellIdx)
|
||||
|
||||
# 检查是否已存在同 remark 的卖单(避免重复挂单)
|
||||
if not any(o.order_remark == sell_remark for o in orders):
|
||||
# 注意:必须同时查 QMT 订单簿和本地 orderGrid
|
||||
# - QMT 订单簿:已确认的订单(onOrderCreateAsync 之后)
|
||||
# - orderGrid:本地下单后、回调前的新单(orderAsync 返回后直接写入)
|
||||
# 两者并集才能完整覆盖所有已存在订单,防止 onOrderCreateAsync 回调
|
||||
# 之前再次触发 refreshGridOrder 导致重复下单
|
||||
qmt_has_order = any(o.order_remark == sell_remark for o in orders)
|
||||
local_has_order = sellIdx in self.orderGrid
|
||||
if not qmt_has_order and not local_has_order:
|
||||
# 卖单价格超过涨停价 → 今日无法成交,跳过下单
|
||||
# 防御性检查:若属性未初始化(初始化顺序导致),先获取
|
||||
if not hasattr(self, 'todayUpStopPrice') or self.todayUpStopPrice is None:
|
||||
@@ -208,7 +218,10 @@ class SFGridStrategy:
|
||||
buy_remark = self._make_remark(OrderTypeBuy, buyIdx)
|
||||
|
||||
# 检查是否已存在同 remark 的买单(避免重复挂单)
|
||||
if not any(o.order_remark == buy_remark for o in orders):
|
||||
# 必须同时查 QMT 订单簿和本地 orderGrid(见上方卖单注释)
|
||||
qmt_has_order = any(o.order_remark == buy_remark for o in orders)
|
||||
local_has_order = buyIdx in self.orderGrid
|
||||
if not qmt_has_order and not local_has_order:
|
||||
# 买单价格低于跌停价 → 今日无法成交,跳过下单
|
||||
# 防御性检查:若属性未初始化(初始化顺序导致),先获取
|
||||
if not hasattr(self, 'todayDownStopPrice') or self.todayDownStopPrice is None:
|
||||
@@ -434,6 +447,17 @@ class SFGridStrategy:
|
||||
if self.tradeTarget.grid_index == 0:
|
||||
self.tradeTarget.init_price = trade.traded_price # type: ignore
|
||||
|
||||
# ── 同步更新持仓量 ──
|
||||
# 注意:xtquant 的成交推送不包含最新持仓,此处根据成交方向估算变动
|
||||
# 买入成交(建仓/补仓)→ 持仓增加
|
||||
# 卖出成交(减仓/清仓)→ 持仓减少
|
||||
if gridIdx > self.tradeTarget.grid_index:
|
||||
# 买入方向:持仓增加
|
||||
self.tradeTarget.current_position += int(trade.traded_volume) # type: ignore
|
||||
elif gridIdx < self.tradeTarget.grid_index:
|
||||
# 卖出方向:持仓减少
|
||||
self.tradeTarget.current_position -= int(trade.traded_volume) # type: ignore
|
||||
|
||||
# ── 网格方向判断 ──
|
||||
# 比较成交单的网格索引 vs 当前网格索引,判断价格移动方向
|
||||
oriIdx = self.tradeTarget.grid_index # 成交前的网格位置
|
||||
|
||||
+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):
|
||||
"""+ 按钮:从评分列表添加标的并打开网格配置"""
|
||||
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
with open('core/ui/flet/app_v2.py', 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
# Patch 1: Add lock alongside _score_refreshing init
|
||||
old_init = " self._score_refreshing = False # 刷新锁"
|
||||
new_init = " self._score_refreshing = False\n self._score_lock = threading.Lock()"
|
||||
content = content.replace(old_init, new_init, 1)
|
||||
print('Patch 1 (init):', 'OK' if old_init not in content else 'NOT FOUND')
|
||||
|
||||
# Patch 2: _prev_day - use lock instead of flag guard
|
||||
old_prev = ''' def _prev_day(_):
|
||||
if self._score_refreshing:
|
||||
return
|
||||
self._score_cur_date -= timedelta(days=1)
|
||||
self._score_date_label.value = str(self._score_cur_date)
|
||||
self._score_refreshing = True
|
||||
self._score_nav_btns[0].disabled = True
|
||||
self._score_nav_btns[1].disabled = True
|
||||
self._score_nav_btns[2].disabled = True
|
||||
self._refresh_scoring()'''
|
||||
new_prev = ''' 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()'''
|
||||
content = content.replace(old_prev, new_prev, 1)
|
||||
print('Patch 2 (_prev_day):', 'OK' if old_prev not in content else 'NOT FOUND')
|
||||
|
||||
# Patch 3: _next_day - use lock
|
||||
old_next = ''' def _next_day(_):
|
||||
if self._score_refreshing:
|
||||
return
|
||||
if self._score_cur_date >= self._score_max_date:
|
||||
return
|
||||
self._score_cur_date += timedelta(days=1)
|
||||
self._score_date_label.value = str(self._score_cur_date)
|
||||
self._score_refreshing = True
|
||||
self._score_nav_btns[0].disabled = True
|
||||
self._score_nav_btns[1].disabled = True
|
||||
self._score_nav_btns[2].disabled = True
|
||||
self._refresh_scoring()'''
|
||||
new_next = ''' 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()'''
|
||||
content = content.replace(old_next, new_next, 1)
|
||||
print('Patch 3 (_next_day):', 'OK' if old_next not in content else 'NOT FOUND')
|
||||
|
||||
# Patch 4: _today - use lock
|
||||
old_today = ''' def _today(_):
|
||||
if self._score_refreshing:
|
||||
return
|
||||
if self._score_cur_date >= self._score_max_date:
|
||||
return
|
||||
self._score_cur_date = self._score_max_date
|
||||
self._score_date_label.value = str(self._score_cur_date)
|
||||
self._score_refreshing = True
|
||||
self._score_nav_btns[0].disabled = True
|
||||
self._score_nav_btns[1].disabled = True
|
||||
self._score_nav_btns[2].disabled = True
|
||||
self._refresh_scoring()'''
|
||||
new_today = ''' def _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()'''
|
||||
content = content.replace(old_today, new_today, 1)
|
||||
print('Patch 4 (_today):', 'OK' if old_today not in content else 'NOT FOUND')
|
||||
|
||||
# Patch 5: _revert_nav_btns - release lock at end
|
||||
old_revert = ''' 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()'''
|
||||
new_revert = ''' 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()
|
||||
self._score_lock.release()'''
|
||||
content = content.replace(old_revert, new_revert, 1)
|
||||
print('Patch 5 (_revert_nav_btns):', 'OK' if old_revert not in content else 'NOT FOUND')
|
||||
|
||||
with open('core/ui/flet/app_v2.py', 'w', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
print('Done writing')
|
||||
Reference in New Issue
Block a user