完成模型更新
This commit is contained in:
@@ -50,5 +50,5 @@ RANK_MODEL = 'rank'
|
||||
TOP_MODEL = 'top'
|
||||
STACKING_MODEL = 'stacking'
|
||||
|
||||
# Stacking 选股阈值
|
||||
STACKING_THRESHOLD = 0.35
|
||||
# Stacking 选股阈值 (v6.7r3 最优阈值 0.331)
|
||||
STACKING_THRESHOLD = 0.33
|
||||
@@ -1,9 +1,10 @@
|
||||
"""
|
||||
扩展特征 v3.4 (16维)
|
||||
扩展特征 v3.4 + v6.7新增 (20维: 16维 v3.4 + 4维 v6.7新增)
|
||||
"""
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from core.scoring.features.v3_2_features import _ols_slope
|
||||
from core.scoring.features.v3_3_features import _grid_touch_count
|
||||
from core.scoring.config import GRID_LOW, GRID_HIGH
|
||||
|
||||
|
||||
@@ -104,6 +105,29 @@ def calculate_features_v3_4(ctx) -> pd.DataFrame:
|
||||
feat['wick_ratio_20d'] = np.mean(
|
||||
wick_len / np.where(total_len > 0, total_len, 1)) * 100
|
||||
|
||||
# ── v6.7 新增 4 维特征 ──────────────────────────────
|
||||
# 53. vol_decay_5d: 近5日波动率 / 近20日波动率 (波动率用对数收益std)
|
||||
log_ret = np.diff(np.log(np.maximum(closes, 1e-10)))
|
||||
vol_5d = np.std(log_ret[-5:], ddof=1) if len(log_ret) >= 5 else 0
|
||||
vol_20d = np.std(log_ret[-20:], ddof=1) if len(log_ret) >= 20 else vol_5d
|
||||
feat['vol_decay_5d'] = float(vol_5d / vol_20d) if vol_20d > 0 else 0.0
|
||||
|
||||
# 54. grid_touch_relative_10d: 10日振幅比 / 60日振幅比
|
||||
range_10d = float(np.max(highs[-10:]) - np.min(lows[-10:]))
|
||||
range_60d = float(np.max(highs[-60:]) - np.min(lows[-60:])) if len(highs) >= 60 else range_10d
|
||||
close_now = float(closes[-1])
|
||||
close_60d_mean = float(np.mean(closes[-60:])) if len(closes) >= 60 else close_now
|
||||
if close_now > 0 and close_60d_mean > 0 and range_60d > 0:
|
||||
feat['grid_touch_relative_10d'] = (range_10d / close_now) / (range_60d / close_60d_mean)
|
||||
else:
|
||||
feat['grid_touch_relative_10d'] = 0.0
|
||||
|
||||
# 55. vol_decay_x_grid_balance: vol_decay × 网格均衡度
|
||||
feat['vol_decay_x_grid_balance'] = feat['vol_decay_5d'] * feat.get('grid_room_balance', 0.0)
|
||||
|
||||
# 56. vol_decay_x_dist_lower: vol_decay × 下轨距离
|
||||
feat['vol_decay_x_dist_lower'] = feat['vol_decay_5d'] * feat.get('dist_to_grid_lower', 0.0)
|
||||
|
||||
features[code] = feat
|
||||
|
||||
return pd.DataFrame.from_dict(features, orient='index')
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
grid_seeker v6.6 三级模型推理管道
|
||||
grid_seeker v6.7r3 三级模型推理管道
|
||||
Rank → Top → Stacking → stacking_probability (最终排序)
|
||||
"""
|
||||
import pickle
|
||||
@@ -18,7 +18,7 @@ from core.logger import LogLevel, PrintLog
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Rank 模型输入特征 (52维, v3.4, 直接从模型文件的 selected_features 读取)
|
||||
# Rank 模型输入特征 (56维 v6.7/v3.4, 同时支持 feat_names 和 selected_features)
|
||||
# ============================================================
|
||||
def _get_rank_features() -> list:
|
||||
import pickle
|
||||
@@ -27,17 +27,26 @@ def _get_rank_features() -> list:
|
||||
with open(path, 'rb') as f:
|
||||
obj = pickle.load(f)
|
||||
if isinstance(obj, dict):
|
||||
sf = obj.get('selected_features', [])
|
||||
# v6.7r3 使用 feat_names, v6.6 使用 selected_features
|
||||
sf = obj.get('feat_names', []) or obj.get('selected_features', [])
|
||||
if sf:
|
||||
return sf
|
||||
raise RuntimeError("无法从 rank.pkl 读取 selected_features")
|
||||
raise RuntimeError("无法从 rank.pkl 读取 feat_names 或 selected_features")
|
||||
|
||||
RANK_FEATURE_COLS = _get_rank_features()
|
||||
|
||||
# Top/Stacking 模型只用 52 维基础特征(不含 v6.7 新增的4维)
|
||||
# v6.7 新增: vol_decay_5d, grid_touch_relative_10d, vol_decay_x_grid_balance, vol_decay_x_dist_lower
|
||||
_V67_NEW_FEATS = {
|
||||
'vol_decay_5d', 'grid_touch_relative_10d',
|
||||
'vol_decay_x_grid_balance', 'vol_decay_x_dist_lower'
|
||||
}
|
||||
BASE_52_COLS = [f for f in RANK_FEATURE_COLS if f not in _V67_NEW_FEATS]
|
||||
|
||||
|
||||
class GridSeekerPipeline:
|
||||
"""
|
||||
grid_seeker v6.6 三级模型评分管道。
|
||||
grid_seeker v6.7r3 三级模型评分管道。
|
||||
|
||||
Usage:
|
||||
engine = GridSeekerPipeline()
|
||||
@@ -120,7 +129,7 @@ class GridSeekerPipeline:
|
||||
DataFrame indexed by stock_code, 含 stacking_probability / rank 等列,
|
||||
按 stacking_probability 降序排列
|
||||
"""
|
||||
PrintLog(LogLevel.INFO, f'[scorer] ===== grid_seeker v6.6 评分开始 ({trade_date}) =====')
|
||||
PrintLog(LogLevel.INFO, f'[scorer] ===== grid_seeker v6.7r3 评分开始 ({trade_date}) =====')
|
||||
|
||||
# 1. 特征工程
|
||||
pipeline = FeaturePipeline(trade_date)
|
||||
@@ -140,16 +149,16 @@ class GridSeekerPipeline:
|
||||
self.rank_model, feature_df, RANK_FEATURE_COLS
|
||||
)
|
||||
|
||||
# 3. Stage 2: Top 模型 → top_elite_prob (53维 = 52 + rank_predicted_rounds)
|
||||
# 3. Stage 2: Top 模型 → top_elite_prob (53维 = 52基础 + rank)
|
||||
PrintLog(LogLevel.INFO, '[scorer] Stage 2/3: Top 模型...')
|
||||
top_cols = RANK_FEATURE_COLS + ['rank_predicted_rounds']
|
||||
top_cols = BASE_52_COLS + ['rank_predicted_rounds']
|
||||
feature_df['top_elite_prob'] = self._predict_with_model(
|
||||
self.top_model, feature_df, top_cols
|
||||
)
|
||||
|
||||
# 4. Stage 3: Stacking 模型 → stacking_probability (54维 = 52 + rank + top)
|
||||
# 4. Stage 3: Stacking 模型 → stacking_probability (55维 = 52基础 + rank + top)
|
||||
PrintLog(LogLevel.INFO, '[scorer] Stage 3/3: Stacking 模型...')
|
||||
stk_cols = RANK_FEATURE_COLS + ['rank_predicted_rounds', 'top_elite_prob']
|
||||
stk_cols = BASE_52_COLS + ['rank_predicted_rounds', 'top_elite_prob']
|
||||
feature_df['stacking_probability'] = self._predict_with_model(
|
||||
self.stacking_model, feature_df, stk_cols
|
||||
)
|
||||
|
||||
@@ -5,7 +5,7 @@ K线数据同步 — 个股日K + 指数日K
|
||||
线程锁: KlineStockSync / KlineIndexSync 各自内部锁
|
||||
"""
|
||||
import pandas as pd
|
||||
from datetime import date, timedelta
|
||||
from datetime import date, datetime, timedelta
|
||||
from core.scoring.sync.base import BaseSync
|
||||
from core.scoring.models import KlineStock, KlineIndex
|
||||
from core.scoring.config import TRACKED_INDICES
|
||||
@@ -111,7 +111,11 @@ class KlineStockSync(BaseSync):
|
||||
stock_code = full_code.split('.')[0]
|
||||
records = []
|
||||
for td in close_df.columns:
|
||||
td_date = td.date() if hasattr(td, 'date') else td
|
||||
# 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:
|
||||
self.stats['skipped'] = self.stats.get('skipped', 0) + 1
|
||||
continue
|
||||
@@ -193,7 +197,10 @@ class KlineIndexSync(BaseSync):
|
||||
for full_code in close_df.index:
|
||||
index_code = full_code.split('.')[0]
|
||||
for td in close_df.columns:
|
||||
td_date = td.date() if hasattr(td, 'date') else td
|
||||
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 latest is not None and td_date <= latest:
|
||||
self.stats['skipped'] = self.stats.get('skipped', 0) + 1
|
||||
continue
|
||||
|
||||
@@ -13,7 +13,7 @@ class SectorFeaturesSync(BaseSync):
|
||||
"""行业聚合指数同步 — kline_stock + industry → sector_features_daily"""
|
||||
|
||||
def _fetch(self, **kwargs):
|
||||
"""从数据库加载原始数据, 计算行业指数特征"""
|
||||
"""从数据库加载原始数据, 计算行业指数特征(分块处理避免内存溢出)"""
|
||||
PrintLog(LogLevel.INFO, '[sync] SectorFeatures: 加载原始数据...')
|
||||
|
||||
# 1. 加载行业映射: code → industry_name
|
||||
@@ -24,49 +24,81 @@ class SectorFeaturesSync(BaseSync):
|
||||
code_to_industry = {row['code']: row['industry_name'] for row in industries}
|
||||
PrintLog(LogLevel.INFO, f'[sync] SectorFeatures: {len(code_to_industry)} 条行业映射')
|
||||
|
||||
# 2. 加载 K 线数据
|
||||
kline_rows = (KlineStock
|
||||
.select(
|
||||
KlineStock.stock_code,
|
||||
KlineStock.trade_date,
|
||||
KlineStock.open,
|
||||
KlineStock.high,
|
||||
KlineStock.low,
|
||||
KlineStock.close,
|
||||
)
|
||||
.order_by(KlineStock.stock_code, KlineStock.trade_date)
|
||||
.dicts())
|
||||
# 2. 分块加载 K 线数据,避免内存溢出
|
||||
# 聚合结果: {(trade_date, sector_name): [sum_pct_chg, sum_amp, count]}
|
||||
sector_daily_agg = {} # key: (date, sector) -> {'ret_sum': float, 'amp_sum': float, 'count': int}
|
||||
CHUNK_SIZE = 50000
|
||||
last_date_per_stock = {} # stock_code -> prev_close
|
||||
|
||||
if not kline_rows:
|
||||
PrintLog(LogLevel.WARNING, '[sync] SectorFeatures: KlineStock 表为空')
|
||||
PrintLog(LogLevel.INFO, '[sync] SectorFeatures: 分块处理K线数据...')
|
||||
chunk_num = 0
|
||||
while True:
|
||||
chunk_num += 1
|
||||
rows = list(KlineStock
|
||||
.select(
|
||||
KlineStock.stock_code,
|
||||
KlineStock.trade_date,
|
||||
KlineStock.open,
|
||||
KlineStock.high,
|
||||
KlineStock.low,
|
||||
KlineStock.close,
|
||||
)
|
||||
.order_by(KlineStock.stock_code, KlineStock.trade_date)
|
||||
.offset((chunk_num - 1) * CHUNK_SIZE)
|
||||
.limit(CHUNK_SIZE)
|
||||
.dicts())
|
||||
if not rows:
|
||||
break
|
||||
|
||||
PrintLog(LogLevel.INFO, f'[sync] SectorFeatures: 处理块 {chunk_num} ({len(rows)} 行)...')
|
||||
|
||||
for row in rows:
|
||||
code = str(row['stock_code'])
|
||||
td = row['trade_date']
|
||||
open_p = float(row['open'])
|
||||
high = float(row['high'])
|
||||
low = float(row['low'])
|
||||
close = float(row['close'])
|
||||
|
||||
sector = code_to_industry.get(code)
|
||||
if sector is None:
|
||||
continue
|
||||
|
||||
# 计算日收益率和振幅
|
||||
prev_close = last_date_per_stock.get(code)
|
||||
if prev_close is not None and prev_close > 0 and open_p > 0 and close > 0:
|
||||
pct_chg = (close - prev_close) / prev_close * 100
|
||||
amp = (high - low) / open_p * 100
|
||||
key = (td, sector)
|
||||
if key not in sector_daily_agg:
|
||||
sector_daily_agg[key] = {'ret_sum': 0.0, 'amp_sum': 0.0, 'count': 0}
|
||||
sector_daily_agg[key]['ret_sum'] += pct_chg
|
||||
sector_daily_agg[key]['amp_sum'] += amp
|
||||
sector_daily_agg[key]['count'] += 1
|
||||
|
||||
last_date_per_stock[code] = close
|
||||
|
||||
if not sector_daily_agg:
|
||||
PrintLog(LogLevel.WARNING, '[sync] SectorFeatures: 无有效K线数据')
|
||||
return None
|
||||
|
||||
df = pd.DataFrame(kline_rows)
|
||||
df['trade_date'] = pd.to_datetime(df['trade_date'])
|
||||
PrintLog(LogLevel.INFO, f'[sync] SectorFeatures: {len(df)} 条K线数据')
|
||||
PrintLog(LogLevel.INFO, f'[sync] SectorFeatures: 聚合完成, {len(sector_daily_agg)} 个行业-日组合')
|
||||
|
||||
# 3. 映射行业
|
||||
df['sector_name'] = df['stock_code'].map(code_to_industry)
|
||||
df = df.dropna(subset=['sector_name'])
|
||||
# 3. 构建聚合 DataFrame
|
||||
agg_data = []
|
||||
for (td, sector), vals in sector_daily_agg.items():
|
||||
agg_data.append({
|
||||
'trade_date': td,
|
||||
'sector_name': sector,
|
||||
'sector_ret': vals['ret_sum'] / vals['count'],
|
||||
'sector_amplitude': vals['amp_sum'] / vals['count'],
|
||||
})
|
||||
agg = pd.DataFrame(agg_data)
|
||||
agg = agg.sort_values(['sector_name', 'trade_date'])
|
||||
agg['trade_date'] = pd.to_datetime(agg['trade_date'])
|
||||
PrintLog(LogLevel.INFO, f'[sync] SectorFeatures: {len(agg)} 行, {agg["sector_name"].nunique()} 个行业')
|
||||
|
||||
# 4. 逐股计算日收益率和振幅
|
||||
df = df.sort_values(['stock_code', 'trade_date'])
|
||||
df['prev_close'] = df.groupby('stock_code')['close'].shift(1)
|
||||
df['pct_chg'] = (df['close'] - df['prev_close']) / df['prev_close'] * 100
|
||||
df['amplitude'] = (df['high'] - df['low']) / df['open'] * 100
|
||||
|
||||
# 清理无效值
|
||||
df = df.dropna(subset=['pct_chg', 'amplitude'])
|
||||
|
||||
# 5. 按行业+日期聚合
|
||||
agg = (df.groupby(['trade_date', 'sector_name'])
|
||||
.agg(
|
||||
sector_ret=('pct_chg', 'mean'),
|
||||
sector_amplitude=('amplitude', 'mean'),
|
||||
)
|
||||
.reset_index())
|
||||
|
||||
# 6. 构建行业指数 (基值=100)
|
||||
# 4. 构建行业指数 (基值=100)
|
||||
agg = agg.sort_values(['sector_name', 'trade_date'])
|
||||
agg['sector_index'] = agg.groupby('sector_name')['sector_ret'].transform(
|
||||
lambda x: (1 + x / 100).cumprod() * 100
|
||||
@@ -77,7 +109,7 @@ class SectorFeaturesSync(BaseSync):
|
||||
first_val = group['sector_index'].iloc[0]
|
||||
agg.loc[idx, 'sector_index'] = group['sector_index'] / first_val * 100
|
||||
|
||||
# 7. 计算 EMA 均线
|
||||
# 5. 计算 EMA 均线
|
||||
agg['ema10'] = (agg.groupby('sector_name')['sector_index']
|
||||
.transform(lambda x: x.ewm(span=10, min_periods=1).mean()))
|
||||
agg['ema20'] = (agg.groupby('sector_name')['sector_index']
|
||||
@@ -85,7 +117,7 @@ class SectorFeaturesSync(BaseSync):
|
||||
agg['ema200'] = (agg.groupby('sector_name')['sector_index']
|
||||
.transform(lambda x: x.ewm(span=200, min_periods=1).mean()))
|
||||
|
||||
# 8. 趋势评分: close>ema200 得1分 + ema10>ema20 得1分
|
||||
# 6. 趋势评分: close>ema200 得1分 + ema10>ema20 得1分
|
||||
agg['score'] = (
|
||||
(agg['sector_index'] > agg['ema200']).astype(int) +
|
||||
(agg['ema10'] > agg['ema20']).astype(int)
|
||||
|
||||
@@ -173,6 +173,9 @@ class SFGridStrategy:
|
||||
# 检查是否已存在同 remark 的卖单(避免重复挂单)
|
||||
if not any(o.order_remark == sell_remark for o in orders):
|
||||
# 卖单价格超过涨停价 → 今日无法成交,跳过下单
|
||||
# 防御性检查:若属性未初始化(初始化顺序导致),先获取
|
||||
if not hasattr(self, 'todayUpStopPrice') or self.todayUpStopPrice is None:
|
||||
self.todayUpStopPrice = qmtv.dailyUpStop(self.tradeTarget.stock_code) # type: ignore
|
||||
if sellPrice > self.todayUpStopPrice:
|
||||
PrintLog(LogLevel.INFO,
|
||||
f'|- 标的[{self.tradeTarget.targetName()}] '
|
||||
@@ -207,6 +210,9 @@ class SFGridStrategy:
|
||||
# 检查是否已存在同 remark 的买单(避免重复挂单)
|
||||
if not any(o.order_remark == buy_remark for o in orders):
|
||||
# 买单价格低于跌停价 → 今日无法成交,跳过下单
|
||||
# 防御性检查:若属性未初始化(初始化顺序导致),先获取
|
||||
if not hasattr(self, 'todayDownStopPrice') or self.todayDownStopPrice is None:
|
||||
self.todayDownStopPrice = qmtv.dailyDownStop(self.tradeTarget.stock_code) # type: ignore
|
||||
if buyPrice < self.todayDownStopPrice:
|
||||
PrintLog(LogLevel.INFO,
|
||||
f'|- 标的[{self.tradeTarget.targetName()}] '
|
||||
|
||||
+69
-6
@@ -324,9 +324,9 @@ class _GridPanel:
|
||||
return self._col
|
||||
|
||||
def _rebuild(self):
|
||||
# (width, expand): 0=固定宽, >0=弹性比重
|
||||
_C = [(35, 0), (0, 2), (70, 1), (0, 2), (50, 1), (55, 1), (60, 1), (0, 1)]
|
||||
H = ["ID", "股票", "市场价", "网格基准", "持仓", "成本", "状态", "操作"]
|
||||
# (width, expand): 0=固定宽, >0=弹性比重; 新增排名列(50px)
|
||||
_C = [(35, 0), (0, 2), (70, 1), (0, 2), (50, 1), (55, 1), (50, 0), (60, 1), (0, 1)]
|
||||
H = ["ID", "股票", "市场价", "网格基准", "持仓", "成本", "排名", "状态", "操作"]
|
||||
header = []
|
||||
for h, (w, e) in zip(H, _C):
|
||||
if e > 0:
|
||||
@@ -335,6 +335,22 @@ class _GridPanel:
|
||||
header.append(ft.Container(_text(h, bold=True), width=w, padding=4))
|
||||
rows = [ft.Row(header, spacing=0), ft.Divider(height=1, color='#e0e0e0')]
|
||||
|
||||
# 获取最新评分排名
|
||||
rank_map = {} # stock_code -> score_rank
|
||||
try:
|
||||
from core.scoring.models import ScoringResult
|
||||
from peewee import fn
|
||||
latest_date = ScoringResult.select(fn.MAX(ScoringResult.trade_date)).scalar()
|
||||
if latest_date:
|
||||
scored = (ScoringResult
|
||||
.select(ScoringResult.stock_code, ScoringResult.score_rank)
|
||||
.where(ScoringResult.trade_date == latest_date)
|
||||
.dicts())
|
||||
for r in scored:
|
||||
rank_map[r['stock_code']] = r['score_rank']
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
for tid, t in self._data.tradeTargets.items():
|
||||
if t.strategy_type != STRATEGY_TYPE_GRID: continue
|
||||
pg = t.getPriceGrid()
|
||||
@@ -358,6 +374,22 @@ class _GridPanel:
|
||||
))
|
||||
gcell = ft.Row(gcells, spacing=3) if len(gcells) > 1 else gcells[0]
|
||||
|
||||
# 排名列
|
||||
plain = t.stock_code.split('.')[0] if '.' in t.stock_code else t.stock_code
|
||||
rank = rank_map.get(plain, None)
|
||||
if rank is not None and rank <= 50:
|
||||
rank_str = f'#{rank}'
|
||||
rank_color = '#4CAF50'
|
||||
elif rank is not None and rank <= 100:
|
||||
rank_str = f'#{rank}'
|
||||
rank_color = '#2196F3'
|
||||
elif rank is not None:
|
||||
rank_str = f'#{rank}'
|
||||
rank_color = '#888888'
|
||||
else:
|
||||
rank_str = '—'
|
||||
rank_color = '#888888'
|
||||
|
||||
# 操作按钮
|
||||
_ICON = 22 # 图标大小,比默认 18 好按
|
||||
if t.enabled:
|
||||
@@ -385,6 +417,7 @@ class _GridPanel:
|
||||
f'{mp:.3f}', gcell,
|
||||
str(t.current_position),
|
||||
f'{self._data.avgPrices.get(tid, 0):.3f}',
|
||||
rank_str,
|
||||
'▶运行中' if t.enabled else '⏸已暂停',
|
||||
btns]
|
||||
row_cells = []
|
||||
@@ -392,6 +425,8 @@ class _GridPanel:
|
||||
content = cells_text[i]
|
||||
if i == 2: # 市场价用颜色
|
||||
content = _text(cells_text[i], color=pcolor, bold=True)
|
||||
elif i == 6: # 排名用颜色
|
||||
content = _text(cells_text[i], color=rank_color, bold=True)
|
||||
elif isinstance(content, str):
|
||||
content = _text(content)
|
||||
elif isinstance(content, list):
|
||||
@@ -495,8 +530,25 @@ class _DrawerPanel:
|
||||
|
||||
def _refresh_grid(self):
|
||||
# (width, expand): 0=固定宽, >0=弹性比重; 股票列 expand 自动填充剩余空间
|
||||
_C = [(35, 0), (0, 1), (80, 0), (60, 0), (70, 0), (65, 0)]
|
||||
H = ["ID", "股票", "市场价", "持仓", "成本", "操作"]
|
||||
# 新增: 排名列 (50px)
|
||||
_C = [(35, 0), (0, 1), (80, 0), (60, 0), (70, 0), (50, 0), (65, 0)]
|
||||
H = ["ID", "股票", "市场价", "持仓", "成本", "排名", "操作"]
|
||||
|
||||
# 获取最新评分排名
|
||||
rank_map = {} # stock_code -> score_rank
|
||||
try:
|
||||
from core.scoring.models import ScoringResult
|
||||
from peewee import fn
|
||||
latest_date = ScoringResult.select(fn.MAX(ScoringResult.trade_date)).scalar()
|
||||
if latest_date:
|
||||
rows = (ScoringResult
|
||||
.select(ScoringResult.stock_code, ScoringResult.score_rank)
|
||||
.where(ScoringResult.trade_date == latest_date)
|
||||
.dicts())
|
||||
for r in rows:
|
||||
rank_map[r['stock_code']] = r['score_rank']
|
||||
except Exception:
|
||||
pass # 数据库未就绪时忽略
|
||||
|
||||
def _cell(text, w, e, color=None):
|
||||
if e > 0:
|
||||
@@ -509,15 +561,26 @@ class _DrawerPanel:
|
||||
for tid, t in self._data.tradeTargets.items():
|
||||
if t.strategy_type == STRATEGY_TYPE_GRID: continue
|
||||
mp = self._data.marketPrices.get(tid, 0) or 0
|
||||
plain = t.stock_code.split('.')[0] if '.' in t.stock_code else t.stock_code
|
||||
rank = rank_map.get(plain, None)
|
||||
rank_str = f"#{rank}" if rank is not None else "—"
|
||||
# 排名颜色: top50 绿色, top100 蓝色, 其他灰色
|
||||
if rank is not None and rank <= 50:
|
||||
rank_color = '#4CAF50'
|
||||
elif rank is not None and rank <= 100:
|
||||
rank_color = '#2196F3'
|
||||
else:
|
||||
rank_color = '#888888'
|
||||
cells = [
|
||||
_cell(str(tid), *_C[0]),
|
||||
_cell(f'{t.stock_code} {t.stock_name}', *_C[1]),
|
||||
_cell(f'{mp:.3f}', *_C[2]),
|
||||
_cell(str(t.current_position), *_C[3]),
|
||||
_cell(f'{self._data.avgPrices.get(tid, 0):.3f}', *_C[4]),
|
||||
_cell(rank_str, *_C[5], color=rank_color),
|
||||
ft.Container(ft.IconButton(ft.Icons.SETTINGS, icon_size=20, tooltip="网格配置",
|
||||
on_click=lambda e, tt=t: self._dialogs.open_config(tt)),
|
||||
width=_C[5][0], padding=0),
|
||||
width=_C[6][0], padding=0),
|
||||
]
|
||||
row = ft.Row(cells, spacing=0)
|
||||
rows.append(ft.Container(row, padding=ft.Padding(0, 2, 0, 2)))
|
||||
|
||||
Reference in New Issue
Block a user