mirror of
https://github.com/shy3130/tick-stock-panel.git
synced 2026-11-02 10:26:13 +00:00
- 策略: 引擎只加载 data/strategies/{custom,ai,composite} 与研究模板目录,
原 25 个内置策略一次性迁移为用户自定义文件(ID 不变, 引用零迁移);
研究模板移至 app/strategy/research, 打包 spec 同步; 新装环境策略库为空
- 信号: 管线不再硬编码任何信号表达式; 涨跌停判定价成为运行时列
limit_up_price/limit_down_price(生效价∓0.005 容差), 20 个信号转为
data/user_data/custom_signals 定义(显式 column 沿用 signal_* 原列名);
依赖解析/注入顺序/当日路径 prev-join 全部定义驱动; 语义统一为历史路径
口径(顺带修正当日路径 BOLL 比较符与翘板/炸板空值两处既有偏差)
- 前端: 信号库单列表(全部可编辑), SignalPicker/监控选项/信号标签全走
定义接口; 回测页新增免责声明(模拟性质/数据源准确性不作保证/不构成
投资建议)
- 测试: 25 个策略夹具与 20 个信号种子入库 tests/fixtures, 受影响测试
全部自包含化(不依赖运行目录用户数据); 77 万行 MA 死叉新旧表达式对拍
逐行一致; 回测实跑 45 笔卖点信号出场验证矩阵链路
- 同批包含工作区既有进度: 策略结果 ext 列同日投影修复(含测试)、数据源插件、fuyao/auth/api_gateway/data_integrity 调整、README 信号库
描述与 strategy.md 导入路径补充
验证: 后端 2592 passed(空数据目录 CI 模拟 2588 passed, 4 例为模拟环境
伪影复验排除); 前端 vitest 125 passed + tsc 0 错 + vite build 通过
130 lines
4.7 KiB
Python
130 lines
4.7 KiB
Python
"""日内序列表 (PullConfig.time_field) 读侧取值: 每个 symbol 必须收敛到最新一盘。
|
|
|
|
多盘并存的分区里, 策略/自选/告警/个股详情 (_load_ext_value_maps)、enriched 因子帧
|
|
(ext_factors._timeseries_frame) 与板块资金流 (sector_rotation._load_sector_flow)
|
|
都按 symbol 去重取一行。去重只看行序, 所以写入端必须保证分区内按时间列升序:
|
|
合并去重后的行序不稳定时, 各 symbol 取到的是随机一盘。
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from datetime import date
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
|
|
import polars as pl
|
|
import pytest
|
|
|
|
from app.api import screener
|
|
from app.factors import ext_factors
|
|
from app.services import sector_rotation
|
|
from app.services.ext_data import ExtConfig, ExtConfigStore, ExtField, PullConfig, rows_to_parquet
|
|
|
|
DAY = date(2026, 9, 15)
|
|
SESSIONS = ("09:15:00", "09:20:00", "09:25:00")
|
|
N_SYMBOLS = 2000
|
|
|
|
|
|
def _config() -> ExtConfig:
|
|
return ExtConfig(
|
|
id="auction",
|
|
label="集合竞价",
|
|
mode="timeseries",
|
|
fields=[
|
|
ExtField("symbol", "string", "代码"),
|
|
ExtField("ts", "string", "时刻"),
|
|
ExtField("v", "float", "值"),
|
|
],
|
|
pull=PullConfig(url="https://x", time_field="ts"),
|
|
)
|
|
|
|
|
|
def _symbols() -> list[str]:
|
|
return [f"{i:06d}.SZ" for i in range(1, N_SYMBOLS + 1)]
|
|
|
|
|
|
def _write_three_sessions(data_dir: Path) -> ExtConfig:
|
|
"""按真实拉取节奏逐盘写入: 每一盘一次 rows_to_parquet, 走合并去重分支。"""
|
|
cfg = _config()
|
|
ExtConfigStore(data_dir).upsert(cfg)
|
|
for value, ts in enumerate(SESSIONS, start=1):
|
|
rows = [{"symbol": sym, "ts": ts, "v": float(value)} for sym in _symbols()]
|
|
rows_to_parquet(rows, cfg, data_dir, snapshot_date=DAY)
|
|
return cfg
|
|
|
|
|
|
@pytest.fixture()
|
|
def data_dir(tmp_path: Path) -> Path:
|
|
screener._ext_value_map_cache.clear()
|
|
sector_rotation.invalidate_cache()
|
|
d = tmp_path / "data"
|
|
d.mkdir()
|
|
return d
|
|
|
|
|
|
def test_value_maps_take_latest_session_after_merge(data_dir):
|
|
_write_three_sessions(data_dir)
|
|
repo = SimpleNamespace(store=SimpleNamespace(db=None, data_dir=data_dir))
|
|
|
|
vmap = screener._load_ext_value_maps(repo, "auction.v")["auction__v"]
|
|
|
|
assert len(vmap) == N_SYMBOLS
|
|
picked = {v for v in vmap.values()}
|
|
assert picked == {3.0}, f"应全部取最新一盘 09:25 (v=3.0), 实际取到 {sorted(picked)}"
|
|
|
|
|
|
def test_ext_factor_frame_takes_latest_session_after_merge(data_dir):
|
|
cfg = _write_three_sessions(data_dir)
|
|
|
|
frame = ext_factors._timeseries_frame(data_dir, cfg, cfg.fields)
|
|
|
|
value_col = next(c for c in frame.columns if c not in ("symbol", "_ext_date") and c.endswith("v"))
|
|
assert frame.height == N_SYMBOLS
|
|
assert set(frame[value_col].to_list()) == {3.0}
|
|
|
|
|
|
def test_sector_flow_takes_latest_session_after_merge(data_dir):
|
|
_write_three_sessions(data_dir)
|
|
|
|
flow = sector_rotation._load_sector_flow(data_dir, "auction.v")
|
|
|
|
assert flow is not None
|
|
assert flow.height == N_SYMBOLS
|
|
assert set(flow["_flow"].to_list()) == {3.0}
|
|
|
|
|
|
def test_fresh_partition_sorted_even_if_upstream_returns_latest_first(data_dir):
|
|
"""首次落盘 (无旧分区, 不经合并去重): 上游按时间倒序返回也要按时间列升序落盘。"""
|
|
cfg = _config()
|
|
ExtConfigStore(data_dir).upsert(cfg)
|
|
rows = [
|
|
{"symbol": "000001.SZ", "ts": "09:25:00", "v": 3.0},
|
|
{"symbol": "000001.SZ", "ts": "09:20:00", "v": 2.0},
|
|
{"symbol": "000001.SZ", "ts": "09:15:00", "v": 1.0},
|
|
]
|
|
rows_to_parquet(rows, cfg, data_dir, snapshot_date=DAY)
|
|
repo = SimpleNamespace(store=SimpleNamespace(db=None, data_dir=data_dir))
|
|
|
|
vmap = screener._load_ext_value_maps(repo, "auction.v")["auction__v"]
|
|
assert vmap == {"000001.SZ": 3.0}
|
|
flow = sector_rotation._load_sector_flow(data_dir, "auction.v")
|
|
assert flow is not None and flow["_flow"].to_list() == [3.0]
|
|
|
|
|
|
def test_snapshot_table_without_time_field_row_order_unchanged(data_dir):
|
|
"""未配置 time_field 的时序表: 写入不排序, 行为与改造前一致 (每 symbol 一行)。"""
|
|
cfg = ExtConfig(
|
|
id="plain",
|
|
label="普通时序表",
|
|
mode="timeseries",
|
|
fields=[ExtField("symbol", "string", "代码"), ExtField("v", "float", "值")],
|
|
pull=PullConfig(url="https://x"),
|
|
)
|
|
ExtConfigStore(data_dir).upsert(cfg)
|
|
rows = [{"symbol": "000002.SZ", "v": 2.0}, {"symbol": "000001.SZ", "v": 1.0}]
|
|
rows_to_parquet(rows, cfg, data_dir, snapshot_date=DAY)
|
|
|
|
out = pl.read_parquet(
|
|
data_dir / "ext_data" / "plain" / "timeseries" / f"date={DAY.isoformat()}" / "part.parquet"
|
|
)
|
|
assert out["symbol"].to_list() == ["000002.SZ", "000001.SZ"]
|