mirror of
https://ghfast.top/https://github.com/aeroxw/tick-stock-panel.git
synced 2026-09-12 16:44:15 +08:00
feat(v0.2): 市场阶段与主线识别 + 因子挖掘全链路 + 数据层完善
- 市场环境: 新增情绪周期6阶段(冰点/启动/主升/高潮/退潮/修复, 连板梯队驱动, EMA平滑+2日确认+弱档否决, 平均段长9.7天)与概念/行业主线排名(涨停梯队聚合, 可配置宽基/风格标签过滤); 市场环境页重构, regime 透明加列, 与5档state并存 - 挖掘: 因子与策略挖掘全链路(API/worker/进程锁/候选库/前端工作台/文档), 周度调度默认关闭且永不自动发布 - 回测: 财务快照因子(点时口径), 批量回测预计算共享下期收益, 信号路径矩阵列依赖展开修复(consecutive_limit_ups 缺列报错) - 数据/性能: enriched 生成与预热治理, 重任务限流, 行情/K线缓存复用, 时区修复 - 测试: 后端全量 914 通过; GUI 黑盒验证截图存证 gui-test-screenshots/
This commit is contained in:
+135
-27
@@ -26,6 +26,7 @@ from app.backtest.matrix import (
|
||||
load_market_data_matrix_from_parquet,
|
||||
)
|
||||
from app.config import settings
|
||||
from app.enriched_generation import EnrichedGenerationUnavailableError
|
||||
from app.parquet import scan_enriched_parquet
|
||||
from app.tickflow.repository import KlineRepository
|
||||
|
||||
@@ -213,8 +214,11 @@ class PanelCache:
|
||||
columns: list[str] | None,
|
||||
compute_fn,
|
||||
asset_type: str = "stock",
|
||||
generation: str | None = None,
|
||||
) -> pl.DataFrame:
|
||||
key = self._make_key(symbols, start, end, columns, asset_type)
|
||||
key = self._make_key(
|
||||
symbols, start, end, columns, asset_type, generation
|
||||
)
|
||||
now = time.monotonic()
|
||||
|
||||
with self._lock:
|
||||
@@ -280,13 +284,20 @@ class PanelCache:
|
||||
self._cache.clear()
|
||||
|
||||
@staticmethod
|
||||
def _make_key(symbols: list[str] | None, start: date, end: date, columns: list[str] | None, asset_type: str = "stock") -> str:
|
||||
def _make_key(
|
||||
symbols: list[str] | None,
|
||||
start: date,
|
||||
end: date,
|
||||
columns: list[str] | None,
|
||||
asset_type: str = "stock",
|
||||
generation: str | None = None,
|
||||
) -> str:
|
||||
if symbols is None:
|
||||
h = "all"
|
||||
else:
|
||||
h = hashlib.md5(",".join(sorted(symbols)).encode()).hexdigest()[:12]
|
||||
cols = "all" if columns is None else hashlib.md5(",".join(sorted(columns)).encode()).hexdigest()[:8]
|
||||
return f"{asset_type}:{h}:{start}:{end}:{cols}"
|
||||
return f"{asset_type}:{generation or 'unmanaged'}:{h}:{start}:{end}:{cols}"
|
||||
|
||||
|
||||
# ================================================================
|
||||
@@ -302,6 +313,23 @@ class BacktestEngine:
|
||||
|
||||
# ── 数据加载 ──────────────────────────────────────
|
||||
|
||||
def data_generation(self, asset_type: str = "stock") -> str | None:
|
||||
loader = getattr(self.repo, "get_matrix_data_generation", None)
|
||||
return loader(asset_type) if callable(loader) else None
|
||||
|
||||
def assert_data_generation(
|
||||
self,
|
||||
asset_type: str,
|
||||
expected: str | None,
|
||||
) -> None:
|
||||
if expected is None:
|
||||
return
|
||||
current = self.data_generation(asset_type)
|
||||
if current != expected:
|
||||
raise EnrichedGenerationUnavailableError(
|
||||
"enriched data changed while the snapshot was being read"
|
||||
)
|
||||
|
||||
def load_panel(
|
||||
self,
|
||||
symbols: list[str] | None,
|
||||
@@ -309,9 +337,36 @@ class BacktestEngine:
|
||||
end: date,
|
||||
columns: list[str] | None = None,
|
||||
asset_type: str = "stock",
|
||||
*,
|
||||
expected_generation: str | None = None,
|
||||
) -> pl.DataFrame:
|
||||
"""加载 enriched 数据面板,带缓存。asset_type='etf' 时读 ETF enriched。"""
|
||||
return self._cache.get_or_compute(symbols, start, end, columns, self._load_panel_inner, asset_type=asset_type)
|
||||
attempts = 1 if expected_generation is not None else 2
|
||||
for attempt in range(attempts):
|
||||
generation = (
|
||||
expected_generation
|
||||
if expected_generation is not None
|
||||
else self.data_generation(asset_type)
|
||||
)
|
||||
panel = self._cache.get_or_compute(
|
||||
symbols,
|
||||
start,
|
||||
end,
|
||||
columns,
|
||||
self._load_panel_inner,
|
||||
asset_type=asset_type,
|
||||
generation=generation,
|
||||
)
|
||||
try:
|
||||
self.assert_data_generation(asset_type, generation)
|
||||
except EnrichedGenerationUnavailableError:
|
||||
if attempt + 1 >= attempts:
|
||||
raise
|
||||
continue
|
||||
return panel
|
||||
raise EnrichedGenerationUnavailableError(
|
||||
"unable to read a stable enriched data snapshot"
|
||||
)
|
||||
|
||||
def load_panel_for_backtest(
|
||||
self,
|
||||
@@ -338,6 +393,25 @@ class BacktestEngine:
|
||||
if df.is_empty():
|
||||
return df
|
||||
|
||||
from app.backtest.fundamentals import (
|
||||
attach_fundamental_factors,
|
||||
load_fundamental_snapshot,
|
||||
)
|
||||
|
||||
fundamental_names = sorted(
|
||||
getattr(feature_plan, "fundamental_columns", frozenset())
|
||||
or frozenset()
|
||||
)
|
||||
if fundamental_names:
|
||||
# 财务因子列不落 enriched 存储, 在加载口按公告日门控并入。
|
||||
df = attach_fundamental_factors(
|
||||
df,
|
||||
load_fundamental_snapshot(
|
||||
self.repo.store.data_dir if self.repo is not None else None
|
||||
),
|
||||
fundamental_names,
|
||||
)
|
||||
|
||||
instruments = (
|
||||
self.repo.get_instruments_asset(asset_type)
|
||||
if self.repo is not None
|
||||
@@ -402,6 +476,8 @@ class BacktestEngine:
|
||||
cache_profile: MatrixCacheProfile | None = None,
|
||||
coverage_start: date | None = None,
|
||||
coverage_end: date | None = None,
|
||||
expected_generation: str | None = None,
|
||||
cancel_event: threading.Event | None = None,
|
||||
) -> MarketDataMatrix:
|
||||
"""Load a matrix-native backtest directly from projected parquet batches."""
|
||||
if feature_plan.execution_backend != "matrix_native":
|
||||
@@ -432,35 +508,67 @@ class BacktestEngine:
|
||||
)
|
||||
generation_loader = getattr(self.repo, "get_matrix_data_generation", None)
|
||||
source_generation = (
|
||||
generation_loader(asset_type)
|
||||
if cache_root is not None and callable(generation_loader)
|
||||
else None
|
||||
)
|
||||
try:
|
||||
return load_market_data_matrix_from_parquet(
|
||||
parquet_root,
|
||||
start,
|
||||
end,
|
||||
field_columns=field_columns,
|
||||
symbols=symbols,
|
||||
instruments=instruments,
|
||||
cache_root=cache_root,
|
||||
coverage_start=coverage_start,
|
||||
coverage_end=coverage_end,
|
||||
cache_field_columns=cache_fields,
|
||||
cache_max_bytes=cache_max_bytes,
|
||||
profile_generation=(
|
||||
cache_profile.generation if cache_profile is not None else "request"
|
||||
),
|
||||
source_generation=source_generation,
|
||||
expected_generation
|
||||
if expected_generation is not None
|
||||
else (
|
||||
generation_loader(asset_type)
|
||||
if callable(generation_loader)
|
||||
else None
|
||||
)
|
||||
except pa.ArrowException as exc:
|
||||
raise ValueError(f"direct market matrix parquet scan failed: {exc}") from exc
|
||||
)
|
||||
attempts = 1 if expected_generation is not None else 2
|
||||
for attempt in range(attempts):
|
||||
try:
|
||||
market = load_market_data_matrix_from_parquet(
|
||||
parquet_root,
|
||||
start,
|
||||
end,
|
||||
field_columns=field_columns,
|
||||
symbols=symbols,
|
||||
instruments=instruments,
|
||||
cache_root=cache_root,
|
||||
coverage_start=coverage_start,
|
||||
coverage_end=coverage_end,
|
||||
cache_field_columns=cache_fields,
|
||||
cache_max_bytes=cache_max_bytes,
|
||||
profile_generation=(
|
||||
cache_profile.generation if cache_profile is not None else "request"
|
||||
),
|
||||
source_generation=source_generation,
|
||||
cancel_event=cancel_event,
|
||||
)
|
||||
self.assert_data_generation(asset_type, source_generation)
|
||||
from app.backtest.fundamentals import attach_matrix_fundamental_fields
|
||||
|
||||
fundamental_names = sorted(
|
||||
getattr(feature_plan, "fundamental_columns", frozenset())
|
||||
or frozenset()
|
||||
)
|
||||
if fundamental_names:
|
||||
# 财务因子不落 enriched 存储: 矩阵加载后按公告日门控附加字段。
|
||||
market = attach_matrix_fundamental_fields(
|
||||
market,
|
||||
self.repo.store.data_dir if self.repo is not None else None,
|
||||
fundamental_names,
|
||||
)
|
||||
return market
|
||||
except EnrichedGenerationUnavailableError:
|
||||
if attempt + 1 >= attempts:
|
||||
raise
|
||||
source_generation = self.data_generation(asset_type)
|
||||
except pa.ArrowException as exc:
|
||||
raise ValueError(f"direct market matrix parquet scan failed: {exc}") from exc
|
||||
raise EnrichedGenerationUnavailableError(
|
||||
"unable to read a stable enriched matrix snapshot"
|
||||
)
|
||||
|
||||
def cache_stats(self) -> dict:
|
||||
"""暴露 PanelCache 遥测快照 (扫盘耗时/次数/命中/复用), 供上层量化 IO 占比。"""
|
||||
return self._cache.stats()
|
||||
|
||||
def clear_panel_cache(self) -> None:
|
||||
self._cache.invalidate()
|
||||
|
||||
def _load_panel_inner(
|
||||
self,
|
||||
symbols: list[str] | None,
|
||||
|
||||
Reference in New Issue
Block a user