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:
shy3130
2026-08-16 23:39:07 +08:00
parent eb869c7ad2
commit 697c27bb02
129 changed files with 20324 additions and 602 deletions
+43 -2
View File
@@ -1,6 +1,7 @@
"""扩展数据服务 — 配置管理 + 文件解析 + Parquet 存储。"""
from __future__ import annotations
import copy
import json
import logging
import re
@@ -187,17 +188,48 @@ class ExtConfig:
# 配置持久化
# ---------------------------------------------------------------------------
# load_all 进程内缓存: kline/screener/watchlist 等热路径每请求调用, 每次都
# iterdir + 逐 config.json read_text+parse 纯重复; 以配置目录的
# (目录名, mtime_ns, size) 签名失效 (新增/编辑/删除配置都会改变签名)。
_load_all_cache: dict[str, tuple[tuple, list[ExtConfig]]] = {}
def _ext_config_dir_signature(base: Path) -> tuple | None:
"""配置目录下所有 config.json 的 (目录名, mtime_ns, size) 签名; 出错返回 None (禁用缓存)。"""
try:
sig = []
for d in sorted(base.iterdir()):
cp = d / "config.json"
if d.is_dir() and cp.exists():
st = cp.stat()
sig.append((d.name, st.st_mtime_ns, st.st_size))
return tuple(sig)
except Exception: # noqa: BLE001
return None
class ExtConfigStore:
"""扩展数据配置文件读写 — 每个表独立目录 data/ext/{config_id}/config.json。"""
# 与创建端点 CreateExtReq.id 的 pattern 一致; load_all 之外的 config_id
# 来自 URL path 参数, 必须先过白名单再拼路径, 防止 ../ 穿越删除。
_VALID_ID = re.compile(r"^[a-zA-Z0-9_]+$")
def __init__(self, data_dir: Path) -> None:
self._base = data_dir / "ext_data"
def _config_path(self, config_id: str) -> Path:
if not self._VALID_ID.match(config_id):
raise ValueError(f"非法 config_id: {config_id!r}")
return self._base / config_id / "config.json"
def load_all(self) -> list[ExtConfig]:
# 兼容旧版: 如果目录为空且旧配置文件存在则迁移
sig = _ext_config_dir_signature(self._base)
if sig is not None:
cached = _load_all_cache.get(str(self._base))
if cached is not None and cached[0] == sig:
return copy.deepcopy(cached[1])
if not self._base.exists() or not any(self._base.iterdir()):
old = self._base.parent / "ext_configs.json"
if not old.exists():
@@ -215,10 +247,16 @@ class ExtConfigStore:
configs.append(ExtConfig.from_dict(raw))
except Exception as e:
logger.warning("扩展表配置解析失败 %s: %s", cp, e)
if sig is not None and configs:
# 缓存存私有副本, 命中时返回深拷贝, 调用方改配置对象不会污染缓存。
_load_all_cache[str(self._base)] = (sig, copy.deepcopy(configs))
return configs
def get(self, config_id: str) -> ExtConfig | None:
cp = self._config_path(config_id)
try:
cp = self._config_path(config_id)
except ValueError:
return None
if not cp.exists():
return None
try:
@@ -238,7 +276,10 @@ class ExtConfigStore:
def delete(self, config_id: str) -> bool:
import shutil
cp = self._config_path(config_id)
try:
cp = self._config_path(config_id)
except ValueError:
return False
if not cp.exists():
return False
shutil.rmtree(cp.parent, ignore_errors=True)