Files
shy3130 697c27bb02 feat(v0.2): 市场阶段与主线识别 + 因子挖掘全链路 + 数据层完善
- 市场环境: 新增情绪周期6阶段(冰点/启动/主升/高潮/退潮/修复, 连板梯队驱动,
  EMA平滑+2日确认+弱档否决, 平均段长9.7天)与概念/行业主线排名(涨停梯队聚合,
  可配置宽基/风格标签过滤); 市场环境页重构, regime 透明加列, 与5档state并存
- 挖掘: 因子与策略挖掘全链路(API/worker/进程锁/候选库/前端工作台/文档),
  周度调度默认关闭且永不自动发布
- 回测: 财务快照因子(点时口径), 批量回测预计算共享下期收益,
  信号路径矩阵列依赖展开修复(consecutive_limit_ups 缺列报错)
- 数据/性能: enriched 生成与预热治理, 重任务限流, 行情/K线缓存复用, 时区修复
- 测试: 后端全量 914 通过; GUI 黑盒验证截图存证 gui-test-screenshots/
2026-08-16 23:39:07 +08:00

140 lines
4.3 KiB
Python

"""Lightweight mining date availability checks shared by API and workers."""
from __future__ import annotations
from dataclasses import asdict, dataclass
from datetime import date
from pathlib import Path
from typing import Any
from app.backtest.mining import (
nested_fold_count,
required_outer_folds,
required_trading_bars,
validation_config_for_profile,
)
from app.tickflow.repository import enriched_dirname
@dataclass(frozen=True)
class MiningAvailability:
asset_type: str
budget_profile: str
trading_bars: int
required_bars: int
outer_folds: int
required_outer_folds: int
eligible: bool
available_start: date | None
available_end: date | None
effective_start: date | None
effective_end: date | None
suggested_start: date | None
def to_dict(self) -> dict[str, Any]:
return {
key: value.isoformat() if isinstance(value, date) else value
for key, value in asdict(self).items()
}
def enriched_partition_dates(
data_dir: Path,
asset_type: str,
start: date | None = None,
end: date | None = None,
) -> list[date]:
root = data_dir / enriched_dirname(asset_type)
values: set[date] = set()
for partition in root.glob("date=*"):
try:
value = date.fromisoformat(partition.name.removeprefix("date="))
except ValueError:
continue
if start is not None and value < start:
continue
if end is not None and value > end:
continue
if (partition / "part.parquet").is_file():
values.add(value)
return sorted(values)
def mining_availability(
data_dir: Path,
*,
asset_type: str,
budget_profile: str,
start: date | None = None,
end: date | None = None,
) -> MiningAvailability:
if asset_type not in {"stock", "etf"}:
raise ValueError(f"unsupported mining asset type: {asset_type}")
if start is not None and end is not None and start > end:
raise ValueError("mining start must not be after end")
config = validation_config_for_profile(budget_profile)
required_folds = required_outer_folds(budget_profile)
required_bars = required_trading_bars(config, required_folds)
all_dates = enriched_partition_dates(data_dir, asset_type)
scoped = [
value
for value in all_dates
if (start is None or value >= start) and (end is None or value <= end)
]
dates_through_end = [
value for value in all_dates if end is None or value <= end
]
suggested_start = (
dates_through_end[-required_bars]
if len(dates_through_end) >= required_bars
else None
)
trading_bars = len(scoped)
return MiningAvailability(
asset_type=asset_type,
budget_profile=budget_profile,
trading_bars=trading_bars,
required_bars=required_bars,
outer_folds=nested_fold_count(trading_bars, config),
required_outer_folds=required_folds,
eligible=trading_bars >= required_bars,
available_start=all_dates[0] if all_dates else None,
available_end=all_dates[-1] if all_dates else None,
effective_start=scoped[0] if scoped else None,
effective_end=scoped[-1] if scoped else None,
suggested_start=suggested_start,
)
def require_mining_availability(
data_dir: Path,
*,
asset_type: str,
budget_profile: str,
start: date | None = None,
end: date | None = None,
) -> MiningAvailability:
availability = mining_availability(
data_dir,
asset_type=asset_type,
budget_profile=budget_profile,
start=start,
end=end,
)
if availability.eligible:
return availability
if availability.effective_start is None:
effective_range = "contains no enriched data"
else:
effective_range = (
f"{availability.effective_start.isoformat()} to "
f"{availability.effective_end.isoformat()}"
)
fold_label = "outer fold" if availability.required_outer_folds == 1 else "outer folds"
raise ValueError(
f"{budget_profile} mining requires at least {availability.required_bars} "
f"enriched trading bars for {availability.required_outer_folds} {fold_label}; "
f"effective range {effective_range} has {availability.trading_bars}"
)