This commit is contained in:
shy3130
2026-10-15 16:10:20 +08:00
parent 67ee83fba9
commit 1c2e4db980
6 changed files with 88 additions and 18 deletions
+3 -2
View File
@@ -278,8 +278,9 @@ async def _application_lifespan(app: FastAPI):
_screener_svc = ScreenerService(repo)
_etf_screener_svc = ScreenerService(repo, asset_type="etf")
# 去内置化补全: 原内置策略按 ID 种入 custom (一次性, 已自行迁移的不覆盖)。
# 必须在 StrategyEngine 创建前完成, 否则首载拿不到刚种入的文件。
# 去内置化补全(仅升级用户): 原内置策略按 ID 补回 custom (一次性,
# 已自行迁移的不覆盖; 新装无旧版使用痕迹, 跳过保持策略库为空)。
# 必须在 StrategyEngine 创建前完成, 否则首载拿不到刚补回的文件。
_seeded_strategies = 0
try:
from app.strategy.strategy_seeds import seed_missing_builtins
+33 -8
View File
@@ -1,9 +1,11 @@
"""策略种子 — 去内置化迁移补全: 原内置策略按 ID 种入用户自定义目录。
"""策略种子 — 去内置化迁移补全: 仅旧版本升级用户, 原内置策略按 ID 补回。
与 custom_signals.seed_if_empty 同族机制 (仅空判断 vs 按 ID 补缺的取舍
见各 docstring): 升级用户的 data/strategies/custom 往往已有自己的策略,
但丢失了原 25 个内置策略 —— 因此按文件名逐一补缺, 已存在(含用户已自行
迁移的同名策略)一律不覆盖; .seeded_v1 标记保证一次性, 删除后重启不复活。
新版本没有内置策略 (产品设计如此, 新装环境策略库为空)。此迁移只服务
「从有内置策略的旧版本升级上来」的用户: 他们的策略池/监控引用了内置
策略 ID, 升级后丢失。判断依据是数据目录里的旧版使用痕迹 — 新装首次
启动既无日K数据也无自选/策略文件, 视为新用户直接跳过并落标记。
已存在(含用户已自行迁移的同名策略)一律不覆盖; 标记保证一次性,
删除后重启不复活。
"""
from __future__ import annotations
@@ -15,12 +17,30 @@ logger = logging.getLogger(__name__)
SEEDS_DIR = Path(__file__).resolve().parent / "seeds"
def _has_upgrade_traces(data_dir: Path) -> bool:
"""数据目录是否有旧版本的使用痕迹 (新装首次启动为 False)。
三个信号任一命中即视为升级用户:
- strategies/{custom,ai,composite} 下已有任何策略文件 (用户自己的)
- kline_daily 已有 parquet (拉过日K; 内置策略跑起来必须有数据)
- watchlist.parquet 存在 (加过自选)
"""
for sub in ("custom", "ai", "composite"):
if any((data_dir / "strategies" / sub).glob("*.py")):
return True
if any((data_dir / "kline_daily").rglob("*.parquet")):
return True
return (data_dir / "user_data" / "watchlist.parquet").exists()
def seed_missing_builtins(data_dir: Path) -> int:
"""把内置策略种子按文件名补入 data/strategies/custom; 返回种入数量。
"""仅升级用户: 把内置策略种子按文件名补入 data/strategies/custom。
- 幂等: ``.seeded_v1`` 标记存在 (无论目录后来是否被清空) 不再种入。
- 仅写缺失的同名文件, 已有一律不覆盖: 全新环境拿到全部 25 个;
已自行迁移过的老环境零写入; 只迁移了一部分的按缺口补全。
- 新用户 (无旧版使用痕迹) 跳过并落标记 — 新版本没有内置策略,
新装策略库保持为空; 之后再拉数据/用久了也不会回头种 (标记已落)。
- 仅写缺失的同名文件, 已有一律不覆盖: 已自行迁移过的老环境零写入,
只迁移了一部分的按缺口补全。
- 写入前逐一过 _validate_safety (种子随包分发, 纵深防御),
校验失败跳过该文件不阻断启动。
"""
@@ -30,6 +50,11 @@ def seed_missing_builtins(data_dir: Path) -> int:
if marker.exists():
return 0
if not _has_upgrade_traces(data_dir):
marker.write_text("skipped: fresh install\n", encoding="utf-8")
logger.info("strategy seeds: fresh install, skip seeding")
return 0
from app.strategy.ai_generator import AIStrategyGenerator
written = 0
+49 -5
View File
@@ -1,8 +1,10 @@
"""策略种子 seed_missing_builtins — 去内置化补全回归。
"""策略种子 seed_missing_builtins — 去内置化补全回归(仅升级用户)。
原 25 个内置策略在去内置化后仅存在于 tests/fixtures 与部分老用户的
custom 目录, 升级用户丢失。此处回归种入语义:
- 空目录一次写入全部 25 个, 标记防重;
新版本没有内置策略: 新装环境策略库为空、不种入; 只有检测到旧版使用
痕迹 (已有策略/日K数据/自选) 的升级数据目录才把 25 个原内置策略按
文件名补回 custom。此处回归:
- 新装目录 (无痕迹) 跳过种入并落标记, 之后用久了也不回头种;
- 升级目录 (有痕迹) 一次写入全部 25 个, 标记防重;
- 已有同名文件一律不覆盖 (用户自行迁移过的零写入);
- 删除后重启不复活;
- 种入的文件能被引擎真实加载。
@@ -14,8 +16,46 @@ from app.strategy.strategy_seeds import SEEDS_DIR, seed_missing_builtins
_EXPECTED_COUNT = len([f for f in SEEDS_DIR.glob("*.py") if not f.name.startswith("_")])
def test_seed_writes_all_and_marks(tmp_path):
def _make_upgrade_dir(tmp_path) -> None:
"""造升级用户痕迹: 拉过日K (旧版本使用的最普遍特征)。"""
d = tmp_path / "kline_daily" / "date=2026-01-01"
d.mkdir(parents=True)
(d / "part.parquet").write_bytes(b"fake")
def test_fresh_install_skips_seeding(tmp_path):
"""新装用户: 无旧版痕迹, 不种入, 策略库保持为空。"""
n = seed_missing_builtins(tmp_path)
custom = tmp_path / "strategies" / "custom"
assert n == 0
assert len(list(custom.glob("*.py"))) == 0
assert (custom / ".seeded_v1").exists()
assert "skipped" in (custom / ".seeded_v1").read_text(encoding="utf-8")
def test_fresh_install_never_seeds_later_even_after_usage(tmp_path):
"""新用户用久后 (出现数据痕迹) 重启: 标记已落, 不回头种入。"""
seed_missing_builtins(tmp_path)
_make_upgrade_dir(tmp_path) # 之后拉了数据
assert seed_missing_builtins(tmp_path) == 0
assert len(list((tmp_path / "strategies" / "custom").glob("*.py"))) == 0
def test_watchlist_counts_as_upgrade_trace(tmp_path):
"""自选记录也构成升级痕迹 (没拉过K线但用过的用户)。"""
w = tmp_path / "user_data"
w.mkdir(parents=True)
(w / "watchlist.parquet").write_bytes(b"fake")
assert seed_missing_builtins(tmp_path) == _EXPECTED_COUNT
def test_seed_writes_all_and_marks(tmp_path):
_make_upgrade_dir(tmp_path)
n = seed_missing_builtins(tmp_path)
assert n == _EXPECTED_COUNT == 25
custom = tmp_path / "strategies" / "custom"
assert len(list(custom.glob("*.py"))) == 25
@@ -28,6 +68,7 @@ def test_seed_never_overwrites_existing(tmp_path):
custom = tmp_path / "strategies" / "custom"
custom.mkdir(parents=True)
# 用户已自行迁移过 1 个 (内容改成自定义版本) + 1 个自己的策略
# (已有策略文件本身就是升级痕迹)
(custom / "boll_breakout.py").write_text("# 用户自定义版本\n", encoding="utf-8")
(custom / "my_own.py").write_text("# 用户自己的策略\n", encoding="utf-8")
@@ -40,10 +81,12 @@ def test_seed_never_overwrites_existing(tmp_path):
def test_seed_no_resurrect_after_user_deletes(tmp_path):
_make_upgrade_dir(tmp_path)
seed_missing_builtins(tmp_path)
custom = tmp_path / "strategies" / "custom"
for f in custom.glob("*.py"):
f.unlink()
assert seed_missing_builtins(tmp_path) == 0
assert len(list(custom.glob("*.py"))) == 0
@@ -51,6 +94,7 @@ def test_seed_no_resurrect_after_user_deletes(tmp_path):
def test_seeded_strategies_load_in_engine(tmp_path):
from app.strategy.engine import StrategyEngine
_make_upgrade_dir(tmp_path)
seed_missing_builtins(tmp_path)
engine = StrategyEngine(strategy_dirs=[tmp_path / "strategies" / "custom"])
+1 -1
View File
@@ -8,7 +8,7 @@
## 🔍 策略引擎(Screener)
**策略全部为用户自定义文件**(`data/strategies/custom/`,可编辑可删除;首次启动自动种入 25 个示例策略,升级缺失的按 ID 补回,删除后重启不复活)。挖掘用的研究模板保留在 `backend/app/strategy/research/`(research_only,不进策略列表),见 [strategy.md → 示例策略](./strategy.md#示例策略):
**策略全部为用户自定义文件**(`data/strategies/custom/`,可编辑可删除;新装环境策略库为空,旧版本升级用户自动补回 25 个原内置策略)。挖掘用的研究模板保留在 `backend/app/strategy/research/`(research_only,不进策略列表),见 [strategy.md → 示例策略](./strategy.md#示例策略):
| 类型 | 代表策略 |
| :---------- | :------------------------------------------------------- |
+1 -1
View File
@@ -15,7 +15,7 @@
当前已可用的主要扩展能力:
- 自定义、AI 和叠加策略目录:`data/strategies/`。策略与信号已全面去内置化——无内置策略目录,首次启动自动种入 25 个示例策略(按 ID 补缺不覆盖,删除后不复活;默认信号定义同样由种子自动种入;旧数据目录升级时自动迁移并归档失效策略文件)。策略参数类型支持 `float/int/bool/select/date/string`;每策略可配「叠加条件」(`overlay_filter`,存于策略 override):字段+运算符+值的多行 AND 硬过滤,选股/回测/监控三处一致生效,不改策略代码。
- 自定义、AI 和叠加策略目录:`data/strategies/`。策略与信号已全面去内置化——无内置策略目录,新装环境策略库为空(默认信号定义由种子自动种入;旧版本升级用户自动补回原内置策略、失效策略文件自动迁移归档)。策略参数类型支持 `float/int/bool/select/date/string`;每策略可配「叠加条件」(`overlay_filter`,存于策略 override):字段+运算符+值的多行 AND 硬过滤,选股/回测/监控三处一致生效,不改策略代码。
- 数据源 Provider 与 `plugin.yaml` 机制:详见 [`plugin-development.md`](plugin-development.md)。
- 扩展数据与声明式分析页面:适合不需要自定义 React 交互的页面。
- 前端源码扩展注册:`frontend/src/custom/<namespace>/extension.tsx`,支持静态页面、导航和已开放插槽。
+1 -1
View File
@@ -18,7 +18,7 @@
`backend/app/strategy/research/` 保留一个仅供挖掘 worker 使用的受控因子排名研究模板(research_only)。它不出现在普通策略列表,也不能通过普通策略 API 直接运行或删除;挖掘结果发布时会生成独立策略。详见 [因子与策略挖掘](./mining.md)。
策略引擎只加载用户数据目录(`data/strategies/custom|ai|composite`)与研究模板目录,**不再有内置策略目录**;新装环境首次启动自动种入 25 个示例策略为自定义文件(按 ID 补缺、不覆盖已有,删除后重启不复活)。AI 生成的策略只落入 `data/strategies/ai/`。
策略引擎只加载用户数据目录(`data/strategies/custom|ai|composite`)与研究模板目录,**不再有内置策略目录**;新装环境策略库为空。旧版本升级用户首次启动时自动把 25 个原内置策略补回为自定义文件(按 ID 补缺、不覆盖已有,删除后重启不复活)。AI 生成的策略只落入 `data/strategies/ai/`。
---