5 Commits

Author SHA1 Message Date
kyugao de83f97c05 update 2026-07-06 09:12:38 +08:00
kyugao fd5337f163 update 2026-06-24 17:22:24 +08:00
kyugao 5b1412eab0 update 2026-06-24 16:13:44 +08:00
kyugao 363efd6d2d update 2026-06-24 14:02:08 +08:00
kyugao 6494f43ddd 完成模型更新 2026-06-24 12:19:08 +08:00
36 changed files with 5108 additions and 3555 deletions
+1
View File
@@ -9,3 +9,4 @@ example.db.bak
venv/ venv/
flet_desktop/ flet_desktop/
.flet/ .flet/
sfgrid.log
+27
View File
@@ -443,6 +443,33 @@ class RealQmtV:
"""获取股票名称""" """获取股票名称"""
return self.cacheStockDetail(stock_code)['InstrumentName'] 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): def dailyUpStop(self, stock_code: str):
"""获取涨停价""" """获取涨停价"""
detail = self.cacheStockDetail(stock_code) detail = self.cacheStockDetail(stock_code)
+3 -3
View File
@@ -10,7 +10,7 @@ GRID_HIGH = 11 # 网格上限
GRID_STEP = 1 # 网格间距(整数格) GRID_STEP = 1 # 网格间距(整数格)
# ---- 候选股过滤 ---- # ---- 候选股过滤 ----
FILTER_MIN_CLOSE = 8 # 最低收盘价 FILTER_MIN_CLOSE = 5 # 最低收盘价(覆盖网格策略持仓股)
FILTER_MAX_CLOSE = 13 # 最高收盘价 FILTER_MAX_CLOSE = 13 # 最高收盘价
REQUIRE_DAYS = 120 # 最少交易日数 REQUIRE_DAYS = 120 # 最少交易日数
@@ -50,5 +50,5 @@ RANK_MODEL = 'rank'
TOP_MODEL = 'top' TOP_MODEL = 'top'
STACKING_MODEL = 'stacking' STACKING_MODEL = 'stacking'
# Stacking 选股阈值 # Stacking 选股阈值 (v6.7r3 最优阈值 0.331)
STACKING_THRESHOLD = 0.35 STACKING_THRESHOLD = 0.33
+25 -1
View File
@@ -1,9 +1,10 @@
""" """
扩展特征 v3.4 (16维) 扩展特征 v3.4 + v6.7新增 (20维: 16维 v3.4 + 4维 v6.7新增)
""" """
import numpy as np import numpy as np
import pandas as pd import pandas as pd
from core.scoring.features.v3_2_features import _ols_slope 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 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( feat['wick_ratio_20d'] = np.mean(
wick_len / np.where(total_len > 0, total_len, 1)) * 100 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 features[code] = feat
return pd.DataFrame.from_dict(features, orient='index') return pd.DataFrame.from_dict(features, orient='index')
+12
View File
@@ -128,6 +128,18 @@ def load_candidates(trade_date: date) -> DataContext:
PrintLog(LogLevel.INFO, PrintLog(LogLevel.INFO,
f'[validator] 候选: {len(candidates)} 通过, {len(excluded)} 排除') f'[validator] 候选: {len(candidates)} 通过, {len(excluded)} 排除')
# 3.5 补充:强制加入网格持仓股(不受价格过滤限制)
from core.sfgrid.model import SFGridTradeTarget
pos_rows = list(SFGridTradeTarget
.select(SFGridTradeTarget.stock_code)
.where(SFGridTradeTarget.enabled == True)
.dicts())
pos_codes = [r['stock_code'].split('.')[0] for r in pos_rows]
forced = [c for c in pos_codes if c not in candidates and c not in excluded]
if forced:
PrintLog(LogLevel.INFO, f'[validator] 强制加入网格持仓股: {forced}')
candidates.extend(forced)
# 4. 裁剪K线到只含候选股 (保留最近180日) # 4. 裁剪K线到只含候选股 (保留最近180日)
kline_df = kline_df[kline_df['stock_code'].isin(candidates)].copy() kline_df = kline_df[kline_df['stock_code'].isin(candidates)].copy()
cut_date = trade_date - timedelta(days=365) cut_date = trade_date - timedelta(days=365)
+19 -10
View File
@@ -1,5 +1,5 @@
""" """
grid_seeker v6.6 三级模型推理管道 grid_seeker v6.7r3 三级模型推理管道
Rank → Top → Stacking → stacking_probability (最终排序) Rank → Top → Stacking → stacking_probability (最终排序)
""" """
import pickle 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: def _get_rank_features() -> list:
import pickle import pickle
@@ -27,17 +27,26 @@ def _get_rank_features() -> list:
with open(path, 'rb') as f: with open(path, 'rb') as f:
obj = pickle.load(f) obj = pickle.load(f)
if isinstance(obj, dict): 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: if sf:
return sf return sf
raise RuntimeError("无法从 rank.pkl 读取 selected_features") raise RuntimeError("无法从 rank.pkl 读取 feat_names 或 selected_features")
RANK_FEATURE_COLS = _get_rank_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: class GridSeekerPipeline:
""" """
grid_seeker v6.6 三级模型评分管道。 grid_seeker v6.7r3 三级模型评分管道。
Usage: Usage:
engine = GridSeekerPipeline() engine = GridSeekerPipeline()
@@ -120,7 +129,7 @@ class GridSeekerPipeline:
DataFrame indexed by stock_code, 含 stacking_probability / rank 等列, DataFrame indexed by stock_code, 含 stacking_probability / rank 等列,
按 stacking_probability 降序排列 按 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. 特征工程 # 1. 特征工程
pipeline = FeaturePipeline(trade_date) pipeline = FeaturePipeline(trade_date)
@@ -140,16 +149,16 @@ class GridSeekerPipeline:
self.rank_model, feature_df, RANK_FEATURE_COLS 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 模型...') 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( feature_df['top_elite_prob'] = self._predict_with_model(
self.top_model, feature_df, top_cols 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 模型...') 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( feature_df['stacking_probability'] = self._predict_with_model(
self.stacking_model, feature_df, stk_cols self.stacking_model, feature_df, stk_cols
) )
+15 -1
View File
@@ -121,12 +121,26 @@ class ScoringResult(BaseModel):
primary_key = CompositeKey('stock_code', 'trade_date') primary_key = CompositeKey('stock_code', 'trade_date')
# ============================================================
# 8. PendingPoolAction — 待执行股票池操作(阶段一标记,阶段二执行)
# ============================================================
class PendingPoolAction(BaseModel):
"""pending_pool_actions 表 — T日标记的操作,待 T+1 执行"""
action_date = DateField() # T日日期(标记日期)
action_type = CharField(max_length=20) # 'eliminate' | 'liquidate'
stock_code = CharField(max_length=6) # 股票代码
reason = TextField(null=True) # 淘汰原因描述
class Meta:
primary_key = CompositeKey('action_date', 'action_type', 'stock_code')
# ============================================================ # ============================================================
# 建表 # 建表
# ============================================================ # ============================================================
ALL_SCORING_TABLES = [ ALL_SCORING_TABLES = [
KlineStock, StockInfo, IndustryMapping, KlineIndex, KlineStock, StockInfo, IndustryMapping, KlineIndex,
MarketRegimeDaily, SectorFeaturesDaily, ScoringResult, MarketRegimeDaily, SectorFeaturesDaily, ScoringResult, PendingPoolAction,
] ]
db.create_tables(ALL_SCORING_TABLES) db.create_tables(ALL_SCORING_TABLES)
+54 -26
View File
@@ -5,7 +5,7 @@ K线数据同步 — 个股日K + 指数日K
线程锁: KlineStockSync / KlineIndexSync 各自内部锁 线程锁: KlineStockSync / KlineIndexSync 各自内部锁
""" """
import pandas as pd 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.sync.base import BaseSync
from core.scoring.models import KlineStock, KlineIndex from core.scoring.models import KlineStock, KlineIndex
from core.scoring.config import TRACKED_INDICES from core.scoring.config import TRACKED_INDICES
@@ -58,19 +58,18 @@ class KlineStockSync(BaseSync):
def _fetch(self, **kwargs): def _fetch(self, **kwargs):
from xtquant import xtdata from xtquant import xtdata
# 增量判断 # 增量判断: 以数据库最新一条记录为准
latest = _latest_date(KlineStock) latest = _latest_date(KlineStock)
today = date.today() 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 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 "" start_str = start_date.strftime('%Y%m%d') if start_date else ""
end_str = end_date.strftime('%Y%m%d')
PrintLog(LogLevel.INFO, 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股") all_stocks = xtdata.get_stock_list_in_sector("沪深A股")
PrintLog(LogLevel.INFO, f'[sync] KlineStock: {len(all_stocks)} 只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}%)') PrintLog(LogLevel.INFO, f'[sync] KlineStock: {i}/{total} ({i*100//total}%)')
try: 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: except Exception:
self.stats['errors'] += 1 self.stats['errors'] += 1
continue continue
@@ -91,8 +90,9 @@ class KlineStockSync(BaseSync):
try: try:
result = xtdata.get_market_data( result = xtdata.get_market_data(
field_list=field_list, stock_list=[code], period='1d', field_list=field_list, stock_list=[code], period='1d',
count=self.count, dividend_type='none', fill_data=False) start_time=start_str, end_time=end_str,
inserted += self._upsert_incremental(code, result, start_date) dividend_type='none', fill_data=False)
inserted += self._upsert_incremental(code, result, start_date, end_date)
except Exception: except Exception:
self.stats['errors'] += 1 self.stats['errors'] += 1
@@ -102,7 +102,7 @@ class KlineStockSync(BaseSync):
f'跳过={self.stats["skipped"]} 错误={self.stats["errors"]}') f'跳过={self.stats["skipped"]} 错误={self.stats["errors"]}')
return self.stats 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: if not result:
return 0 return 0
close_df = result.get('close') close_df = result.get('close')
@@ -110,9 +110,23 @@ class KlineStockSync(BaseSync):
return 0 return 0
stock_code = full_code.split('.')[0] stock_code = full_code.split('.')[0]
records = [] records = []
vol_df = result.get('volume')
for td in close_df.columns: for td in close_df.columns:
td_date = td.date() if hasattr(td, 'date') else td # xtdata 返回的列名可能是字符串 'YYYYMMDD' 或 datetime,需统一转成 date
if start_date is not None and td_date <= start_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
# 过滤: 不在增量范围内的跳过 (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 self.stats['skipped'] = self.stats.get('skipped', 0) + 1
continue continue
close_val = close_df.loc[full_code, td] close_val = close_df.loc[full_code, td]
@@ -126,7 +140,7 @@ class KlineStockSync(BaseSync):
'high': _safe_get(result.get('high'), full_code, td), 'high': _safe_get(result.get('high'), full_code, td),
'low': _safe_get(result.get('low'), full_code, td), 'low': _safe_get(result.get('low'), full_code, td),
'close': float(close_val), 'close': float(close_val),
'volume': _safe_get(result.get('volume'), full_code, td), 'volume': float(vol),
}) })
if records: if records:
with db.atomic(): with db.atomic():
@@ -161,40 +175,54 @@ class KlineIndexSync(BaseSync):
latest = _latest_date(KlineIndex) latest = _latest_date(KlineIndex)
today = date.today() 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 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 "" start_str = start_date.strftime('%Y%m%d') if start_date else ""
end_str = end_date.strftime('%Y%m%d')
PrintLog(LogLevel.INFO, PrintLog(LogLevel.INFO,
f'[sync] KlineIndex: 增量同步,起点={start_str or "全部"}') f'[sync] KlineIndex: 增量同步 {start_str} ~ {end_str}')
for code in index_codes: for code in index_codes:
try: 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: except Exception:
self.stats['errors'] += 1 self.stats['errors'] += 1
field_list = ['open', 'high', 'low', 'close', 'volume'] field_list = ['open', 'high', 'low', 'close', 'volume']
result = xtdata.get_market_data( result = xtdata.get_market_data(
field_list=field_list, stock_list=index_codes, period='1d', field_list=field_list, stock_list=index_codes, period='1d',
count=self.count, dividend_type='none', fill_data=False) start_time=start_str, end_time=end_str,
return result or {} dividend_type='none', fill_data=False)
def _upsert(self, data): def _upsert(self, data):
if not data: if not data:
return return
latest = _latest_date(KlineIndex) latest = _latest_date(KlineIndex)
today = date.today()
start_date = (latest + timedelta(days=1)) if latest else None
end_date = today - timedelta(days=1)
records = [] records = []
close_df = data.get('close') close_df = data.get('close')
vol_df = data.get('volume')
if close_df is None or close_df.empty: if close_df is None or close_df.empty:
return return
for full_code in close_df.index: for full_code in close_df.index:
index_code = full_code.split('.')[0] index_code = full_code.split('.')[0]
for td in close_df.columns: for td in close_df.columns:
td_date = td.date() if hasattr(td, 'date') else td if isinstance(td, str):
if latest is not None and td_date <= latest: td_date = datetime.strptime(td, '%Y%m%d').date()
else:
td_date = td.date() if hasattr(td, 'date') else td
# 增量范围过滤 (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 self.stats['skipped'] = self.stats.get('skipped', 0) + 1
continue continue
close_val = close_df.loc[full_code, td] close_val = close_df.loc[full_code, td]
@@ -208,7 +236,7 @@ class KlineIndexSync(BaseSync):
'high': _safe_get(data.get('high'), full_code, td), 'high': _safe_get(data.get('high'), full_code, td),
'low': _safe_get(data.get('low'), full_code, td), 'low': _safe_get(data.get('low'), full_code, td),
'close': float(close_val), 'close': float(close_val),
'volume': _safe_get(data.get('volume'), full_code, td), 'volume': float(vol),
}) })
if records: if records:
with db.atomic(): with db.atomic():
+73 -41
View File
@@ -13,7 +13,7 @@ class SectorFeaturesSync(BaseSync):
"""行业聚合指数同步 — kline_stock + industry → sector_features_daily""" """行业聚合指数同步 — kline_stock + industry → sector_features_daily"""
def _fetch(self, **kwargs): def _fetch(self, **kwargs):
"""从数据库加载原始数据, 计算行业指数特征""" """从数据库加载原始数据, 计算行业指数特征(分块处理避免内存溢出)"""
PrintLog(LogLevel.INFO, '[sync] SectorFeatures: 加载原始数据...') PrintLog(LogLevel.INFO, '[sync] SectorFeatures: 加载原始数据...')
# 1. 加载行业映射: code → industry_name # 1. 加载行业映射: code → industry_name
@@ -24,49 +24,81 @@ class SectorFeaturesSync(BaseSync):
code_to_industry = {row['code']: row['industry_name'] for row in industries} code_to_industry = {row['code']: row['industry_name'] for row in industries}
PrintLog(LogLevel.INFO, f'[sync] SectorFeatures: {len(code_to_industry)} 条行业映射') PrintLog(LogLevel.INFO, f'[sync] SectorFeatures: {len(code_to_industry)} 条行业映射')
# 2. 加载 K 线数据 # 2. 分块加载 K 线数据,避免内存溢出
kline_rows = (KlineStock # 聚合结果: {(trade_date, sector_name): [sum_pct_chg, sum_amp, count]}
.select( sector_daily_agg = {} # key: (date, sector) -> {'ret_sum': float, 'amp_sum': float, 'count': int}
KlineStock.stock_code, CHUNK_SIZE = 50000
KlineStock.trade_date, last_date_per_stock = {} # stock_code -> prev_close
KlineStock.open,
KlineStock.high,
KlineStock.low,
KlineStock.close,
)
.order_by(KlineStock.stock_code, KlineStock.trade_date)
.dicts())
if not kline_rows: PrintLog(LogLevel.INFO, '[sync] SectorFeatures: 分块处理K线数据...')
PrintLog(LogLevel.WARNING, '[sync] SectorFeatures: KlineStock 表为空') 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 return None
df = pd.DataFrame(kline_rows) PrintLog(LogLevel.INFO, f'[sync] SectorFeatures: 聚合完成, {len(sector_daily_agg)} 个行业-日组合')
df['trade_date'] = pd.to_datetime(df['trade_date'])
PrintLog(LogLevel.INFO, f'[sync] SectorFeatures: {len(df)} 条K线数据')
# 3. 映射行业 # 3. 构建聚合 DataFrame
df['sector_name'] = df['stock_code'].map(code_to_industry) agg_data = []
df = df.dropna(subset=['sector_name']) 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. 逐股计算日收益率和振幅 # 4. 构建行业指数 (基值=100)
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)
agg = agg.sort_values(['sector_name', 'trade_date']) agg = agg.sort_values(['sector_name', 'trade_date'])
agg['sector_index'] = agg.groupby('sector_name')['sector_ret'].transform( agg['sector_index'] = agg.groupby('sector_name')['sector_ret'].transform(
lambda x: (1 + x / 100).cumprod() * 100 lambda x: (1 + x / 100).cumprod() * 100
@@ -77,7 +109,7 @@ class SectorFeaturesSync(BaseSync):
first_val = group['sector_index'].iloc[0] first_val = group['sector_index'].iloc[0]
agg.loc[idx, 'sector_index'] = group['sector_index'] / first_val * 100 agg.loc[idx, 'sector_index'] = group['sector_index'] / first_val * 100
# 7. 计算 EMA 均线 # 5. 计算 EMA 均线
agg['ema10'] = (agg.groupby('sector_name')['sector_index'] agg['ema10'] = (agg.groupby('sector_name')['sector_index']
.transform(lambda x: x.ewm(span=10, min_periods=1).mean())) .transform(lambda x: x.ewm(span=10, min_periods=1).mean()))
agg['ema20'] = (agg.groupby('sector_name')['sector_index'] agg['ema20'] = (agg.groupby('sector_name')['sector_index']
@@ -85,7 +117,7 @@ class SectorFeaturesSync(BaseSync):
agg['ema200'] = (agg.groupby('sector_name')['sector_index'] agg['ema200'] = (agg.groupby('sector_name')['sector_index']
.transform(lambda x: x.ewm(span=200, min_periods=1).mean())) .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['score'] = (
(agg['sector_index'] > agg['ema200']).astype(int) + (agg['sector_index'] > agg['ema200']).astype(int) +
(agg['ema10'] > agg['ema20']).astype(int) (agg['ema10'] > agg['ema20']).astype(int)
+8
View File
@@ -5,3 +5,11 @@ EventTradeTargetDeleted = "trade_target_deleted"
# 评分系统事件 # 评分系统事件
EventScoringCompleted = "scoring_completed" # 评分完成, data: {'date', 'count'} EventScoringCompleted = "scoring_completed" # 评分完成, data: {'date', 'count'}
EventSyncProgress = "sync_progress" # 同步进度, data: {'source', 'status', 'stats'} EventSyncProgress = "sync_progress" # 同步进度, data: {'source', 'status', 'stats'}
# 股票池管理器事件(阶段一)
# T日收盘标记完成, data: {action: 'eliminate'|'liquidate', stock_codes: list[str], count: int}
EventPoolMark = "pool_mark"
# T+1日执行完成(阶段二用)
# data: {action: 'eliminate'|'liquidate'|'refill', stock_codes: list[str], count: int}
EventPoolActionExecute = "pool_action_execute"
+486
View File
@@ -0,0 +1,486 @@
"""
pool_manager.py — 网格自动交易股票池管理器(阶段一:T日标记)
===================================================================
后台 daemon 线程运行,每日定时:
09:25 K线数据同步
09:30 执行评分
15:30 沉寂检测标记
周五15:30 额外执行周度淘汰标记
所有操作只记录到数据库,不执行真实交易(阶段二实现)。
"""
from __future__ import annotations
import threading
import time
from collections import defaultdict
from datetime import date, datetime, timedelta
import numpy as np
import pandas as pd
from core.logger import LogLevel, PrintLog
from core.scoring.models import PendingPoolAction, ScoringResult
from core.sfgrid.model import SFGridTradeTarget
from core.sfgrid.bus_events import EventPoolMark
from core.eventbus import event_bus
# ============================================================
# 配置
# ============================================================
TOP_N = 10 # 最大持仓数
TOP_MODEL_N = 50 # 评分池 Top N
ELIM_WINDOW = 2 # 连续 N 周不在 Top50 则淘汰
SLUMBER_DAYS = 10 # 沉寂触发天数(连续)
SLUMBER_TRIGGERS = 3 # 沉寂触发特征数(5个中触发几个)
# ============================================================
# 沉寂检测 — 直接复用 ref_backtest_strategy.py 的纯函数
# ============================================================
def is_slumbering(df: pd.DataFrame,
lookback_60: int = 60,
lookback_20: int = 20,
min_triggers: int = SLUMBER_TRIGGERS) -> bool:
"""
检测一只股是否陷入'沉寂'(资金离场后长期低位震荡)。
5 特征,>= min_triggers 触发则返回 True。
df 要求:包含 close/high/low/volume 列,index 为日期升序,
至少 60 条记录。
"""
if df is None or len(df) < lookback_60:
return False
# 过滤停牌日期(volume=0 的行会导致 log_ret = NaN
active = df[df["volume"] > 0]
if len(active) < lookback_60:
return False
sub = active.tail(lookback_60)
close = sub["close"].values
high = sub["high"].values
low = sub["low"].values
vol = sub["volume"].values
# 1. 波动率塌陷
log_ret = np.log(close[1:] / close[:-1])
if len(log_ret) < lookback_20:
return False
vol_20d = float(np.std(log_ret[-lookback_20:], ddof=1))
vol_60d = float(np.std(log_ret, ddof=1))
vol_collapse = (vol_60d > 0) and (vol_20d / vol_60d < 0.6)
# 2. 振幅萎缩
amp_20d = float(np.mean((high[-lookback_20:] - low[-lookback_20:]) / close[-lookback_20:]) * 100)
amp_shrink = amp_20d < 2.5
# 3. 成交量枯竭
avg_vol_20 = float(np.mean(vol[-lookback_20:]))
avg_vol_60 = float(np.mean(vol))
vol_dry = (avg_vol_60 > 0) and (avg_vol_20 / avg_vol_60 < 0.5)
# 4. 价格弱势
price_max_60 = float(np.max(close))
price_weak = price_max_60 > 0 and (close[-1] / price_max_60) < 0.85
# 5. 反弹失败
recent_high_30 = float(np.max(high[-30:]))
past_high_60 = float(np.max(high))
rebound_fail = past_high_60 > 0 and (recent_high_30 / past_high_60) < 0.95
triggers = [vol_collapse, amp_shrink, vol_dry, price_weak, rebound_fail]
return sum(triggers) >= min_triggers
# ============================================================
# 工具函数
# ============================================================
def is_trading_day(td: date) -> bool:
"""简单判断是否为交易日(周一~周五)"""
return td.weekday() < 5 # 0=周一, 4=周五
def get_week_id(td: date) -> int:
"""返回年内周序号(周一为起始)"""
return td.isocalendar()[1]
def seconds_to_target(target_hour: int, target_minute: int) -> float:
"""计算从现在到目标时间(当天 target_hour:target_minute)的秒数。"""
now = datetime.now()
today_target = datetime(now.year, now.month, now.day, target_hour, target_minute, 0)
if now >= today_target:
# 今天已过,推到明天
today_target += timedelta(days=1)
return (today_target - now).total_seconds()
# ============================================================
# 股票池管理器
# ============================================================
class PoolManager:
"""
网格股票池自动管理器(阶段一:T日标记)
使用 threading.Timer 递归调度,实现每日 09:25 / 09:30 / 15:30 定时任务。
所有操作只写入数据库,不执行真实交易。
"""
def __init__(self):
self._thread: threading.Thread | None = None
self._stop_event = threading.Event()
# 周度淘汰历史:key=股票代码,value=[(week_id, rank), ...]
# rank=0 表示在 Top50rank=-1 表示不在 Top50
self._top50_history: dict[str, list[tuple[int, int]]] = defaultdict(list)
# 调试:手动触发时传入自定义日期(仅供测试用)
self._override_date: date | None = None
# 加载历史排名数据(从 ScoringResult 重建)
self._rebuild_top50_history()
# ---- 对外控制接口 ----
def start(self):
"""启动后台管理线程(幂等)"""
if self._thread is not None and self._thread.is_alive():
PrintLog(LogLevel.WARNING, '[PoolManager] 已启动,忽略重复调用')
return
self._stop_event.clear()
self._thread = threading.Thread(target=self._run_loop, daemon=True, name='PoolManager')
self._thread.start()
PrintLog(LogLevel.INFO, '[PoolManager] 已启动')
def stop(self):
"""停止后台管理线程"""
self._stop_event.set()
if self._thread is not None:
self._thread.join(timeout=5)
self._thread = None
PrintLog(LogLevel.INFO, '[PoolManager] 已停止')
# ---- 每日定时任务 ----
def _run_loop(self):
"""后台线程主循环:计算出距下次任务的时间,注册下一个 Timer"""
while not self._stop_event.is_set():
now = datetime.now()
td = self._override_date or now.date()
# 确定当天要执行的任务及距其的秒数
delay, task_name = self._compute_next_delay(now, td)
# 保证 Timer 不会太久(最多 24 小时),处理节假日顺延
if delay <= 0:
delay = 60 # 异常时等 1 分钟重算
PrintLog(LogLevel.INFO,
f'[PoolManager] 计划任务 "{task_name}"{delay:.0f} 秒后执行')
timer = threading.Timer(delay, self._execute_task, args=(task_name, td))
timer.name = f'PoolManager-{task_name}'
timer.start()
# 等待定时器完成或停止信号
timer.join()
if self._stop_event.is_set():
break
def _compute_next_delay(self, now: datetime, td: date) -> tuple[float, str]:
"""
计算距下一个任务的时间和任务名称。
任务顺序:09:25 → 09:30 → 15:30 → (下一天 09:25)
"""
h, m = now.hour, now.minute
if h < 9 or (h == 9 and m < 25):
# 现在在 09:25 之前 → 先执行 09:25
return seconds_to_target(9, 25), '_sync_data'
elif h == 9 and 25 <= m < 30:
# 09:25~09:30 之间 → 立即执行 09:30
return 0.0, '_run_scoring'
elif (h == 9 and m >= 30) or h < 15:
# 09:30 之后、15:30 之前 → 执行 15:30
return seconds_to_target(15, 30), '_mark_slumber'
elif h >= 15:
# 15:30 之后 → 推到下一天 09:25
delay = seconds_to_target(9, 25) + (td.weekday() < 4 and 1 or 3) * 86400 # 工作日+1,周末+3
return delay, '_sync_data'
# 默认兜底
return seconds_to_target(9, 25), '_sync_data'
def _execute_task(self, task_name: str, td: date):
"""根据任务名执行对应任务"""
try:
if task_name == '_sync_data':
self._sync_data()
# 同步完成后自动调度评分
self._run_scoring()
# 调度 15:30
self._schedule_next(target_hour=15, target_minute=30,
task_name='_mark_slumber', td=td)
elif task_name == '_run_scoring':
self._run_scoring()
# 评分完成后调度沉寂检测
self._schedule_next(target_hour=15, target_minute=30,
task_name='_mark_slumber', td=td)
elif task_name == '_mark_slumber':
self._mark_slumber(td)
# 如果是周五,额外调度淘汰标记
if td.weekday() == 4: # 周五
self._mark_weekly_elim(td)
# 调度下一天 09:25
self._schedule_next(target_hour=9, target_minute=25,
task_name='_sync_data', td=td)
except Exception as e:
PrintLog(LogLevel.ERROR, f'[PoolManager] 任务 {task_name} 执行异常: {e}')
def _schedule_next(self, target_hour: int, target_minute: int,
task_name: str, td: date):
"""注册一个 Timer,在指定时间执行 task_name"""
# 计算 delay
delay = seconds_to_target(target_hour, target_minute)
# 如果 target 在过去(如周末顺延),加 1 天
if delay <= 0:
delay += 86400
def wrapper():
now = datetime.now()
# 重新计算真实日期
exec_td = self._override_date or now.date()
self._execute_task(task_name, exec_td)
timer = threading.Timer(delay, wrapper, name=f'PoolManager-sched-{task_name}')
timer.start()
# ---- 任务实现 ----
def _sync_data(self):
"""09:25 — 同步 K 线数据"""
PrintLog(LogLevel.INFO, '[PoolManager] 开始同步K线数据...')
try:
from core.scoring.sync.kline_sync import KlineStockSync
syncer = KlineStockSync()
syncer.run()
PrintLog(LogLevel.INFO, '[PoolManager] K线数据同步完成')
except Exception as e:
PrintLog(LogLevel.ERROR, f'[PoolManager] K线同步失败: {e}')
def _run_scoring(self):
"""09:30 — 执行评分"""
PrintLog(LogLevel.INFO, '[PoolManager] 开始执行评分...')
try:
from core.scoring.inference.scorer import GridSeekerPipeline
pipeline = GridSeekerPipeline()
td = self._override_date or date.today()
result = pipeline.run(trade_date=td)
if not result.empty:
pipeline.persist(result, trade_date=td)
PrintLog(LogLevel.INFO, f'[PoolManager] 评分完成,写入 {len(result)} 条结果')
# 触发 UI 刷新
event_bus.publish(EventPoolMark, {
'action': 'scoring',
'stock_codes': [],
'count': len(result),
})
else:
PrintLog(LogLevel.WARNING, '[PoolManager] 评分无结果')
except Exception as e:
PrintLog(LogLevel.ERROR, f'[PoolManager] 评分失败: {e}')
def _mark_slumber(self, td: date):
"""15:30 — 沉寂检测,标记待卖出股"""
if not is_trading_day(td):
PrintLog(LogLevel.INFO, '[PoolManager] 非交易日,跳过沉寂检测')
return
PrintLog(LogLevel.INFO, '[PoolManager] 开始沉寂检测...')
# 获取所有 enabled=True 的持仓
targets = SFGridTradeTarget.select().where(SFGridTradeTarget.enabled == True)
if not targets:
PrintLog(LogLevel.INFO, '[PoolManager] 无持仓,跳过沉寂检测')
return
marked: list[str] = []
today_str = td.strftime('%Y-%m-%d')
for tgt in targets:
try:
# 读取该股 K 线数据(从本地 SQLite KlineStock 表)
from core.scoring.models import KlineStock
klines = list(KlineStock
.select()
.where(KlineStock.stock_code == tgt.stock_code)
.order_by(KlineStock.trade_date)
.dicts())
if not klines or len(klines) < 60:
continue
df = pd.DataFrame(klines)
if 'trade_date' in df.columns and not pd.api.types.is_datetime64_any_dtype(df['trade_date']):
df['trade_date'] = pd.to_datetime(df['trade_date'])
df = df.set_index('trade_date').sort_index()
if is_slumbering(df):
self._write_pending(td, 'liquidate', tgt.stock_code,
reason=f'沉寂检测触发({today_str}')
marked.append(tgt.stock_code)
PrintLog(LogLevel.INFO,
f'[PoolManager] 沉寂标记: {tgt.stock_code}')
except Exception as e:
PrintLog(LogLevel.WARNING,
f'[PoolManager] 沉寂检测异常 {tgt.stock_code}: {e}')
if marked:
event_bus.publish(EventPoolMark, {
'action': 'liquidate',
'stock_codes': marked,
'count': len(marked),
})
PrintLog(LogLevel.INFO,
f'[PoolManager] 沉寂检测完成,标记 {len(marked)} 只股')
else:
PrintLog(LogLevel.INFO, '[PoolManager] 沉寂检测完成,无股触发')
def _mark_weekly_elim(self, td: date):
"""周五 15:30 — 周度评分淘汰标记(连续2周不在Top50则卖出)"""
if not is_trading_day(td):
return
PrintLog(LogLevel.INFO, '[PoolManager] 开始周度淘汰检测...')
# 获取最近 N 周的评分排名
week_id = get_week_id(td)
today_str = td.strftime('%Y-%m-%d')
# 查今日 Top50
top50_codes: Set[str] = set()
rows = (ScoringResult
.select(ScoringResult.stock_code)
.where(ScoringResult.trade_date == td)
.where(ScoringResult.score_rank <= TOP_MODEL_N)
.dicts())
for r in rows:
top50_codes.add(r['stock_code'])
if not top50_codes:
PrintLog(LogLevel.WARNING, '[PoolManager] 今日无评分数据,无法进行淘汰检测')
return
# 更新历史排名
targets = SFGridTradeTarget.select().where(SFGridTradeTarget.enabled == True)
for tgt in targets:
code = tgt.stock_code
rank_in_top50 = 0 if code in top50_codes else -1
self._top50_history[code].append((week_id, rank_in_top50))
# 只保留近 8 周记录,防止内存膨胀
if len(self._top50_history[code]) > 8:
self._top50_history[code] = self._top50_history[code][-8:]
# 检测连续 N 周不在 Top50 的股
marked: list[str] = []
for tgt in targets:
code = tgt.stock_code
hist = self._top50_history.get(code, [])
if len(hist) < ELIM_WINDOW:
continue
recent = hist[-ELIM_WINDOW:]
if all(rank == -1 for _, rank in recent):
self._write_pending(td, 'eliminate', code,
reason=f'连续{ELIM_WINDOW}周不在Top50{today_str}')
marked.append(code)
PrintLog(LogLevel.INFO,
f'[PoolManager] 淘汰标记: {code},历史: {recent}')
if marked:
event_bus.publish(EventPoolMark, {
'action': 'eliminate',
'stock_codes': marked,
'count': len(marked),
})
PrintLog(LogLevel.INFO,
f'[PoolManager] 周度淘汰检测完成,标记 {len(marked)} 只股')
else:
PrintLog(LogLevel.INFO, '[PoolManager] 周度淘汰检测完成,无股触发')
def _write_pending(self, td: date, action_type: str, stock_code: str, reason: str = ''):
"""写入 pending_pool_actions 表(幂等)"""
try:
PendingPoolAction.insert(
action_date=td,
action_type=action_type,
stock_code=stock_code,
reason=reason,
).on_conflict_replace().execute()
except Exception as e:
PrintLog(LogLevel.WARNING,
f'[PoolManager] 写入待处理操作失败: {e}')
def _rebuild_top50_history(self):
"""启动时从 ScoringResult 表重建 _top50_history(用于淘汰判断)"""
try:
rows = (ScoringResult
.select(ScoringResult.stock_code, ScoringResult.trade_date,
ScoringResult.score_rank)
.where(ScoringResult.score_rank <= TOP_MODEL_N)
.order_by(ScoringResult.trade_date)
.dicts())
week_groups: dict = defaultdict(list)
for r in rows:
td = r['trade_date']
if isinstance(td, str):
td = datetime.strptime(td, '%Y-%m-%d').date()
week_id = get_week_id(td)
week_groups[(r['stock_code'], week_id)].append(r['score_rank'])
# 取每周最新一条(排名最靠前的)
for (code, week_id), ranks in week_groups.items():
best_rank = min(ranks)
rank_val = 0 if best_rank <= TOP_MODEL_N else -1
self._top50_history[code].append((week_id, rank_val))
# 去重,每 week_id 只留一条
for code in self._top50_history:
seen = set()
cleaned = []
for w, r in self._top50_history[code]:
if w not in seen:
seen.add(w)
cleaned.append((w, r))
self._top50_history[code] = cleaned
PrintLog(LogLevel.INFO,
f'[PoolManager] 历史排名已重建,{len(self._top50_history)} 只股有历史数据')
except Exception as e:
PrintLog(LogLevel.ERROR, f'[PoolManager] 重建历史排名失败: {e}')
# ---- 手动触发(供 UI 调试按钮调用)----
def trigger_sync(self):
"""手动触发数据同步"""
threading.Thread(target=self._sync_data, daemon=True).start()
def trigger_scoring(self):
"""手动触发评分"""
threading.Thread(target=self._run_scoring, daemon=True).start()
def trigger_slumber(self, td: date | None = None):
"""手动触发沉寂检测"""
td = td or (self._override_date or date.today())
threading.Thread(target=self._mark_slumber, args=(td,), daemon=True).start()
def trigger_weekly_elim(self, td: date | None = None):
"""手动触发周度淘汰检测"""
td = td or (self._override_date or date.today())
threading.Thread(target=self._mark_weekly_elim, args=(td,), daemon=True).start()
+37 -7
View File
@@ -54,10 +54,17 @@ class SFGridStrategy:
""" """
self.tradeTarget: model.SFGridTradeTarget = tradeTarget self.tradeTarget: model.SFGridTradeTarget = tradeTarget
# orderGrid 必须在所有可能触发回调的操作之前初始化
# orderGrid: 网格索引 → 订单编号(seq 或 order_id)的映射
# seq 是 xtquant 返回的下单序号(下单瞬间),order_id 是交易所返回的正式订单号(异步回调后更新)
self.orderGrid = {} # {grid_index: order_seq | order_id}
# 数据更新锁:保护 orderGrid 和 tradeTarget 的并发访问 # 数据更新锁:保护 orderGrid 和 tradeTarget 的并发访问
# QMT 回调在独立线程中触发,必须在可能触发回调的操作之前创建 # QMT 回调在独立线程中触发,必须在可能触发回调的操作之前创建
# 注意:这个锁必须在订阅事件之前创建,防止事件在初始化期间触发 # 注意:这个锁必须在订阅事件之前创建,防止事件在初始化期间触发
self.dataUpdateLock = threading.Lock() # 注意:必须使用 RLock 而非 Lock,因为 refreshGridOrder 在持有此锁时也会被调用
#(如 onOrderTrade 回调中),Lock 会导致同一线程重复获取时永久阻塞(死锁)
self.dataUpdateLock = threading.RLock()
# 订阅事件总线:监听订单创建、成交、失败三种事件 # 订阅事件总线:监听订单创建、成交、失败三种事件
event_bus.subscribe(eBus.MarketOrderCreated, self.onOrderCreateAsync) event_bus.subscribe(eBus.MarketOrderCreated, self.onOrderCreateAsync)
@@ -73,10 +80,6 @@ class SFGridStrategy:
f'|- [DEBUG] 标的{tradeTarget.targetName()} 构造开始: ' f'|- [DEBUG] 标的{tradeTarget.targetName()} 构造开始: '
f'网格={tradeTarget.grid_index}, 启用={tradeTarget.enabled}') f'网格={tradeTarget.grid_index}, 启用={tradeTarget.enabled}')
# orderGrid: 网格索引 → 订单编号(seq 或 order_id)的映射
# seq 是 xtquant 返回的下单序号(下单瞬间),order_id 是交易所返回的正式订单号(异步回调后更新)
self.orderGrid = {} # {grid_index: order_seq | order_id}
# 加载券商侧已存在的未成交订单,恢复到 orderGrid 中 # 加载券商侧已存在的未成交订单,恢复到 orderGrid 中
self.loadExistOrders() self.loadExistOrders()
@@ -171,8 +174,18 @@ class SFGridStrategy:
sell_remark = self._make_remark(OrderTypeSell, sellIdx) sell_remark = self._make_remark(OrderTypeSell, sellIdx)
# 检查是否已存在同 remark 的卖单(避免重复挂单) # 检查是否已存在同 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:
self.todayUpStopPrice = qmtv.dailyUpStop(self.tradeTarget.stock_code) # type: ignore
if sellPrice > self.todayUpStopPrice: if sellPrice > self.todayUpStopPrice:
PrintLog(LogLevel.INFO, PrintLog(LogLevel.INFO,
f'|- 标的[{self.tradeTarget.targetName()}] ' f'|- 标的[{self.tradeTarget.targetName()}] '
@@ -205,8 +218,14 @@ class SFGridStrategy:
buy_remark = self._make_remark(OrderTypeBuy, buyIdx) buy_remark = self._make_remark(OrderTypeBuy, buyIdx)
# 检查是否已存在同 remark 的买单(避免重复挂单) # 检查是否已存在同 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:
self.todayDownStopPrice = qmtv.dailyDownStop(self.tradeTarget.stock_code) # type: ignore
if buyPrice < self.todayDownStopPrice: if buyPrice < self.todayDownStopPrice:
PrintLog(LogLevel.INFO, PrintLog(LogLevel.INFO,
f'|- 标的[{self.tradeTarget.targetName()}] ' f'|- 标的[{self.tradeTarget.targetName()}] '
@@ -428,6 +447,17 @@ class SFGridStrategy:
if self.tradeTarget.grid_index == 0: if self.tradeTarget.grid_index == 0:
self.tradeTarget.init_price = trade.traded_price # type: ignore 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 当前网格索引,判断价格移动方向 # 比较成交单的网格索引 vs 当前网格索引,判断价格移动方向
oriIdx = self.tradeTarget.grid_index # 成交前的网格位置 oriIdx = self.tradeTarget.grid_index # 成交前的网格位置
-836
View File
@@ -1,836 +0,0 @@
"""
Flet UI — 完整对齐 Tkinter 版布局、数据流、刷新机制。
"""
import asyncio
import time
import threading
import flet as ft
from core.qmt_real import RealQmtV, qmtv
from core.logger import LogLevel, PrintLog
from core.sfgrid.model import SFGridTradeTarget, STRATEGY_TYPE_GRID, STRATEGY_TYPE_UNCLASSIFIED
from core.sfgrid.sfgrid_strategy import SFGridStrategy
from core.eventbus import event_bus, MarketDataUpdate, EventMarketActiveSwitch
from core.sfgrid.bus_events import EventTradeTargetUpdate
# ── 委托状态 / 方向映射 ──
_ORDER_STATUS = {48: '未报', 49: '待报', 50: '已报', 51: '已报待撤', 52: '部成待撤',
53: '部撤', 54: '已撤', 55: '部成', 56: '已成', 57: '废单'}
def _fmt_time(t) -> str:
"""格式化 QMT 时间为 HH:MM:SS(北京时间,Unix timestamp → 本地时间)"""
if not t:
return ''
import datetime
try:
ts = int(t)
if ts > 1e12: # 毫秒级
ts //= 1000
return datetime.datetime.fromtimestamp(ts).strftime('%H:%M:%S')
except (ValueError, OSError):
return str(t)
def _direction(ot: int) -> str:
return '' if ot == 23 else '' if ot == 24 else str(ot)
def _plain(code: str) -> str:
return code.split('.')[0] if '.' in code else code
# ══════════════════════════════════════════════════════════════════════
# QmtApp
# ══════════════════════════════════════════════════════════════════════
class QmtApp:
"""Flet 版 QMT 交易界面,布局、数据流对齐 core/ui/tkinter/sfgrid_view.py"""
def __init__(self, page: ft.Page):
self.page = page
self.page.title = "神之一手"
self.page.window.width = 1400
self.page.window.height = 800
self.page.padding = 0
# ── 状态(对齐 Tkinter TradeTargetUI ──
self.tradeTargetData: dict[int, SFGridTradeTarget] = {}
self.stockCodeIdMap: dict[str, int] = {}
self.strategy_ctrl: dict[int, SFGridStrategy] = {}
self.targetMarketPrice: dict[int, float] = {}
self.targetPreClose: dict[int, float] = {} # 昨收
self.targetAvgPrice: dict[int, float] = {}
self.marketData: dict[str, dict] = {} # stock_code → {stock_name, last_price, time}
self.listening_stock: list = []
self.monitor_price: float = 10.0
self._market_active: bool = qmtv.isMarketActive
self._refresh_cycle: int = 0
self._drawer_open: bool = False
self._selected_target = None
self._prices_loaded: bool = False
self._orders: list = []
self._trades: list = []
self._run_startup()
# ══════════════════════════════════════════════════════════════
# 启动流程(对齐 tkinter/splash.py
# ══════════════════════════════════════════════════════════════
def _run_startup(self):
"""启动进度 — 先渲染 splash,再异步执行启动步骤"""
bar = ft.ProgressBar(width=340, value=0, color='#0078d4')
self._splash_status = ft.Text("正在初始化...", size=13)
self._splash_bar = bar
splash = ft.Container(
ft.Column([
ft.Text("神之一手", size=22, weight=ft.FontWeight.BOLD, color='#0078d4'),
ft.Text("交易系统", size=14, color='#666666'),
ft.Container(height=20),
self._splash_status,
ft.Container(height=8),
bar,
], alignment=ft.MainAxisAlignment.CENTER, horizontal_alignment=ft.CrossAxisAlignment.CENTER),
width=380, height=200,
bgcolor=ft.Colors.SURFACE,
border_radius=12,
shadow=ft.BoxShadow(blur_radius=20, color='#20000000'),
alignment=ft.Alignment.CENTER,
)
self.page.add(ft.Container(
content=splash,
alignment=ft.Alignment.CENTER, expand=True,
bgcolor='#F5F5F5',
))
self.page.update()
# 异步执行启动,确保 splash 先渲染
asyncio.ensure_future(self._do_startup())
async def _do_startup(self):
"""异步启动流程 — splash 已渲染,逐步执行并更新进度"""
# 给渲染一帧的时间
await asyncio.sleep(0.05)
steps = [
("正在检查 QMT 环境...", 0.10, lambda: RealQmtV._discover_qmt_port() or True),
("正在初始化交易器...", 0.35, lambda: qmtv.init_qmtv()),
("正在连接 QMT...", 0.55, lambda: qmtv.connect() or True),
("正在加载持仓数据...", 0.75, lambda: self._init_data()),
("正在构建界面...", 0.85, lambda: None),
("正在初始化策略...", 0.92, lambda: self._init_strategies()),
]
for text, pct, action in steps:
self._splash_status.value = text
self._splash_bar.value = pct
self.page.update()
try:
result = action()
if result is False:
self._show_error(f"启动失败: {text}")
return
except Exception as e:
self._show_error(f"启动异常: {text}\n{e}")
return
self._splash_status.value = "启动完成"
self._splash_bar.value = 1.0
self.page.update()
await asyncio.sleep(0.3)
self.page.clean()
self._build_main_ui()
self.page.update()
# 主动拉取市价(不等行情推送)
self._pull_prices()
# 加载委托/成交数据
self._refresh_orders()
self._refresh_trades()
self._rebuild_tables()
self.page.update()
# 订阅事件 + 后台刷新
event_bus.subscribe(MarketDataUpdate, self._on_market_data)
event_bus.subscribe(EventMarketActiveSwitch, self._on_market_active_switch)
event_bus.subscribe(EventTradeTargetUpdate, self._on_strategy_update)
threading.Thread(target=self._refresh_loop, daemon=True).start()
def _show_error(self, msg: str):
self.page.clean()
self.page.add(ft.Container(
content=ft.Column([
ft.Icon(ft.Icons.ERROR_OUTLINE, size=48, color=ft.Colors.RED),
ft.Text(msg, size=16),
ft.ElevatedButton("重试", on_click=lambda e: self._retry()),
], alignment=ft.MainAxisAlignment.CENTER, horizontal_alignment=ft.CrossAxisAlignment.CENTER),
alignment=ft.Alignment.CENTER, expand=True,
))
self.page.update()
def _retry(self):
self.page.clean()
self._run_startup()
# ══════════════════════════════════════════════════════════════
# 数据初始化(对齐 Tkinter init_trade_target_pool
# ══════════════════════════════════════════════════════════════
def _init_data(self):
positions = qmtv.getAllPositions()
PrintLog(LogLevel.INFO, f'[Flet] 持仓: {len(positions)}')
for code, pos in positions.items():
existing = SFGridTradeTarget.get_or_none(SFGridTradeTarget.stock_code == code)
if existing is None:
name = getattr(pos, 'instrument_name', '') or qmtv.getInstrumentName(code)
SFGridTradeTarget.create(
stock_code=code, stock_name=name,
current_position=int(pos.volume),
init_price=float(getattr(pos, 'avg_price', 0) or 0),
grid_index=0, enabled=False,
grid_start_price=float(getattr(pos, 'avg_price', 0) or 0) or 10.0,
grid_size=1.0, grid_volume=200, grid_upper_count=1, grid_lower_count=10,
)
# 获取昨收价(需要带后缀的完整代码)
try:
from xtquant import xtdata
for stock_code, pos in positions.items():
full_code = stock_code
if '.' not in stock_code:
c = stock_code
full_code = f'{c}.SH' if c.startswith(('6', '5', '9')) else f'{c}.SZ'
detail = xtdata.get_instrument_detail(full_code)
if detail:
pre_close = detail.get('PreClose', 0) if isinstance(detail, dict) else getattr(detail, 'PreClose', 0)
if pre_close > 0:
self.targetPreClose[stock_code] = float(pre_close)
PrintLog(LogLevel.INFO, f'[Flet] 已获取 {len(self.targetPreClose)} 个标的昨收价')
except Exception as e:
PrintLog(LogLevel.DEBUG, f'[Flet] 昨收价获取异常: {e}')
results = list(SFGridTradeTarget.select())
for t in results:
pos = positions.get(t.stock_code)
t.current_position = 0 if pos is None else int(pos.volume)
tid = t.get_id()
self.tradeTargetData[tid] = t
self.stockCodeIdMap[t.stock_code] = tid
if pos is not None:
self.targetAvgPrice[tid] = float(getattr(pos, 'avg_price', 0) or 0)
def _init_strategies(self):
from core.sfgrid.model import STRATEGY_TYPE_GRID
for tid, t in self.tradeTargetData.items():
if t.strategy_type == STRATEGY_TYPE_GRID and t.enabled:
self.strategy_ctrl[tid] = SFGridStrategy(t)
# ══════════════════════════════════════════════════════════════
# 主界面构建(对齐 Tkinter create_tables_area
# ══════════════════════════════════════════════════════════════
def _build_main_ui(self):
# ── 右侧面板内容 ──
self._tab_orders = ft.Tab(label="当前委托")
self._tab_trades = ft.Tab(label="当日成交")
right_bar = ft.TabBar(tabs=[
ft.Tab(label="实时价格监控"),
self._tab_orders,
self._tab_trades,
ft.Tab(label="未分类持仓"),
])
self._uncl_list = ft.ListView([self._build_unclassified_table()], expand=True)
self._right_view = ft.TabBarView(controls=[
self._build_market_view(),
self._build_order_view(),
self._build_trade_view(),
self._uncl_list,
], expand=True)
panel_content = ft.Container(
content=ft.Column([
ft.Container(ft.Text("监控面板", size=14, weight=ft.FontWeight.BOLD), padding=ft.Padding(10, 10, 10, 5)),
ft.Tabs(ft.Column([right_bar, self._right_view], expand=True), length=4, expand=True),
], expand=True),
width=700, bgcolor=ft.Colors.SURFACE,
)
# ── 遮罩层(点击关闭) ──
backdrop = ft.Container(
bgcolor='#44000000', expand=True,
on_click=lambda e: self._hide_overlay(),
)
# ── overlay 行:遮罩 + 面板 ──
self._overlay = ft.Container(
ft.Row([backdrop, panel_content], spacing=0),
visible=False, expand=True,
)
# ── 标题栏(始终可见,选中行后显示操作按钮) ──
self._sidebar_icon = _PanelIcon('sidebar', active=False, on_click=lambda e: self._toggle_overlay())
self._sel_actions = ft.Row([], spacing=4) # 动态操作按钮
self._sel_info = ft.Text("", size=12, color='#666666')
grid_title = ft.Container(
ft.Row([
ft.Row([
ft.Text("网格策略持仓", size=13, weight=ft.FontWeight.BOLD),
self._sel_info,
self._sel_actions,
]),
ft.Row([
ft.IconButton(ft.Icons.REFRESH, tooltip="刷新", icon_size=18,
on_click=lambda e: self._manual_refresh()),
self._sidebar_icon,
], spacing=0),
], alignment=ft.MainAxisAlignment.SPACE_BETWEEN),
padding=ft.Padding(10, 10, 10, 5),
)
# ── 表格(Stack 内,可被 overlay 覆盖) ──
self._grid_list = self._build_grid_table() # 回到 DataTable
grid_body = ft.Container(
content=self._grid_list, expand=True,
padding=ft.Padding(10, 0, 10, 10),
)
self.page.add(ft.Column([
grid_title,
ft.Stack([grid_body, self._overlay], expand=True),
], expand=True))
# ── 表格工具 ──
def _dt(self, cols: list[str], rows: list[list[str]], col_widths: list = None) -> ft.Control:
"""构建 DataTable"""
data_cols = [ft.DataColumn(ft.Text(h)) for h in cols]
data_rows = []
for r in rows:
cells = []
for i, c in enumerate(r):
w = col_widths[i] if col_widths and i < len(col_widths) else None
cells.append(ft.DataCell(ft.Text(str(c), overflow=ft.TextOverflow.ELLIPSIS,
max_lines=1, width=w)))
data_rows.append(ft.DataRow(cells=cells))
if not data_rows:
data_rows.append(ft.DataRow(cells=[ft.DataCell(ft.Text("")) for _ in cols]))
return ft.ListView([ft.DataTable(
columns=data_cols, rows=data_rows,
width=float('inf'),
heading_row_height=36, data_row_min_height=32,
)], expand=True)
# ── 各表格 ──
def _pending_tags(self, stock_code: str) -> list:
"""返回该标的下挂单的方向标签列表:''(买单) / ''(卖单)"""
tags = []
_TERMINAL = {54, 56, 57}
for o in self._orders:
if _plain(getattr(o, 'stock_code', '')) != stock_code:
continue
if getattr(o, 'order_status', 0) in _TERMINAL:
continue
ot = getattr(o, 'order_type', 0)
if ot == 23 and '' not in tags:
tags.append('')
elif ot == 24 and '' not in tags:
tags.append('')
return tags
def _tag_badge(self, text: str, color: str) -> ft.Container:
return ft.Container(
ft.Text(text, size=10, color='white', weight=ft.FontWeight.BOLD),
bgcolor=color, border_radius=4, padding=ft.Padding(3, 1, 3, 1),
)
def _on_grid_row_select(self, target):
"""DataRow 选中回调 — 在标题栏显示操作按钮"""
self._selected_target = target
name = f'{target.stock_code} {target.stock_name}'
self._sel_info.value = f" | 已选: {name}"
actions = []
if target.enabled:
actions.append(ft.ElevatedButton("⏸ 暂停", on_click=lambda e, t=target: self._on_stop_trade(t), height=28))
else:
actions.append(ft.ElevatedButton("▶ 启动", on_click=lambda e, t=target: self._on_start_trade(t), height=28))
actions.append(ft.ElevatedButton("⚙ 设置", on_click=lambda e, t=target: self._open_grid_config(t), height=28))
self._sel_actions.controls = actions
self.page.update()
def _build_grid_table(self) -> ft.Control:
"""网格表格 — DataTable + on_select_change"""
cols = ["ID", "股票", "市场价", "持仓", "成本", "网格基准", "状态"]
data_cols = [ft.DataColumn(ft.Text(h)) for h in cols]
data_rows = []
is_sel = self._selected_target is not None
sel_id = self._selected_target.get_id() if self._selected_target else -1
for tid, t in self.tradeTargetData.items():
if t.strategy_type != 1:
continue
pg = t.getPriceGrid()
idx = t.grid_index
grid_base = pg[idx] if 0 <= idx < len(pg) else 0
mp = self.targetMarketPrice.get(tid, 0) or 0
pre_close = self.targetPreClose.get(t.stock_code, 0) or 0
up = mp > pre_close and pre_close > 0
down = mp < pre_close and mp > 0 and pre_close > 0
pcolor = '#CC0000' if up else '#009900' if down else None
gtext = ft.Text(f'{grid_base:.2f}', weight=ft.FontWeight.BOLD)
gparts = [gtext]
if mp > grid_base > 0:
gparts.append(ft.Text('', color='#CC0000', weight=ft.FontWeight.BOLD))
elif 0 < mp < grid_base:
gparts.append(ft.Text('', color='#009900', weight=ft.FontWeight.BOLD))
for tag in self._pending_tags(t.stock_code):
gparts.append(self._tag_badge(tag, '#E67E22' if tag == '' else '#3498DB'))
gcell = ft.Row(gparts, spacing=3) if len(gparts) > 1 else gtext
dr = ft.DataRow(cells=[
ft.DataCell(ft.Text(str(tid))),
ft.DataCell(ft.Text(f'{t.stock_code} {t.stock_name}')),
ft.DataCell(ft.Text(f'{mp:.3f}', color=pcolor, weight=ft.FontWeight.BOLD)),
ft.DataCell(ft.Text(str(t.current_position))),
ft.DataCell(ft.Text(f'{self.targetAvgPrice.get(tid, 0):.3f}')),
ft.DataCell(gcell),
ft.DataCell(ft.Text('▶运行中' if t.enabled else '⏸已暂停')),
], selected=(is_sel and tid == sel_id))
dr.on_select_change = lambda e, t=t: self._on_grid_row_select(t)
data_rows.append(dr)
if not data_rows:
data_rows.append(ft.DataRow(cells=[ft.DataCell(ft.Text("")) for _ in cols]))
return ft.ListView([ft.DataTable(columns=data_cols, rows=data_rows,
width=float('inf'),
heading_row_height=36, data_row_min_height=32)], expand=True)
def _build_unclassified_table(self) -> ft.Control:
cols = ["ID", "股票", "市场价", "当前持仓", "平均成本"]
rows = []
for tid, t in self.tradeTargetData.items():
if t.strategy_type == STRATEGY_TYPE_GRID:
continue
mp = self.targetMarketPrice.get(tid, 0) or 0
rows.append([
str(tid),
f'{t.stock_code} {t.stock_name}',
f'{mp:.3f}',
str(t.current_position),
f'{self.targetAvgPrice.get(tid, 0):.3f}',
])
return self._dt(cols, rows)
def _build_market_view(self) -> ft.Control:
"""实时价格监控 — 监控配置 + 表格"""
price_input = ft.TextField(value=str(self.monitor_price), width=80, height=32,
text_size=13, content_padding=ft.Padding(4, 0, 4, 0))
confirm_btn = ft.ElevatedButton("确认", on_click=lambda e: self._set_monitor_price(price_input.value), height=32)
self._market_table = self._dt(["时间", "股票名称", "最新价格"], [])
return ft.Column([
ft.Row([
ft.Text("监控配置", size=13), ft.Text("价格", size=13),
price_input, confirm_btn,
]),
ft.Container(content=self._market_table, expand=True),
], expand=True)
def _build_order_view(self) -> ft.Control:
self._order_table = self._dt(
["时间", "代码", "名称", "方向", "委托价", "委托量", "已成交", "均价", "状态"], [],
col_widths=[65, 55, 70, 35, 60, 80, 55, 50])
return self._order_table
def _build_trade_view(self) -> ft.Control:
self._trade_table = self._dt(
["时间", "代码", "名称", "方向", "成交价", "成交量", "成交金额", "手续费"], [],
col_widths=[65, 55, 70, 35, 65, 60, 70, 55])
return self._trade_table
# ══════════════════════════════════════════════════════════════
# 事件回调(对齐 Tkinter onMarketDataUpdated
# ══════════════════════════════════════════════════════════════
def _on_market_data(self, data: dict):
"""行情数据回调 — 来自 QMT 推送"""
need_rebuild = not self._prices_loaded
updated_count = 0
for stock_code, tick in data.items():
plain = _plain(stock_code)
tid = self.stockCodeIdMap.get(plain)
lp = tick.get('lastPrice', 0)
if tid is not None and tid in self.tradeTargetData:
self.targetMarketPrice[tid] = lp
self.tradeTargetData[tid].market_price = lp
updated_count += 1
else:
# 非目标标的:监控价格触发时记录
if lp == self.monitor_price or stock_code in self.listening_stock:
if stock_code not in self.listening_stock:
self.listening_stock.append(stock_code)
t_str = time.strftime("%H:%M:%S")
name = qmtv.getInstrumentName(stock_code)
self.marketData[stock_code] = {'stock_name': name, 'last_price': lp, 'time': t_str}
if need_rebuild and not self._prices_loaded and updated_count > 0:
self._prices_loaded = True
self._rebuild_tables()
self.page.update()
def _on_market_active_switch(self, is_active: bool):
self._market_active = is_active
def _on_strategy_update(self, target):
"""策略数据变更 — 成交后立即刷新表格"""
self._rebuild_tables()
self.page.update()
# ══════════════════════════════════════════════════════════════
# 刷新循环(对齐 Tkinter refresh_loop
# ══════════════════════════════════════════════════════════════
def _pull_prices(self):
"""主动拉取缺失的市价(对齐 Tkinter refresh_loop"""
for tid, t in self.tradeTargetData.items():
if tid not in self.targetMarketPrice or self.targetMarketPrice[tid] == 0:
price = qmtv.getLastPrice(t.stock_code)
if price > 0:
self.targetMarketPrice[tid] = price
t.market_price = price
def _manual_refresh(self):
self._pull_prices()
self._refresh_positions()
self._refresh_orders()
self._refresh_trades()
self._rebuild_tables()
self.page.update()
def _refresh_positions(self):
positions = qmtv.getAllPositions()
for t in self.tradeTargetData.values():
pos = positions.get(t.stock_code)
t.current_position = 0 if pos is None else int(pos.volume)
def _refresh_orders(self):
try:
self._orders = list(qmtv.queryTodayOrders())
except Exception:
pass
def _refresh_trades(self):
try:
self._trades = list(qmtv.queryTodayTrades())
except Exception:
pass
def _rebuild_tables(self):
"""重建所有表格数据"""
self._grid_list.controls = [self._build_grid_table()]
if not self._selected_target:
self._sel_info.value = ""
self._sel_actions.controls = []
self._uncl_list.controls = [self._build_unclassified_table()]
# 委托 — 过滤已终结订单(已撤/已成/废单),按 order_id 去重
_TERMINAL = {54, 56, 57}
o_map = {} # order_id → latest order
for o in self._orders:
oid = str(getattr(o, 'order_id', ''))
if not oid:
continue
o_map[oid] = o # 后面的覆盖前面的
o_rows = []
for o in o_map.values():
st = getattr(o, 'order_status', 0)
if st in _TERMINAL:
continue
tv = getattr(o, 'traded_volume', 0) or 0
ov = getattr(o, 'order_volume', 0) or 0
o_rows.append([
_fmt_time(getattr(o, 'order_time', 0)),
_plain(getattr(o, 'stock_code', '')),
getattr(o, 'instrument_name', '') or '',
_direction(getattr(o, 'order_type', 0)),
f"{getattr(o, 'price', 0):.3f}",
f"{tv}/{ov}",
f"{getattr(o, 'traded_price', 0):.3f}" if getattr(o, 'traded_price', 0) > 0 else '-',
_ORDER_STATUS.get(st, '未知'),
])
self._tab_orders.label = f"当前委托 ({len(o_rows)})" if o_rows else "当前委托"
self._order_table.controls = [self._dt(
["时间", "代码", "名称", "方向", "委托价", "已成交/委托量", "均价", "状态"], o_rows,
col_widths=[65, 55, 70, 35, 60, 80, 55, 50])]
# 成交 — 按 traded_id 去重(保留最后一条)
t_map = {}
for t in self._trades:
tid = str(getattr(t, 'traded_id', ''))
if not tid:
continue
t_map[tid] = t
t_rows = []
for t in t_map.values():
t_rows.append([
_fmt_time(getattr(t, 'traded_time', 0)),
_plain(getattr(t, 'stock_code', '')),
getattr(t, 'instrument_name', '') or '',
_direction(getattr(t, 'order_type', 0)),
f"{getattr(t, 'traded_price', 0):.3f}",
str(getattr(t, 'traded_volume', 0)),
f"{getattr(t, 'traded_amount', 0):.2f}",
f"{getattr(t, 'commission', 0):.2f}",
])
self._tab_trades.label = f"当日成交 ({len(t_rows)})" if t_rows else "当日成交"
self._trade_table.controls = [self._dt(
["时间", "代码", "名称", "方向", "成交价", "成交量", "成交金额", "手续费"], t_rows,
col_widths=[65, 55, 70, 35, 65, 60, 70, 55])]
# 市场监控
m_rows = []
for sc, d in self.marketData.items():
m_rows.append([d['time'], f"{d['stock_name']}-{sc}", f"{d['last_price']:.3f}"])
self._market_table.controls = [self._dt(["时间", "股票名称", "最新价格"], m_rows)]
def _refresh_loop(self):
"""后台定时刷新 — 对齐 Tkinter: 5s 拉价 + 30s 委托/成交"""
while True:
time.sleep(5)
self._refresh_cycle += 1
try:
self._pull_prices()
self._refresh_positions()
if self._refresh_cycle % 6 == 0:
self._refresh_orders()
self._refresh_trades()
self._rebuild_tables()
self.page.update()
except Exception:
pass
# ══════════════════════════════════════════════════════════════
# 工具栏按钮
# ══════════════════════════════════════════════════════════════
def _on_start_trade(self, target):
PrintLog(LogLevel.INFO, f'[Flet-按钮] 启动按钮被点击: {target.stock_code}')
if target.enabled:
self._show_toast("该标的正运行中")
return
name = f'{target.stock_code} {target.stock_name}'
dlg = ft.AlertDialog(
title=ft.Text("确认启动"),
content=ft.Text(f"确定要启动交易吗?\n\n{name}"),
actions=[
ft.TextButton("取消", on_click=lambda e: self._close_dialog(dlg)),
ft.TextButton("确定", on_click=lambda e, t=target: self._do_start(t)),
],
)
self.page.show_dialog(dlg)
def _do_start(self, target):
self.page.pop_dialog()
target.enabled = True
target.save()
from core.sfgrid.sfgrid_strategy import SFGridStrategy
self.strategy_ctrl[target.get_id()] = SFGridStrategy(target)
self._rebuild_tables()
self.page.update()
PrintLog(LogLevel.INFO, f'[Flet] 启动交易: {target.targetName()}')
def _on_stop_trade(self, target):
PrintLog(LogLevel.INFO, f'[Flet-按钮] 暂停按钮被点击: {target.stock_code}')
if not target.enabled:
self._show_toast("该标的已暂停")
return
name = f'{target.stock_code} {target.stock_name}'
dlg = ft.AlertDialog(
title=ft.Text("确认暂停"),
content=ft.Text(f"确定要暂停交易吗?\n\n{name}"),
actions=[
ft.TextButton("取消", on_click=lambda e: self._close_dialog(dlg)),
ft.TextButton("确定", on_click=lambda e, t=target: self._do_stop(t)),
],
)
self.page.show_dialog(dlg)
def _do_stop(self, target):
self.page.pop_dialog()
target.enabled = False
target.save()
ctrl = self.strategy_ctrl.pop(target.get_id(), None)
if ctrl:
ctrl.enabledTrading(False)
self._rebuild_tables()
self.page.update()
PrintLog(LogLevel.INFO, f'[Flet] 暂停交易: {target.targetName()}')
def _open_grid_config(self, target):
"""网格配置对话框 — 对齐 Tkinter create_grid_config_window"""
PrintLog(LogLevel.INFO, f'[Flet-按钮] 设置按钮被点击: {target.stock_code}')
base = ft.TextField(label="基准价格", value=str(target.grid_start_price), width=120, text_size=13)
gsize = ft.TextField(label="网格大小", value=str(target.grid_size), width=120, text_size=13)
gvol = ft.TextField(label="网格交易量(手)", value=str(target.grid_volume), width=120, text_size=13)
gupper = ft.TextField(label="上方网格数", value=str(target.grid_upper_count), width=120, text_size=13)
glower = ft.TextField(label="下方网格数", value=str(target.grid_lower_count), width=120, text_size=13)
gidx = ft.TextField(label="当前网格层级", value=str(target.grid_index), width=120, text_size=13)
col1 = ft.Column([base, gsize, gvol], spacing=8)
col2 = ft.Column([gupper, glower, gidx], spacing=8)
grid_preview = ft.Text("", size=11, italic=True)
def _preview(e):
try:
bp = float(base.value)
gs = float(gsize.value)
up = int(gupper.value)
lo = int(glower.value)
prices = []
for i in range(up, 0, -1):
prices.append(f"{bp + gs * i:.2f}(卖{up - i + 1})")
prices.append(f"{bp:.2f}←(基准)")
for i in range(1, lo + 1):
p = bp - gs * i
if p > 0:
prices.append(f"{p:.2f}(买{i})")
grid_preview.value = " ".join(prices)
grid_preview.update()
except ValueError:
grid_preview.value = "请输入有效数字"
grid_preview.update()
def _save(e):
try:
target.grid_start_price = float(base.value)
target.grid_size = float(gsize.value)
target.grid_volume = int(gvol.value)
target.grid_upper_count = int(gupper.value)
target.grid_lower_count = int(glower.value)
target.grid_index = int(gidx.value)
target.save()
self._close_dialog()
self._rebuild_tables()
self.page.update()
PrintLog(LogLevel.INFO, f'[Flet] 网格配置已保存: {target.targetName()}')
except ValueError:
self._show_toast("请输入有效的数值")
dlg = ft.AlertDialog(
title=ft.Text(f"网格配置 - {target.stock_code} {target.stock_name}"),
content=ft.Column([
ft.Row([col1, col2], spacing=20),
ft.ElevatedButton("预览网格序列", on_click=_preview),
grid_preview,
], spacing=10, tight=True, height=320),
actions=[
ft.TextButton("取消", on_click=lambda e: self._close_dialog(dlg)),
ft.ElevatedButton("保存", on_click=_save),
],
)
self.page.show_dialog(dlg)
def _show_toast(self, msg: str):
dlg = ft.AlertDialog(title=ft.Text("提示"), content=ft.Text(msg),
actions=[ft.TextButton("确定", on_click=lambda e: self._close_dialog(dlg))])
self.page.show_dialog(dlg)
def _close_dialog(self, dlg=None):
self.page.pop_dialog()
self.page.update()
def _toggle_overlay(self):
self._drawer_open = not self._drawer_open
self._overlay.visible = self._drawer_open
self._sidebar_icon.set_active(self._drawer_open)
self.page.update()
def _hide_overlay(self):
self._drawer_open = False
self._overlay.visible = False
self._sidebar_icon.set_active(False)
self.page.update()
def _set_monitor_price(self, val: str):
try:
self.monitor_price = float(val)
self.marketData.clear()
self.listening_stock.clear()
self._rebuild_tables()
self.page.update()
except ValueError:
pass
# ══════════════════════════════════════════════════════════════════════
# PanelIcon — 对齐 Tkinter 版 Canvas 手绘图标
# ══════════════════════════════════════════════════════════════════════
class _PanelIcon(ft.Container):
"""VSCode 风格面板切换图标 — 两个色块拼成的分栏图标"""
_SIZE = 22
_M = 3
_COLORS = {
'light': {'bg': '#f0f0f0', 'hover': '#d4d4d4', 'off': '#b0b0b0', 'on': '#808080', 'active': '#0078d4'},
'dark': {'bg': '#3c3c3c', 'hover': '#505050', 'off': '#6a6a6a', 'on': '#a0a0a0', 'active': '#ffffff'},
}
def __init__(self, kind: str, active: bool = True, on_click=None):
self._kind = kind
self._active = active
c = self._COLORS['light'] # 默认亮色,后续可扩展暗色检测
self._bg = c['bg']
self._hover_bg = c['hover']
self._off = c['off']
self._on = c['on']
self._active_color = c['active']
rects = self._build_rects()
super().__init__(
content=rects,
width=self._SIZE, height=self._SIZE,
bgcolor=self._bg, border_radius=3,
ink=True, on_click=on_click,
padding=ft.Padding(self._M, self._M, self._M, self._M),
)
def _build_rects(self):
off, on, act = self._off, self._on, self._active_color
bar_w, bar_h = 6, self._SIZE - self._M * 2 - 2
if self._kind == 'sidebar':
c1 = on if self._active else off
c2 = act if self._active else off
return ft.Row([
ft.Container(width=bar_w, height=bar_h, bgcolor=c1, border_radius=1),
ft.Container(width=2), # gap
ft.Container(width=bar_w, height=bar_h, bgcolor=c2, border_radius=1),
], spacing=0)
else:
c1 = on if self._active else off
c2 = act if self._active else off
return ft.Column([
ft.Container(width=bar_h, height=bar_w, bgcolor=c1, border_radius=1),
ft.Container(height=2), # gap
ft.Container(width=bar_h, height=bar_w, bgcolor=c2, border_radius=1),
], spacing=0)
def set_active(self, active: bool):
self._active = active
self.content = self._build_rects()
# ══════════════════════════════════════════════════════════════════════
# 入口
# ══════════════════════════════════════════════════════════════════════
def main(page: ft.Page):
QmtApp(page)
def run():
ft.app(target=main)
def run_web():
ft.app(target=main, view=ft.AppView.WEB_BROWSER, port=8550)
+392 -38
View File
@@ -17,19 +17,21 @@ from core.logger import LogLevel, PrintLog
from core.sfgrid.model import SFGridTradeTarget, STRATEGY_TYPE_GRID from core.sfgrid.model import SFGridTradeTarget, STRATEGY_TYPE_GRID
from core.sfgrid.sfgrid_strategy import SFGridStrategy from core.sfgrid.sfgrid_strategy import SFGridStrategy
from core.eventbus import event_bus, MarketDataUpdate, EventMarketActiveSwitch from core.eventbus import event_bus, MarketDataUpdate, EventMarketActiveSwitch
from core.sfgrid.bus_events import EventTradeTargetUpdate from core.sfgrid.bus_events import EventTradeTargetUpdate, EventPoolMark
# ── 工具函数 ── # ── 工具函数 ──
_ORDER_STATUS = {48: '未报', 49: '待报', 50: '已报', 51: '已报待撤', 52: '部成待撤', _ORDER_STATUS = {48: '未报', 49: '待报', 50: '已报', 51: '已报待撤', 52: '部成待撤',
53: '部撤', 54: '已撤', 55: '部成', 56: '已成', 57: '废单'} 53: '部撤', 54: '已撤', 55: '部成', 56: '已成', 57: '废单'}
from datetime import date as _date
def _fmt_time(t) -> str: def _fmt_time(t) -> str:
if not t: return '' if not t: return ''
import datetime from datetime import datetime as _dt
try: try:
ts = int(t) ts = int(t)
if ts > 1e12: ts //= 1000 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): except (ValueError, OSError):
return str(t) return str(t)
@@ -78,6 +80,7 @@ class _DataStore:
self.monitorPrice: float = 10.0 self.monitorPrice: float = 10.0
self.marketLog: dict[str, dict] = {} # stock_code → {name, price, time} self.marketLog: dict[str, dict] = {} # stock_code → {name, price, time}
self.listeningStocks: list = [] self.listeningStocks: list = []
self.poolLog: list[str] = [] # 股票池管理器日志(list,方便 append)
# ── 初始化 ── # ── 初始化 ──
@@ -301,6 +304,22 @@ class _DataStore:
elif ot == 24 and '' not in tags: tags.append('') elif ot == 24 and '' not in tags: tags.append('')
return tags return tags
def pool_action_tags(self, stock_code: str) -> list[str]:
"""某标的的股票池待执行标记(eliminate/liquidate"""
plain = _plain(stock_code)
from core.scoring.models import PendingPoolAction
rows = list(PendingPoolAction
.select(PendingPoolAction.action_type)
.where(PendingPoolAction.stock_code == plain)
.dicts())
tags = []
for r in rows:
if r['action_type'] == 'eliminate' and '淘汰' not in tags:
tags.append('淘汰')
elif r['action_type'] == 'liquidate' and '沉寂' not in tags:
tags.append('沉寂')
return tags
# ══════════════════════════════════════════════════════════════ # ══════════════════════════════════════════════════════════════
# GridPanel — 网格策略持仓表格 # GridPanel — 网格策略持仓表格
@@ -324,9 +343,9 @@ class _GridPanel:
return self._col return self._col
def _rebuild(self): def _rebuild(self):
# (width, expand): 0=固定宽, >0=弹性比重 # (width, expand): 0=固定宽, >0=弹性比重; 新增排名列(50px)
_C = [(35, 0), (0, 2), (70, 1), (0, 2), (50, 1), (55, 1), (60, 1), (0, 1)] _C = [(35, 0), (0, 2), (70, 1), (80, 1), (50, 1), (55, 1), (50, 0), (60, 1), (0, 1)]
H = ["ID", "股票", "市场价", "网格基准", "持仓", "成本", "状态", "操作"] H = ["ID", "股票", "市场价", "网格基准", "持仓", "成本", "排名", "状态", "操作"]
header = [] header = []
for h, (w, e) in zip(H, _C): for h, (w, e) in zip(H, _C):
if e > 0: if e > 0:
@@ -335,6 +354,22 @@ class _GridPanel:
header.append(ft.Container(_text(h, bold=True), width=w, padding=4)) header.append(ft.Container(_text(h, bold=True), width=w, padding=4))
rows = [ft.Row(header, spacing=0), ft.Divider(height=1, color='#e0e0e0')] 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(): for tid, t in self._data.tradeTargets.items():
if t.strategy_type != STRATEGY_TYPE_GRID: continue if t.strategy_type != STRATEGY_TYPE_GRID: continue
pg = t.getPriceGrid() pg = t.getPriceGrid()
@@ -358,6 +393,22 @@ class _GridPanel:
)) ))
gcell = ft.Row(gcells, spacing=3) if len(gcells) > 1 else gcells[0] 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 好按 _ICON = 22 # 图标大小,比默认 18 好按
if t.enabled: if t.enabled:
@@ -381,10 +432,30 @@ class _GridPanel:
on_click=lambda e, tt=t: self._dialogs.confirm_remove(tt)), on_click=lambda e, tt=t: self._dialogs.confirm_remove(tt)),
] ]
cells_text = [str(tid), f'{t.stock_code} {t.stock_name}', # 股票列:名称 + 标记标签
pool_tags = self._data.pool_action_tags(t.stock_code)
tag_chips = []
if pool_tags:
# 有淘汰/沉寂时,显示操作标签,不显示默认标签
for tag in pool_tags:
color = '#E67E22' if tag == '淘汰' else '#E74C3C'
tag_chips.append(ft.Container(
_text(tag, color='white', bold=True, size=10),
bgcolor=color, border_radius=4, padding=ft.Padding(2, 1, 2, 1),
))
else:
# 无操作标签时,显示默认"网格"标签
tag_chips.append(ft.Container(
_text('网格', color='white', bold=True, size=10),
bgcolor='#2E7D32', border_radius=4, padding=ft.Padding(2, 1, 2, 1),
))
name_cell = ft.Row([_text(f'{t.stock_code} {t.stock_name}')] + tag_chips, spacing=4)
cells_text = [str(tid), name_cell,
f'{mp:.3f}', gcell, f'{mp:.3f}', gcell,
str(t.current_position), str(t.current_position),
f'{self._data.avgPrices.get(tid, 0):.3f}', f'{self._data.avgPrices.get(tid, 0):.3f}',
rank_str,
'▶运行中' if t.enabled else '⏸已暂停', '▶运行中' if t.enabled else '⏸已暂停',
btns] btns]
row_cells = [] row_cells = []
@@ -392,6 +463,8 @@ class _GridPanel:
content = cells_text[i] content = cells_text[i]
if i == 2: # 市场价用颜色 if i == 2: # 市场价用颜色
content = _text(cells_text[i], color=pcolor, bold=True) 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): elif isinstance(content, str):
content = _text(content) content = _text(content)
elif isinstance(content, list): elif isinstance(content, list):
@@ -439,6 +512,7 @@ class _DrawerPanel:
self._dataset_table = ft.ListView(expand=True) self._dataset_table = ft.ListView(expand=True)
self._dataset_col = ft.Column(scroll=ft.ScrollMode.AUTO, expand=True) self._dataset_col = ft.Column(scroll=ft.ScrollMode.AUTO, expand=True)
self._scoring_table = ft.ListView(expand=True) self._scoring_table = ft.ListView(expand=True)
self._backtest_col = ft.Column(scroll=ft.ScrollMode.AUTO, expand=True)
bar = ft.TabBar(tabs=[ bar = ft.TabBar(tabs=[
ft.Tab(label="实时价格监控"), ft.Tab(label="实时价格监控"),
@@ -491,12 +565,28 @@ class _DrawerPanel:
self._refresh_trades() self._refresh_trades()
self._refresh_market() self._refresh_market()
self._refresh_dataset() self._refresh_dataset()
self._refresh_scoring()
def _refresh_grid(self): def _refresh_grid(self):
# (width, expand): 0=固定宽, >0=弹性比重; 股票列 expand 自动填充剩余空间 # (width, expand): 0=固定宽, >0=弹性比重; 股票列 expand 自动填充剩余空间
_C = [(35, 0), (0, 1), (80, 0), (60, 0), (70, 0), (65, 0)] # 新增: 排名列 (50px)
H = ["ID", "股票", "市场价", "持仓", "成本", "操作"] _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): def _cell(text, w, e, color=None):
if e > 0: if e > 0:
@@ -509,15 +599,26 @@ class _DrawerPanel:
for tid, t in self._data.tradeTargets.items(): for tid, t in self._data.tradeTargets.items():
if t.strategy_type == STRATEGY_TYPE_GRID: continue if t.strategy_type == STRATEGY_TYPE_GRID: continue
mp = self._data.marketPrices.get(tid, 0) or 0 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 = [ cells = [
_cell(str(tid), *_C[0]), _cell(str(tid), *_C[0]),
_cell(f'{t.stock_code} {t.stock_name}', *_C[1]), _cell(f'{t.stock_code} {t.stock_name}', *_C[1]),
_cell(f'{mp:.3f}', *_C[2]), _cell(f'{mp:.3f}', *_C[2]),
_cell(str(t.current_position), *_C[3]), _cell(str(t.current_position), *_C[3]),
_cell(f'{self._data.avgPrices.get(tid, 0):.3f}', *_C[4]), _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="网格配置", ft.Container(ft.IconButton(ft.Icons.SETTINGS, icon_size=20, tooltip="网格配置",
on_click=lambda e, tt=t: self._dialogs.open_config(tt)), 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) row = ft.Row(cells, spacing=0)
rows.append(ft.Container(row, padding=ft.Padding(0, 2, 0, 2))) rows.append(ft.Container(row, padding=ft.Padding(0, 2, 0, 2)))
@@ -603,10 +704,15 @@ class _DrawerPanel:
def _build_dataset_tab(self) -> ft.Control: def _build_dataset_tab(self) -> ft.Control:
self._dataset_status = ft.Text("就绪", size=12, color='#888888') self._dataset_status = ft.Text("就绪", size=12, color='#888888')
self._backtest_label = ft.Text("v6.7r3 回测曲线", size=11, color='#aaaaaa')
self._backtest_img = ft.Container(visible=False) # 回测图表占位,运行时替换为 Image
return ft.Column([ return ft.Column([
self._dataset_status, self._dataset_status,
self._dataset_col, self._dataset_col,
], expand=True, spacing=6) ft.Divider(height=2, color='#444444'),
self._backtest_label,
self._backtest_img,
], expand=True, spacing=6, scroll=ft.ScrollMode.AUTO)
def _run_sync(self, targets: list): def _run_sync(self, targets: list):
# 线程锁 # 线程锁
@@ -649,6 +755,92 @@ class _DrawerPanel:
threading.Thread(target=_do, daemon=True).start() threading.Thread(target=_do, daemon=True).start()
def _refresh_backtest(self):
"""加载 v6.7r3 回测图表,嵌入数据集 Tab"""
import pandas as pd
import io, base64, json
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
from pathlib import Path
model_dir = Path(__file__).parent.parent.parent.parent / 'models'
daily_csv = model_dir / 'backtest_v67r3_daily.csv'
summary_json = model_dir / 'backtest_v67r3_summary.json'
if not daily_csv.exists():
self._backtest_label.value = "回测数据不存在"
self._backtest_label.update()
return
try:
df_daily = pd.read_csv(daily_csv, parse_dates=['date'])
summary = json.loads(summary_json.read_text()) if summary_json.exists() else {}
# 更新标题行
start_val = summary.get('initial_cash', 60000)
final_val = summary.get('final_total_value', 0)
self._backtest_label.value = (f"v6.7r3 回测 初始:{start_val:,.0f} → 终值:{final_val:,.0f} "
f"收益:{summary.get('total_return_pct',0):.1f}% "
f"夏普:{summary.get('annual_sharpe',0):.2f} "
f"回撤:{summary.get('max_drawdown_pct',0):.1f}%")
self._backtest_label.update()
# 渲染总资产曲线
import matplotlib.font_manager as mfont
# 查找中文字体
cjk_names = ['Microsoft YaHei', 'SimHei', 'Noto Sans SC', 'WenQuanYi']
cjk_font = next((f.fname for f in mfont.fontManager.ttflist
if any(n in f.name for n in cjk_names)), None)
if cjk_font:
prop = mfont.FontProperties(fname=cjk_font)
plt.rcParams['font.family'] = prop.get_name()
plt.rcParams['axes.unicode_minus'] = False
fig, ax = plt.subplots(figsize=(7, 3.5), dpi=100)
fig.patch.set_facecolor('#1e1e1e')
ax.set_facecolor('#2d2d2d')
ax.plot(df_daily['date'], df_daily['total_asset'], color='#4CAF50', linewidth=1.5)
ax.fill_between(df_daily['date'], df_daily['total_asset'], alpha=0.1, color='#4CAF50')
ax.set_title('总资产曲线 (v6.7r3)', color='#ffffff', fontsize=10)
ax.set_ylabel('', color='#cccccc', fontsize=9)
ax.tick_params(colors='#cccccc', labelsize=8)
ax.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m'))
ax.xaxis.set_major_locator(mdates.MonthLocator(interval=3))
plt.setp(ax.xaxis.get_majorticklabels(), rotation=30, ha='right')
for spine in ax.spines.values():
spine.set_color('#555555')
ax.grid(True, alpha=0.15, color='#888888')
# 标注起点终点
ax.annotate(f'{start_val:,.0f}', xy=(df_daily['date'].iloc[0], start_val),
xytext=(3, 5), textcoords='offset points', color='#aaaaaa', fontsize=8)
ax.annotate(f'{final_val:,.0f}', xy=(df_daily['date'].iloc[-1], final_val),
xytext=(3, 5), textcoords='offset points', color='#4CAF50', fontsize=8, fontweight='bold')
plt.tight_layout()
buf = io.BytesIO()
fig.savefig(buf, format='png', bbox_inches='tight', facecolor=fig.get_facecolor())
buf.seek(0)
img_b64 = base64.b64encode(buf.read()).decode()
plt.close(fig)
# 用实际 Image 替换占位的 Container
new_img = ft.Image(
src=f'data:image/png;base64,{img_b64}',
fit=ft.BoxFit.CONTAIN,
height=240,
visible=True,
)
# 找到 placeholder Container 在 Column 中的位置,替换
idx = self._backtest_label.parent.controls.index(self._backtest_img)
self._backtest_label.parent.controls[idx] = new_img
self._backtest_img = new_img
self._backtest_label.parent.update()
except Exception as ex:
self._backtest_label.value = f"图表加载失败: {ex}"
self._backtest_label.color = '#F44336'
self._backtest_label.update()
def _set_sync_btns_disabled(self, disabled: bool): def _set_sync_btns_disabled(self, disabled: bool):
for btn in self._sync_btns: for btn in self._sync_btns:
btn.disabled = disabled btn.disabled = disabled
@@ -713,40 +905,101 @@ class _DrawerPanel:
rows.append(ft.Divider(height=1, color='#f0f0f0')) rows.append(ft.Divider(height=1, color='#f0f0f0'))
self._dataset_col.controls = rows self._dataset_col.controls = rows
self._refresh_backtest()
# ── Tab 6: 每日评分 ── # ── Tab 6: 每日评分 ──
def _build_scoring_tab(self) -> ft.Control: def _build_scoring_tab(self) -> ft.Control:
from datetime import date, timedelta from datetime import timedelta
self._score_cur_date = date.today() 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_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_status = ft.Text("无评分数据", size=12, color='#888888')
self._score_refreshing = False
self._score_lock = threading.Lock()
self._score_max_date = today # 导航上限:不允许看未来日期
def _prev_day(_): def _prev_day(_):
if not self._score_lock.acquire(blocking=False):
return
self._score_cur_date -= timedelta(days=1) self._score_cur_date -= timedelta(days=1)
self._score_date_label.value = str(self._score_cur_date) self._score_date_label.value = str(self._score_cur_date)
self._score_date_label.update() 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._refresh_scoring()
def _next_day(_): 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_cur_date += timedelta(days=1)
self._score_date_label.value = str(self._score_cur_date) self._score_date_label.value = str(self._score_cur_date)
self._score_date_label.update() 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._refresh_scoring()
def _today(_): 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.value = str(self._score_cur_date)
self._score_date_label.update() 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._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([ date_row = ft.Row([
ft.IconButton(ft.Icons.CHEVRON_LEFT, icon_size=22, tooltip="前一天", on_click=_prev_day), prev_btn,
self._score_date_label, self._score_date_label,
ft.IconButton(ft.Icons.CHEVRON_RIGHT, icon_size=22, tooltip="后一天", on_click=_next_day), next_btn,
ft.IconButton(ft.Icons.TODAY, icon_size=20, tooltip="回到今天", on_click=_today), today_btn,
ft.Container(ft.Divider(height=20), width=2), 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_sync(['kline', 'stocks', 'industry', 'market', 'sector']), height=32),
ft.ElevatedButton("执行评分", on_click=lambda e: self._run_scoring(), height=32), ft.ElevatedButton("执行评分", on_click=lambda e: self._run_scoring(), height=32),
ft.Container(ft.Divider(height=20), width=2),
ft.ElevatedButton("沉寂检测", on_click=lambda e: self._data._pool_manager.trigger_slumber(), height=32, tooltip="手动触发沉寂检测(阶段一标记)"),
ft.ElevatedButton("淘汰检测", on_click=lambda e: self._data._pool_manager.trigger_weekly_elim(), height=32, tooltip="手动触发周度淘汰检测(阶段一标记)"),
self._score_status, self._score_status,
], spacing=6, vertical_alignment=ft.CrossAxisAlignment.CENTER) ], spacing=6, vertical_alignment=ft.CrossAxisAlignment.CENTER)
return ft.Column([ return ft.Column([
@@ -763,34 +1016,68 @@ class _DrawerPanel:
from core.scoring.inference.scorer import GridSeekerPipeline from core.scoring.inference.scorer import GridSeekerPipeline
trade_date = self._score_cur_date trade_date = self._score_cur_date
# 检查当日 K线数据是否已同步 # 检查 K线数据同步到哪一天
from peewee import fn from peewee import fn
from core.scoring.models import KlineStock from core.scoring.models import KlineStock
latest_kline = KlineStock.select(fn.MAX(KlineStock.trade_date)).scalar() 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.color = '#F44336'
self._score_status.update() self._score_status.update()
return 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: try:
engine = GridSeekerPipeline() engine = GridSeekerPipeline()
rankings = engine.run(trade_date) rankings = engine.run(persist_date)
if not rankings.empty: if not rankings.empty:
engine.persist(rankings, trade_date) engine.persist(rankings, persist_date)
self._score_status.value = f"{trade_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: except FileNotFoundError:
self._score_status.value = "模型文件缺失" self._score_status.value = "模型文件缺失"
except Exception as ex: except Exception as ex:
self._score_status.value = f"失败: {ex}" self._score_status.value = f"失败: {ex}"
self._score_status.color = '#F44336'
self._score_status.update() self._score_status.update()
self._refresh_scoring() self._refresh_scoring()
threading.Thread(target=_do, daemon=True).start() threading.Thread(target=_do, daemon=True).start()
def _refresh_scoring(self): def _refresh_scoring(self):
import time as _time
from core.scoring.models import ScoringResult from core.scoring.models import ScoringResult
from core.qmt import qmtv 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 # (width, expand): 排名60 + 名称弹性 + 概率65 + 轮数58 + 堆叠概率65 + 操作32
_C = [(60, 0), (0, 1), (65, 1), (58, 1), (65, 1), (32, 1)] _C = [(60, 0), (0, 1), (65, 1), (58, 1), (65, 1), (32, 1)]
@@ -821,28 +1108,40 @@ class _DrawerPanel:
if not scored_rows: if not scored_rows:
self._score_status.value = f"{trade_date} 无评分数据" self._score_status.value = f"{trade_date} 无评分数据"
self._score_status.color = '#F44336' 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._scoring_table.controls = rows
# 一次性批量更新:标签 + 状态 + 导航按钮
self._score_date_label.update()
self._score_status.update() self._score_status.update()
self._revert_nav_btns()
return return
shown = 0 # 批量预加载: 1次DB查询 (替代原来每行1+1次,最快方案)
plain_codes = []
full_codes = []
for r in scored_rows: for r in scored_rows:
code = r['stock_code'] code = r['stock_code']
plain = code.split('.')[0] if '.' in code else code plain = code.split('.')[0] if '.' in code else code
# 过滤 ST plain_codes.append(plain)
from core.scoring.models import StockInfo full_codes.append(f'{plain}.SH' if plain.startswith(('6', '5', '9')) else f'{plain}.SZ')
if plain.startswith(('6', '5', '9')):
full_code = f'{plain}.SH' from core.scoring.models import StockInfo
else: st_map = {}
full_code = f'{plain}.SZ' name_map = {}
st = StockInfo.get_or_none(StockInfo.code == full_code) for row in StockInfo.select(StockInfo.code, StockInfo.listing_status, StockInfo.name).where(StockInfo.code.in_(full_codes)).dicts():
if st and st.listing_status == 'ST': 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 continue
name = '' name = name_map.get(plain, '')
try:
name = qmtv.getInstrumentName(plain)
except Exception:
pass
already_in = plain in self._data.stockCodeIdMap already_in = plain in self._data.stockCodeIdMap
shown += 1 shown += 1
@@ -873,7 +1172,26 @@ class _DrawerPanel:
self._scoring_table.controls = rows self._scoring_table.controls = rows
self._score_status.value = f"{trade_date} 候选 {shown}" self._score_status.value = f"{trade_date} 候选 {shown}"
self._score_status.color = '#4CAF50' self._score_status.color = '#4CAF50'
# 一次性批量更新:标签 + 状态 + 导航按钮
self._score_date_label.update()
self._score_status.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): def _on_add_from_scoring(self, stock_code: str, stock_name: str):
"""+ 按钮:从评分列表添加标的并打开网格配置""" """+ 按钮:从评分列表添加标的并打开网格配置"""
@@ -1173,8 +1491,14 @@ class QmtApp:
event_bus.subscribe(MarketDataUpdate, self._on_market_data) event_bus.subscribe(MarketDataUpdate, self._on_market_data)
event_bus.subscribe(EventMarketActiveSwitch, self._on_market_active) event_bus.subscribe(EventMarketActiveSwitch, self._on_market_active)
event_bus.subscribe(EventTradeTargetUpdate, lambda t: self._refresh_ui()) event_bus.subscribe(EventTradeTargetUpdate, lambda t: self._refresh_ui())
event_bus.subscribe(EventPoolMark, self._on_pool_mark)
threading.Thread(target=self._refresh_loop, daemon=True).start() threading.Thread(target=self._refresh_loop, daemon=True).start()
# 启动股票池管理器(阶段一:T日标记)
from core.sfgrid.pool_manager import PoolManager
self._data._pool_manager = PoolManager()
self._data._pool_manager.start()
def _show_error(self, msg: str): def _show_error(self, msg: str):
self.page.clean() self.page.clean()
self.page.add(ft.Container( self.page.add(ft.Container(
@@ -1278,6 +1602,36 @@ class QmtApp:
self._prices_loaded = True self._prices_loaded = True
self._refresh_ui() self._refresh_ui()
def _on_pool_mark(self, data: dict):
"""响应股票池标记事件,在日志区显示"""
action = data.get('action', '')
count = data.get('count', 0)
codes = data.get('stock_codes', [])
if action == 'scoring':
msg = f'[池] 评分完成,共 {count} 只候选'
elif action == 'liquidate':
msg = f'[池] 沉寂检测完成,标记 {count} 只股: {", ".join(codes)}'
elif action == 'eliminate':
msg = f'[池] 周度淘汰检测完成,标记 {count} 只股: {", ".join(codes)}'
else:
msg = f'[池] 未知事件: {data}'
self._data.poolLog.append(msg)
# 追加到 UI 日志
if hasattr(self, '_pool_log_text') and self._pool_log_text:
log = self._pool_log_text
log.value = '\n'.join(self._data.poolLog[-200:]) if self._data.poolLog else ''
log.update()
from core.logger import PrintLog, LogLevel
PrintLog(LogLevel.INFO, msg)
def trigger_pool_slumber(self):
"""供 UI 按钮手动触发沉寂检测"""
self._data._pool_manager.trigger_slumber()
def trigger_pool_elim(self):
"""供 UI 按钮手动触淘汰检测"""
self._data._pool_manager.trigger_weekly_elim()
def _on_market_active(self, is_active: bool): def _on_market_active(self, is_active: bool):
from core.logger import PrintLog, LogLevel from core.logger import PrintLog, LogLevel
PrintLog(LogLevel.INFO, f'[UI] 收到市场状态切换事件 is_active={is_active}') PrintLog(LogLevel.INFO, f'[UI] 收到市场状态切换事件 is_active={is_active}')
View File
-142
View File
@@ -1,142 +0,0 @@
import tkinter as tk
from tkinter import ttk
from core.logger import LogLevel, LogData, PrintLog
from core.ui.tkinter.sfgrid_view import TradeTargetUI
# 检测运行环境,决定使用真实或模拟 QMT
def get_qmt_module():
try:
# 尝试导入真实 QMT,如果失败则使用模拟
from core.qmt import qmtv
return qmtv
except ImportError:
from core.qmt_dummy import qmtv
return qmtv
qmtv = get_qmt_module()
from core.eventbus import EventPrintLog
from core.eventbus import event_bus as eBus
class MainWindow:
def __init__(self, configLogLevel:str, progress=None):
self.root = tk.Tk()
self.root.title("神之一手 - 交易系统")
self.root.geometry("1400x700")
self.logLevel = LogLevel[configLogLevel]
PrintLog(LogLevel.DEBUG, f"系统启动成功 {self.logLevel.name}")
# 存储各个Frame的引用
self.strategy_frames = {}
# 日志面板可见性标志
self.log_visible = False
self.create_ui(progress)
eBus.subscribe(EventPrintLog, self.on_log_event)
def create_ui(self, progress=None):
"""创建UI界面"""
# 主容器
main_container = ttk.Frame(self.root)
main_container.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# 中间主体区域
content_area = ttk.Frame(main_container)
content_area.pack(fill=tk.BOTH, expand=True)
# 右侧内容区域容器
self.content_container = ttk.Frame(content_area)
self.content_container.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
# 创建策略Frame
strategy_names = ["网格"]
self.create_strategy_frames(strategy_names, progress)
# 创建全局日志面板(默认隐藏)
self.create_global_log_panel(main_container)
# 默认显示第一个策略
self.show_strategy_frame(0)
def create_global_log_panel(self, parent):
"""创建全局日志面板"""
# 日志区域(默认隐藏)
self.log_frame = ttk.LabelFrame(parent, text="操作日志", padding=10)
# 默认不显示,通过工具栏按钮控制
# 创建日志表格
columns = ("timestamp", "level", "message")
self.log_table = ttk.Treeview(self.log_frame, columns=columns, show='headings', height=8)
log_column_configs = {
"timestamp": ("时间", 100),
"level": ("级别", 50),
"message": ("消息", 1150) # 调整宽度适应全局布局
}
for col in columns:
title, width = log_column_configs[col]
self.log_table.heading(col, text=title)
self.log_table.column(col, width=width, anchor=tk.W)
# 添加初始日志
from datetime import datetime
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
self.log_table.insert('', tk.END, values=(timestamp, "INFO", "系统启动成功"))
# 滚动条
scrollbar = ttk.Scrollbar(self.log_frame, orient=tk.VERTICAL, command=self.log_table.yview)
self.log_table.configure(yscrollcommand=scrollbar.set)
self.log_table.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
def on_log_event(self, event:LogData):
if self.logLevel.value <= event.level.value:
self.add_log(event.level, event.message)
def add_log(self, level:LogLevel, message):
"""添加日志记录 - 全局方法"""
from datetime import datetime
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
self.log_table.insert('', 0, values=(timestamp, level.name, message))
def clear_logs(self):
"""清空日志记录"""
# 删除所有日志项
for item in self.log_table.get_children():
self.log_table.delete(item)
def create_strategy_frames(self, strategy_names, progress=None):
"""创建各个策略的Frame"""
frame = TradeTargetUI(self.content_container, progress=progress)
self.strategy_frames[0] = frame
def show_strategy_frame(self, index):
"""显示策略Frame"""
if index in self.strategy_frames:
self.strategy_frames[index].pack(fill=tk.BOTH, expand=True)
def toggle_log_panel(self):
"""切换日志面板的显示/隐藏"""
if self.log_visible:
self.log_frame.pack_forget()
self.log_visible = False
else:
self.log_frame.pack(side=tk.BOTTOM, fill=tk.X, pady=(5, 0))
self.log_visible = True
def on_exit(self):
"""退出程序"""
from tkinter import messagebox
result = messagebox.askyesno("确认退出", "确定要退出系统吗?")
if result:
self.root.destroy()
def run(self):
"""运行程序"""
self.root.mainloop()
File diff suppressed because it is too large Load Diff
-112
View File
@@ -1,112 +0,0 @@
"""
启动进度窗口 — 无边框小窗口,负责整个初始化流程。
"""
import time
import tkinter as tk
from tkinter import ttk, messagebox
class SplashWindow:
"""初始化进度窗口,所有者启动逻辑"""
def __init__(self):
self.root = tk.Tk()
self.root.title("神之一手")
self.root.geometry("380x120")
self.root.resizable(False, False)
self.root.overrideredirect(True)
self.root.update_idletasks()
sw = self.root.winfo_screenwidth()
sh = self.root.winfo_screenheight()
w, h = 380, 120
self.root.geometry(f"{w}x{h}+{(sw - w) // 2}+{(sh - h) // 2}")
frame = ttk.Frame(self.root, padding=20)
frame.pack(fill=tk.BOTH, expand=True)
ttk.Label(frame, text="神之一手", font=('Microsoft YaHei', 14, 'bold')).pack(pady=(0, 5))
self._status = ttk.Label(frame, text="正在初始化...", font=('Microsoft YaHei', 9))
self._status.pack(pady=(0, 10))
self._bar = ttk.Progressbar(frame, mode='determinate', length=340)
self._bar.pack()
self.root.update()
def progress(self, text: str, pct: float):
self._status.configure(text=text)
self._bar.configure(value=pct)
self.root.update()
def _destroy(self):
self.root.destroy()
def run(self):
"""执行完整启动流程,成功返回主窗口,失败返回 None"""
from core.qmt_real import RealQmtV, qmtv as selected_qmtv
while True:
_t_total = time.time()
# 步骤1: 探测 QMT 环境
self.progress("正在检查 QMT 环境...", 10)
_t = time.time()
try:
discovered = RealQmtV._discover_qmt_port()
except Exception:
discovered = 0
print(f'[计时] 步骤1-探测QMT环境: {time.time() - _t:.2f}s')
if not discovered:
self._destroy()
messagebox.showerror(
"启动失败",
"未能自动探测到 QMT 环境。\n\n"
"请确认:\n"
"1. 极简QMT(GJQMT)已启动并登录\n"
"2. XtMiniQmt.exe 和 miniquote.exe 进程在运行"
)
return None
# 步骤2: 初始化交易器
self.progress("正在初始化交易器...", 35)
_t = time.time()
selected_qmtv.init_qmtv()
print(f'[计时] 步骤2-初始化交易器: {time.time() - _t:.2f}s')
# 步骤3: 连接 QMT
self.progress("正在连接 QMT...", 55)
_t = time.time()
connected = selected_qmtv.connect()
print(f'[计时] 步骤3-连接QMT: {time.time() - _t:.2f}s')
if not connected:
self._destroy()
option = messagebox.askokcancel(
"连接失败",
"QMT 连接失败。\n\n"
"请确认极简QMT 已启动并登录交易账号。\n"
"点击「确定」重试,或「取消」退出。"
)
if not option:
return None
# 重试:重新创建进度窗口
self.__init__()
continue
# 步骤4: 加载主界面
self.progress("正在加载持仓与策略...", 75)
_t = time.time()
from core.ui.tkinter.main_window import MainWindow
window = MainWindow('INFO', progress=lambda t, p: self.progress(t, 75 + p * 0.2))
print(f'[计时] 步骤4-主界面加载: {time.time() - _t:.2f}s')
window.root.update()
# 步骤5: 完成
self.progress("启动完成", 100)
self.root.update()
self.root.after(300, self._destroy)
print(f'[计时] 总启动耗时: {time.time() - _t_total:.2f}s')
return window
-259
View File
@@ -1,259 +0,0 @@
from kuanke.wizard import *
from jqdata import *
import pandas as pd
import numpy as np
# ==================== 初始化 ====================
def initialize(context):
set_params(context)
# 开启防未来函数
set_option('avoid_future_data', True)
# 用真实价格交易
set_option('use_real_price', True)
# 过滤order中低于error级别的日志
log.set_level('order', 'error')
log.set_level('system', 'error')
log.set_level('strategy', 'debug')
set_benchmark('000001.XSHG')
set_order_cost(OrderCost(open_tax=0, close_tax=0.001, open_commission=0.0002, close_commission=0.0002, min_commission=5), type='stock')
set_slippage(FixedSlippage(0.01))
run_daily(before_trading, '9:30')
# -------------------- 参数设置 --------------------
def set_params(context):
context.max_price = 6
context.min_price = 5.01
context.grid_base_min = 1 # 最小价格
context.grid_base_max = 5 # 建仓价格
context.grid_interval = 0.5 # 下跌n元加仓
context.profit_target = 0.5 # 上涨n元清仓
context.min_stocks = 10
context.max_stocks = 25
context.base_max_stocks = 25
context.max_layers = 7
context.base_position_pct = 0.15
context.max_position_pct = 0.15
context.target_usage = 0.98
context.reserve_ratio = 0.02
context.first_round_max = 10
context.add_batch_size = 3
context.add_cash_threshold = 0.4
g.stock_pool = []
g.grid_info = {}
g.monitoring_stocks = set()
g.first_round_done = False
# ==================== 盘前 ====================
def before_trading(context):
january_clear(context)
if context.current_dt.month == 1:
return
stock_pool = get_stock_pool(context)
g.stock_pool = stock_pool
g.monitoring_stocks.update([s for s in stock_pool if s not in g.grid_info])
g.first_round_done = len(g.grid_info) >= context.first_round_max
# -------------------- 股票池 --------------------
def get_stock_pool(context):
# 1. 全部 A 股(不含退市)
df_sec = get_all_securities(types=['stock'], date=context.previous_date)
codes = list(df_sec.index)
# 2. 过滤 ST、科创板、北交所
def is_valid(code):
name = df_sec.loc[code, 'display_name']
if 'ST' in name or '退' in name or 'st' in name:
return False
if code.startswith('688'): # 科创板
return False
if code.startswith('83') or code.startswith('87') or code.startswith('9'): # 北交所
return False
return True
codes = [c for c in codes if is_valid(c)]
if not codes:
return []
# 3. 过滤停牌 & 价格区间
try:
price_df = get_price(codes,
end_date=context.current_dt,
count=1,
fields=['pre_close'],
panel=False)
if price_df is None or price_df.empty:
return []
# 过滤价格区间
price_df = price_df[
(price_df['pre_close'].notna()) &
(price_df['pre_close'] >= context.min_price) &
(price_df['pre_close'] <= context.max_price)
]
valid_codes = price_df['code'].tolist()
except Exception as e:
log.error(f"获取价格数据失败: {e}")
return []
if not valid_codes:
return []
# 4. 过滤停牌(开盘价缺失)
try:
open_df = get_price(valid_codes,
end_date=context.current_dt,
count=1,
fields=['open'],
panel=False)
if open_df is None or open_df.empty:
return []
# 过滤掉开盘价为空的股票
open_df = open_df[open_df['open'].notna()]
final_codes = open_df['code'].tolist()
except Exception as e:
log.error(f"获取开盘价数据失败: {e}")
return []
return final_codes
# -------------------- 一月清仓 --------------------
def january_clear(context):
if context.current_dt.month == 1:
log.info("进入1月,执行年度清仓...")
for stock in list(context.portfolio.positions.keys()):
order_target(stock, 0)
if stock in g.grid_info:
del g.grid_info[stock]
g.monitoring_stocks.add(stock)
# ==================== 盘中 ====================
def handle_data(context, data):
if context.current_dt.month == 1:
return
manage_positions(context, data)
usage = (context.portfolio.total_value - context.portfolio.available_cash) / context.portfolio.total_value
dynamic_max = get_dynamic_max_stocks(context)
if len(g.grid_info) < dynamic_max and usage < context.target_usage:
try_build_new(context, data)
# -------------------- 动态上限 --------------------
def get_dynamic_max_stocks(context):
return context.max_stocks if g.first_round_done else context.first_round_max
# -------------------- 建仓 --------------------
def try_build_new(context, data):
position_pct = context.max_position_pct
dynamic_max = get_dynamic_max_stocks(context)
count = 0
for stock in list(g.monitoring_stocks):
if len(g.grid_info) >= dynamic_max or count >= 3:
break
price = data[stock].close
if context.grid_base_min <= price <= context.grid_base_max:
total_value = context.portfolio.total_value
stock_amount = total_value * position_pct
grid = GridInfo(price, stock_amount, context.max_layers, context.grid_interval, context.profit_target)
layer_amount = grid.get_layer_amount(0)
buy_amount = int(layer_amount / price / 100) * 100
if buy_amount > 0:
order(stock, buy_amount)
grid.add_position(price, buy_amount, 0)
g.grid_info[stock] = grid
g.monitoring_stocks.discard(stock)
count += 1
log.info(f"[建仓] {stock} 价格{price:.2f} 数量{buy_amount}")
# -------------------- 管理持仓 --------------------
def manage_positions(context, data):
for stock, grid in list(g.grid_info.items()):
price = data[stock].close
# 止盈
sellable = grid.get_sellable_positions(price)
if sellable:
for idx, pos in reversed(sellable):
order(stock, -pos['amount'])
grid.remove_position(idx)
profit = (price - pos['price']) * pos['amount']
log.info(f"[止盈] {stock} 盈利{profit:.2f}")
# 加仓
layer = grid.should_add_layer(price)
if layer is not None:
layer_amount = grid.get_layer_amount(layer)
buy_amount = int(layer_amount / price / 100) * 100
if buy_amount > 0:
order(stock, buy_amount)
grid.add_position(price, buy_amount, layer)
log.info(f"[加仓] {stock} 层级{layer} 数量{buy_amount}")
else:
log.info(f"[加仓失败] {stock} 层级{layer} 金额不足")
# 清仓
if len(grid.positions) == 0:
del g.grid_info[stock]
g.monitoring_stocks.add(stock)
log.info(f"[清仓] {stock}")
# ==================== 盘后 ====================
def after_trading_end(context):
log.info(f"持仓数:{len(g.grid_info)},监控数:{len(g.monitoring_stocks)}")
# ==================== 网格类 ====================
class GridInfo:
def __init__(self, base_price, total_amount, max_layers, interval, profit_target):
self.base_price = float(base_price)
self.total_amount = float(total_amount)
self.max_layers = int(max_layers)
self.interval = float(interval)
self.profit_target = float(profit_target)
self.layer_prices = {i: base_price - i * interval for i in range(self.max_layers)}
self.layer_weights = self._calc_weights()
self.positions = []
def _calc_weights(self):
weights = {i: 1.0 + 0.05 * i for i in range(self.max_layers)}
total = sum(list(weights.values()))
return {k: v / total for k, v in weights.items()}
def get_layer_amount(self, layer):
return self.total_amount * self.layer_weights[layer]
def add_position(self, price, amount, layer):
self.positions.append({'price': price, 'amount': amount, 'layer': layer})
def get_sellable_positions(self, current_price):
return [(i, p) for i, p in enumerate(self.positions) if current_price >= p['price'] + self.profit_target]
def remove_position(self, index):
return self.positions.pop(index)
def should_add_layer(self, current_price):
for layer in range(self.max_layers):
target = self.layer_prices[layer]
diff = abs(current_price - target)
# 获取该层级的所有持仓
layer_positions = [p for p in self.positions if p['layer'] == layer]
has_position = len(layer_positions) > 0
if diff <= 0.1 and not has_position:
return layer
return None
-197
View File
@@ -1,197 +0,0 @@
# v6.6 模型发布说明
**版本**: v6.6
**发布日期**: 2026-06-16
**状态**: 生产就绪
---
## 一、模型架构
v6.6 采用 **Stacking Calibrated** 三层融合架构:
```
输入特征 (52维 v3.4)
┌─────────────────┐ ┌─────────────────┐
│ Rank 模型 │ │ Top 模型 │
│ LGBMRegressor │ │ LGBMClassifier │
│ 预测网格轮回次数 │ │ 分类精英股票 │
│ CV MAE: 0.2053 │ │ CV PR-AUC: 0.53│
│ CV R²: 0.2258 │ │ │
└────────┬────────┘ └────────┬────────┘
│ │
└──────────┬───────────┘
┌─────────────────────┐
│ Stacking 模型 │
│ LGBMClassifier │
│ CV PR-AUC: 0.8207 │
│ 最优阈值: 0.35 │
└──────────┬──────────┘
最终 top 概率
```
---
## 二、训练数据
| 指标 | 数值 |
|------|------|
| 特征版本 | v3.4 (52维) |
| 原始股票数 | 4,358 |
| 过滤后股票数 | 1,533 |
| 训练样本数 | 205,491 |
| 观察窗口 | 120天 |
| 预测窗口 | 60天 |
| 零触碰率 | 71.1% |
| Elite率 | 20.5% |
---
## 三、模型性能
### Rank模型 (回归)
| 指标 | 数值 |
|------|------|
| CV MAE | 0.2053 |
| CV R² | 0.2258 |
| 最优参数 | num_leaves=63, min_child_samples=30, max_depth=7 |
### Top模型 (分类)
| 指标 | 数值 |
|------|------|
| CV PR-AUC | 0.5333 |
| 最优参数 | num_leaves=63, min_child_samples=30, max_depth=-1 |
### Stacking融合模型
| 指标 | 数值 |
|------|------|
| CV PR-AUC | **0.8207** |
| 最优阈值 | 0.35 |
| 最优参数 | num_leaves=31, min_child_samples=20, max_depth=-1 |
---
## 四、Top 10 重要特征
### Rank模型
| 排名 | 特征 | 重要性% |
|------|------|---------|
| 1 | dist_to_grid_upper | 4.38% |
| 2 | dist_to_grid_lower | 4.26% |
| 3 | price_cv | 4.09% |
| 4 | amount_mean_20d | 3.98% |
| 5 | range_compression_20d | 3.48% |
### Stacking融合
| 排名 | 特征 | 重要性% |
|------|------|---------|
| 1 | rank_predicted_rounds | 20.87% |
| 2 | top_elite_prob | 19.38% |
| 3 | ma20_deviation_pct | 5.35% |
| 4 | atr_pct | 4.93% |
| 5 | dist_to_grid_lower | 3.75% |
---
## 五、周评分回测结果 (2023-04 ~ 2026-04)
| 指标 | v6.6 数值 |
|------|-----------|
| **总收益率** | **+36.79%** |
| 年化夏普 | 0.8494 |
| 最大回撤 | -12.70% |
| 胜率(周) | 48.7% |
| 交易次数 | 277 (147买, 130卖) |
| 最大持仓 | 10只 |
### 年度收益
| 年度 | 收益 |
|------|------|
| 2023 | +2.09% |
| 2024 | +28.34% |
| 2025 | +7.32% |
| 2026 | +0.52% |
---
## 六、与v6.5对比
| 指标 | v6.5 | v6.6 | 变化 |
|------|------|------|------|
| Stacking PR-AUC | 0.5234 | **0.8207** | +57% |
| 特征数 | 37维 | **52维** | +15维 |
| Top PR-AUC | 0.3923 | 0.5333 | +36% |
| 回测收益率 | 59.82% | 36.79%* | - |
| 回测最大回撤 | -33.47% | **-12.70%** | -62% |
\* v6.6使用周评分回测(157周),v6.5使用日评分回测(723天),粒度不同
---
## 七、模型文件
| 文件 | 说明 |
|------|------|
| `rank.pkl` | Rank模型 (预测网格轮回次数) |
| `top.pkl` | Top模型 (分类精英股票) |
| `stacking.pkl` | Stacking融合模型 |
| `rank_feature_importance.csv` | Rank特征重要性 |
| `top_feature_importance.csv` | Top特征重要性 |
| `stacking_feature_importance.csv` | Stacking特征重要性 |
| `training_summary.json` | 训练摘要 |
---
## 八、使用方式
```python
import pickle
import numpy as np
# 加载模型
with open("rank.pkl", "rb") as f:
rank_md = pickle.load(f)
with open("top.pkl", "rb") as f:
top_md = pickle.load(f)
with open("stacking.pkl", "rb") as f:
stacking_md = pickle.load(f)
# 预测
rank_pred = rank_md["model"].predict(features)
top_prob = top_md["model"].predict_proba(features)[:, 1]
stacking_prob = stacking_md["model"].predict_proba(features)[:, 1]
# 选股
threshold = 0.35 # stacking最优阈值
top_picks = scores[scores["stacking_prob"] >= threshold]
```
---
## 九、Registry配置
```yaml
# grid_seeker/registry.yaml
production:
version: v6.6
architecture: stacking_calibrated
models:
stacking: versions/6.6/output/stacking.pkl
rank: versions/6.6/output/rank.pkl
top: versions/6.6/output/top.pkl
feature_version: v3.4
training_samples: 205491
```
---
## 十、注意事项
1. **特征版本**: 必须使用 v3.4 特征(52维),与v6.5/v6.4不兼容
2. **Stacking阈值**: 推荐使用 0.35 作为选股阈值
3. **持股上限**: 建议不超过10只
4. **价格区间**: 适合7-10元区间股票
+727
View File
@@ -0,0 +1,727 @@
date,cash,holding_market_value,total_asset
2023-05-04,28289.86,33283.08,61572.94
2023-05-05,29353.86,32311.96,61665.82
2023-05-08,29553.86,32215.16,61769.020000000004
2023-05-09,27953.86,32879.84,60833.7
2023-05-10,26153.86,35374.479999999996,61528.34
2023-05-11,29753.86,33225.380000000005,62979.240000000005
2023-05-12,24353.86,37200.04,61553.9
2023-05-15,25667.86,35345.6,61013.46
2023-05-16,20867.86,38643.44,59511.3
2023-05-17,17667.86,42281.4,59949.26
2023-05-18,17667.86,43420.1,61087.96
2023-05-19,19999.86,40128.84,60128.7
2023-05-22,19136.34,40407.96,59544.3
2023-05-23,19136.34,39661.66,58798.0
2023-05-24,19136.34,39738.92,58875.259999999995
2023-05-25,17736.34,40531.479999999996,58267.81999999999
2023-05-26,17736.34,41098.4,58834.740000000005
2023-05-29,14136.34,44736.28,58872.619999999995
2023-05-30,14336.34,45939.42,60275.759999999995
2023-05-31,14336.34,45642.6,59978.94
2023-06-01,20433.92,41484.86,61918.78
2023-06-02,22233.92,40253.44,62487.36
2023-06-05,20683.899999999998,42477.36,63161.259999999995
2023-06-06,24283.899999999998,37810.38,62094.28
2023-06-07,22683.899999999998,40119.560000000005,62803.46000000001
2023-06-08,21083.899999999998,40887.58,61971.479999999996
2023-06-09,23083.899999999998,39432.4,62516.3
2023-06-12,23083.899999999998,39468.780000000006,62552.68000000001
2023-06-13,23083.899999999998,40270.58,63354.479999999996
2023-06-14,24883.899999999998,38792.08,63675.979999999996
2023-06-15,26883.899999999998,36531.32000000001,63415.22
2023-06-16,27171.899999999998,36662.04,63833.94
2023-06-19,30577.359999999997,33518.72,64096.08
2023-06-20,28977.359999999997,35500.700000000004,64478.06
2023-06-21,27177.359999999997,35306.82,62484.17999999999
2023-06-26,25315.46,35218.7,60534.159999999996
2023-06-27,20515.46,40464.58,60980.04
2023-06-28,20515.46,39961.2,60476.659999999996
2023-06-29,20515.46,40167.38,60682.84
2023-06-30,20515.46,40323.66,60839.12
2023-07-03,28555.76,32259.06,60814.82
2023-07-04,28555.76,32140.9,60696.66
2023-07-05,28555.76,31760.359999999997,60316.119999999995
2023-07-06,28555.76,32029.4,60585.16
2023-07-07,27155.76,33151.0,60306.759999999995
2023-07-10,30335.76,30607.5,60943.259999999995
2023-07-11,30335.76,29987.239999999998,60323.0
2023-07-12,27135.76,32715.800000000003,59851.56
2023-07-13,28735.76,32062.980000000003,60798.740000000005
2023-07-14,29091.76,31705.800000000003,60797.56
2023-07-17,27259.08,33179.8,60438.880000000005
2023-07-18,27259.08,33110.299999999996,60369.38
2023-07-19,27259.08,33071.82,60330.9
2023-07-20,27259.08,32716.16,59975.240000000005
2023-07-21,27608.82,32509.1,60117.92
2023-07-24,29909.840000000004,30081.24,59991.08
2023-07-25,29909.840000000004,30276.039999999997,60185.880000000005
2023-07-26,31797.840000000004,27988.0,59785.840000000004
2023-07-27,31797.840000000004,27916.859999999997,59714.7
2023-07-28,30397.840000000004,29960.780000000002,60358.62000000001
2023-07-31,31034.120000000003,29691.679999999997,60725.8
2023-08-01,31326.78,29226.62,60553.399999999994
2023-08-02,33126.78,27590.860000000004,60717.64
2023-08-03,31326.78,29124.66,60451.44
2023-08-04,31326.78,29340.219999999998,60667.0
2023-08-07,28402.46,32404.219999999998,60806.67999999999
2023-08-08,28402.46,32277.72,60680.18
2023-08-09,28602.46,31887.659999999996,60490.119999999995
2023-08-10,28602.46,32160.539999999997,60763.0
2023-08-11,28602.46,31713.8,60316.259999999995
2023-08-14,30216.02,30586.119999999995,60802.14
2023-08-15,30216.02,30597.48,60813.5
2023-08-16,32216.02,28753.199999999997,60969.22
2023-08-17,30416.02,31062.44,61478.46
2023-08-18,33136.700000000004,28257.34,61394.04000000001
2023-08-21,31336.700000000004,30055.36,61392.060000000005
2023-08-22,29536.700000000004,32326.899999999998,61863.600000000006
2023-08-23,29536.700000000004,31548.5,61085.200000000004
2023-08-24,29536.700000000004,31743.98,61280.68000000001
2023-08-25,26136.700000000004,34300.62,60437.32000000001
2023-08-28,29708.780000000006,31279.519999999997,60988.3
2023-08-29,30196.780000000006,32665.699999999997,62862.48
2023-08-30,32196.780000000006,31247.879999999997,63444.66
2023-08-31,34196.780000000006,29057.66,63254.44
2023-09-01,33946.780000000006,29311.820000000003,63258.600000000006
2023-09-04,34135.520000000004,29560.14,63695.66
2023-09-05,34509.780000000006,29185.22,63695.00000000001
2023-09-06,32709.780000000006,30561.239999999998,63271.020000000004
2023-09-07,33082.920000000006,30092.64,63175.560000000005
2023-09-08,29682.920000000006,33243.98,62926.90000000001
2023-09-11,28129.100000000006,35377.44,63506.54000000001
2023-09-12,30129.100000000006,33934.32,64063.420000000006
2023-09-13,31929.100000000006,32089.72,64018.82000000001
2023-09-14,31929.100000000006,31721.36,63650.46000000001
2023-09-15,33929.100000000006,29926.239999999998,63855.340000000004
2023-09-18,35984.3,28165.4,64149.700000000004
2023-09-19,36370.3,27593.84,63964.14
2023-09-20,36917.340000000004,27446.239999999998,64363.58
2023-09-21,35117.340000000004,29112.16,64229.5
2023-09-22,35117.340000000004,29932.44,65049.78
2023-09-25,35673.340000000004,30188.22,65861.56
2023-09-26,33873.340000000004,31843.26,65716.6
2023-09-27,33873.340000000004,32595.0,66468.34
2023-09-28,39873.340000000004,27709.4,67582.74
2023-10-09,41247.340000000004,26008.479999999996,67255.82
2023-10-10,43605.08,24212.319999999996,67817.4
2023-10-11,43805.08,23993.4,67798.48000000001
2023-10-12,44083.08,23915.480000000003,67998.56
2023-10-13,44083.08,23683.52,67766.6
2023-10-16,43689.08,23793.599999999995,67482.68
2023-10-17,42089.08,25216.94,67306.02
2023-10-18,38489.08,28344.4,66833.48000000001
2023-10-19,36689.08,29789.940000000002,66479.02
2023-10-20,33289.08,32655.699999999997,65944.78
2023-10-23,31442.520000000004,33523.66,64966.18000000001
2023-10-24,29642.520000000004,36040.58,65683.1
2023-10-25,33642.520000000004,33017.58,66660.1
2023-10-26,34288.520000000004,33149.32,67437.84
2023-10-27,36088.520000000004,31940.920000000002,68029.44
2023-10-30,37285.62,32011.34,69296.96
2023-10-31,39938.9,29558.3,69497.2
2023-11-01,38429.700000000004,30972.719999999994,69402.42
2023-11-02,40429.700000000004,28891.5,69321.20000000001
2023-11-03,40429.700000000004,29377.8,69807.5
2023-11-06,41176.26,29848.48,71024.74
2023-11-07,45432.26,26553.34,71985.6
2023-11-08,45782.26,26698.7,72480.96
2023-11-09,48079.740000000005,24910.94,72990.68000000001
2023-11-10,46479.740000000005,25900.26,72380.0
2023-11-13,48501.3,24292.92,72794.22
2023-11-14,46701.3,26683.64,73384.94
2023-11-15,46957.340000000004,26097.679999999997,73055.02
2023-11-16,47157.340000000004,26279.88,73437.22
2023-11-17,49341.340000000004,24523.559999999998,73864.9
2023-11-20,51584.740000000005,23124.5,74709.24
2023-11-21,51940.740000000005,22778.399999999998,74719.14
2023-11-22,49054.740000000005,26431.32,75486.06
2023-11-23,50182.740000000005,25830.2,76012.94
2023-11-24,49700.740000000005,27047.760000000002,76748.5
2023-11-27,52958.340000000004,24999.68,77958.02
2023-11-28,46244.340000000004,30826.699999999997,77071.04000000001
2023-11-29,41844.340000000004,33468.94,75313.28
2023-11-30,37244.340000000004,37343.26,74587.6
2023-12-01,33892.340000000004,39456.38,73348.72
2023-12-04,29451.340000000004,43211.36,72662.70000000001
2023-12-05,36811.340000000004,40589.1,77400.44
2023-12-06,43811.340000000004,34448.659999999996,78260.0
2023-12-07,49721.340000000004,30807.46,80528.8
2023-12-08,50831.340000000004,30194.3,81025.64
2023-12-11,41721.94,37976.44,79698.38
2023-12-12,41967.94,38844.28,80812.22
2023-12-13,41967.94,40205.56,82173.5
2023-12-14,48057.94,35801.66,83859.6
2023-12-15,52387.94,31884.0,84271.94
2023-12-18,54779.94,31022.0,85801.94
2023-12-19,56379.94,28964.0,85343.94
2023-12-20,58379.94,28260.0,86639.94
2023-12-21,60909.94,24672.0,85581.94
2023-12-22,52909.94,32946.0,85855.94
2023-12-25,54909.94,32782.0,87691.94
2023-12-26,54909.94,33254.0,88163.94
2023-12-27,57197.94,31222.0,88419.94
2023-12-28,55761.94,33084.0,88845.94
2023-12-29,56387.94,33956.0,90343.94
2024-01-02,62813.94,29938.0,92751.94
2024-01-03,65205.94,27608.0,92813.94
2024-01-04,63405.94,29272.0,92677.94
2024-01-05,56405.94,34602.0,91007.94
2024-01-08,58405.94,31830.0,90235.94
2024-01-09,48405.94,40204.0,88609.94
2024-01-10,43605.94,45832.0,89437.94
2024-01-11,45405.94,44096.0,89501.94
2024-01-12,42405.94,42438.0,84843.94
2024-01-15,35605.94,46922.0,82527.94
2024-01-16,34005.94,52684.0,86689.94
2024-01-17,37205.94,49538.0,86743.94
2024-01-18,39005.94,49904.0,88909.94
2024-01-19,44405.94,46458.0,90863.94
2024-01-22,45355.94,42936.0,88291.94
2024-01-23,45355.94,44114.0,89469.94
2024-01-24,47895.94,42334.0,90229.94
2024-01-25,46695.94,42518.0,89213.94
2024-01-26,43543.94,44782.0,88325.94
2024-01-29,39113.94,44472.0,83585.94
2024-01-30,33513.94,50754.0,84267.94
2024-01-31,40113.94,46114.0,86227.94
2024-02-01,38705.94,44274.0,82979.94
2024-02-02,27305.940000000002,52690.0,79995.94
2024-02-05,22445.940000000002,55230.0,77675.94
2024-02-06,27245.940000000002,56198.0,83443.94
2024-02-07,36845.94,46666.0,83511.94
2024-02-08,31419.940000000002,51926.0,83345.94
2024-02-19,36311.94,48834.0,85145.94
2024-02-20,39861.28,47910.66,87771.94
2024-02-21,39861.28,48018.96,87880.23999999999
2024-02-22,41861.28,45818.24,87679.51999999999
2024-02-23,41861.28,44551.96,86413.23999999999
2024-02-26,45030.24,41690.74,86720.98
2024-02-27,48777.32,41789.439999999995,90566.76
2024-02-28,49161.3,39334.85999999999,88496.16
2024-02-29,45761.3,44599.96,90361.26000000001
2024-03-01,49575.3,40919.28,90494.58
2024-03-04,52677.94,37579.03999999999,90256.98
2024-03-05,51277.94,38087.04,89364.98000000001
2024-03-06,53277.94,36919.08,90197.02
2024-03-07,53677.94,35949.98,89627.92000000001
2024-03-08,53677.94,35737.8,89415.74
2024-03-11,57519.94,32557.06,90077.0
2024-03-12,59847.920000000006,30947.68,90795.6
2024-03-13,59847.920000000006,30429.56,90277.48000000001
2024-03-14,60047.920000000006,30268.980000000003,90316.90000000001
2024-03-15,60047.920000000006,31026.780000000002,91074.70000000001
2024-03-18,62559.920000000006,29269.68,91829.6
2024-03-19,62905.560000000005,29223.520000000004,92129.08000000002
2024-03-20,65165.560000000005,27477.68,92643.24
2024-03-21,65873.84,26703.800000000003,92577.64
2024-03-22,65873.84,26076.62,91950.45999999999
2024-03-25,61065.36,29863.06,90928.42
2024-03-26,59265.36,31568.499999999996,90833.86
2024-03-27,55865.36,34504.659999999996,90370.01999999999
2024-03-28,60401.36,30693.719999999998,91095.08
2024-03-29,60401.36,30781.6,91182.95999999999
2024-04-01,66053.02,26153.920000000002,92206.94
2024-04-02,66617.02,25795.5,92412.52
2024-04-03,65179.58,26641.44,91821.02
2024-04-08,63035.58,27350.300000000003,90385.88
2024-04-09,61435.58,29304.18,90739.76000000001
2024-04-10,56635.58,33710.5,90346.08
2024-04-11,56903.58,32962.08,89865.66
2024-04-12,56903.58,32507.18,89410.76000000001
2024-04-15,45899.58,40168.12,86067.70000000001
2024-04-16,36699.58,44627.16,81326.74
2024-04-17,33499.58,52978.979999999996,86478.56
2024-04-18,33499.58,53139.6,86639.18
2024-04-19,33499.58,52126.22,85625.8
2024-04-22,35961.58,49286.22,85247.8
2024-04-23,39761.58,47887.979999999996,87649.56
2024-04-24,41361.58,47785.3,89146.88
2024-04-25,41161.58,47469.36,88630.94
2024-04-26,42761.58,46693.66,89455.24
2024-04-29,46142.72,44846.68,90989.4
2024-04-30,51342.72,39697.380000000005,91040.1
2024-05-06,59221.060000000005,32499.46,91720.52
2024-05-07,59421.060000000005,32774.7,92195.76000000001
2024-05-08,59805.060000000005,31778.78,91583.84
2024-05-09,58005.060000000005,33495.740000000005,91500.80000000002
2024-05-10,56205.060000000005,34632.66,90837.72
2024-05-13,54405.060000000005,35785.520000000004,90190.58000000002
2024-05-14,56405.060000000005,34677.5,91082.56
2024-05-15,56791.060000000005,34668.46,91459.52
2024-05-16,58591.060000000005,33183.68,91774.74
2024-05-17,60869.060000000005,31647.16,92516.22
2024-05-20,60891.060000000005,31465.64,92356.70000000001
2024-05-21,61091.060000000005,31509.34,92600.40000000001
2024-05-22,61405.060000000005,31812.32,93217.38
2024-05-23,61405.060000000005,32031.88,93436.94
2024-05-24,59605.060000000005,32880.44,92485.5
2024-05-27,59387.50000000001,33578.0,92965.5
2024-05-28,63387.50000000001,29672.0,93059.5
2024-05-29,61787.50000000001,31334.0,93121.5
2024-05-30,64141.5,28916.0,93057.5
2024-05-31,64141.5,29130.0,93271.5
2024-06-03,59560.399999999994,32518.96,92079.35999999999
2024-06-04,57760.399999999994,33912.6,91673.0
2024-06-05,56160.399999999994,34984.96,91145.35999999999
2024-06-06,47960.399999999994,40566.12,88526.51999999999
2024-06-07,47960.399999999994,42130.56,90090.95999999999
2024-06-11,50552.399999999994,40529.64,91082.04
2024-06-12,54152.399999999994,37975.64,92128.04
2024-06-13,56152.399999999994,36528.12,92680.51999999999
2024-06-14,56152.399999999994,36035.36,92187.76
2024-06-17,58145.759999999995,33718.0,91863.76
2024-06-18,60345.759999999995,32524.0,92869.76
2024-06-19,64277.759999999995,29616.0,93893.76
2024-06-20,59921.759999999995,33686.0,93607.76
2024-06-21,62239.759999999995,31984.0,94223.76
2024-06-24,56927.759999999995,35350.0,92277.76
2024-06-25,53927.759999999995,37340.0,91267.76
2024-06-26,52527.759999999995,40366.0,92893.76
2024-06-27,52527.759999999995,39326.0,91853.76
2024-06-28,52527.759999999995,40070.0,92597.76
2024-07-01,57551.759999999995,35500.0,93051.76
2024-07-02,57551.759999999995,36244.0,93795.76
2024-07-03,57551.759999999995,36146.0,93697.76
2024-07-04,55751.759999999995,36268.0,92019.76
2024-07-05,52351.759999999995,40466.0,92817.76
2024-07-08,53531.759999999995,38242.0,91773.76
2024-07-09,52891.759999999995,40390.0,93281.76
2024-07-10,51451.719999999994,41648.04,93099.76
2024-07-11,51451.719999999994,42945.86,94397.57999999999
2024-07-12,51451.719999999994,42662.64,94114.35999999999
2024-07-15,52778.87999999999,40392.92,93171.79999999999
2024-07-16,52778.87999999999,41218.92,93997.79999999999
2024-07-17,57090.87999999999,36641.42,93732.29999999999
2024-07-18,55290.87999999999,38284.96,93575.84
2024-07-19,55290.87999999999,38665.44,93956.31999999999
2024-07-22,60996.87999999999,34089.5,95086.37999999999
2024-07-23,60996.87999999999,33558.479999999996,94555.35999999999
2024-07-24,62796.87999999999,31740.760000000002,94537.63999999998
2024-07-25,61256.87999999999,33841.24,95098.12
2024-07-26,61456.87999999999,33980.7,95437.57999999999
2024-07-29,59777.73999999999,35923.119999999995,95700.85999999999
2024-07-30,61777.73999999999,34529.72,96307.45999999999
2024-07-31,64101.73999999999,33334.0,97435.73999999999
2024-08-01,65125.73999999999,32018.0,97143.73999999999
2024-08-02,61925.73999999999,34468.0,96393.73999999999
2024-08-05,62605.73999999999,32892.0,95497.73999999999
2024-08-06,62605.73999999999,34194.0,96799.73999999999
2024-08-07,64405.73999999999,32874.0,97279.73999999999
2024-08-08,64605.73999999999,31938.0,96543.73999999999
2024-08-09,63005.73999999999,33154.0,96159.73999999999
2024-08-12,61315.73999999999,34852.0,96167.73999999999
2024-08-13,59515.73999999999,36410.0,95925.73999999999
2024-08-14,59515.73999999999,35968.0,95483.73999999999
2024-08-15,58515.73999999999,37140.0,95655.73999999999
2024-08-16,56715.73999999999,39058.0,95773.73999999999
2024-08-19,56764.19999999999,38072.64,94836.84
2024-08-20,56764.19999999999,38040.52,94804.71999999999
2024-08-21,55164.19999999999,38808.92,93973.12
2024-08-22,53564.19999999999,39192.04,92756.23999999999
2024-08-23,55364.19999999999,37672.44,93036.63999999998
2024-08-26,56940.25999999999,36384.899999999994,93325.15999999997
2024-08-27,57540.25999999999,35327.28,92867.53999999998
2024-08-28,52940.25999999999,39837.560000000005,92777.81999999999
2024-08-29,51892.25999999999,41458.62,93350.87999999999
2024-08-30,52183.51999999999,42771.06,94954.57999999999
2024-09-02,56710.33999999999,37626.06,94336.4
2024-09-03,58710.33999999999,36281.9,94992.23999999999
2024-09-04,58710.33999999999,35629.16,94339.5
2024-09-05,58710.33999999999,36421.92,95132.25999999998
2024-09-06,58710.33999999999,35878.58,94588.91999999998
2024-09-09,58906.33999999999,35322.54000000001,94228.88
2024-09-10,57306.33999999999,37236.6,94542.93999999999
2024-09-11,57306.33999999999,36978.36,94284.69999999998
2024-09-12,62642.33999999999,31378.359999999997,94020.69999999998
2024-09-13,62642.33999999999,30741.5,93383.84
2024-09-18,58008.74,34825.06,92833.79999999999
2024-09-19,58359.32,35364.84,93724.16
2024-09-20,58359.32,34882.94,93242.26000000001
2024-09-23,59825.32,33529.54,93354.86
2024-09-24,59825.32,35009.0,94834.32
2024-09-25,62217.32,33031.08,95248.4
2024-09-26,62723.32000000001,33992.1,96715.42000000001
2024-09-27,69235.32,29121.08,98356.40000000001
2024-09-30,80349.32,21369.96,101719.28
2024-10-08,85077.28,19476.0,104553.28
2024-10-09,77483.28,25692.0,103175.28
2024-10-10,65483.28,35852.0,101335.28
2024-10-11,56083.28,43112.0,99195.28
2024-10-14,54845.28,47550.0,102395.28
2024-10-15,62245.28,40032.0,102277.28
2024-10-16,64429.28,38264.0,102693.28
2024-10-17,71101.28,33832.0,104933.28
2024-10-18,71101.28,35670.0,106771.28
2024-10-21,81899.28,27310.0,109209.28
2024-10-22,78499.28,29890.0,108389.28
2024-10-23,79145.28,31210.0,110355.28
2024-10-24,83457.28,27304.0,110761.28
2024-10-25,87055.28,25324.0,112379.28
2024-10-28,82369.28,32322.0,114691.28
2024-10-29,94723.28,21330.0,116053.28
2024-10-30,93926.66,22730.62,116657.28
2024-10-31,92874.66,23630.56,116505.22
2024-11-01,90986.66,26929.52,117916.18000000001
2024-11-04,88286.66,31429.6,119716.26000000001
2024-11-05,99332.66,22827.28,122159.94
2024-11-06,100274.82,22557.2,122832.02
2024-11-07,97836.82,26869.02,124705.84000000001
2024-11-08,100636.82,23667.66,124304.48000000001
2024-11-11,97436.82,27560.78,124997.6
2024-11-12,97436.82,27386.06,124822.88
2024-11-13,95636.82,29058.3,124695.12000000001
2024-11-14,97636.82,26820.22,124457.04000000001
2024-11-15,96557.20000000001,28499.719999999998,125056.92000000001
2024-11-18,99403.46,26804.239999999998,126207.70000000001
2024-11-19,87403.46,38321.020000000004,125724.48000000001
2024-11-20,91665.46,36476.68,128142.14000000001
2024-11-21,97525.46,31670.04,129195.5
2024-11-22,102164.48000000001,26932.02,129096.50000000001
2024-11-25,100326.48000000001,29415.52,129742.00000000001
2024-11-26,98726.48000000001,30748.68,129475.16
2024-11-27,97126.48000000001,32619.879999999997,129746.36000000002
2024-11-28,101950.1,29081.36,131031.46
2024-11-29,101950.1,29600.280000000002,131550.38
2024-12-02,108792.78,23990.58,132783.36
2024-12-03,105192.78,27452.98,132645.76
2024-12-04,103392.78,27856.059999999998,131248.84
2024-12-05,103392.78,28402.48,131795.26
2024-12-06,103392.78,28411.32,131804.1
2024-12-09,97964.12,32072.58,130036.7
2024-12-10,96164.12,34057.399999999994,130221.51999999999
2024-12-11,94564.12,35279.64,129843.76
2024-12-12,94844.12,35000.0,129844.12
2024-12-13,96844.12,33198.0,130042.12
2024-12-16,92244.12,37306.0,129550.12
2024-12-17,90444.12,36824.0,127268.12
2024-12-18,85644.12,41096.0,126740.12
2024-12-19,82444.12,45412.0,127856.12
2024-12-20,82444.12,44764.0,127208.12
2024-12-23,86146.12,38902.0,125048.12
2024-12-24,88234.12,37016.0,125250.12
2024-12-25,83634.12,39390.0,123024.12
2024-12-26,85918.12,38396.0,124314.12
2024-12-27,89318.12,35982.0,125300.12
2024-12-30,84450.12,39132.0,123582.12
2024-12-31,86250.12,38410.0,124660.12
2025-01-02,85608.12,39822.0,125430.12
2025-01-03,87408.12,37578.0,124986.12
2025-01-06,87180.12,36526.0,123706.12
2025-01-07,85580.12,39196.0,124776.12
2025-01-08,85580.12,39276.0,124856.12
2025-01-09,85580.12,40108.0,125688.12
2025-01-10,85580.12,38096.0,123676.12
2025-01-13,86472.12,36632.0,123104.12
2025-01-14,90534.12,35542.0,126076.12
2025-01-15,92964.12,33396.0,126360.12
2025-01-16,92964.12,33474.0,126438.12
2025-01-17,92964.12,32718.0,125682.12
2025-01-20,92444.12,33334.0,125778.12
2025-01-21,90644.12,34562.0,125206.12
2025-01-22,88844.12,35200.0,124044.12
2025-01-23,92964.12,31104.0,124068.12
2025-01-24,92964.12,30942.0,123906.12
2025-01-27,93264.12,29920.0,123184.12
2025-02-05,95550.12,28902.0,124452.12
2025-02-06,95386.12,30334.0,125720.12
2025-02-07,99682.12,27098.0,126780.12
2025-02-10,101682.12,26282.0,127964.12
2025-02-11,102255.36,25700.760000000002,127956.12
2025-02-12,106503.36,22042.74,128546.1
2025-02-13,106503.36,21696.8,128200.16
2025-02-14,106503.36,21418.78,127922.14
2025-02-17,106503.36,22392.739999999998,128896.1
2025-02-18,106863.36,21594.6,128457.95999999999
2025-02-19,107549.72,21399.64,128949.36
2025-02-20,105749.72,23389.28,129139.0
2025-02-21,105749.72,23782.4,129532.12
2025-02-24,107749.72,22060.94,129810.66
2025-02-25,106050.72,23940.18,129990.9
2025-02-26,106050.72,24078.079999999998,130128.8
2025-02-27,104528.72,25574.48,130103.2
2025-02-28,102728.72,26542.96,129271.68
2025-03-03,99240.72,30762.88,130003.6
2025-03-04,104273.36,27485.84,131759.2
2025-03-05,106519.36,25571.420000000002,132090.78
2025-03-06,103171.36,29194.32,132365.68
2025-03-07,105425.36,27040.4,132465.76
2025-03-10,105425.36,27684.64,133110.0
2025-03-11,105757.36,27476.72,133234.08000000002
2025-03-12,107757.36,26166.48,133923.84
2025-03-13,110301.82,23848.38,134150.2
2025-03-14,115013.82,19782.36,134796.18
2025-03-17,115373.82,19713.06,135086.88
2025-03-18,116287.82,19132.559999999998,135420.38
2025-03-19,112687.82,22446.72,135134.54
2025-03-20,111223.82,24020.72,135244.54
2025-03-21,109423.82,25211.120000000003,134634.94
2025-03-24,102104.94,32562.0,134666.94
2025-03-25,104460.1,30420.84,134880.94
2025-03-26,106460.1,29221.84,135681.94
2025-03-27,106660.1,28888.8,135548.9
2025-03-28,103822.1,31484.3,135306.4
2025-03-31,100434.62000000001,34067.08,134501.7
2025-04-01,101400.62000000001,32980.64,134381.26
2025-04-02,101600.62000000001,32563.239999999998,134163.86000000002
2025-04-03,99800.62000000001,34518.32,134318.94
2025-04-07,87000.62000000001,41837.2,128837.82
2025-04-08,80800.62000000001,49853.04,130653.66
2025-04-09,88600.62000000001,46010.08,134610.7
2025-04-10,98000.62000000001,38655.6,136656.22
2025-04-11,99600.62000000001,37063.76,136664.38
2025-04-14,106254.62000000001,31782.72,138037.34000000003
2025-04-15,104654.62000000001,33317.28,137971.90000000002
2025-04-16,106747.54000000001,31677.72,138425.26
2025-04-17,108747.54000000001,30180.46,138928.0
2025-04-18,109341.54000000001,30561.36,139902.90000000002
2025-04-21,107815.54000000001,32425.2,140240.74000000002
2025-04-22,107815.54000000001,31336.8,139152.34
2025-04-23,109415.54000000001,30456.64,139872.18
2025-04-24,109657.54000000001,29465.66,139123.2
2025-04-25,106835.54000000001,32281.18,139116.72
2025-04-28,106345.54000000001,32082.7,138428.24000000002
2025-04-29,102745.54000000001,35483.0,138228.54
2025-04-30,102745.54000000001,35759.3,138504.84000000003
2025-05-06,104510.84000000001,35040.0,139550.84000000003
2025-05-07,107283.84000000001,32649.0,139932.84000000003
2025-05-08,104437.84000000001,35792.24,140230.08000000002
2025-05-09,102637.84000000001,37585.88,140223.72
2025-05-12,103367.84000000001,37894.72,141262.56
2025-05-13,103367.84000000001,37302.54,140670.38
2025-05-14,103763.84000000001,36662.0,140425.84000000003
2025-05-15,101963.84000000001,38680.0,140643.84000000003
2025-05-16,101963.84000000001,38900.0,140863.84000000003
2025-05-19,101963.84000000001,39840.0,141803.84000000003
2025-05-20,105763.84000000001,36696.0,142459.84000000003
2025-05-21,107363.84000000001,35314.0,142677.84000000003
2025-05-22,109423.84000000001,33250.0,142673.84000000003
2025-05-23,109423.84000000001,32758.0,142181.84000000003
2025-05-26,110715.84000000001,32416.0,143131.84000000003
2025-05-27,114765.84000000001,28756.0,143521.84000000003
2025-05-28,118565.84000000001,24386.0,142951.84000000003
2025-05-29,118565.84000000001,24558.0,143123.84000000003
2025-05-30,115165.84000000001,27606.0,142771.84000000003
2025-06-03,113691.88,29479.86,143171.74
2025-06-04,117691.88,26375.22,144067.1
2025-06-05,118367.88,25461.82,143829.7
2025-06-06,118567.88,25523.98,144091.86000000002
2025-06-09,118468.06,26145.58,144613.64
2025-06-10,119242.06,26013.68,145255.74
2025-06-11,115642.06,29543.460000000003,145185.52
2025-06-12,119442.06,26731.8,146173.86
2025-06-13,121827.45999999999,23271.14,145098.59999999998
2025-06-16,118827.45999999999,26558.000000000004,145385.46
2025-06-17,119851.45999999999,25974.019999999997,145825.47999999998
2025-06-18,118051.45999999999,27032.4,145083.86
2025-06-19,112651.45999999999,31251.42,143902.88
2025-06-20,109651.45999999999,33717.020000000004,143368.47999999998
2025-06-23,108051.45999999999,36004.66,144056.12
2025-06-24,108051.45999999999,36450.659999999996,144502.12
2025-06-25,108567.45999999999,36190.72,144758.18
2025-06-26,108767.45999999999,36286.82,145054.28
2025-06-27,108767.45999999999,36246.7,145014.15999999997
2025-06-30,108321.45999999999,37252.479999999996,145573.94
2025-07-01,115200.18,31736.68,146936.86
2025-07-02,117302.28,30046.74,147349.02
2025-07-03,117302.28,29892.84,147195.12
2025-07-04,117302.28,29383.94,146686.22
2025-07-07,117586.28,29861.88,147448.16
2025-07-08,117586.28,29913.9,147500.18
2025-07-09,117912.28,29733.94,147646.22
2025-07-10,118112.28,29558.0,147670.28
2025-07-11,116312.28,31418.0,147730.28
2025-07-14,114316.28,33108.0,147424.28
2025-07-15,112716.28,34186.0,146902.28
2025-07-16,114716.28,32426.0,147142.28
2025-07-17,114960.2,32414.08,147374.28
2025-07-18,113160.2,33946.38,147106.58
2025-07-21,116704.2,30680.16,147384.36
2025-07-22,116980.59999999999,30577.559999999998,147558.15999999997
2025-07-23,116980.59999999999,30195.579999999998,147176.18
2025-07-24,115180.59999999999,32133.800000000003,147314.4
2025-07-25,115180.59999999999,32153.94,147334.53999999998
2025-07-28,115180.59999999999,32243.62,147424.22
2025-07-29,115180.59999999999,31921.92,147102.52
2025-07-30,115180.59999999999,31987.88,147168.47999999998
2025-07-31,113580.59999999999,33084.16,146664.76
2025-08-01,113316.59999999999,33357.8,146674.4
2025-08-04,116562.59999999999,30359.96,146922.56
2025-08-05,114762.59999999999,32258.28,147020.88
2025-08-06,114762.59999999999,32168.1,146930.69999999998
2025-08-07,114762.59999999999,32379.9,147142.5
2025-08-08,119086.59999999999,28390.0,147476.59999999998
2025-08-11,119218.59999999999,28752.0,147970.59999999998
2025-08-12,117418.59999999999,30502.0,147920.59999999998
2025-08-13,121062.59999999999,26722.0,147784.59999999998
2025-08-14,117662.59999999999,29492.0,147154.59999999998
2025-08-15,119662.59999999999,28798.0,148460.59999999998
2025-08-18,123252.59999999999,26458.0,149710.59999999998
2025-08-19,122090.59999999999,27948.0,150038.59999999998
2025-08-20,120290.59999999999,29984.0,150274.59999999998
2025-08-21,126290.59999999999,24298.0,150588.59999999998
2025-08-22,126652.59999999999,23932.0,150584.59999999998
2025-08-25,126652.59999999999,24162.0,150814.59999999998
2025-08-26,126852.59999999999,24104.0,150956.59999999998
2025-08-27,123646.59999999999,26804.0,150450.59999999998
2025-08-28,118640.59999999999,32296.0,150936.59999999998
2025-08-29,116840.59999999999,33900.0,150740.59999999998
2025-09-01,122140.01999999999,29354.24,151494.25999999998
2025-09-02,120726.01999999999,30304.0,151030.02
2025-09-03,117326.01999999999,32868.0,150194.02
2025-09-04,114126.01999999999,35582.0,149708.02
2025-09-05,114126.01999999999,36400.0,150526.02
2025-09-08,114126.01999999999,36670.0,150796.02
2025-09-09,114126.01999999999,35764.0,149890.02
2025-09-10,114126.01999999999,36264.0,150390.02
2025-09-11,116326.01999999999,34852.0,151178.02
2025-09-12,116580.01999999999,34638.0,151218.02
2025-09-15,116840.01999999999,34224.0,151064.02
2025-09-16,116840.01999999999,34266.0,151106.02
2025-09-17,116840.01999999999,34016.0,150856.02
2025-09-18,115522.01999999999,34918.0,150440.02
2025-09-19,115522.01999999999,34548.0,150070.02
2025-09-22,116524.01999999999,33574.0,150098.02
2025-09-23,111924.01999999999,37476.0,149400.02
2025-09-24,112264.01999999999,37360.0,149624.02
2025-09-25,112264.01999999999,36962.0,149226.02
2025-09-26,112264.01999999999,36744.0,149008.02
2025-09-29,116726.01999999999,32582.0,149308.02
2025-09-30,116726.01999999999,32560.0,149286.02
2025-10-09,118370.01999999999,30188.0,148558.02
2025-10-10,116570.01999999999,31992.0,148562.02
2025-10-13,112600.01999999999,36462.0,149062.02
2025-10-14,112600.01999999999,36110.0,148710.02
2025-10-15,112600.01999999999,36570.0,149170.02
2025-10-16,114600.01999999999,34026.0,148626.02
2025-10-17,113000.01999999999,34894.0,147894.02
2025-10-20,112998.01999999999,35922.0,148920.02
2025-10-21,114998.01999999999,34854.0,149852.02
2025-10-22,114998.01999999999,34606.0,149604.02
2025-10-23,113198.01999999999,36532.0,149730.02
2025-10-24,113198.01999999999,36844.0,150042.02
2025-10-27,116430.01999999999,33332.0,149762.02
2025-10-28,116430.01999999999,33280.0,149710.02
2025-10-29,116430.01999999999,33934.0,150364.02
2025-10-30,116430.01999999999,33806.0,150236.02
2025-10-31,116430.01999999999,34148.0,150578.02
2025-11-03,121148.01999999999,29008.0,150156.02
2025-11-04,120108.01999999999,30110.0,150218.02
2025-11-05,120108.01999999999,30552.0,150660.02
2025-11-06,118308.01999999999,32286.0,150594.02
2025-11-07,118702.01999999999,31342.0,150044.02
2025-11-10,118702.01999999999,31688.0,150390.02
2025-11-11,123102.01999999999,27790.0,150892.02
2025-11-12,118426.01999999999,32408.0,150834.02
2025-11-13,119016.01999999999,32752.0,151768.02
2025-11-14,119016.01999999999,33214.0,152230.02
2025-11-17,123270.01999999999,29114.0,152384.02
2025-11-18,119670.01999999999,31876.0,151546.02
2025-11-19,118070.01999999999,33268.0,151338.02
2025-11-20,116470.01999999999,34108.0,150578.02
2025-11-21,109870.01999999999,38562.0,148432.02
2025-11-24,109938.01999999999,38512.0,148450.02
2025-11-25,111938.01999999999,37762.0,149700.02
2025-11-26,111938.01999999999,37170.0,149108.02
2025-11-27,111938.01999999999,37172.0,149110.02
2025-11-28,111938.01999999999,37834.0,149772.02
2025-12-01,115825.68,33942.38,149768.06
2025-12-02,115825.68,33994.38,149820.06
2025-12-03,117825.68,31736.9,149562.58
2025-12-04,112425.68,36213.44,148639.12
2025-12-05,109425.68,40135.04,149560.72
2025-12-08,112603.68,37374.88,149978.56
2025-12-09,116729.68,33392.68,150122.36
2025-12-10,117313.68,33230.36,150544.03999999998
2025-12-11,115769.68,34872.0,150641.68
2025-12-12,115769.68,35290.0,151059.68
2025-12-15,118523.68,33424.0,151947.68
2025-12-16,118723.68,32706.0,151429.68
2025-12-17,111723.68,39646.0,151369.68
2025-12-18,111723.68,39308.0,151031.68
2025-12-19,110123.68,41948.0,152071.68
2025-12-22,116025.68,36470.0,152495.68
2025-12-23,116225.68,36004.0,152229.68
2025-12-24,115195.68,37596.0,152791.68
2025-12-25,115571.68,37138.0,152709.68
2025-12-26,115371.68,37724.0,153095.68
2025-12-29,118263.68,34576.0,152839.68
2025-12-30,116959.68,35302.0,152261.68
2025-12-31,115159.68,36376.0,151535.68
2026-01-05,114658.76,36630.8,151289.56
2026-01-06,113458.76,38219.44,151678.2
2026-01-07,113854.76,37808.0,151662.76
2026-01-08,112054.76,40598.0,152652.76
2026-01-09,113854.76,39384.0,153238.76
2026-01-12,114728.76,38852.0,153580.76
2026-01-13,120616.76,32848.0,153464.76
2026-01-14,121016.76,32938.0,153954.76
2026-01-15,121216.76,32146.0,153362.76
2026-01-16,117816.76,34904.0,152720.76
2026-01-19,118854.76,34260.0,153114.76
2026-01-20,117516.76,35734.0,153250.76
2026-01-21,120376.76,34212.0,154588.76
2026-01-22,120376.76,34816.0,155192.76
2026-01-23,120376.76,35548.0,155924.76
2026-01-26,119168.76,36470.0,155638.76
2026-01-27,119368.76,36470.0,155838.76
2026-01-28,119368.76,35508.0,154876.76
2026-01-29,117768.76,37012.0,154780.76
2026-01-30,118104.76,36686.0,154790.76
2026-02-02,123830.76,30870.0,154700.76
2026-02-03,123830.76,31876.000000000004,155706.76
2026-02-04,123830.76,31492.0,155322.76
2026-02-05,123830.76,31724.0,155554.76
2026-02-06,123830.76,32184.0,156014.76
2026-02-09,124128.76,32940.0,157068.76
2026-02-10,130252.76000000001,27476.0,157728.76
2026-02-11,128452.76000000001,29178.0,157630.76
2026-02-12,130452.76000000001,27600.0,158052.76
2026-02-13,130652.76000000001,27454.0,158106.76
2026-02-24,129478.76000000001,29166.0,158644.76
2026-02-25,129678.76000000001,29674.0,159352.76
2026-02-26,129878.76000000001,29368.0,159246.76
2026-02-27,128078.76000000001,31448.0,159526.76
2026-03-02,126568.76000000001,31928.0,158496.76
2026-03-03,123168.76000000001,33314.0,156482.76
2026-03-04,121530.76000000001,35378.0,156908.76
2026-03-05,123530.76000000001,33996.0,157526.76
2026-03-06,121930.76000000001,35786.0,157716.76
2026-03-09,120130.76000000001,37128.0,157258.76
2026-03-10,122130.76000000001,36208.0,158338.76
2026-03-11,122498.76000000001,35428.0,157926.76
2026-03-12,122498.76000000001,34782.0,157280.76
2026-03-13,122818.76000000001,33916.0,156734.76
2026-03-16,119866.98000000001,36893.56,156760.54
2026-03-17,120266.98000000001,35880.16,156147.14
2026-03-18,120266.98000000001,36240.88,156507.86000000002
2026-03-19,115666.98000000001,39534.56,155201.54
2026-03-20,114066.98000000001,39770.48,153837.46000000002
2026-03-23,112544.98000000001,40740.8,153285.78000000003
2026-03-24,111144.98000000001,44227.28,155372.26
2026-03-25,113144.98000000001,43183.8,156328.78000000003
2026-03-26,114944.98000000001,40874.96,155819.94
2026-03-27,114944.98000000001,41492.52,156437.5
2026-03-30,118332.98000000001,38077.28,156410.26
2026-03-31,118332.98000000001,37634.52,155967.5
2026-04-01,121430.98000000001,35512.04,156943.02000000002
2026-04-02,121430.98000000001,34988.72,156419.7
2026-04-03,118420.98000000001,37343.56,155764.54
2026-04-07,117714.98000000001,38003.479999999996,155718.46000000002
2026-04-08,117980.98000000001,39241.32,157222.30000000002
2026-04-09,117980.98000000001,38689.08,156670.06
2026-04-10,118180.98000000001,38458.6,156639.58000000002
2026-04-13,116651.58000000002,39420.0,156071.58000000002
2026-04-14,115051.58000000002,40588.0,155639.58000000002
2026-04-15,114451.58000000002,40568.0,155019.58000000002
2026-04-16,113051.58000000002,41612.0,154663.58000000002
2026-04-17,111851.58000000002,42540.0,154391.58000000002
2026-04-20,111939.58000000002,43314.0,155253.58000000002
2026-04-21,111939.58000000002,43070.0,155009.58000000002
2026-04-22,111939.58000000002,42160.0,154099.58000000002
2026-04-23,110939.58000000002,41660.0,152599.58000000002
2026-04-24,109899.58000000002,42320.0,152219.58000000002
2026-04-27,113003.58000000002,39480.0,152483.58000000002
2026-04-28,112317.58000000002,38134.0,150451.58000000002
2026-04-29,113317.58000000002,37898.0,151215.58000000002
2026-04-30,111717.58000000002,39436.0,151153.58000000002
1 date cash holding_market_value total_asset
2 2023-05-04 28289.86 33283.08 61572.94
3 2023-05-05 29353.86 32311.96 61665.82
4 2023-05-08 29553.86 32215.16 61769.020000000004
5 2023-05-09 27953.86 32879.84 60833.7
6 2023-05-10 26153.86 35374.479999999996 61528.34
7 2023-05-11 29753.86 33225.380000000005 62979.240000000005
8 2023-05-12 24353.86 37200.04 61553.9
9 2023-05-15 25667.86 35345.6 61013.46
10 2023-05-16 20867.86 38643.44 59511.3
11 2023-05-17 17667.86 42281.4 59949.26
12 2023-05-18 17667.86 43420.1 61087.96
13 2023-05-19 19999.86 40128.84 60128.7
14 2023-05-22 19136.34 40407.96 59544.3
15 2023-05-23 19136.34 39661.66 58798.0
16 2023-05-24 19136.34 39738.92 58875.259999999995
17 2023-05-25 17736.34 40531.479999999996 58267.81999999999
18 2023-05-26 17736.34 41098.4 58834.740000000005
19 2023-05-29 14136.34 44736.28 58872.619999999995
20 2023-05-30 14336.34 45939.42 60275.759999999995
21 2023-05-31 14336.34 45642.6 59978.94
22 2023-06-01 20433.92 41484.86 61918.78
23 2023-06-02 22233.92 40253.44 62487.36
24 2023-06-05 20683.899999999998 42477.36 63161.259999999995
25 2023-06-06 24283.899999999998 37810.38 62094.28
26 2023-06-07 22683.899999999998 40119.560000000005 62803.46000000001
27 2023-06-08 21083.899999999998 40887.58 61971.479999999996
28 2023-06-09 23083.899999999998 39432.4 62516.3
29 2023-06-12 23083.899999999998 39468.780000000006 62552.68000000001
30 2023-06-13 23083.899999999998 40270.58 63354.479999999996
31 2023-06-14 24883.899999999998 38792.08 63675.979999999996
32 2023-06-15 26883.899999999998 36531.32000000001 63415.22
33 2023-06-16 27171.899999999998 36662.04 63833.94
34 2023-06-19 30577.359999999997 33518.72 64096.08
35 2023-06-20 28977.359999999997 35500.700000000004 64478.06
36 2023-06-21 27177.359999999997 35306.82 62484.17999999999
37 2023-06-26 25315.46 35218.7 60534.159999999996
38 2023-06-27 20515.46 40464.58 60980.04
39 2023-06-28 20515.46 39961.2 60476.659999999996
40 2023-06-29 20515.46 40167.38 60682.84
41 2023-06-30 20515.46 40323.66 60839.12
42 2023-07-03 28555.76 32259.06 60814.82
43 2023-07-04 28555.76 32140.9 60696.66
44 2023-07-05 28555.76 31760.359999999997 60316.119999999995
45 2023-07-06 28555.76 32029.4 60585.16
46 2023-07-07 27155.76 33151.0 60306.759999999995
47 2023-07-10 30335.76 30607.5 60943.259999999995
48 2023-07-11 30335.76 29987.239999999998 60323.0
49 2023-07-12 27135.76 32715.800000000003 59851.56
50 2023-07-13 28735.76 32062.980000000003 60798.740000000005
51 2023-07-14 29091.76 31705.800000000003 60797.56
52 2023-07-17 27259.08 33179.8 60438.880000000005
53 2023-07-18 27259.08 33110.299999999996 60369.38
54 2023-07-19 27259.08 33071.82 60330.9
55 2023-07-20 27259.08 32716.16 59975.240000000005
56 2023-07-21 27608.82 32509.1 60117.92
57 2023-07-24 29909.840000000004 30081.24 59991.08
58 2023-07-25 29909.840000000004 30276.039999999997 60185.880000000005
59 2023-07-26 31797.840000000004 27988.0 59785.840000000004
60 2023-07-27 31797.840000000004 27916.859999999997 59714.7
61 2023-07-28 30397.840000000004 29960.780000000002 60358.62000000001
62 2023-07-31 31034.120000000003 29691.679999999997 60725.8
63 2023-08-01 31326.78 29226.62 60553.399999999994
64 2023-08-02 33126.78 27590.860000000004 60717.64
65 2023-08-03 31326.78 29124.66 60451.44
66 2023-08-04 31326.78 29340.219999999998 60667.0
67 2023-08-07 28402.46 32404.219999999998 60806.67999999999
68 2023-08-08 28402.46 32277.72 60680.18
69 2023-08-09 28602.46 31887.659999999996 60490.119999999995
70 2023-08-10 28602.46 32160.539999999997 60763.0
71 2023-08-11 28602.46 31713.8 60316.259999999995
72 2023-08-14 30216.02 30586.119999999995 60802.14
73 2023-08-15 30216.02 30597.48 60813.5
74 2023-08-16 32216.02 28753.199999999997 60969.22
75 2023-08-17 30416.02 31062.44 61478.46
76 2023-08-18 33136.700000000004 28257.34 61394.04000000001
77 2023-08-21 31336.700000000004 30055.36 61392.060000000005
78 2023-08-22 29536.700000000004 32326.899999999998 61863.600000000006
79 2023-08-23 29536.700000000004 31548.5 61085.200000000004
80 2023-08-24 29536.700000000004 31743.98 61280.68000000001
81 2023-08-25 26136.700000000004 34300.62 60437.32000000001
82 2023-08-28 29708.780000000006 31279.519999999997 60988.3
83 2023-08-29 30196.780000000006 32665.699999999997 62862.48
84 2023-08-30 32196.780000000006 31247.879999999997 63444.66
85 2023-08-31 34196.780000000006 29057.66 63254.44
86 2023-09-01 33946.780000000006 29311.820000000003 63258.600000000006
87 2023-09-04 34135.520000000004 29560.14 63695.66
88 2023-09-05 34509.780000000006 29185.22 63695.00000000001
89 2023-09-06 32709.780000000006 30561.239999999998 63271.020000000004
90 2023-09-07 33082.920000000006 30092.64 63175.560000000005
91 2023-09-08 29682.920000000006 33243.98 62926.90000000001
92 2023-09-11 28129.100000000006 35377.44 63506.54000000001
93 2023-09-12 30129.100000000006 33934.32 64063.420000000006
94 2023-09-13 31929.100000000006 32089.72 64018.82000000001
95 2023-09-14 31929.100000000006 31721.36 63650.46000000001
96 2023-09-15 33929.100000000006 29926.239999999998 63855.340000000004
97 2023-09-18 35984.3 28165.4 64149.700000000004
98 2023-09-19 36370.3 27593.84 63964.14
99 2023-09-20 36917.340000000004 27446.239999999998 64363.58
100 2023-09-21 35117.340000000004 29112.16 64229.5
101 2023-09-22 35117.340000000004 29932.44 65049.78
102 2023-09-25 35673.340000000004 30188.22 65861.56
103 2023-09-26 33873.340000000004 31843.26 65716.6
104 2023-09-27 33873.340000000004 32595.0 66468.34
105 2023-09-28 39873.340000000004 27709.4 67582.74
106 2023-10-09 41247.340000000004 26008.479999999996 67255.82
107 2023-10-10 43605.08 24212.319999999996 67817.4
108 2023-10-11 43805.08 23993.4 67798.48000000001
109 2023-10-12 44083.08 23915.480000000003 67998.56
110 2023-10-13 44083.08 23683.52 67766.6
111 2023-10-16 43689.08 23793.599999999995 67482.68
112 2023-10-17 42089.08 25216.94 67306.02
113 2023-10-18 38489.08 28344.4 66833.48000000001
114 2023-10-19 36689.08 29789.940000000002 66479.02
115 2023-10-20 33289.08 32655.699999999997 65944.78
116 2023-10-23 31442.520000000004 33523.66 64966.18000000001
117 2023-10-24 29642.520000000004 36040.58 65683.1
118 2023-10-25 33642.520000000004 33017.58 66660.1
119 2023-10-26 34288.520000000004 33149.32 67437.84
120 2023-10-27 36088.520000000004 31940.920000000002 68029.44
121 2023-10-30 37285.62 32011.34 69296.96
122 2023-10-31 39938.9 29558.3 69497.2
123 2023-11-01 38429.700000000004 30972.719999999994 69402.42
124 2023-11-02 40429.700000000004 28891.5 69321.20000000001
125 2023-11-03 40429.700000000004 29377.8 69807.5
126 2023-11-06 41176.26 29848.48 71024.74
127 2023-11-07 45432.26 26553.34 71985.6
128 2023-11-08 45782.26 26698.7 72480.96
129 2023-11-09 48079.740000000005 24910.94 72990.68000000001
130 2023-11-10 46479.740000000005 25900.26 72380.0
131 2023-11-13 48501.3 24292.92 72794.22
132 2023-11-14 46701.3 26683.64 73384.94
133 2023-11-15 46957.340000000004 26097.679999999997 73055.02
134 2023-11-16 47157.340000000004 26279.88 73437.22
135 2023-11-17 49341.340000000004 24523.559999999998 73864.9
136 2023-11-20 51584.740000000005 23124.5 74709.24
137 2023-11-21 51940.740000000005 22778.399999999998 74719.14
138 2023-11-22 49054.740000000005 26431.32 75486.06
139 2023-11-23 50182.740000000005 25830.2 76012.94
140 2023-11-24 49700.740000000005 27047.760000000002 76748.5
141 2023-11-27 52958.340000000004 24999.68 77958.02
142 2023-11-28 46244.340000000004 30826.699999999997 77071.04000000001
143 2023-11-29 41844.340000000004 33468.94 75313.28
144 2023-11-30 37244.340000000004 37343.26 74587.6
145 2023-12-01 33892.340000000004 39456.38 73348.72
146 2023-12-04 29451.340000000004 43211.36 72662.70000000001
147 2023-12-05 36811.340000000004 40589.1 77400.44
148 2023-12-06 43811.340000000004 34448.659999999996 78260.0
149 2023-12-07 49721.340000000004 30807.46 80528.8
150 2023-12-08 50831.340000000004 30194.3 81025.64
151 2023-12-11 41721.94 37976.44 79698.38
152 2023-12-12 41967.94 38844.28 80812.22
153 2023-12-13 41967.94 40205.56 82173.5
154 2023-12-14 48057.94 35801.66 83859.6
155 2023-12-15 52387.94 31884.0 84271.94
156 2023-12-18 54779.94 31022.0 85801.94
157 2023-12-19 56379.94 28964.0 85343.94
158 2023-12-20 58379.94 28260.0 86639.94
159 2023-12-21 60909.94 24672.0 85581.94
160 2023-12-22 52909.94 32946.0 85855.94
161 2023-12-25 54909.94 32782.0 87691.94
162 2023-12-26 54909.94 33254.0 88163.94
163 2023-12-27 57197.94 31222.0 88419.94
164 2023-12-28 55761.94 33084.0 88845.94
165 2023-12-29 56387.94 33956.0 90343.94
166 2024-01-02 62813.94 29938.0 92751.94
167 2024-01-03 65205.94 27608.0 92813.94
168 2024-01-04 63405.94 29272.0 92677.94
169 2024-01-05 56405.94 34602.0 91007.94
170 2024-01-08 58405.94 31830.0 90235.94
171 2024-01-09 48405.94 40204.0 88609.94
172 2024-01-10 43605.94 45832.0 89437.94
173 2024-01-11 45405.94 44096.0 89501.94
174 2024-01-12 42405.94 42438.0 84843.94
175 2024-01-15 35605.94 46922.0 82527.94
176 2024-01-16 34005.94 52684.0 86689.94
177 2024-01-17 37205.94 49538.0 86743.94
178 2024-01-18 39005.94 49904.0 88909.94
179 2024-01-19 44405.94 46458.0 90863.94
180 2024-01-22 45355.94 42936.0 88291.94
181 2024-01-23 45355.94 44114.0 89469.94
182 2024-01-24 47895.94 42334.0 90229.94
183 2024-01-25 46695.94 42518.0 89213.94
184 2024-01-26 43543.94 44782.0 88325.94
185 2024-01-29 39113.94 44472.0 83585.94
186 2024-01-30 33513.94 50754.0 84267.94
187 2024-01-31 40113.94 46114.0 86227.94
188 2024-02-01 38705.94 44274.0 82979.94
189 2024-02-02 27305.940000000002 52690.0 79995.94
190 2024-02-05 22445.940000000002 55230.0 77675.94
191 2024-02-06 27245.940000000002 56198.0 83443.94
192 2024-02-07 36845.94 46666.0 83511.94
193 2024-02-08 31419.940000000002 51926.0 83345.94
194 2024-02-19 36311.94 48834.0 85145.94
195 2024-02-20 39861.28 47910.66 87771.94
196 2024-02-21 39861.28 48018.96 87880.23999999999
197 2024-02-22 41861.28 45818.24 87679.51999999999
198 2024-02-23 41861.28 44551.96 86413.23999999999
199 2024-02-26 45030.24 41690.74 86720.98
200 2024-02-27 48777.32 41789.439999999995 90566.76
201 2024-02-28 49161.3 39334.85999999999 88496.16
202 2024-02-29 45761.3 44599.96 90361.26000000001
203 2024-03-01 49575.3 40919.28 90494.58
204 2024-03-04 52677.94 37579.03999999999 90256.98
205 2024-03-05 51277.94 38087.04 89364.98000000001
206 2024-03-06 53277.94 36919.08 90197.02
207 2024-03-07 53677.94 35949.98 89627.92000000001
208 2024-03-08 53677.94 35737.8 89415.74
209 2024-03-11 57519.94 32557.06 90077.0
210 2024-03-12 59847.920000000006 30947.68 90795.6
211 2024-03-13 59847.920000000006 30429.56 90277.48000000001
212 2024-03-14 60047.920000000006 30268.980000000003 90316.90000000001
213 2024-03-15 60047.920000000006 31026.780000000002 91074.70000000001
214 2024-03-18 62559.920000000006 29269.68 91829.6
215 2024-03-19 62905.560000000005 29223.520000000004 92129.08000000002
216 2024-03-20 65165.560000000005 27477.68 92643.24
217 2024-03-21 65873.84 26703.800000000003 92577.64
218 2024-03-22 65873.84 26076.62 91950.45999999999
219 2024-03-25 61065.36 29863.06 90928.42
220 2024-03-26 59265.36 31568.499999999996 90833.86
221 2024-03-27 55865.36 34504.659999999996 90370.01999999999
222 2024-03-28 60401.36 30693.719999999998 91095.08
223 2024-03-29 60401.36 30781.6 91182.95999999999
224 2024-04-01 66053.02 26153.920000000002 92206.94
225 2024-04-02 66617.02 25795.5 92412.52
226 2024-04-03 65179.58 26641.44 91821.02
227 2024-04-08 63035.58 27350.300000000003 90385.88
228 2024-04-09 61435.58 29304.18 90739.76000000001
229 2024-04-10 56635.58 33710.5 90346.08
230 2024-04-11 56903.58 32962.08 89865.66
231 2024-04-12 56903.58 32507.18 89410.76000000001
232 2024-04-15 45899.58 40168.12 86067.70000000001
233 2024-04-16 36699.58 44627.16 81326.74
234 2024-04-17 33499.58 52978.979999999996 86478.56
235 2024-04-18 33499.58 53139.6 86639.18
236 2024-04-19 33499.58 52126.22 85625.8
237 2024-04-22 35961.58 49286.22 85247.8
238 2024-04-23 39761.58 47887.979999999996 87649.56
239 2024-04-24 41361.58 47785.3 89146.88
240 2024-04-25 41161.58 47469.36 88630.94
241 2024-04-26 42761.58 46693.66 89455.24
242 2024-04-29 46142.72 44846.68 90989.4
243 2024-04-30 51342.72 39697.380000000005 91040.1
244 2024-05-06 59221.060000000005 32499.46 91720.52
245 2024-05-07 59421.060000000005 32774.7 92195.76000000001
246 2024-05-08 59805.060000000005 31778.78 91583.84
247 2024-05-09 58005.060000000005 33495.740000000005 91500.80000000002
248 2024-05-10 56205.060000000005 34632.66 90837.72
249 2024-05-13 54405.060000000005 35785.520000000004 90190.58000000002
250 2024-05-14 56405.060000000005 34677.5 91082.56
251 2024-05-15 56791.060000000005 34668.46 91459.52
252 2024-05-16 58591.060000000005 33183.68 91774.74
253 2024-05-17 60869.060000000005 31647.16 92516.22
254 2024-05-20 60891.060000000005 31465.64 92356.70000000001
255 2024-05-21 61091.060000000005 31509.34 92600.40000000001
256 2024-05-22 61405.060000000005 31812.32 93217.38
257 2024-05-23 61405.060000000005 32031.88 93436.94
258 2024-05-24 59605.060000000005 32880.44 92485.5
259 2024-05-27 59387.50000000001 33578.0 92965.5
260 2024-05-28 63387.50000000001 29672.0 93059.5
261 2024-05-29 61787.50000000001 31334.0 93121.5
262 2024-05-30 64141.5 28916.0 93057.5
263 2024-05-31 64141.5 29130.0 93271.5
264 2024-06-03 59560.399999999994 32518.96 92079.35999999999
265 2024-06-04 57760.399999999994 33912.6 91673.0
266 2024-06-05 56160.399999999994 34984.96 91145.35999999999
267 2024-06-06 47960.399999999994 40566.12 88526.51999999999
268 2024-06-07 47960.399999999994 42130.56 90090.95999999999
269 2024-06-11 50552.399999999994 40529.64 91082.04
270 2024-06-12 54152.399999999994 37975.64 92128.04
271 2024-06-13 56152.399999999994 36528.12 92680.51999999999
272 2024-06-14 56152.399999999994 36035.36 92187.76
273 2024-06-17 58145.759999999995 33718.0 91863.76
274 2024-06-18 60345.759999999995 32524.0 92869.76
275 2024-06-19 64277.759999999995 29616.0 93893.76
276 2024-06-20 59921.759999999995 33686.0 93607.76
277 2024-06-21 62239.759999999995 31984.0 94223.76
278 2024-06-24 56927.759999999995 35350.0 92277.76
279 2024-06-25 53927.759999999995 37340.0 91267.76
280 2024-06-26 52527.759999999995 40366.0 92893.76
281 2024-06-27 52527.759999999995 39326.0 91853.76
282 2024-06-28 52527.759999999995 40070.0 92597.76
283 2024-07-01 57551.759999999995 35500.0 93051.76
284 2024-07-02 57551.759999999995 36244.0 93795.76
285 2024-07-03 57551.759999999995 36146.0 93697.76
286 2024-07-04 55751.759999999995 36268.0 92019.76
287 2024-07-05 52351.759999999995 40466.0 92817.76
288 2024-07-08 53531.759999999995 38242.0 91773.76
289 2024-07-09 52891.759999999995 40390.0 93281.76
290 2024-07-10 51451.719999999994 41648.04 93099.76
291 2024-07-11 51451.719999999994 42945.86 94397.57999999999
292 2024-07-12 51451.719999999994 42662.64 94114.35999999999
293 2024-07-15 52778.87999999999 40392.92 93171.79999999999
294 2024-07-16 52778.87999999999 41218.92 93997.79999999999
295 2024-07-17 57090.87999999999 36641.42 93732.29999999999
296 2024-07-18 55290.87999999999 38284.96 93575.84
297 2024-07-19 55290.87999999999 38665.44 93956.31999999999
298 2024-07-22 60996.87999999999 34089.5 95086.37999999999
299 2024-07-23 60996.87999999999 33558.479999999996 94555.35999999999
300 2024-07-24 62796.87999999999 31740.760000000002 94537.63999999998
301 2024-07-25 61256.87999999999 33841.24 95098.12
302 2024-07-26 61456.87999999999 33980.7 95437.57999999999
303 2024-07-29 59777.73999999999 35923.119999999995 95700.85999999999
304 2024-07-30 61777.73999999999 34529.72 96307.45999999999
305 2024-07-31 64101.73999999999 33334.0 97435.73999999999
306 2024-08-01 65125.73999999999 32018.0 97143.73999999999
307 2024-08-02 61925.73999999999 34468.0 96393.73999999999
308 2024-08-05 62605.73999999999 32892.0 95497.73999999999
309 2024-08-06 62605.73999999999 34194.0 96799.73999999999
310 2024-08-07 64405.73999999999 32874.0 97279.73999999999
311 2024-08-08 64605.73999999999 31938.0 96543.73999999999
312 2024-08-09 63005.73999999999 33154.0 96159.73999999999
313 2024-08-12 61315.73999999999 34852.0 96167.73999999999
314 2024-08-13 59515.73999999999 36410.0 95925.73999999999
315 2024-08-14 59515.73999999999 35968.0 95483.73999999999
316 2024-08-15 58515.73999999999 37140.0 95655.73999999999
317 2024-08-16 56715.73999999999 39058.0 95773.73999999999
318 2024-08-19 56764.19999999999 38072.64 94836.84
319 2024-08-20 56764.19999999999 38040.52 94804.71999999999
320 2024-08-21 55164.19999999999 38808.92 93973.12
321 2024-08-22 53564.19999999999 39192.04 92756.23999999999
322 2024-08-23 55364.19999999999 37672.44 93036.63999999998
323 2024-08-26 56940.25999999999 36384.899999999994 93325.15999999997
324 2024-08-27 57540.25999999999 35327.28 92867.53999999998
325 2024-08-28 52940.25999999999 39837.560000000005 92777.81999999999
326 2024-08-29 51892.25999999999 41458.62 93350.87999999999
327 2024-08-30 52183.51999999999 42771.06 94954.57999999999
328 2024-09-02 56710.33999999999 37626.06 94336.4
329 2024-09-03 58710.33999999999 36281.9 94992.23999999999
330 2024-09-04 58710.33999999999 35629.16 94339.5
331 2024-09-05 58710.33999999999 36421.92 95132.25999999998
332 2024-09-06 58710.33999999999 35878.58 94588.91999999998
333 2024-09-09 58906.33999999999 35322.54000000001 94228.88
334 2024-09-10 57306.33999999999 37236.6 94542.93999999999
335 2024-09-11 57306.33999999999 36978.36 94284.69999999998
336 2024-09-12 62642.33999999999 31378.359999999997 94020.69999999998
337 2024-09-13 62642.33999999999 30741.5 93383.84
338 2024-09-18 58008.74 34825.06 92833.79999999999
339 2024-09-19 58359.32 35364.84 93724.16
340 2024-09-20 58359.32 34882.94 93242.26000000001
341 2024-09-23 59825.32 33529.54 93354.86
342 2024-09-24 59825.32 35009.0 94834.32
343 2024-09-25 62217.32 33031.08 95248.4
344 2024-09-26 62723.32000000001 33992.1 96715.42000000001
345 2024-09-27 69235.32 29121.08 98356.40000000001
346 2024-09-30 80349.32 21369.96 101719.28
347 2024-10-08 85077.28 19476.0 104553.28
348 2024-10-09 77483.28 25692.0 103175.28
349 2024-10-10 65483.28 35852.0 101335.28
350 2024-10-11 56083.28 43112.0 99195.28
351 2024-10-14 54845.28 47550.0 102395.28
352 2024-10-15 62245.28 40032.0 102277.28
353 2024-10-16 64429.28 38264.0 102693.28
354 2024-10-17 71101.28 33832.0 104933.28
355 2024-10-18 71101.28 35670.0 106771.28
356 2024-10-21 81899.28 27310.0 109209.28
357 2024-10-22 78499.28 29890.0 108389.28
358 2024-10-23 79145.28 31210.0 110355.28
359 2024-10-24 83457.28 27304.0 110761.28
360 2024-10-25 87055.28 25324.0 112379.28
361 2024-10-28 82369.28 32322.0 114691.28
362 2024-10-29 94723.28 21330.0 116053.28
363 2024-10-30 93926.66 22730.62 116657.28
364 2024-10-31 92874.66 23630.56 116505.22
365 2024-11-01 90986.66 26929.52 117916.18000000001
366 2024-11-04 88286.66 31429.6 119716.26000000001
367 2024-11-05 99332.66 22827.28 122159.94
368 2024-11-06 100274.82 22557.2 122832.02
369 2024-11-07 97836.82 26869.02 124705.84000000001
370 2024-11-08 100636.82 23667.66 124304.48000000001
371 2024-11-11 97436.82 27560.78 124997.6
372 2024-11-12 97436.82 27386.06 124822.88
373 2024-11-13 95636.82 29058.3 124695.12000000001
374 2024-11-14 97636.82 26820.22 124457.04000000001
375 2024-11-15 96557.20000000001 28499.719999999998 125056.92000000001
376 2024-11-18 99403.46 26804.239999999998 126207.70000000001
377 2024-11-19 87403.46 38321.020000000004 125724.48000000001
378 2024-11-20 91665.46 36476.68 128142.14000000001
379 2024-11-21 97525.46 31670.04 129195.5
380 2024-11-22 102164.48000000001 26932.02 129096.50000000001
381 2024-11-25 100326.48000000001 29415.52 129742.00000000001
382 2024-11-26 98726.48000000001 30748.68 129475.16
383 2024-11-27 97126.48000000001 32619.879999999997 129746.36000000002
384 2024-11-28 101950.1 29081.36 131031.46
385 2024-11-29 101950.1 29600.280000000002 131550.38
386 2024-12-02 108792.78 23990.58 132783.36
387 2024-12-03 105192.78 27452.98 132645.76
388 2024-12-04 103392.78 27856.059999999998 131248.84
389 2024-12-05 103392.78 28402.48 131795.26
390 2024-12-06 103392.78 28411.32 131804.1
391 2024-12-09 97964.12 32072.58 130036.7
392 2024-12-10 96164.12 34057.399999999994 130221.51999999999
393 2024-12-11 94564.12 35279.64 129843.76
394 2024-12-12 94844.12 35000.0 129844.12
395 2024-12-13 96844.12 33198.0 130042.12
396 2024-12-16 92244.12 37306.0 129550.12
397 2024-12-17 90444.12 36824.0 127268.12
398 2024-12-18 85644.12 41096.0 126740.12
399 2024-12-19 82444.12 45412.0 127856.12
400 2024-12-20 82444.12 44764.0 127208.12
401 2024-12-23 86146.12 38902.0 125048.12
402 2024-12-24 88234.12 37016.0 125250.12
403 2024-12-25 83634.12 39390.0 123024.12
404 2024-12-26 85918.12 38396.0 124314.12
405 2024-12-27 89318.12 35982.0 125300.12
406 2024-12-30 84450.12 39132.0 123582.12
407 2024-12-31 86250.12 38410.0 124660.12
408 2025-01-02 85608.12 39822.0 125430.12
409 2025-01-03 87408.12 37578.0 124986.12
410 2025-01-06 87180.12 36526.0 123706.12
411 2025-01-07 85580.12 39196.0 124776.12
412 2025-01-08 85580.12 39276.0 124856.12
413 2025-01-09 85580.12 40108.0 125688.12
414 2025-01-10 85580.12 38096.0 123676.12
415 2025-01-13 86472.12 36632.0 123104.12
416 2025-01-14 90534.12 35542.0 126076.12
417 2025-01-15 92964.12 33396.0 126360.12
418 2025-01-16 92964.12 33474.0 126438.12
419 2025-01-17 92964.12 32718.0 125682.12
420 2025-01-20 92444.12 33334.0 125778.12
421 2025-01-21 90644.12 34562.0 125206.12
422 2025-01-22 88844.12 35200.0 124044.12
423 2025-01-23 92964.12 31104.0 124068.12
424 2025-01-24 92964.12 30942.0 123906.12
425 2025-01-27 93264.12 29920.0 123184.12
426 2025-02-05 95550.12 28902.0 124452.12
427 2025-02-06 95386.12 30334.0 125720.12
428 2025-02-07 99682.12 27098.0 126780.12
429 2025-02-10 101682.12 26282.0 127964.12
430 2025-02-11 102255.36 25700.760000000002 127956.12
431 2025-02-12 106503.36 22042.74 128546.1
432 2025-02-13 106503.36 21696.8 128200.16
433 2025-02-14 106503.36 21418.78 127922.14
434 2025-02-17 106503.36 22392.739999999998 128896.1
435 2025-02-18 106863.36 21594.6 128457.95999999999
436 2025-02-19 107549.72 21399.64 128949.36
437 2025-02-20 105749.72 23389.28 129139.0
438 2025-02-21 105749.72 23782.4 129532.12
439 2025-02-24 107749.72 22060.94 129810.66
440 2025-02-25 106050.72 23940.18 129990.9
441 2025-02-26 106050.72 24078.079999999998 130128.8
442 2025-02-27 104528.72 25574.48 130103.2
443 2025-02-28 102728.72 26542.96 129271.68
444 2025-03-03 99240.72 30762.88 130003.6
445 2025-03-04 104273.36 27485.84 131759.2
446 2025-03-05 106519.36 25571.420000000002 132090.78
447 2025-03-06 103171.36 29194.32 132365.68
448 2025-03-07 105425.36 27040.4 132465.76
449 2025-03-10 105425.36 27684.64 133110.0
450 2025-03-11 105757.36 27476.72 133234.08000000002
451 2025-03-12 107757.36 26166.48 133923.84
452 2025-03-13 110301.82 23848.38 134150.2
453 2025-03-14 115013.82 19782.36 134796.18
454 2025-03-17 115373.82 19713.06 135086.88
455 2025-03-18 116287.82 19132.559999999998 135420.38
456 2025-03-19 112687.82 22446.72 135134.54
457 2025-03-20 111223.82 24020.72 135244.54
458 2025-03-21 109423.82 25211.120000000003 134634.94
459 2025-03-24 102104.94 32562.0 134666.94
460 2025-03-25 104460.1 30420.84 134880.94
461 2025-03-26 106460.1 29221.84 135681.94
462 2025-03-27 106660.1 28888.8 135548.9
463 2025-03-28 103822.1 31484.3 135306.4
464 2025-03-31 100434.62000000001 34067.08 134501.7
465 2025-04-01 101400.62000000001 32980.64 134381.26
466 2025-04-02 101600.62000000001 32563.239999999998 134163.86000000002
467 2025-04-03 99800.62000000001 34518.32 134318.94
468 2025-04-07 87000.62000000001 41837.2 128837.82
469 2025-04-08 80800.62000000001 49853.04 130653.66
470 2025-04-09 88600.62000000001 46010.08 134610.7
471 2025-04-10 98000.62000000001 38655.6 136656.22
472 2025-04-11 99600.62000000001 37063.76 136664.38
473 2025-04-14 106254.62000000001 31782.72 138037.34000000003
474 2025-04-15 104654.62000000001 33317.28 137971.90000000002
475 2025-04-16 106747.54000000001 31677.72 138425.26
476 2025-04-17 108747.54000000001 30180.46 138928.0
477 2025-04-18 109341.54000000001 30561.36 139902.90000000002
478 2025-04-21 107815.54000000001 32425.2 140240.74000000002
479 2025-04-22 107815.54000000001 31336.8 139152.34
480 2025-04-23 109415.54000000001 30456.64 139872.18
481 2025-04-24 109657.54000000001 29465.66 139123.2
482 2025-04-25 106835.54000000001 32281.18 139116.72
483 2025-04-28 106345.54000000001 32082.7 138428.24000000002
484 2025-04-29 102745.54000000001 35483.0 138228.54
485 2025-04-30 102745.54000000001 35759.3 138504.84000000003
486 2025-05-06 104510.84000000001 35040.0 139550.84000000003
487 2025-05-07 107283.84000000001 32649.0 139932.84000000003
488 2025-05-08 104437.84000000001 35792.24 140230.08000000002
489 2025-05-09 102637.84000000001 37585.88 140223.72
490 2025-05-12 103367.84000000001 37894.72 141262.56
491 2025-05-13 103367.84000000001 37302.54 140670.38
492 2025-05-14 103763.84000000001 36662.0 140425.84000000003
493 2025-05-15 101963.84000000001 38680.0 140643.84000000003
494 2025-05-16 101963.84000000001 38900.0 140863.84000000003
495 2025-05-19 101963.84000000001 39840.0 141803.84000000003
496 2025-05-20 105763.84000000001 36696.0 142459.84000000003
497 2025-05-21 107363.84000000001 35314.0 142677.84000000003
498 2025-05-22 109423.84000000001 33250.0 142673.84000000003
499 2025-05-23 109423.84000000001 32758.0 142181.84000000003
500 2025-05-26 110715.84000000001 32416.0 143131.84000000003
501 2025-05-27 114765.84000000001 28756.0 143521.84000000003
502 2025-05-28 118565.84000000001 24386.0 142951.84000000003
503 2025-05-29 118565.84000000001 24558.0 143123.84000000003
504 2025-05-30 115165.84000000001 27606.0 142771.84000000003
505 2025-06-03 113691.88 29479.86 143171.74
506 2025-06-04 117691.88 26375.22 144067.1
507 2025-06-05 118367.88 25461.82 143829.7
508 2025-06-06 118567.88 25523.98 144091.86000000002
509 2025-06-09 118468.06 26145.58 144613.64
510 2025-06-10 119242.06 26013.68 145255.74
511 2025-06-11 115642.06 29543.460000000003 145185.52
512 2025-06-12 119442.06 26731.8 146173.86
513 2025-06-13 121827.45999999999 23271.14 145098.59999999998
514 2025-06-16 118827.45999999999 26558.000000000004 145385.46
515 2025-06-17 119851.45999999999 25974.019999999997 145825.47999999998
516 2025-06-18 118051.45999999999 27032.4 145083.86
517 2025-06-19 112651.45999999999 31251.42 143902.88
518 2025-06-20 109651.45999999999 33717.020000000004 143368.47999999998
519 2025-06-23 108051.45999999999 36004.66 144056.12
520 2025-06-24 108051.45999999999 36450.659999999996 144502.12
521 2025-06-25 108567.45999999999 36190.72 144758.18
522 2025-06-26 108767.45999999999 36286.82 145054.28
523 2025-06-27 108767.45999999999 36246.7 145014.15999999997
524 2025-06-30 108321.45999999999 37252.479999999996 145573.94
525 2025-07-01 115200.18 31736.68 146936.86
526 2025-07-02 117302.28 30046.74 147349.02
527 2025-07-03 117302.28 29892.84 147195.12
528 2025-07-04 117302.28 29383.94 146686.22
529 2025-07-07 117586.28 29861.88 147448.16
530 2025-07-08 117586.28 29913.9 147500.18
531 2025-07-09 117912.28 29733.94 147646.22
532 2025-07-10 118112.28 29558.0 147670.28
533 2025-07-11 116312.28 31418.0 147730.28
534 2025-07-14 114316.28 33108.0 147424.28
535 2025-07-15 112716.28 34186.0 146902.28
536 2025-07-16 114716.28 32426.0 147142.28
537 2025-07-17 114960.2 32414.08 147374.28
538 2025-07-18 113160.2 33946.38 147106.58
539 2025-07-21 116704.2 30680.16 147384.36
540 2025-07-22 116980.59999999999 30577.559999999998 147558.15999999997
541 2025-07-23 116980.59999999999 30195.579999999998 147176.18
542 2025-07-24 115180.59999999999 32133.800000000003 147314.4
543 2025-07-25 115180.59999999999 32153.94 147334.53999999998
544 2025-07-28 115180.59999999999 32243.62 147424.22
545 2025-07-29 115180.59999999999 31921.92 147102.52
546 2025-07-30 115180.59999999999 31987.88 147168.47999999998
547 2025-07-31 113580.59999999999 33084.16 146664.76
548 2025-08-01 113316.59999999999 33357.8 146674.4
549 2025-08-04 116562.59999999999 30359.96 146922.56
550 2025-08-05 114762.59999999999 32258.28 147020.88
551 2025-08-06 114762.59999999999 32168.1 146930.69999999998
552 2025-08-07 114762.59999999999 32379.9 147142.5
553 2025-08-08 119086.59999999999 28390.0 147476.59999999998
554 2025-08-11 119218.59999999999 28752.0 147970.59999999998
555 2025-08-12 117418.59999999999 30502.0 147920.59999999998
556 2025-08-13 121062.59999999999 26722.0 147784.59999999998
557 2025-08-14 117662.59999999999 29492.0 147154.59999999998
558 2025-08-15 119662.59999999999 28798.0 148460.59999999998
559 2025-08-18 123252.59999999999 26458.0 149710.59999999998
560 2025-08-19 122090.59999999999 27948.0 150038.59999999998
561 2025-08-20 120290.59999999999 29984.0 150274.59999999998
562 2025-08-21 126290.59999999999 24298.0 150588.59999999998
563 2025-08-22 126652.59999999999 23932.0 150584.59999999998
564 2025-08-25 126652.59999999999 24162.0 150814.59999999998
565 2025-08-26 126852.59999999999 24104.0 150956.59999999998
566 2025-08-27 123646.59999999999 26804.0 150450.59999999998
567 2025-08-28 118640.59999999999 32296.0 150936.59999999998
568 2025-08-29 116840.59999999999 33900.0 150740.59999999998
569 2025-09-01 122140.01999999999 29354.24 151494.25999999998
570 2025-09-02 120726.01999999999 30304.0 151030.02
571 2025-09-03 117326.01999999999 32868.0 150194.02
572 2025-09-04 114126.01999999999 35582.0 149708.02
573 2025-09-05 114126.01999999999 36400.0 150526.02
574 2025-09-08 114126.01999999999 36670.0 150796.02
575 2025-09-09 114126.01999999999 35764.0 149890.02
576 2025-09-10 114126.01999999999 36264.0 150390.02
577 2025-09-11 116326.01999999999 34852.0 151178.02
578 2025-09-12 116580.01999999999 34638.0 151218.02
579 2025-09-15 116840.01999999999 34224.0 151064.02
580 2025-09-16 116840.01999999999 34266.0 151106.02
581 2025-09-17 116840.01999999999 34016.0 150856.02
582 2025-09-18 115522.01999999999 34918.0 150440.02
583 2025-09-19 115522.01999999999 34548.0 150070.02
584 2025-09-22 116524.01999999999 33574.0 150098.02
585 2025-09-23 111924.01999999999 37476.0 149400.02
586 2025-09-24 112264.01999999999 37360.0 149624.02
587 2025-09-25 112264.01999999999 36962.0 149226.02
588 2025-09-26 112264.01999999999 36744.0 149008.02
589 2025-09-29 116726.01999999999 32582.0 149308.02
590 2025-09-30 116726.01999999999 32560.0 149286.02
591 2025-10-09 118370.01999999999 30188.0 148558.02
592 2025-10-10 116570.01999999999 31992.0 148562.02
593 2025-10-13 112600.01999999999 36462.0 149062.02
594 2025-10-14 112600.01999999999 36110.0 148710.02
595 2025-10-15 112600.01999999999 36570.0 149170.02
596 2025-10-16 114600.01999999999 34026.0 148626.02
597 2025-10-17 113000.01999999999 34894.0 147894.02
598 2025-10-20 112998.01999999999 35922.0 148920.02
599 2025-10-21 114998.01999999999 34854.0 149852.02
600 2025-10-22 114998.01999999999 34606.0 149604.02
601 2025-10-23 113198.01999999999 36532.0 149730.02
602 2025-10-24 113198.01999999999 36844.0 150042.02
603 2025-10-27 116430.01999999999 33332.0 149762.02
604 2025-10-28 116430.01999999999 33280.0 149710.02
605 2025-10-29 116430.01999999999 33934.0 150364.02
606 2025-10-30 116430.01999999999 33806.0 150236.02
607 2025-10-31 116430.01999999999 34148.0 150578.02
608 2025-11-03 121148.01999999999 29008.0 150156.02
609 2025-11-04 120108.01999999999 30110.0 150218.02
610 2025-11-05 120108.01999999999 30552.0 150660.02
611 2025-11-06 118308.01999999999 32286.0 150594.02
612 2025-11-07 118702.01999999999 31342.0 150044.02
613 2025-11-10 118702.01999999999 31688.0 150390.02
614 2025-11-11 123102.01999999999 27790.0 150892.02
615 2025-11-12 118426.01999999999 32408.0 150834.02
616 2025-11-13 119016.01999999999 32752.0 151768.02
617 2025-11-14 119016.01999999999 33214.0 152230.02
618 2025-11-17 123270.01999999999 29114.0 152384.02
619 2025-11-18 119670.01999999999 31876.0 151546.02
620 2025-11-19 118070.01999999999 33268.0 151338.02
621 2025-11-20 116470.01999999999 34108.0 150578.02
622 2025-11-21 109870.01999999999 38562.0 148432.02
623 2025-11-24 109938.01999999999 38512.0 148450.02
624 2025-11-25 111938.01999999999 37762.0 149700.02
625 2025-11-26 111938.01999999999 37170.0 149108.02
626 2025-11-27 111938.01999999999 37172.0 149110.02
627 2025-11-28 111938.01999999999 37834.0 149772.02
628 2025-12-01 115825.68 33942.38 149768.06
629 2025-12-02 115825.68 33994.38 149820.06
630 2025-12-03 117825.68 31736.9 149562.58
631 2025-12-04 112425.68 36213.44 148639.12
632 2025-12-05 109425.68 40135.04 149560.72
633 2025-12-08 112603.68 37374.88 149978.56
634 2025-12-09 116729.68 33392.68 150122.36
635 2025-12-10 117313.68 33230.36 150544.03999999998
636 2025-12-11 115769.68 34872.0 150641.68
637 2025-12-12 115769.68 35290.0 151059.68
638 2025-12-15 118523.68 33424.0 151947.68
639 2025-12-16 118723.68 32706.0 151429.68
640 2025-12-17 111723.68 39646.0 151369.68
641 2025-12-18 111723.68 39308.0 151031.68
642 2025-12-19 110123.68 41948.0 152071.68
643 2025-12-22 116025.68 36470.0 152495.68
644 2025-12-23 116225.68 36004.0 152229.68
645 2025-12-24 115195.68 37596.0 152791.68
646 2025-12-25 115571.68 37138.0 152709.68
647 2025-12-26 115371.68 37724.0 153095.68
648 2025-12-29 118263.68 34576.0 152839.68
649 2025-12-30 116959.68 35302.0 152261.68
650 2025-12-31 115159.68 36376.0 151535.68
651 2026-01-05 114658.76 36630.8 151289.56
652 2026-01-06 113458.76 38219.44 151678.2
653 2026-01-07 113854.76 37808.0 151662.76
654 2026-01-08 112054.76 40598.0 152652.76
655 2026-01-09 113854.76 39384.0 153238.76
656 2026-01-12 114728.76 38852.0 153580.76
657 2026-01-13 120616.76 32848.0 153464.76
658 2026-01-14 121016.76 32938.0 153954.76
659 2026-01-15 121216.76 32146.0 153362.76
660 2026-01-16 117816.76 34904.0 152720.76
661 2026-01-19 118854.76 34260.0 153114.76
662 2026-01-20 117516.76 35734.0 153250.76
663 2026-01-21 120376.76 34212.0 154588.76
664 2026-01-22 120376.76 34816.0 155192.76
665 2026-01-23 120376.76 35548.0 155924.76
666 2026-01-26 119168.76 36470.0 155638.76
667 2026-01-27 119368.76 36470.0 155838.76
668 2026-01-28 119368.76 35508.0 154876.76
669 2026-01-29 117768.76 37012.0 154780.76
670 2026-01-30 118104.76 36686.0 154790.76
671 2026-02-02 123830.76 30870.0 154700.76
672 2026-02-03 123830.76 31876.000000000004 155706.76
673 2026-02-04 123830.76 31492.0 155322.76
674 2026-02-05 123830.76 31724.0 155554.76
675 2026-02-06 123830.76 32184.0 156014.76
676 2026-02-09 124128.76 32940.0 157068.76
677 2026-02-10 130252.76000000001 27476.0 157728.76
678 2026-02-11 128452.76000000001 29178.0 157630.76
679 2026-02-12 130452.76000000001 27600.0 158052.76
680 2026-02-13 130652.76000000001 27454.0 158106.76
681 2026-02-24 129478.76000000001 29166.0 158644.76
682 2026-02-25 129678.76000000001 29674.0 159352.76
683 2026-02-26 129878.76000000001 29368.0 159246.76
684 2026-02-27 128078.76000000001 31448.0 159526.76
685 2026-03-02 126568.76000000001 31928.0 158496.76
686 2026-03-03 123168.76000000001 33314.0 156482.76
687 2026-03-04 121530.76000000001 35378.0 156908.76
688 2026-03-05 123530.76000000001 33996.0 157526.76
689 2026-03-06 121930.76000000001 35786.0 157716.76
690 2026-03-09 120130.76000000001 37128.0 157258.76
691 2026-03-10 122130.76000000001 36208.0 158338.76
692 2026-03-11 122498.76000000001 35428.0 157926.76
693 2026-03-12 122498.76000000001 34782.0 157280.76
694 2026-03-13 122818.76000000001 33916.0 156734.76
695 2026-03-16 119866.98000000001 36893.56 156760.54
696 2026-03-17 120266.98000000001 35880.16 156147.14
697 2026-03-18 120266.98000000001 36240.88 156507.86000000002
698 2026-03-19 115666.98000000001 39534.56 155201.54
699 2026-03-20 114066.98000000001 39770.48 153837.46000000002
700 2026-03-23 112544.98000000001 40740.8 153285.78000000003
701 2026-03-24 111144.98000000001 44227.28 155372.26
702 2026-03-25 113144.98000000001 43183.8 156328.78000000003
703 2026-03-26 114944.98000000001 40874.96 155819.94
704 2026-03-27 114944.98000000001 41492.52 156437.5
705 2026-03-30 118332.98000000001 38077.28 156410.26
706 2026-03-31 118332.98000000001 37634.52 155967.5
707 2026-04-01 121430.98000000001 35512.04 156943.02000000002
708 2026-04-02 121430.98000000001 34988.72 156419.7
709 2026-04-03 118420.98000000001 37343.56 155764.54
710 2026-04-07 117714.98000000001 38003.479999999996 155718.46000000002
711 2026-04-08 117980.98000000001 39241.32 157222.30000000002
712 2026-04-09 117980.98000000001 38689.08 156670.06
713 2026-04-10 118180.98000000001 38458.6 156639.58000000002
714 2026-04-13 116651.58000000002 39420.0 156071.58000000002
715 2026-04-14 115051.58000000002 40588.0 155639.58000000002
716 2026-04-15 114451.58000000002 40568.0 155019.58000000002
717 2026-04-16 113051.58000000002 41612.0 154663.58000000002
718 2026-04-17 111851.58000000002 42540.0 154391.58000000002
719 2026-04-20 111939.58000000002 43314.0 155253.58000000002
720 2026-04-21 111939.58000000002 43070.0 155009.58000000002
721 2026-04-22 111939.58000000002 42160.0 154099.58000000002
722 2026-04-23 110939.58000000002 41660.0 152599.58000000002
723 2026-04-24 109899.58000000002 42320.0 152219.58000000002
724 2026-04-27 113003.58000000002 39480.0 152483.58000000002
725 2026-04-28 112317.58000000002 38134.0 150451.58000000002
726 2026-04-29 113317.58000000002 37898.0 151215.58000000002
727 2026-04-30 111717.58000000002 39436.0 151153.58000000002
+37
View File
@@ -0,0 +1,37 @@
date,cash,stock_value,total_value,positions,month
2023-05-31,17736.34,41098.4,58834.74,10,2023-05
2023-06-30,20515.46,40323.66,60839.12,10,2023-06
2023-07-31,30397.84,29960.78,60358.62,9,2023-07
2023-08-31,26136.7,34300.62,60437.32,10,2023-08
2023-09-30,39873.34,27709.4,67582.74,10,2023-09
2023-10-31,36088.52,31940.92,68029.44,10,2023-10
2023-11-30,49700.74,27047.76,76748.5,10,2023-11
2023-12-31,56387.94,33956.0,90343.94,10,2023-12
2024-01-31,43543.94,44782.0,88325.94,10,2024-01
2024-02-29,41861.28,44551.96,86413.24,10,2024-02
2024-03-31,60401.36,30781.6,91182.96,9,2024-03
2024-04-30,51342.72,39697.38,91040.1,10,2024-04
2024-05-31,65813.5,29130.0,94943.5,9,2024-05
2024-06-30,52527.76,40070.0,92597.76,10,2024-06
2024-07-31,61456.88,33980.7,95437.58,10,2024-07
2024-08-31,52183.52,42771.06,94954.58,10,2024-08
2024-09-30,80349.32,21369.96,101719.28,10,2024-09
2024-10-31,87055.28,25324.0,112379.28,10,2024-10
2024-11-30,101950.1,29600.28,131550.38,10,2024-11
2024-12-31,89318.12,35982.0,125300.12,8,2024-12
2025-01-31,95062.12,29920.0,124982.12,9,2025-01
2025-02-28,102728.72,26542.96,129271.68,9,2025-02
2025-03-31,103822.1,31484.3,135306.4,10,2025-03
2025-04-30,102745.54,35759.3,138504.84,10,2025-04
2025-05-31,115165.84,27606.0,142771.84,8,2025-05
2025-06-30,108767.46,36246.7,145014.16,9,2025-06
2025-07-31,115180.6,32153.94,147334.54,10,2025-07
2025-08-31,116840.6,33900.0,150740.6,10,2025-08
2025-09-30,116726.02,32560.0,149286.02,10,2025-09
2025-10-31,116430.02,34148.0,150578.02,10,2025-10
2025-11-30,111938.02,37834.0,149772.02,10,2025-11
2025-12-31,115159.68,36376.0,151535.68,10,2025-12
2026-01-31,118104.76,36686.0,154790.76,10,2026-01
2026-02-28,128078.76,31448.0,159526.76,10,2026-02
2026-03-31,114944.98,41492.52,156437.5,10,2026-03
2026-04-30,111717.58,39436.0,151153.58,10,2026-04
1 date cash stock_value total_value positions month
2 2023-05-31 17736.34 41098.4 58834.74 10 2023-05
3 2023-06-30 20515.46 40323.66 60839.12 10 2023-06
4 2023-07-31 30397.84 29960.78 60358.62 9 2023-07
5 2023-08-31 26136.7 34300.62 60437.32 10 2023-08
6 2023-09-30 39873.34 27709.4 67582.74 10 2023-09
7 2023-10-31 36088.52 31940.92 68029.44 10 2023-10
8 2023-11-30 49700.74 27047.76 76748.5 10 2023-11
9 2023-12-31 56387.94 33956.0 90343.94 10 2023-12
10 2024-01-31 43543.94 44782.0 88325.94 10 2024-01
11 2024-02-29 41861.28 44551.96 86413.24 10 2024-02
12 2024-03-31 60401.36 30781.6 91182.96 9 2024-03
13 2024-04-30 51342.72 39697.38 91040.1 10 2024-04
14 2024-05-31 65813.5 29130.0 94943.5 9 2024-05
15 2024-06-30 52527.76 40070.0 92597.76 10 2024-06
16 2024-07-31 61456.88 33980.7 95437.58 10 2024-07
17 2024-08-31 52183.52 42771.06 94954.58 10 2024-08
18 2024-09-30 80349.32 21369.96 101719.28 10 2024-09
19 2024-10-31 87055.28 25324.0 112379.28 10 2024-10
20 2024-11-30 101950.1 29600.28 131550.38 10 2024-11
21 2024-12-31 89318.12 35982.0 125300.12 8 2024-12
22 2025-01-31 95062.12 29920.0 124982.12 9 2025-01
23 2025-02-28 102728.72 26542.96 129271.68 9 2025-02
24 2025-03-31 103822.1 31484.3 135306.4 10 2025-03
25 2025-04-30 102745.54 35759.3 138504.84 10 2025-04
26 2025-05-31 115165.84 27606.0 142771.84 8 2025-05
27 2025-06-30 108767.46 36246.7 145014.16 9 2025-06
28 2025-07-31 115180.6 32153.94 147334.54 10 2025-07
29 2025-08-31 116840.6 33900.0 150740.6 10 2025-08
30 2025-09-30 116726.02 32560.0 149286.02 10 2025-09
31 2025-10-31 116430.02 34148.0 150578.02 10 2025-10
32 2025-11-30 111938.02 37834.0 149772.02 10 2025-11
33 2025-12-31 115159.68 36376.0 151535.68 10 2025-12
34 2026-01-31 118104.76 36686.0 154790.76 10 2026-01
35 2026-02-28 128078.76 31448.0 159526.76 10 2026-02
36 2026-03-31 114944.98 41492.52 156437.5 10 2026-03
37 2026-04-30 111717.58 39436.0 151153.58 10 2026-04
+21
View File
@@ -0,0 +1,21 @@
{
"version": "v6.7r2",
"rank_model": "lambdarank (56-dim v6.7)",
"backtest_start": "2023-05-01",
"backtest_end": "2026-04-30",
"n_weeks": 154,
"n_stocks_in_pool": 5378,
"top_n": 10,
"shares_per_grid": 200,
"initial_cash": 60000.0,
"final_total_value": 151153.58,
"total_return_pct": 151.92,
"annual_return_pct": 36.1,
"annual_sharpe": 1.7404,
"max_drawdown_pct": -12.1,
"weekly_win_rate_pct": 59.48,
"n_trades_buy": 1144,
"n_trades_sell": 866,
"realized_pnl_total": 95495.58,
"n_lifecycles": 431
}
File diff suppressed because it is too large Load Diff
+155
View File
@@ -0,0 +1,155 @@
date,cash,stock_value,total_value,positions,year
2023-05-05,29353.86,32311.96,61665.82,10,2023
2023-05-12,24353.86,37200.04,61553.9,10,2023
2023-05-19,19999.86,40128.84,60128.7,10,2023
2023-05-26,17736.34,41098.4,58834.74,10,2023
2023-06-02,22233.92,40253.44,62487.36,10,2023
2023-06-09,23083.9,39432.4,62516.3,10,2023
2023-06-16,27171.9,36662.04,63833.94,10,2023
2023-06-21,27177.36,35306.82,62484.18,10,2023
2023-06-30,20515.46,40323.66,60839.12,10,2023
2023-07-07,27155.76,33151.0,60306.76,10,2023
2023-07-14,29091.76,31705.8,60797.56,10,2023
2023-07-21,29114.82,32509.1,61623.92,9,2023
2023-07-28,30397.84,29960.78,60358.62,9,2023
2023-08-04,31326.78,29340.22,60667.0,10,2023
2023-08-11,28602.46,31713.8,60316.26,10,2023
2023-08-18,33136.7,28257.34,61394.04,10,2023
2023-08-25,26136.7,34300.62,60437.32,10,2023
2023-09-01,33946.78,29311.82,63258.6,10,2023
2023-09-08,29682.92,33243.98,62926.9,10,2023
2023-09-15,33929.1,29926.24,63855.34,10,2023
2023-09-22,35117.34,29932.44,65049.78,10,2023
2023-09-28,39873.34,27709.4,67582.74,10,2023
2023-10-13,44083.08,23683.52,67766.6,10,2023
2023-10-20,33289.08,32655.7,65944.78,10,2023
2023-10-27,36088.52,31940.92,68029.44,10,2023
2023-11-03,40429.7,29377.8,69807.5,10,2023
2023-11-10,46479.74,25900.26,72380.0,10,2023
2023-11-17,49341.34,24523.56,73864.9,10,2023
2023-11-24,49700.74,27047.76,76748.5,10,2023
2023-12-01,33892.34,39456.38,73348.72,10,2023
2023-12-08,50831.34,30194.3,81025.64,10,2023
2023-12-15,52387.94,31884.0,84271.94,10,2023
2023-12-22,52909.94,32946.0,85855.94,10,2023
2023-12-29,56387.94,33956.0,90343.94,10,2023
2024-01-05,56405.94,34602.0,91007.94,10,2024
2024-01-12,42405.94,42438.0,84843.94,10,2024
2024-01-19,44405.94,46458.0,90863.94,10,2024
2024-01-26,43543.94,44782.0,88325.94,10,2024
2024-02-02,27305.94,52690.0,79995.94,10,2024
2024-02-08,31419.94,51926.0,83345.94,10,2024
2024-02-23,41861.28,44551.96,86413.24,10,2024
2024-03-01,49575.3,40919.28,90494.58,10,2024
2024-03-08,53677.94,35737.8,89415.74,10,2024
2024-03-15,60047.92,31026.78,91074.7,10,2024
2024-03-22,65873.84,26076.62,91950.46,10,2024
2024-03-29,60401.36,30781.6,91182.96,9,2024
2024-04-03,66965.58,26641.44,93607.02,9,2024
2024-04-12,56903.58,32507.18,89410.76,10,2024
2024-04-19,33499.58,52126.22,85625.8,10,2024
2024-04-26,42761.58,46693.66,89455.24,10,2024
2024-04-30,51342.72,39697.38,91040.1,10,2024
2024-05-10,56205.06,34632.66,90837.72,10,2024
2024-05-17,60869.06,31647.16,92516.22,10,2024
2024-05-24,59605.06,32880.44,92485.5,10,2024
2024-05-31,65813.5,29130.0,94943.5,9,2024
2024-06-07,47960.4,42130.56,90090.96,10,2024
2024-06-14,56152.4,36035.36,92187.76,10,2024
2024-06-21,62239.76,31984.0,94223.76,10,2024
2024-06-28,52527.76,40070.0,92597.76,10,2024
2024-07-05,52351.76,40466.0,92817.76,10,2024
2024-07-12,51451.72,42662.64,94114.36,10,2024
2024-07-19,55290.88,38665.44,93956.32,10,2024
2024-07-26,61456.88,33980.7,95437.58,10,2024
2024-08-02,61925.74,34468.0,96393.74,10,2024
2024-08-09,63005.74,33154.0,96159.74,10,2024
2024-08-16,56715.74,39058.0,95773.74,10,2024
2024-08-23,55364.2,37672.44,93036.64,10,2024
2024-08-30,52183.52,42771.06,94954.58,10,2024
2024-09-06,60506.34,35878.58,96384.92,9,2024
2024-09-13,62642.34,30741.5,93383.84,8,2024
2024-09-20,58359.32,34882.94,93242.26,10,2024
2024-09-27,69235.32,29121.08,98356.4,10,2024
2024-09-30,80349.32,21369.96,101719.28,10,2024
2024-10-11,56083.28,43112.0,99195.28,10,2024
2024-10-18,71101.28,35670.0,106771.28,10,2024
2024-10-25,87055.28,25324.0,112379.28,10,2024
2024-11-01,90986.66,26929.52,117916.18,10,2024
2024-11-08,100636.82,23667.66,124304.48,10,2024
2024-11-15,96557.2,28499.72,125056.92,10,2024
2024-11-22,102164.48,26932.02,129096.5,10,2024
2024-11-29,101950.1,29600.28,131550.38,10,2024
2024-12-06,103392.78,28411.32,131804.1,10,2024
2024-12-13,96844.12,33198.0,130042.12,10,2024
2024-12-20,82444.12,44764.0,127208.12,10,2024
2024-12-27,89318.12,35982.0,125300.12,8,2024
2025-01-03,87408.12,37578.0,124986.12,10,2025
2025-01-10,85580.12,38096.0,123676.12,10,2025
2025-01-17,92964.12,32718.0,125682.12,9,2025
2025-01-24,97680.12,30942.0,128622.12,8,2025
2025-01-27,95062.12,29920.0,124982.12,9,2025
2025-02-07,99682.12,27098.0,126780.12,9,2025
2025-02-14,106503.36,21418.78,127922.14,9,2025
2025-02-21,105749.72,23782.4,129532.12,9,2025
2025-02-28,102728.72,26542.96,129271.68,9,2025
2025-03-07,105425.36,27040.4,132465.76,10,2025
2025-03-14,115013.82,19782.36,134796.18,9,2025
2025-03-21,109423.82,25211.12,134634.94,9,2025
2025-03-28,103822.1,31484.3,135306.4,10,2025
2025-04-03,99800.62,34518.32,134318.94,10,2025
2025-04-11,99600.62,37063.76,136664.38,10,2025
2025-04-18,109341.54,30561.36,139902.9,10,2025
2025-04-25,106835.54,32281.18,139116.72,10,2025
2025-04-30,102745.54,35759.3,138504.84,10,2025
2025-05-09,102637.84,37585.88,140223.72,10,2025
2025-05-16,101963.84,38900.0,140863.84,10,2025
2025-05-23,109423.84,32758.0,142181.84,9,2025
2025-05-30,115165.84,27606.0,142771.84,8,2025
2025-06-06,118567.88,25523.98,144091.86,10,2025
2025-06-13,121827.46,23271.14,145098.6,9,2025
2025-06-20,109651.46,33717.02,143368.48,9,2025
2025-06-27,108767.46,36246.7,145014.16,9,2025
2025-07-04,117302.28,29383.94,146686.22,10,2025
2025-07-11,116312.28,31418.0,147730.28,10,2025
2025-07-18,113160.2,33946.38,147106.58,10,2025
2025-07-25,115180.6,32153.94,147334.54,10,2025
2025-08-01,113316.6,33357.8,146674.4,10,2025
2025-08-08,119086.6,28390.0,147476.6,10,2025
2025-08-15,119662.6,28798.0,148460.6,9,2025
2025-08-22,126652.6,23932.0,150584.6,10,2025
2025-08-29,116840.6,33900.0,150740.6,10,2025
2025-09-05,114126.02,36400.0,150526.02,10,2025
2025-09-12,116580.02,34638.0,151218.02,10,2025
2025-09-19,115522.02,34548.0,150070.02,10,2025
2025-09-26,115364.02,36744.0,152108.02,9,2025
2025-09-30,116726.02,32560.0,149286.02,10,2025
2025-10-10,116570.02,31992.0,148562.02,10,2025
2025-10-17,113000.02,34894.0,147894.02,10,2025
2025-10-24,113198.02,36844.0,150042.02,10,2025
2025-10-31,116430.02,34148.0,150578.02,10,2025
2025-11-07,118702.02,31342.0,150044.02,10,2025
2025-11-14,119016.02,33214.0,152230.02,10,2025
2025-11-21,109870.02,38562.0,148432.02,10,2025
2025-11-28,111938.02,37834.0,149772.02,10,2025
2025-12-05,109425.68,40135.04,149560.72,10,2025
2025-12-12,115769.68,35290.0,151059.68,10,2025
2025-12-19,110123.68,41948.0,152071.68,10,2025
2025-12-26,115371.68,37724.0,153095.68,10,2025
2025-12-31,115159.68,36376.0,151535.68,10,2025
2026-01-09,113854.76,39384.0,153238.76,10,2026
2026-01-16,117816.76,34904.0,152720.76,10,2026
2026-01-23,120376.76,35548.0,155924.76,10,2026
2026-01-30,118104.76,36686.0,154790.76,10,2026
2026-02-06,123830.76,32184.0,156014.76,10,2026
2026-02-13,130652.76,27454.0,158106.76,10,2026
2026-02-27,128078.76,31448.0,159526.76,10,2026
2026-03-06,121930.76,35786.0,157716.76,9,2026
2026-03-13,122818.76,33916.0,156734.76,9,2026
2026-03-20,114066.98,39770.48,153837.46,10,2026
2026-03-27,114944.98,41492.52,156437.5,10,2026
2026-04-03,118420.98,37343.56,155764.54,10,2026
2026-04-10,118180.98,38458.6,156639.58,10,2026
2026-04-17,111851.58,42540.0,154391.58,10,2026
2026-04-24,109899.58,42320.0,152219.58,10,2026
2026-04-30,111717.58,39436.0,151153.58,10,2026
1 date cash stock_value total_value positions year
2 2023-05-05 29353.86 32311.96 61665.82 10 2023
3 2023-05-12 24353.86 37200.04 61553.9 10 2023
4 2023-05-19 19999.86 40128.84 60128.7 10 2023
5 2023-05-26 17736.34 41098.4 58834.74 10 2023
6 2023-06-02 22233.92 40253.44 62487.36 10 2023
7 2023-06-09 23083.9 39432.4 62516.3 10 2023
8 2023-06-16 27171.9 36662.04 63833.94 10 2023
9 2023-06-21 27177.36 35306.82 62484.18 10 2023
10 2023-06-30 20515.46 40323.66 60839.12 10 2023
11 2023-07-07 27155.76 33151.0 60306.76 10 2023
12 2023-07-14 29091.76 31705.8 60797.56 10 2023
13 2023-07-21 29114.82 32509.1 61623.92 9 2023
14 2023-07-28 30397.84 29960.78 60358.62 9 2023
15 2023-08-04 31326.78 29340.22 60667.0 10 2023
16 2023-08-11 28602.46 31713.8 60316.26 10 2023
17 2023-08-18 33136.7 28257.34 61394.04 10 2023
18 2023-08-25 26136.7 34300.62 60437.32 10 2023
19 2023-09-01 33946.78 29311.82 63258.6 10 2023
20 2023-09-08 29682.92 33243.98 62926.9 10 2023
21 2023-09-15 33929.1 29926.24 63855.34 10 2023
22 2023-09-22 35117.34 29932.44 65049.78 10 2023
23 2023-09-28 39873.34 27709.4 67582.74 10 2023
24 2023-10-13 44083.08 23683.52 67766.6 10 2023
25 2023-10-20 33289.08 32655.7 65944.78 10 2023
26 2023-10-27 36088.52 31940.92 68029.44 10 2023
27 2023-11-03 40429.7 29377.8 69807.5 10 2023
28 2023-11-10 46479.74 25900.26 72380.0 10 2023
29 2023-11-17 49341.34 24523.56 73864.9 10 2023
30 2023-11-24 49700.74 27047.76 76748.5 10 2023
31 2023-12-01 33892.34 39456.38 73348.72 10 2023
32 2023-12-08 50831.34 30194.3 81025.64 10 2023
33 2023-12-15 52387.94 31884.0 84271.94 10 2023
34 2023-12-22 52909.94 32946.0 85855.94 10 2023
35 2023-12-29 56387.94 33956.0 90343.94 10 2023
36 2024-01-05 56405.94 34602.0 91007.94 10 2024
37 2024-01-12 42405.94 42438.0 84843.94 10 2024
38 2024-01-19 44405.94 46458.0 90863.94 10 2024
39 2024-01-26 43543.94 44782.0 88325.94 10 2024
40 2024-02-02 27305.94 52690.0 79995.94 10 2024
41 2024-02-08 31419.94 51926.0 83345.94 10 2024
42 2024-02-23 41861.28 44551.96 86413.24 10 2024
43 2024-03-01 49575.3 40919.28 90494.58 10 2024
44 2024-03-08 53677.94 35737.8 89415.74 10 2024
45 2024-03-15 60047.92 31026.78 91074.7 10 2024
46 2024-03-22 65873.84 26076.62 91950.46 10 2024
47 2024-03-29 60401.36 30781.6 91182.96 9 2024
48 2024-04-03 66965.58 26641.44 93607.02 9 2024
49 2024-04-12 56903.58 32507.18 89410.76 10 2024
50 2024-04-19 33499.58 52126.22 85625.8 10 2024
51 2024-04-26 42761.58 46693.66 89455.24 10 2024
52 2024-04-30 51342.72 39697.38 91040.1 10 2024
53 2024-05-10 56205.06 34632.66 90837.72 10 2024
54 2024-05-17 60869.06 31647.16 92516.22 10 2024
55 2024-05-24 59605.06 32880.44 92485.5 10 2024
56 2024-05-31 65813.5 29130.0 94943.5 9 2024
57 2024-06-07 47960.4 42130.56 90090.96 10 2024
58 2024-06-14 56152.4 36035.36 92187.76 10 2024
59 2024-06-21 62239.76 31984.0 94223.76 10 2024
60 2024-06-28 52527.76 40070.0 92597.76 10 2024
61 2024-07-05 52351.76 40466.0 92817.76 10 2024
62 2024-07-12 51451.72 42662.64 94114.36 10 2024
63 2024-07-19 55290.88 38665.44 93956.32 10 2024
64 2024-07-26 61456.88 33980.7 95437.58 10 2024
65 2024-08-02 61925.74 34468.0 96393.74 10 2024
66 2024-08-09 63005.74 33154.0 96159.74 10 2024
67 2024-08-16 56715.74 39058.0 95773.74 10 2024
68 2024-08-23 55364.2 37672.44 93036.64 10 2024
69 2024-08-30 52183.52 42771.06 94954.58 10 2024
70 2024-09-06 60506.34 35878.58 96384.92 9 2024
71 2024-09-13 62642.34 30741.5 93383.84 8 2024
72 2024-09-20 58359.32 34882.94 93242.26 10 2024
73 2024-09-27 69235.32 29121.08 98356.4 10 2024
74 2024-09-30 80349.32 21369.96 101719.28 10 2024
75 2024-10-11 56083.28 43112.0 99195.28 10 2024
76 2024-10-18 71101.28 35670.0 106771.28 10 2024
77 2024-10-25 87055.28 25324.0 112379.28 10 2024
78 2024-11-01 90986.66 26929.52 117916.18 10 2024
79 2024-11-08 100636.82 23667.66 124304.48 10 2024
80 2024-11-15 96557.2 28499.72 125056.92 10 2024
81 2024-11-22 102164.48 26932.02 129096.5 10 2024
82 2024-11-29 101950.1 29600.28 131550.38 10 2024
83 2024-12-06 103392.78 28411.32 131804.1 10 2024
84 2024-12-13 96844.12 33198.0 130042.12 10 2024
85 2024-12-20 82444.12 44764.0 127208.12 10 2024
86 2024-12-27 89318.12 35982.0 125300.12 8 2024
87 2025-01-03 87408.12 37578.0 124986.12 10 2025
88 2025-01-10 85580.12 38096.0 123676.12 10 2025
89 2025-01-17 92964.12 32718.0 125682.12 9 2025
90 2025-01-24 97680.12 30942.0 128622.12 8 2025
91 2025-01-27 95062.12 29920.0 124982.12 9 2025
92 2025-02-07 99682.12 27098.0 126780.12 9 2025
93 2025-02-14 106503.36 21418.78 127922.14 9 2025
94 2025-02-21 105749.72 23782.4 129532.12 9 2025
95 2025-02-28 102728.72 26542.96 129271.68 9 2025
96 2025-03-07 105425.36 27040.4 132465.76 10 2025
97 2025-03-14 115013.82 19782.36 134796.18 9 2025
98 2025-03-21 109423.82 25211.12 134634.94 9 2025
99 2025-03-28 103822.1 31484.3 135306.4 10 2025
100 2025-04-03 99800.62 34518.32 134318.94 10 2025
101 2025-04-11 99600.62 37063.76 136664.38 10 2025
102 2025-04-18 109341.54 30561.36 139902.9 10 2025
103 2025-04-25 106835.54 32281.18 139116.72 10 2025
104 2025-04-30 102745.54 35759.3 138504.84 10 2025
105 2025-05-09 102637.84 37585.88 140223.72 10 2025
106 2025-05-16 101963.84 38900.0 140863.84 10 2025
107 2025-05-23 109423.84 32758.0 142181.84 9 2025
108 2025-05-30 115165.84 27606.0 142771.84 8 2025
109 2025-06-06 118567.88 25523.98 144091.86 10 2025
110 2025-06-13 121827.46 23271.14 145098.6 9 2025
111 2025-06-20 109651.46 33717.02 143368.48 9 2025
112 2025-06-27 108767.46 36246.7 145014.16 9 2025
113 2025-07-04 117302.28 29383.94 146686.22 10 2025
114 2025-07-11 116312.28 31418.0 147730.28 10 2025
115 2025-07-18 113160.2 33946.38 147106.58 10 2025
116 2025-07-25 115180.6 32153.94 147334.54 10 2025
117 2025-08-01 113316.6 33357.8 146674.4 10 2025
118 2025-08-08 119086.6 28390.0 147476.6 10 2025
119 2025-08-15 119662.6 28798.0 148460.6 9 2025
120 2025-08-22 126652.6 23932.0 150584.6 10 2025
121 2025-08-29 116840.6 33900.0 150740.6 10 2025
122 2025-09-05 114126.02 36400.0 150526.02 10 2025
123 2025-09-12 116580.02 34638.0 151218.02 10 2025
124 2025-09-19 115522.02 34548.0 150070.02 10 2025
125 2025-09-26 115364.02 36744.0 152108.02 9 2025
126 2025-09-30 116726.02 32560.0 149286.02 10 2025
127 2025-10-10 116570.02 31992.0 148562.02 10 2025
128 2025-10-17 113000.02 34894.0 147894.02 10 2025
129 2025-10-24 113198.02 36844.0 150042.02 10 2025
130 2025-10-31 116430.02 34148.0 150578.02 10 2025
131 2025-11-07 118702.02 31342.0 150044.02 10 2025
132 2025-11-14 119016.02 33214.0 152230.02 10 2025
133 2025-11-21 109870.02 38562.0 148432.02 10 2025
134 2025-11-28 111938.02 37834.0 149772.02 10 2025
135 2025-12-05 109425.68 40135.04 149560.72 10 2025
136 2025-12-12 115769.68 35290.0 151059.68 10 2025
137 2025-12-19 110123.68 41948.0 152071.68 10 2025
138 2025-12-26 115371.68 37724.0 153095.68 10 2025
139 2025-12-31 115159.68 36376.0 151535.68 10 2025
140 2026-01-09 113854.76 39384.0 153238.76 10 2026
141 2026-01-16 117816.76 34904.0 152720.76 10 2026
142 2026-01-23 120376.76 35548.0 155924.76 10 2026
143 2026-01-30 118104.76 36686.0 154790.76 10 2026
144 2026-02-06 123830.76 32184.0 156014.76 10 2026
145 2026-02-13 130652.76 27454.0 158106.76 10 2026
146 2026-02-27 128078.76 31448.0 159526.76 10 2026
147 2026-03-06 121930.76 35786.0 157716.76 9 2026
148 2026-03-13 122818.76 33916.0 156734.76 9 2026
149 2026-03-20 114066.98 39770.48 153837.46 10 2026
150 2026-03-27 114944.98 41492.52 156437.5 10 2026
151 2026-04-03 118420.98 37343.56 155764.54 10 2026
152 2026-04-10 118180.98 38458.6 156639.58 10 2026
153 2026-04-17 111851.58 42540.0 154391.58 10 2026
154 2026-04-24 109899.58 42320.0 152219.58 10 2026
155 2026-04-30 111717.58 39436.0 151153.58 10 2026
BIN
View File
Binary file not shown.
+57
View File
@@ -0,0 +1,57 @@
{
"version": "v6.7-rank-lambdarank-mvp",
"feature_version": "v3.4",
"objective": "lambdarank",
"metric": "ndcg@5,10",
"ndcg_at_5": 0.5108501630463596,
"ndcg_at_10": 0.577072484777789,
"spearman": 0.5896180660523218,
"spearman_baseline_regression": 0.573016131374212,
"spearman_ratio_vs_baseline": 1.0289728923307881,
"best_iter": 29,
"train_seconds": 1.4682528972625732,
"n_train": 254234,
"n_val": 73785,
"top10_features": [
{
"name": "dist_to_grid_lower",
"gain": 26718.906676471233
},
{
"name": "amp_x_grid",
"gain": 5439.311601281166
},
{
"name": "cross_freq_x_bb",
"gain": 4232.088328957558
},
{
"name": "amp_x_grid_vol",
"gain": 3015.4479908943176
},
{
"name": "dist_to_grid_upper",
"gain": 2406.65438079834
},
{
"name": "ln_float_mv",
"gain": 1248.8392915129662
},
{
"name": "avg_daily_amp",
"gain": 794.0413353443146
},
{
"name": "amount_mean_20d",
"gain": 670.1584417819977
},
{
"name": "vol_decay_x_dist_lower",
"gain": 659.5440436601639
},
{
"name": "vol_decay_x_grid_balance",
"gain": 653.8379725217819
}
]
}
+158
View File
@@ -0,0 +1,158 @@
# v6.7r3 代码使用说明
`code/` 目录包含 v6.7r3 策略的**自包含**实现,可在不依赖项目其他模块的情况下独立运行。
## 文件清单
| 文件 | 大小 | 作用 |
|---|---|---|
| `strategy.py` | ~22 KB | 完整策略实现(模型加载/特征/网格/沉寂/周度淘汰) |
## 文件结构
```
strategy.py
├── 配置常量 (网格/价格/沉寂/周度)
├── 1. 持仓 + 网格交易
│ ├── Position dataclass
│ ├── compute_initial_position() 初始建仓
│ ├── compute_single_position() 单格建仓
│ └── simulate_grid_day() 单日网格 (LIFO)
├── 2. 5 特征沉寂检测
│ └── is_slumbering() 5 特征 ≥ 3 触发
├── 3. 模型加载 + 三件套预测
│ ├── ModelBundle class
│ │ ├── load 3 .pkl
│ │ └── predict(X56) → {rank_score, top_prob, stack_prob}
├── 4. 特征计算
│ ├── compute_52_base_features() 52 维 v3.4 基础
│ └── compute_4_v67_new_features() 4 维 v6.7 新增
├── 5. 评分池
│ └── score_pool() 单日全市场评分
├── 6. 主回测入口(精简版)
│ └── quick_backtest() 生产级完整版见 tools/backtest_v67r2.py
└── 7. 入口示例
└── __main__ 加载模型 + 加载行情 + 跑回测
```
## 快速开始
```bash
# 1. 安装依赖
pip install pandas numpy lightgbm scipy
# 2. 准备数据
# 方式 A: 用项目内的 dump_market_data_to_parquet.py 拉 Postgres
python tools/dump_market_data_to_parquet.py
# 方式 B: 直接用现有 parquet (release/v6.7r3 之前已生成 5025 个)
# 3. 运行
cd release/v6.7r3/code
python strategy.py
```
## 核心 API 速查
### 1. 加载模型
```python
from strategy import ModelBundle
models = ModelBundle("../models")
# 等价: models = ModelBundle("release/v6.7r3/models")
print(models.rank_feats[:5]) # ['rolling_grid_ratio_20d', ...]
```
### 2. 三件套预测
```python
import numpy as np
import pandas as pd
# 加载单只股 120 日窗口
df_window = pd.read_parquet("data/market_data/share/000001.parquet")
df_window["date"] = pd.to_datetime(df_window["date"])
df_window = df_window.tail(120).reset_index(drop=True)
# 算 56 维特征 (这里用简化版, 生产建议用项目 core.features)
from strategy import compute_52_base_features, compute_4_v67_new_features
fd = compute_52_base_features(df_window)
fd = compute_4_v67_new_features(df_window, fd)
X56 = np.array([fd.get(k, 0.0) for k in models.rank_feats], dtype=np.float64).reshape(1, -1)
# 三件套预测
pred = models.predict(X56)
print(f"rank_score: {pred['rank_score'][0]:.3f}")
print(f"top_prob: {pred['top_prob'][0]:.3f}")
print(f"stack_prob: {pred['stack_prob'][0]:.3f}") # 月末选股用
```
### 3. 沉寂检测
```python
from strategy import is_slumbering
df = pd.read_parquet("data/market_data/share/000001.parquet")
df["date"] = pd.to_datetime(df["date"])
slumbering = is_slumbering(df, min_triggers=3)
# True = 5 特征中至少 3 个触发 → 资金离场
```
### 4. 网格交易(单日)
```python
from strategy import Position, simulate_grid_day
pos = Position(code="000001", base=10, queue=[10.0, 9.5], entry_date="2023-05-01")
new_base, new_queue, trades = simulate_grid_day(
pos.base, pos.queue,
open_p=9.4, high_p=10.6, low_p=9.3, close_p=10.5,
)
# 触发 buy at 9.0 (low 9.3 <= 9.0? no, 9.3 > 9.0 不触发)
# 触发 sell at 10.0 (high 10.6 >= 10.0, 卖出 1 格)
# 最终: base=11, queue=[9.5] (1 格清仓)
```
### 5. 评分池(每日全市场)
```python
from strategy import score_pool, ModelBundle
import pandas as pd
# 假设 kline_cache 是 dict[code, DataFrame]
models = ModelBundle("../models")
pool = score_pool(pd.Timestamp("2024-03-15"), kline_cache, models, models.rank_feats)
# pool 列: code6, latest_close, rank_score, top_prob, stack_prob
# 按 stack_prob 降序, 取 top 10
top10 = pool.head(10)
```
## 关键参数(可调)
| 参数 | 默认 | 说明 | 调优方向 |
|---|---|---|---|
| `WEEKLY_ELIM_N` | 2 | 周度淘汰: 连续 N 周不在 top 50 | 1 太频, 3+ 太慢 |
| `SLUMBER_TRIGGERS` | 3 | 沉寂检测: >= 3/5 特征触发 | 2 太宽, 4 太严 |
| `SLUMBER_DAYS` | 10 | 连续触发多少天清仓 | 5 太短, 20 太长 |
| `TOP_N` | 10 | 最大持仓数 | 5-15 视资金量 |
| `SHARES_PER_GRID` | 200 | 单格股数 (2 手) | 100 (1 手) 也可 |
| `REFILL_PRICE` | 9.0-9.8 | 补仓价格区间 | 紧贴 9-10 网格上限 |
## 依赖项目其他模块?
为保持 `code/` 目录**自包含**:
- ✅ 不依赖 `core/features.py` (内置 `compute_52_base_features` 简化版)
- ✅ 不依赖 `training/dataset_builder.py` (内置 `compute_4_v67_new_features`)
- ❌ 需要数据: parquet 行情文件
**生产部署建议**:用 `core/features.calculate_features` 替换 `compute_52_base_features`,
它有完整版 v3.4 52 维特征实现,精度更高。
## 完整版 vs 精简版
| 维度 | `code/strategy.py` (精简) | `tools/backtest_v67r2.py` (生产) |
|---|---|---|
| 特征计算 | 简化版 (10 维示例) | 完整版 (52 维 v3.4 + 4 维 v6.7) |
| 数据加载 | dict of DataFrame | parquet 目录 + score cache |
| 日志 | 无 | 详细进度打印 |
| 输出 | dict 指标 | CSV/JSON/PNG 完整产物 |
| 速度 | 慢 (无缓存) | 快 (有 score cache) |
| 用途 | 教学/集成 | 完整回测 |
**生产环境**:用 `tools/backtest_v67r2.py` 跑回测,确保完整功能。
**集成到实盘系统**:用 `code/strategy.py` 中的 `ModelBundle``quick_backtest` 作为模板。
@@ -0,0 +1,574 @@
"""
v6.7r3 策略核心实现 —— 自包含版
================================
包含:
1. 模型加载
2. 56 维特征计算
3. 三件套预测 (rank → top → stacking)
4. 网格交易模拟器
5. 5 特征沉寂检测
6. 周度评分淘汰
7. 月末调仓 + 清仓补入
依赖:
pip install pandas numpy lightgbm scipy
(内嵌了核心算法, 不依赖项目其他模块, 可独立运行)
"""
from __future__ import annotations
import math
import pickle
import time
from collections import defaultdict
from dataclasses import dataclass, field
from pathlib import Path
from typing import Dict, List, Optional, Tuple
import numpy as np
import pandas as pd
# ============================================================
# 0. 配置
# ============================================================
# 网格
INITIAL_CASH = 60_000.0
TOP_N = 10
TOP_MODEL_N = 50
SHARES_PER_GRID = 200
MIN_BUY_PRICE = 7.0
MAX_BUY_PRICE = 10.0
REFILL_MIN_PRICE = 9.0
REFILL_MAX_PRICE = 9.8
GRID_LOWER, GRID_UPPER = 1, 11
# 沉寂检测
SLUMBER_TRIGGERS = 3 # >= 3/5 特征触发
SLUMBER_DAYS = 10 # 连续 10 日触发
SLUMBER_LOOKBACK_60 = 60
SLUMBER_LOOKBACK_20 = 20
# 周度淘汰
WEEKLY_ELIM_N = 2 # 连续 2 周不在 top 50 → 卖
# ============================================================
# 1. 持仓 + 网格交易
# ============================================================
@dataclass
class Position:
code: str
base: int # 当前基准价 (整数)
queue: list = field(default_factory=list) # 持仓队列: 每格成本价
entry_date: str = ""
def compute_initial_position(close: float) -> Tuple[int, list]:
"""初始建仓: base=ceil(close), queue=[10, 9, ..., base] 且 >= close"""
base = math.ceil(close)
base = max(GRID_LOWER, min(base, GRID_UPPER))
queue = [g for g in range(GRID_UPPER, base - 1, -1) if g >= close]
return base, queue
def compute_single_position(close: float) -> Tuple[int, list]:
"""单格建仓"""
base = math.ceil(close)
base = max(GRID_LOWER, min(base, GRID_UPPER))
return base, [base]
def simulate_grid_day(base, queue, open_p, high_p, low_p, close_p, can_buy=True):
"""单日网格交易 (LIFO)"""
trades = []
new_base, new_queue = base, list(queue)
# 1) 买
buy_price = new_base - 1
if low_p <= buy_price and new_base > GRID_LOWER and can_buy:
new_base = buy_price
new_queue.append(buy_price)
trades.append({"direction": "buy", "price": buy_price, "shares": SHARES_PER_GRID, "pnl": 0.0})
# 2) 卖 (循环)
while True:
sell_price = new_base + 1
if high_p >= sell_price and new_queue:
buy_cost = new_queue.pop()
pnl = (sell_price - buy_cost) * SHARES_PER_GRID
trades.append({"direction": "sell", "price": sell_price, "shares": SHARES_PER_GRID, "pnl": pnl})
new_base = sell_price
else:
break
return new_base, new_queue, trades
# ============================================================
# 2. 5 特征沉寂检测
# ============================================================
def is_slumbering(df: pd.DataFrame, lookback_60=60, lookback_20=20,
min_triggers=SLUMBER_TRIGGERS) -> bool:
"""检测一只股是否陷入'沉寂' (资金离场后长期低位震荡).
5 特征, >= min_triggers 触发.
"""
if df is None or len(df) < lookback_60:
return False
sub = df.tail(lookback_60)
close = sub["close"].values
high = sub["high"].values
low = sub["low"].values
vol = sub["volume"].values
# 1. 波动率塌陷
log_ret = np.log(close[1:] / close[:-1])
if len(log_ret) < lookback_20:
return False
vol_20d = float(np.std(log_ret[-lookback_20:], ddof=1))
vol_60d = float(np.std(log_ret, ddof=1))
vol_collapse = (vol_60d > 0) and (vol_20d / vol_60d < 0.6)
# 2. 振幅萎缩
amp_20d = float(np.mean((high[-lookback_20:] - low[-lookback_20:]) / close[-lookback_20:]) * 100)
amp_shrink = amp_20d < 2.5
# 3. 成交量枯竭
avg_vol_20 = float(np.mean(vol[-lookback_20:]))
avg_vol_60 = float(np.mean(vol))
vol_dry = (avg_vol_60 > 0) and (avg_vol_20 / avg_vol_60 < 0.5)
# 4. 价格弱势
price_max_60 = float(np.max(close))
price_weak = price_max_60 > 0 and (close[-1] / price_max_60) < 0.85
# 5. 反弹失败
recent_high_30 = float(np.max(high[-30:]))
past_high_60 = float(np.max(high))
rebound_fail = past_high_60 > 0 and (recent_high_30 / past_high_60) < 0.95
triggers = [vol_collapse, amp_shrink, vol_dry, price_weak, rebound_fail]
return sum(triggers) >= min_triggers
# ============================================================
# 3. 模型加载 + 三件套预测
# ============================================================
class ModelBundle:
"""v6.7r3 三件套模型封装"""
def __init__(self, model_dir: str | Path):
model_dir = Path(model_dir)
with open(model_dir / "rank_lambdarank.pkl", "rb") as f:
rank_b = pickle.load(f)
with open(model_dir / "top_v67r2.pkl", "rb") as f:
top_b = pickle.load(f)
with open(model_dir / "stacking_v67r2.pkl", "rb") as f:
stack_b = pickle.load(f)
self.rank_model = rank_b["model"]
self.rank_feats = rank_b["feat_names"] # 56 维
self.top_model = top_b["model"]
self.top_feats = top_b["feat_names"] # 53 维
self.stack_model = stack_b["model"]
self.stack_feats = stack_b["feat_names"] # 55 维
@staticmethod
def _safe_predict_proba(model, X):
if hasattr(model, "predict_proba"):
return model.predict_proba(X)[:, 1]
return model.predict(X, raw_score=False)
def predict(self, X56: np.ndarray) -> dict:
"""输入 56 维特征矩阵 (n, 56), 返回三件套预测 dict.
返回: rank_score (n,), top_prob (n,), stack_prob (n,)
"""
rank_pred = self.rank_model.predict(X56)
# 52 维基础特征在 X56 中的索引
base_52_idx = [self.rank_feats.index(f) for f in self.top_feats if f != "rank_predicted_rounds"]
X53 = np.column_stack([X56[:, base_52_idx], rank_pred])
top_prob = self._safe_predict_proba(self.top_model, X53)
X55 = np.column_stack([X53, top_prob])
stack_prob = self._safe_predict_proba(self.stack_model, X55)
return {
"rank_score": rank_pred,
"top_prob": top_prob,
"stack_prob": stack_prob,
}
# ============================================================
# 4. 特征计算 (从项目 core/features.py 抽取, 关键函数)
# ============================================================
def _log_returns(close: np.ndarray) -> np.ndarray:
return np.log(close[1:] / close[:-1])
def _ema(arr: np.ndarray, period: int) -> np.ndarray:
"""指数移动平均"""
alpha = 2.0 / (period + 1)
out = np.zeros_like(arr)
out[0] = arr[0]
for i in range(1, len(arr)):
out[i] = alpha * arr[i] + (1 - alpha) * out[i-1]
return out
def compute_52_base_features(df: pd.DataFrame, total_share: Optional[float] = None) -> dict:
"""计算 v3.4 的 52 维基础特征 (简化版, 不完全等同于原版).
注: 完整版在 core/features.py, 这里用 pandas/numpy 简化.
"""
n = len(df)
if n < 60:
return None
close = df["close"].values
high = df["high"].values
low = df["low"].values
open_ = df["open"].values
vol = df["volume"].values
feats = {}
# 1. rolling_grid_ratio_20d
feats["rolling_grid_ratio_20d"] = float(np.mean((close[-20:] >= 1) & (close[-20:] <= 11)) * 100)
# 2. cross_freq_20d
diff = close[1:] - close[:-1]
sign_change = np.sum(np.abs(np.diff(np.sign(diff[-19:]))) > 0)
feats["cross_freq_20d"] = float(sign_change / 19 * 100) if 19 > 0 else 0.0
# 3. avg_daily_amp
feats["avg_daily_amp"] = float(np.mean((high - low) / open_) * 100) if n > 0 else 0.0
# 4. high_amp_days
feats["high_amp_days"] = float(np.mean((high - low) / open_ > 0.02) * 100) if n > 0 else 0.0
# 5. atr_pct
tr = np.maximum(high - low, np.maximum(np.abs(high - np.roll(close, 1)),
np.abs(low - np.roll(close, 1))))
feats["atr_pct"] = float(np.mean(tr[-14:]) / close[-1] * 100) if close[-1] > 0 else 0.0
# 6. volatility_20d (年化)
log_ret = _log_returns(close)
feats["volatility_20d"] = float(np.std(log_ret[-20:], ddof=1) * np.sqrt(252) * 100) if len(log_ret) >= 20 else 0.0
# 7. price_cv
feats["price_cv"] = float(np.std(close, ddof=1) / np.mean(close) * 100) if n > 1 and np.mean(close) > 0 else 0.0
# 8. bb_width (布林带宽)
ma20 = np.mean(close[-20:])
sd20 = np.std(close[-20:], ddof=1)
feats["bb_width"] = float((4 * sd20) / ma20 * 100) if ma20 > 0 else 0.0
# 9. volume_ratio
avg_vol_20 = float(np.mean(vol[-20:])) if n >= 20 else float(np.mean(vol))
avg_vol_60 = float(np.mean(vol[-60:])) if n >= 60 else float(np.mean(vol))
feats["volume_ratio"] = avg_vol_20 / avg_vol_60 if avg_vol_60 > 0 else 0.0
# 10. obv_slope
direction = np.sign(np.diff(close))
direction = np.concatenate([[0], direction])
obv = np.cumsum(direction * vol)
if len(obv) >= 20:
x = np.arange(20)
y = obv[-20:]
feats["obv_slope"] = float((np.polyfit(x, y, 1)[0]) / (np.mean(np.abs(y)) + 1e-10))
else:
feats["obv_slope"] = 0.0
# ... (其他 42 维特征省略, 完整版在 core/features.py)
# 这里只展示 10 个核心特征的计算模式
# 实际部署时建议直接调用项目 core.features.calculate_features
return feats
def compute_4_v67_new_features(df: pd.DataFrame, fd: dict) -> dict:
"""计算 v6.7 新增的 4 维特征"""
n = len(df)
if n < 60:
return fd
close = df["close"].values
high = df["high"].values
low = df["low"].values
# 53. vol_decay_5d
log_ret = np.log(close[1:] / close[:-1])
if len(log_ret) >= 20:
vol_5d = float(np.std(log_ret[-5:], ddof=1))
vol_20d = float(np.std(log_ret[-20:], ddof=1))
fd["vol_decay_5d"] = vol_5d / vol_20d if vol_20d > 0 else 0.0
else:
fd["vol_decay_5d"] = 0.0
# 54. grid_touch_relative_10d
if n >= 60:
range_10d = float(np.max(high[-10:]) - np.min(low[-10:]))
range_60d = float(np.max(high[-60:]) - np.min(low[-60:]))
close_now = float(close[-1])
close_60d_mean = float(np.mean(close[-60:]))
if close_now > 0 and close_60d_mean > 0 and range_60d > 0:
fd["grid_touch_relative_10d"] = (range_10d / close_now) / (range_60d / close_60d_mean)
else:
fd["grid_touch_relative_10d"] = 0.0
else:
fd["grid_touch_relative_10d"] = 0.0
# 55. vol_decay_x_grid_balance
fd["vol_decay_x_grid_balance"] = fd.get("vol_decay_5d", 0.0) * fd.get("grid_room_balance", 0.0)
# 56. vol_decay_x_dist_lower
fd["vol_decay_x_dist_lower"] = fd.get("vol_decay_5d", 0.0) * fd.get("dist_to_grid_lower", 0.0)
return fd
# ============================================================
# 5. 评分池(单日)
# ============================================================
def score_pool(date: pd.Timestamp,
kline_cache: Dict[str, pd.DataFrame],
models: ModelBundle,
feat_names: list) -> pd.DataFrame:
"""对所有有 120 日历史的股算 v6.7 三件套预测.
返回 DataFrame: code6, latest_close, rank_score, top_prob, stack_prob
"""
PRICE_MIN, PRICE_MAX = 1.0, 11.0
rows = []
codes = []
closes = []
for code, df in kline_cache.items():
sub = df[df["date"] <= date]
if len(sub) < 120:
continue
latest = float(sub["close"].iloc[-1])
if not (PRICE_MIN <= latest <= PRICE_MAX):
continue
# 算 56 维特征
obs = sub.tail(120).reset_index(drop=True)
fd = compute_52_base_features(obs)
if fd is None:
continue
fd = compute_4_v67_new_features(obs, fd)
try:
X = np.array([fd.get(k, 0.0) for k in feat_names], dtype=np.float64).reshape(1, -1)
pred = models.predict(X)
except Exception:
continue
rows.append(pred)
codes.append(code)
closes.append(latest)
if not rows:
return pd.DataFrame(columns=["code6", "latest_close", "rank_score", "top_prob", "stack_prob"])
import numpy as np
return pd.DataFrame({
"code6": codes,
"latest_close": closes,
"rank_score": [r["rank_score"][0] for r in rows],
"top_prob": [r["top_prob"][0] for r in rows],
"stack_prob": [r["stack_prob"][0] for r in rows],
}).sort_values("stack_prob", ascending=False).reset_index(drop=True)
# ============================================================
# 6. 主回测入口(精简版, 仅展示核心逻辑)
# ============================================================
def quick_backtest(kline_cache: Dict[str, pd.DataFrame],
models: ModelBundle,
start_date: str = "2023-05-01",
end_date: str = "2026-04-30",
weekly_elim_n: int = WEEKLY_ELIM_N,
slumber_days: int = SLUMBER_DAYS) -> dict:
"""精简版 3 年回测 (生产级完整版见 tools/backtest_v67r2.py).
核心流程:
1. 初始建仓
2. 每日: 网格交易 + 沉寂检测
3. 周五: 周度评分淘汰 + 补仓
4. 月末: 清仓补入 (触及 11 元)
Returns:
dict: 总收益率, 年化夏普, 最大回撤, 周胜率, 终值
"""
feat_names = models.rank_feats
# 交易日索引
sample = next(iter(kline_cache.values()))
all_dates = pd.DatetimeIndex(sorted(sample["date"].unique()))
mask = (all_dates >= start_date) & (all_dates <= end_date)
backtest_dates = all_dates[mask]
# 初始评分 (INIT_SELECT_DATE)
init_date = pd.Timestamp("2023-04-28")
init_pool = score_pool(init_date, kline_cache, models, feat_names)
init_pool = init_pool[init_pool["latest_close"].apply(lambda p: MIN_BUY_PRICE <= p <= MAX_BUY_PRICE)]
init_pool = init_pool.sort_values("stack_prob", ascending=False).head(TOP_N)
# 初始建仓
positions: Dict[str, Position] = {}
cash = INITIAL_CASH
for _, row in init_pool.iterrows():
code = row["code6"]
actual_close = float(row["latest_close"])
base, grid_queue = compute_initial_position(actual_close)
if not grid_queue:
continue
positions[code] = Position(code, base, [actual_close] * len(grid_queue), "2023-04-28")
for _ in grid_queue:
cash -= actual_close * SHARES_PER_GRID
# 周度淘汰历史
top50_history = defaultdict(list)
slumber_streak: Dict[str, int] = {}
total_asset_history = []
weekly_pnl = []
for i, cur_date in enumerate(backtest_dates):
cur_str = cur_date.strftime("%Y-%m-%d")
is_week_end = (i == len(backtest_dates) - 1) or (backtest_dates[i + 1].week != cur_date.week)
# === 网格日间交易 ===
for code, pos in list(positions.items()):
if not pos.queue:
continue
df = kline_cache[code]
sub = df[df["date"] == cur_date]
if sub.empty:
continue
row = sub.iloc[0]
new_base, new_queue, day_trades = simulate_grid_day(
pos.base, pos.queue,
float(row["open"]), float(row["high"]), float(row["low"]), float(row["close"]),
)
if day_trades:
pos.base, pos.queue = new_base, new_queue
for t in day_trades:
if t["direction"] == "buy":
cash -= t["price"] * t["shares"]
else:
cash += t["price"] * t["shares"]
# === 沉寂检测 ===
for code, pos in list(positions.items()):
if not pos.queue:
continue
df = kline_cache[code]
sub = df[df["date"] <= cur_date]
if len(sub) < 60:
continue
slumber = is_slumbering(sub)
slumber_streak[code] = slumber_streak.get(code, 0) + 1 if slumber else 0
if slumber_streak[code] >= slumber_days and pos.queue:
# 全仓清仓
px = float(sub["close"].iloc[-1])
cash += px * len(pos.queue) * SHARES_PER_GRID
positions[code] = Position(code, 0, [], cur_str)
slumber_streak[code] = 0
# === 资产快照 ===
mv = sum((float(kline_cache[c][kline_cache[c]["date"] <= cur_date]["close"].iloc[-1])
* len(p.queue) * SHARES_PER_GRID)
for c, p in positions.items() if p.queue)
total_asset = cash + mv
total_asset_history.append(total_asset)
# === 周度淘汰 + 补仓 ===
if is_week_end and i > 0 and weekly_elim_n > 0:
pool = score_pool(cur_date, kline_cache, models, feat_names)
top50 = set(pool.head(TOP_MODEL_N)["code6"].tolist())
for code in list(positions.keys()):
top50_history[code].append(code in top50)
# 连续 N 周不在 top 50 → 卖出
inactive = []
for code, p in positions.items():
if not p.queue:
continue
hist = top50_history.get(code, [])
if len(hist) >= weekly_elim_n and all(x is False for x in hist[-weekly_elim_n:]):
inactive.append(code)
for code in inactive[:1]: # 每月最多淘汰 1 只 (与 v6.3 一致)
pos = positions[code]
sub = kline_cache[code][kline_cache[code]["date"] <= cur_date]
px = float(sub["close"].iloc[-1])
cash += px * len(pos.queue) * SHARES_PER_GRID
positions[code] = Position(code, 0, [], cur_str)
# 补仓
positions = {c: p for c, p in positions.items() if p.queue}
refill_needed = max(0, TOP_N - len(positions))
if refill_needed > 0:
ref_pool = pool[pool["latest_close"].apply(lambda p: REFILL_MIN_PRICE < p < REFILL_MAX_PRICE)]
ref_pool = ref_pool[~ref_pool["code6"].isin(positions.keys())]
ref_pool = ref_pool.head(refill_needed)
for _, row in ref_pool.iterrows():
code = row["code6"]
actual_close = float(row["latest_close"])
base, grid_queue = compute_single_position(actual_close)
if not grid_queue:
continue
cost = actual_close * SHARES_PER_GRID * len(grid_queue)
if cash < cost:
continue
positions[code] = Position(code, base, [actual_close] * len(grid_queue), cur_str)
cash -= cost
# 计算指标
final_value = total_asset_history[-1] if total_asset_history else INITIAL_CASH
total_return = final_value / INITIAL_CASH - 1
rets = np.diff(total_asset_history) / total_asset_history[:-1]
sharpe = float(rets.mean() / rets.std() * np.sqrt(52)) if len(rets) > 1 and rets.std() > 0 else 0.0
cum_max = np.maximum.accumulate(total_asset_history)
dd = (np.array(total_asset_history) - cum_max) / cum_max
max_dd = float(dd.min())
win_rate = float((rets > 0).mean()) if len(rets) > 0 else 0.0
return {
"total_return_pct": round(total_return * 100, 2),
"annual_sharpe": round(sharpe, 4),
"max_drawdown_pct": round(max_dd * 100, 2),
"weekly_win_rate_pct": round(win_rate * 100, 2),
"final_value": round(final_value, 2),
}
# ============================================================
# 7. 入口示例
# ============================================================
if __name__ == "__main__":
# 1. 加载模型
models = ModelBundle("models") # 默认从当前目录的 models/ 加载
print(f"✓ 加载模型: rank {len(models.rank_feats)} 维, "
f"top {len(models.top_feats)} 维, stack {len(models.stack_feats)}")
# 2. 加载行情 (示例: 从 parquet 目录)
# 实际部署时, 从 market_data.kline_stock (Postgres) 或本地 parquet 加载
from pathlib import Path
parquet_dir = Path("data/market_data/share")
kline_cache = {}
for p in parquet_dir.glob("*.parquet"):
df = pd.read_parquet(p)
df["date"] = pd.to_datetime(df["date"])
kline_cache[p.stem] = df
print(f"✓ 加载行情: {len(kline_cache)} 只股")
# 3. 跑精简版回测
metrics = quick_backtest(kline_cache, models)
print("\n=== v6.7r3 三年回测结果 (精简版) ===")
for k, v in metrics.items():
print(f" {k}: {v}")
+23
View File
@@ -0,0 +1,23 @@
date,code,exit_price,realized_pnl,slumber_streak_days
2023-07-21,920870,7.53,-374.0,10
2023-07-25,920414,9.44,-1.9999999999999574,10
2024-03-27,920641,7.56,-734.0000000000001,10
2024-04-03,920001,8.93,-164.00000000000006,10
2024-05-31,603825,8.36,-130.00000000000006,10
2024-09-06,300462,8.98,-16.000000000000014,10
2024-09-11,920641,6.67,-1404.0,10
2024-12-23,920090,6.48,-1365.9999999999995,10
2024-12-25,920021,5.71,-1352.0,10
2025-01-13,920792,8.58,-107.99999999999983,10
2025-01-22,920371,6.9,-1199.9999999999998,10
2025-01-24,920339,7.86,-571.9999999999997,10
2025-01-27,920792,8.99,-85.99999999999994,10
2025-02-05,920810,8.18,-217.99999999999997,10
2025-03-13,002789,7.35,-942.0,10
2025-05-21,300052,10.3,258.00000000000017,10
2025-05-26,920639,9.54,185.9999999999996,10
2025-05-26,920553,10.17,94.00000000000013,10
2025-06-12,300052,9.94,77.99999999999976,10
2025-08-12,300798,9.11,-53.90000000000015,10
2025-09-26,000679,7.75,-501.99999999999994,10
2026-03-03,300086,8.81,-114.00000000000006,10
1 date code exit_price realized_pnl slumber_streak_days
2 2023-07-21 920870 7.53 -374.0 10
3 2023-07-25 920414 9.44 -1.9999999999999574 10
4 2024-03-27 920641 7.56 -734.0000000000001 10
5 2024-04-03 920001 8.93 -164.00000000000006 10
6 2024-05-31 603825 8.36 -130.00000000000006 10
7 2024-09-06 300462 8.98 -16.000000000000014 10
8 2024-09-11 920641 6.67 -1404.0 10
9 2024-12-23 920090 6.48 -1365.9999999999995 10
10 2024-12-25 920021 5.71 -1352.0 10
11 2025-01-13 920792 8.58 -107.99999999999983 10
12 2025-01-22 920371 6.9 -1199.9999999999998 10
13 2025-01-24 920339 7.86 -571.9999999999997 10
14 2025-01-27 920792 8.99 -85.99999999999994 10
15 2025-02-05 920810 8.18 -217.99999999999997 10
16 2025-03-13 002789 7.35 -942.0 10
17 2025-05-21 300052 10.3 258.00000000000017 10
18 2025-05-26 920639 9.54 185.9999999999996 10
19 2025-05-26 920553 10.17 94.00000000000013 10
20 2025-06-12 300052 9.94 77.99999999999976 10
21 2025-08-12 300798 9.11 -53.90000000000015 10
22 2025-09-26 000679 7.75 -501.99999999999994 10
23 2026-03-03 300086 8.81 -114.00000000000006 10
Binary file not shown.
+36
View File
@@ -0,0 +1,36 @@
{
"rank": {
"v6.6": {
"spearman_on_val": 0.5741312551267312
},
"v6.7r2": {
"spearman_on_val": 0.5896180660523218
}
},
"top": {
"v6.6": {
"pr_auc_val": 0.6881080916474673
},
"v6.7r2": {
"pr_auc_val": 0.6953262363660502
},
"delta": 0.007218144718582842
},
"stacking": {
"v6.6": {
"pr_auc_val": 0.6474384440874665,
"optimal_threshold": 0.32116277663299964,
"f1_at_thr": 0.6333791329260092
},
"v6.7r2": {
"pr_auc_val": 0.6640333379409579,
"optimal_threshold": 0.3311218467281351,
"f1_at_thr": 0.6402777365656805
},
"delta_pr_auc": 0.016594893853491333
},
"n_train": 254234,
"n_val": 73785,
"elite_rate_train": 0.2665890478850193,
"elite_rate_val": 0.19013349596801518
}
BIN
View File
Binary file not shown.
+26 -11
View File
@@ -1,31 +1,32 @@
{ {
"version": "v6.6", "version": "v6.7r3",
"feature_version": "v3.4", "feature_version": "v3.4",
"architecture": "stacking_calibrated", "architecture": "stacking_calibrated",
"data_source": "mysql://100.121.118.116:3306/grid_seeker_model_base", "data_source": "mysql://100.121.118.116:3306/grid_seeker_model_base",
"training_date": "2026-05-28T14:23:23.662118", "training_date": "2026-06-24T11:00:00.000000",
"n_stocks_total": 4358, "n_stocks_total": 5378,
"n_stocks_after_filter": 1533, "n_stocks_after_filter": 1533,
"n_training_samples": 205491, "n_training_samples": 254234,
"window_days": 120, "window_days": 120,
"future_days": 60, "future_days": 60,
"step_days": 20, "step_days": 20,
"y_rounds_mean": 0.1998384357465777, "y_rounds_mean": 0.1998384357465777,
"y_rounds_median": 0.0, "y_rounds_median": 0.0,
"y_rounds_zero_rate": 0.7108340511263267, "y_rounds_zero_rate": 0.7108340511263267,
"elite_rate": 20.52109338121864, "elite_rate": 26.66,
"rank": { "rank": {
"cv_mae": 0.2053, "cv_mae": 0.2053,
"cv_r2": 0.2258, "cv_r2": 0.2258,
"spearman": 0.5896,
"best_params": { "best_params": {
"num_leaves": 63, "num_leaves": 63,
"min_child_samples": 30, "min_child_samples": 30,
"max_depth": 7 "max_depth": 7
}, },
"n_features": 52 "n_features": 56
}, },
"top": { "top": {
"cv_pr_auc": 0.5333, "cv_pr_auc": 0.6953,
"best_params": { "best_params": {
"num_leaves": 63, "num_leaves": 63,
"min_child_samples": 30, "min_child_samples": 30,
@@ -34,13 +35,27 @@
"n_features": 53 "n_features": 53
}, },
"stacking": { "stacking": {
"cv_pr_auc": 0.8207, "cv_pr_auc": 0.6640,
"best_params": { "best_params": {
"num_leaves": 31, "num_leaves": 31,
"min_child_samples": 20, "min_child_samples": 20,
"max_depth": -1 "max_depth": -1
}, },
"n_features": 54, "n_features": 55,
"optimal_threshold": 0.35 "optimal_threshold": 0.33
} },
"backtest": {
"start": "2023-05-04",
"end": "2026-04-30",
"total_return_pct": 151.92,
"annual_return_pct": 36.46,
"annual_sharpe": 1.7404,
"max_drawdown_pct": -12.10,
"weekly_win_rate_pct": 59.48,
"final_value": 151154,
"rebalancing_frequency": "weekly",
"elimination_window": "2_weeks"
},
"previous_version": "v6.6",
"previous_version_backup": "models_backup_20260624_110913"
} }
+127
View File
@@ -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')
+4 -21
View File
@@ -1,7 +1,6 @@
# coding:utf-8 # coding:utf-8
""" """
启动入口 — 默认使用 Flet2 UI 启动入口 — Flet UI
使用 --tk 参数切换到 Tkinter UI。
""" """
import sys import sys
import os import os
@@ -10,7 +9,6 @@ import ssl
import traceback import traceback
# 修复 Windows 上 flet_desktop 子进程弹出控制台窗口的问题 # 修复 Windows 上 flet_desktop 子进程弹出控制台窗口的问题
# 原始 Popen 不带 CREATE_NO_WINDOW 标志,会为每个子进程创建控制台窗口
_original_popen = subprocess.Popen _original_popen = subprocess.Popen
class Popen(_original_popen): class Popen(_original_popen):
@@ -22,14 +20,11 @@ class Popen(_original_popen):
subprocess.Popen = Popen subprocess.Popen = Popen
# PyInstaller 打包后,设置 FLET_VIEW_PATH 指向打包内的 Flet 客户端 # PyInstaller 打包后,设置 FLET_VIEW_PATH 指向打包内的 Flet 客户端
# 避免从 GitHub 下载
if getattr(sys, 'frozen', False): if getattr(sys, 'frozen', False):
# 运行在打包后的 exe 中 base_path = sys._MEIPASS
base_path = sys._MEIPASS # PyInstaller 解压到的临时目录
flet_client_path = os.path.join(base_path, '.flet', 'client', 'flet-desktop-full-0.85.3') flet_client_path = os.path.join(base_path, '.flet', 'client', 'flet-desktop-full-0.85.3')
flet_exe = os.path.join(flet_client_path, 'flet', 'flet.exe') flet_exe = os.path.join(flet_client_path, 'flet', 'flet.exe')
# 写入日志便于调试
log_file = os.path.join(os.path.dirname(sys.executable), 'startup_log.txt') log_file = os.path.join(os.path.dirname(sys.executable), 'startup_log.txt')
with open(log_file, 'w') as f: with open(log_file, 'w') as f:
f.write(f'base_path: {base_path}\n') f.write(f'base_path: {base_path}\n')
@@ -41,12 +36,10 @@ if getattr(sys, 'frozen', False):
os.environ['FLET_VIEW_PATH'] = flet_client_path os.environ['FLET_VIEW_PATH'] = flet_client_path
f.write(f'FLET_VIEW_PATH: {os.environ.get("FLET_VIEW_PATH")}\n') f.write(f'FLET_VIEW_PATH: {os.environ.get("FLET_VIEW_PATH")}\n')
# 禁用 SSL 验证,避免证书问题
if hasattr(ssl, '_create_unverified_context'): if hasattr(ssl, '_create_unverified_context'):
ssl._create_default_https_context = ssl._create_unverified_context ssl._create_default_https_context = ssl._create_unverified_context
def excepthook(type, value, tb): def excepthook(type, value, tb):
"""捕获未处理的异常,写入日志"""
log_file = os.path.join(os.path.dirname(sys.executable), 'error_log.txt') log_file = os.path.join(os.path.dirname(sys.executable), 'error_log.txt')
with open(log_file, 'w') as f: with open(log_file, 'w') as f:
f.write(''.join(traceback.format_exception(type, value, tb))) f.write(''.join(traceback.format_exception(type, value, tb)))
@@ -55,15 +48,5 @@ def excepthook(type, value, tb):
sys.excepthook = excepthook sys.excepthook = excepthook
if __name__ == '__main__': if __name__ == '__main__':
if '--tk' in sys.argv: from core.ui.flet.app_v2 import run
from core.ui.tkinter.splash import SplashWindow run()
from tkinter import messagebox
try:
window = SplashWindow().run()
if window:
window.run()
except Exception as e:
messagebox.showerror("错误", f"系统初始化失败: {str(e)}")
else:
from core.ui.flet.app_v2 import run
run()