From 2f762b3bcc378abf9aeb537db6f45e311462cd27 Mon Sep 17 00:00:00 2001 From: wshy Date: Tue, 14 Jul 2026 21:47:26 +0800 Subject: [PATCH] =?UTF-8?q?fix(strategy):=20import=20=E7=99=BD=E5=90=8D?= =?UTF-8?q?=E5=8D=95=E5=8A=A0=20datetime=20+=20date=20=E5=8F=82=E6=95=B0?= =?UTF-8?q?=E5=A4=84=E7=90=86=E8=A1=A5=E8=BF=9B=E6=96=87=E6=A1=A3=20(#123)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题: ai_single_yang_unbroken 策略用 from datetime import date 做 date 类型参数转换, 被安全修复的 import 白名单(只允许 polars)拦截, 策略加载失败消失。同时 date 列(Polars Date)与字符串参数直接比较 报 InvalidOperationError 导致 500。 修复: - ai_generator.py: import 白名单加入 datetime (纯日期运算, 无文件/ 网络/进程能力, 安全)。验证 os/sys/subprocess 仍被拦。 - strategy-guide.md: params type 补全(float/int/bool/select/date); filter_history 要点新增 date 参数必须 fromisoformat 转换的代码示例 - strategy-guide-compact.md: 同步补充 (AI 运行时实际用的文档) 从源头避免 AI 生成的 date 类型参数策略再犯字符串 vs Date 列比较错误。 Co-authored-by: shy3130 --- backend/app/strategy/ai_generator.py | 4 ++-- .../app/strategy/prompts/strategy-guide-compact.md | 13 +++++++++++-- backend/app/strategy/prompts/strategy-guide.md | 13 +++++++++++-- 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/backend/app/strategy/ai_generator.py b/backend/app/strategy/ai_generator.py index c80a560..5f10dfb 100644 --- a/backend/app/strategy/ai_generator.py +++ b/backend/app/strategy/ai_generator.py @@ -116,9 +116,9 @@ class AIStrategyGenerator: return content.split("```", 1)[1].split("```", 1)[0].strip() return content.strip() - # import 白名单: 策略文件只允许 polars (见 strategy-guide.md「只 import polars」)。 + # import 白名单: 策略文件只允许 polars + datetime (纯日期运算, 无文件/网络/进程能力)。 # 白名单而非黑名单 — 黑名单挡不住 ctypes/importlib/builtins/pickle 等未列出的危险模块。 - _ALLOWED_IMPORT_MODULES = frozenset({"polars", "__future__"}) + _ALLOWED_IMPORT_MODULES = frozenset({"polars", "__future__", "datetime"}) @classmethod def _validate_safety(cls, code: str) -> None: diff --git a/backend/app/strategy/prompts/strategy-guide-compact.md b/backend/app/strategy/prompts/strategy-guide-compact.md index 1035fc9..db18a81 100644 --- a/backend/app/strategy/prompts/strategy-guide-compact.md +++ b/backend/app/strategy/prompts/strategy-guide-compact.md @@ -4,7 +4,7 @@ ## 必须遵守 -1. 只 `import polars as pl`,禁止 import 其他模块。 +1. 只允许 `import polars as pl` 和 `from datetime import date/datetime`(date 类型参数比较需要),禁止 import 其他模块。 2. AI 策略只属于 `data/strategies/ai/`,`META.id` 使用用户给定的 `ai_` ID。 3. 不要读写文件,不要使用 `open/exec/eval/compile/__import__/globals/locals/vars/dir/getattr/setattr/delattr/type/input`。 4. `META.params` 只放用户可能调整的阈值;公式常数和固定窗口边界不必参数化。 @@ -32,7 +32,7 @@ META = { "exclude_st": True, "exclude_new_days": 30, }, - "params": [], + "params": [], # type: float/int/bool/select/date;float/int 带 min/max/step "scoring": {}, "order_by": "score", "descending": True, @@ -80,6 +80,15 @@ def filter_history(df: pl.DataFrame, params: dict) -> pl.DataFrame: `filter_history()` 必须返回所有匹配行,不要只过滤最新日期;回测需要全区间命中。 +**date 类型参数必须先转换再与 `date` 列比较**:params 里 `"type": "date"` 的参数从 JSON 传来是字符串(如 `"2024-01-01"`),而数据中 `date` 列是 Polars Date 类型,**不能直接比较**(报 InvalidOperationError)。必须先转换: + +```python +from datetime import date as _date +anchor_raw = params.get("anchor_date", "2024-01-01") +anchor_date = _date.fromisoformat(anchor_raw) if isinstance(anchor_raw, str) else anchor_raw +# 之后才能: pl.col("date") == anchor_date 或 pl.col("date") > anchor_date +``` + ## 常用字段 通用:`symbol`, `date`, `name` diff --git a/backend/app/strategy/prompts/strategy-guide.md b/backend/app/strategy/prompts/strategy-guide.md index 0d04c99..2d7e5de 100644 --- a/backend/app/strategy/prompts/strategy-guide.md +++ b/backend/app/strategy/prompts/strategy-guide.md @@ -35,7 +35,8 @@ META = { }, # 策略参数 (只把用户可能调节的阈值放这里,公式常数不必参数化) - # 每个参数含 id/label/type/default/min/max/step;select 类型用 options + # type 支持: float / int / bool / select(带 options) / date(格式 "YYYY-MM-DD") + # float/int 可带 min/max/step;select 带 options: [{label, value}];date 的 default 是字符串 "params": [ ], @@ -143,6 +144,14 @@ def filter_history(df: pl.DataFrame, params: dict) -> pl.DataFrame: - 只有遇到表达式难以描述的复杂状态机时,才使用 `partition_by("symbol")` + `to_dicts()` 逐股票分析 - **返回所有匹配行,不要过滤 `latest`**;选股引擎会自动取最新日,回测引擎需要全区间命中 - 未声明 `filter_history()` 的策略走普通 `filter()` 路径,不受影响 +- **date 类型参数必须先转换再与 `date` 列比较**:params 里的 `"type": "date"` 参数从 JSON 传来是字符串(如 `"2024-01-01"`),而数据中 `date` 列是 Polars Date 类型,**不能直接比较**,否则报错。必须先转换: + +```python +from datetime import date as _date +anchor_raw = params.get("anchor_date", "2024-01-01") +anchor_date = _date.fromisoformat(anchor_raw) if isinstance(anchor_raw, str) else anchor_raw +# 之后才能: pl.col("date") == anchor_date 或 pl.col("date") > anchor_date +``` ## 3. 常用指标列(参考,可直接使用) @@ -266,7 +275,7 @@ def filter_history(df: pl.DataFrame, params: dict) -> pl.DataFrame: 3. 用户可能调节的数值阈值通过 `params` 暴露;公式常数、固定窗口边界、一次性内部变量不必强行参数化 4. `scoring` 权重总和必须为 1.0 5. 遵循 A 股 T+1 规则 (当日买入次日才能卖出) -6. 只允许 `import polars as pl`,禁止 import 其他模块 +6. 只允许 `import polars as pl` 和 `from datetime import date/datetime`(date 类型参数比较需要),禁止 import 其他模块 7. 禁止使用 `open()`, `exec()`, `eval()`, `os`, `sys`, `subprocess` 8. **贴合用户需求优先**:第3/4节的指标列和信号列仅供参考,能用则用;如果用户需求需要自定义计算(如"前高""上次涨停价""N日内某个事件后X天"),直接在 `filter_history()` 中自行设计和计算,不需要局限于已有列 9. `filter_history()` 中优先用 Polars 向量化语法;仅在复杂状态机无法清晰表达时,才用 `partition_by("symbol")` 逐股票分析