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 通过
114 lines
4.4 KiB
Python
114 lines
4.4 KiB
Python
"""盘中增量 enriched 的 60 日极值与盘后全量口径一致。
|
|
|
|
盘后全量 (compute_indicators)、回测矩阵 (matrix high_60d/low_60d) 与因子
|
|
distance_to_high_60d 的「60日最高/最低」都是 60 根收盘价的极值; 盘中增量路径
|
|
(live_agg + compute_enriched_today) 若改用最高价/最低价, 盘中 high_60d 会被
|
|
历史上影线抬高, 「创60日新高/新低」(close >= high_60d) 几乎不可能成立,
|
|
收盘后全量重算或盘中重启又变回收盘价口径, 同一时刻的值与信号前后跳变。
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from datetime import date, timedelta
|
|
|
|
import polars as pl
|
|
import pytest
|
|
|
|
from app.indicators.pipeline import (
|
|
compute_enriched_today,
|
|
compute_indicators,
|
|
compute_signals,
|
|
)
|
|
from app.tickflow.repository import DataStore, KlineRepository
|
|
|
|
TODAY = date(2026, 8, 3)
|
|
|
|
|
|
def _trading_days(count: int) -> list[date]:
|
|
days: list[date] = []
|
|
d = TODAY - timedelta(days=1)
|
|
while len(days) < count:
|
|
if d.weekday() < 5:
|
|
days.append(d)
|
|
d -= timedelta(days=1)
|
|
return sorted(days)
|
|
|
|
|
|
def _bars() -> pl.DataFrame:
|
|
"""两只票 80 个历史交易日 + 今日。
|
|
|
|
600001.SH 收盘价缓涨且每天带 +0.5 的长上影; 今日收盘创 60 日收盘新高,
|
|
但仍低于昨日上影高点。000002.SZ 镜像构造: 缓跌 + 长下影, 今日收盘创 60 日
|
|
收盘新低, 但仍高于昨日下影低点。
|
|
"""
|
|
rows = []
|
|
days = _trading_days(80)
|
|
for i, d in enumerate(days):
|
|
up = 10.0 + 0.01 * i
|
|
down = 20.0 - 0.01 * i
|
|
rows.append(("600001.SH", d, up, up + 0.5, up - 0.05, up))
|
|
rows.append(("000002.SZ", d, down, down + 0.05, down - 0.5, down))
|
|
last_up = 10.0 + 0.01 * 79
|
|
last_down = 20.0 - 0.01 * 79
|
|
rows.append(("600001.SH", TODAY, last_up, last_up + 0.03, last_up - 0.05, last_up + 0.02))
|
|
rows.append(("000002.SZ", TODAY, last_down, last_down + 0.05, last_down - 0.03, last_down - 0.02))
|
|
return pl.DataFrame(
|
|
rows,
|
|
schema=["symbol", "date", "open", "high", "low", "close"],
|
|
orient="row",
|
|
).with_columns(
|
|
pl.lit(1000.0).alias("volume"),
|
|
(pl.col("close") * 1000.0).alias("amount"),
|
|
pl.col("close").alias("raw_close"),
|
|
pl.col("high").alias("raw_high"),
|
|
pl.col("low").alias("raw_low"),
|
|
).sort(["symbol", "date"])
|
|
|
|
|
|
def _incremental_and_full(tmp_path) -> tuple[pl.DataFrame, pl.DataFrame]:
|
|
bars = _bars()
|
|
history = bars.filter(pl.col("date") < TODAY)
|
|
history_indicators = compute_indicators(history)
|
|
latest = history["date"].max()
|
|
|
|
repo = KlineRepository(DataStore(tmp_path))
|
|
repo._enriched_history_cache = history_indicators
|
|
repo._build_live_agg(latest)
|
|
|
|
today_ohlcv = bars.filter(pl.col("date") == TODAY).drop("raw_close", "raw_high", "raw_low")
|
|
incremental = compute_enriched_today(
|
|
repo.get_live_agg(),
|
|
history_indicators.filter(pl.col("date") == latest),
|
|
today_ohlcv,
|
|
None,
|
|
).sort("symbol")
|
|
full = compute_signals(compute_indicators(bars)).filter(pl.col("date") == TODAY).sort("symbol")
|
|
return incremental, full
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _migrated_signal_definitions(monkeypatch):
|
|
"""注入迁移自内置的信号定义 (信号列定义驱动, 不依赖运行目录)。"""
|
|
from tests.signal_seeds import install_pipeline_caches
|
|
install_pipeline_caches(monkeypatch)
|
|
|
|
|
|
def test_intraday_60d_extremes_match_full_close_basis(tmp_path):
|
|
incremental, full = _incremental_and_full(tmp_path)
|
|
|
|
for column in ("high_60d", "low_60d"):
|
|
assert incremental[column].to_list() == full[column].to_list(), column
|
|
|
|
|
|
def test_intraday_n_day_high_low_signals_match_full(tmp_path):
|
|
incremental, full = _incremental_and_full(tmp_path)
|
|
by_symbol = {row["symbol"]: row for row in incremental.iter_rows(named=True)}
|
|
|
|
# 盘后全量: 今日收盘分别创 60 日收盘新高/新低
|
|
assert full.filter(pl.col("symbol") == "600001.SH")["signal_n_day_high"].item() is True
|
|
assert full.filter(pl.col("symbol") == "000002.SZ")["signal_n_day_low"].item() is True
|
|
# 盘中增量须同口径命中
|
|
assert by_symbol["600001.SH"]["signal_n_day_high"] is True
|
|
assert by_symbol["000002.SZ"]["signal_n_day_low"] is True
|
|
assert incremental["signal_n_day_high"].to_list() == full["signal_n_day_high"].to_list()
|
|
assert incremental["signal_n_day_low"].to_list() == full["signal_n_day_low"].to_list()
|